Skip to content

Commit

Permalink
Support adding artifacts to test runs
Browse files Browse the repository at this point in the history
  • Loading branch information
john-dupuy committed Oct 1, 2021
1 parent c24cb92 commit cdedb85
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 14 deletions.
38 changes: 29 additions & 9 deletions backend/ibutsu_server/controllers/artifact_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def get_artifact(id_):
return artifact.to_dict()


def get_artifact_list(result_id=None, page_size=25, page=1):
def get_artifact_list(result_id=None, run_id=None, page_size=25, page=1):
"""Get a list of artifact files for result
:param id: ID of test result
Expand All @@ -72,6 +72,8 @@ def get_artifact_list(result_id=None, page_size=25, page=1):
query = Artifact.query
if result_id:
query = query.filter(Artifact.result_id == result_id)
if run_id:
query = query.filter(Artifact.run_id == run_id)
total_items = query.count()
offset = (page * page_size) - page_size
total_pages = (total_items // page_size) + (1 if total_items % page_size > 0 else 0)
Expand All @@ -92,6 +94,8 @@ def upload_artifact(body):
:param result_id: ID of result to attach artifact to
:type result_id: str
:param run_id: ID of run to attach artifact to
:type run_id: str
:param filename: filename for storage
:type filename: string
:param file: file to upload
Expand All @@ -102,11 +106,17 @@ def upload_artifact(body):
:rtype: tuple
"""
result_id = body.get("result_id")
run_id = body.get("run_id")
filename = body.get("filename")
additional_metadata = body.get("additional_metadata", {})
file_ = connexion.request.files["file"]
content_type = magic.from_buffer(file_.read())
data = {"contentType": content_type, "resultId": result_id, "filename": filename}
data = {
"contentType": content_type,
"resultId": result_id,
"runId": run_id,
"filename": filename,
}
if additional_metadata:
if isinstance(additional_metadata, str):
try:
Expand All @@ -118,13 +128,23 @@ def upload_artifact(body):
data["additionalMetadata"] = additional_metadata
# Reset the file pointer
file_.seek(0)
artifact = Artifact(
filename=filename,
result_id=data["resultId"],
content=file_.read(),
upload_date=datetime.utcnow(),
data=additional_metadata,
)
if data.get("runId"):
artifact = Artifact(
filename=filename,
run_id=data["runId"],
content=file_.read(),
upload_date=datetime.utcnow(),
data=additional_metadata,
)
else:
artifact = Artifact(
filename=filename,
result_id=data["resultId"],
content=file_.read(),
upload_date=datetime.utcnow(),
data=additional_metadata,
)

session.add(artifact)
session.commit()
return artifact.to_dict(), 201
Expand Down
4 changes: 3 additions & 1 deletion backend/ibutsu_server/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ def to_dict(self):

class Artifact(Model, FileMixin):
__tablename__ = "artifacts"
result_id = Column(PortableUUID(), ForeignKey("results.id"), nullable=False, index=True)
result_id = Column(PortableUUID(), ForeignKey("results.id"), index=True)
run_id = Column(PortableUUID(), ForeignKey("runs.id"), index=True)
filename = Column(Text, index=True)
data = Column(mutable_json_type(dbtype=PortableJSON(), nested=True))
upload_date = Column(DateTime, default=datetime.utcnow, index=True)
Expand Down Expand Up @@ -169,6 +170,7 @@ class Result(Model, ModelMixin):

class Run(Model, ModelMixin):
__tablename__ = "runs"
artifacts = relationship("Artifact")
component = Column(Text, index=True)
created = Column(DateTime, default=datetime.utcnow, index=True)
# this is metadata but it is a reserved attr
Expand Down
34 changes: 32 additions & 2 deletions backend/ibutsu_server/db/upgrades.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sqlalchemy.sql import quoted_name
from sqlalchemy.sql.expression import null

__version__ = 2
__version__ = 3


def get_upgrade_op(session):
Expand All @@ -33,7 +33,7 @@ def upgrade_1(session):
if (
"dashboards" in metadata.tables
and widget_configs is not None
and not widget_configs.columns.contains_column("dashboard_id")
and not hasattr(widget_configs.columns, "dashboard_id")
):
op.add_column("widget_configs", Column("dashboard_id", PortableUUID, server_default=null()))
if engine.url.get_dialect().name != "sqlite":
Expand Down Expand Up @@ -85,3 +85,33 @@ def upgrade_2(session):
[quoted_name("(data->'requirements')", False)],
postgresql_using="gin",
)


def upgrade_3(session):
"""Version 3 upgrade
This upgrade:
- makes the 'result_id' column of artifacts nullable
- adds a 'run_id' to the artifacts table
"""
engine = session.get_bind()
op = get_upgrade_op(session)
metadata = MetaData()
metadata.reflect(bind=engine)
artifacts = metadata.tables.get("artifacts")
if (
"runs" in metadata.tables
and artifacts is not None
and not hasattr(artifacts.columns, "run_id")
):
op.alter_column("artifacts", "result_id", nullable=True, server_default=null())
op.add_column("artifacts", Column("run_id", PortableUUID, server_default=null()))
if engine.url.get_dialect().name != "sqlite":
# SQLite doesn't support ALTER TABLE ADD CONSTRAINT
op.create_foreign_key(
"fk_artifacts_run_id",
"artifacts",
"runs",
["run_id"],
["id"],
)
18 changes: 17 additions & 1 deletion backend/ibutsu_server/openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ paths:
required: false
schema:
type: string
format: uuid
style: form
- description: The run ID to filter by
explode: true
in: query
name: runId
required: false
schema:
type: string
format: uuid
style: form
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PageSize'
Expand All @@ -207,6 +217,9 @@ paths:
resultId:
description: ID of result to attach artifact to
type: string
runId:
description: ID of run to attach artifact to
type: string
filename:
description: ID of pet to update
type: string
Expand All @@ -218,7 +231,6 @@ paths:
description: Additional data to pass to server
type: object
required:
- resultId
- file
- filename
type: object
Expand Down Expand Up @@ -1668,6 +1680,10 @@ components:
description: ID of test result to attach artifact to
example: a16ad60e-bf23-4195-99dc-594858ad3e5e
type: string
run_id:
description: ID of test run to attach artifact to
example: 04af3d2a-55c5-4562-9ed6-3bf30ac6753e
type: string
filename:
description: ID of pet to update
type: string
Expand Down
59 changes: 58 additions & 1 deletion frontend/src/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Card,
CardHeader,
CardBody,
CardFooter,
DataList,
DataListCell,
DataListItem,
Expand All @@ -31,13 +32,16 @@ import {
CatalogIcon,
ChevronRightIcon,
CodeIcon,
FileAltIcon,
FileImageIcon,
InfoCircleIcon,
MessagesIcon,
RepositoryIcon
} from '@patternfly/react-icons';

import { Link } from 'react-router-dom';
import ReactJson from 'react-json-view';
import Editor from '@monaco-editor/react';

import { Settings } from './settings';
import {
Expand Down Expand Up @@ -139,6 +143,8 @@ export class Run extends React.Component {
isError: false,
resultsTree: {core: {data: []}},
treeData: [],
artifacts: [],
artifactTabs: []
};
// Watch the history to update tabs
this.unlisten = this.props.history.listen(() => {
Expand Down Expand Up @@ -270,6 +276,55 @@ export class Run extends React.Component {
});
}

getRunArtifacts() {
fetch(buildUrl(Settings.serverUrl + '/artifact', {runId: this.state.id}))
.then(response => response.json())
.then(data => {
let artifactTabs = [];
data.artifacts.forEach((artifact) => {
fetch(Settings.serverUrl + `/artifact/${artifact.id}/view`)
.then(response => {
let contentType = response.headers.get('Content-Type');
if (contentType.includes('text')) {
response.text().then(text => {
artifactTabs.push(
<Tab key={artifact.id} eventKey={artifact.id} title={<TabTitle icon={FileAltIcon} text={artifact.filename} />} style={{backgroundColor: "white"}}>
<Card>
<CardBody>
<Editor fontFamily="Hack, monospace" theme="dark" value={text} height="40rem" options={{readOnly: true}} />
</CardBody>
<CardFooter>
<Button component="a" href={`${Settings.serverUrl}/artifact/${artifact.id}/download`}>Download {artifact.filename}</Button>
</CardFooter>
</Card>
</Tab>
);
this.setState({artifactTabs});
});
}
else if (contentType.includes('image')) {
response.blob().then(blob => {
let imageUrl = URL.createObjectURL(blob);
artifactTabs.push(
<Tab key={artifact.id} eventKey={artifact.id} title={<TabTitle icon={FileImageIcon} text={artifact.filename} />} style={{backgroundColor: "white"}}>
<Card>
<CardBody>
<img src={imageUrl} alt={artifact.filename}/>
</CardBody>
<CardFooter>
<Button component="a" href={`${Settings.serverUrl}/artifact/${artifact.id}/download`}>Download {artifact.filename}</Button>
</CardFooter>
</Card>
</Tab>
);
this.setState({artifactTabs});
});
}
});
});
});
}

updateTab(tabIndex) {
if (tabIndex === 'results-list') {
this.getResultsForTable();
Expand Down Expand Up @@ -353,6 +408,7 @@ export class Run extends React.Component {
return response.json();
})
.then(data => this.setState({run: data}, () => {
this.getRunArtifacts();
this.updateTab(this.state.activeTab);
}))
.catch(error => console.log(error));
Expand Down Expand Up @@ -429,7 +485,7 @@ export class Run extends React.Component {
let passed = 0, failed = 0, errors = 0, xfailed = 0, xpassed = 0, skipped = 0, not_run = 0;
let created = 0;
let calculatePasses = true;
const { run, columns, rows, classificationTable } = this.state;
const { run, columns, rows, classificationTable, artifactTabs } = this.state;

if (run.start_time) {
created = new Date(run.start_time);
Expand Down Expand Up @@ -762,6 +818,7 @@ export class Run extends React.Component {
<Tab eventKey={'classify-failures'} title={<TabTitle icon={MessagesIcon} text="Classify Failures" />} style={{backgroundColor: "white"}}>
{classificationTable}
</Tab>
{artifactTabs}
<Tab eventKey={'run-object'} title={<TabTitle icon={CodeIcon} text="Run Object" />} style={{backgroundColor: "white"}}>
<Card>
<CardBody>
Expand Down

0 comments on commit cdedb85

Please sign in to comment.