blob: 58b31c5aea58020290df604cafee6fbb89ae74da [file]
import {assert} from '@open-wc/testing';
import './test-setup';
import {
ResultDbV1Client,
TestStatus,
TestVariantStatus,
} from '../resultdb-client';
import {DATA_SYMBOL} from '../checks-fetcher';
import {
installChecksResult,
replaceTextArtifacts,
parseMarkdown,
} from '../checks-result';
import {FaultAttribute} from '../failure-analysis';
function sanitizedAttr(attr: string) {
return `rel="noopener" target="_blank" ${attr}`;
}
suite('checks-result tests', () => {
let sandbox: sinon.SinonSandbox;
const defaultMetaAttr = 'alt="chromeChecksMetadataTagDefault"';
const greenMetaAttr = 'alt="chromeChecksMetadataTagGreen"';
const redMetaAttr = 'alt="chromeChecksMetadataTagRed"';
setup(() => {
sandbox = sinon.createSandbox();
sandbox
.stub(ResultDbV1Client.prototype, 'getAuthorizationHeader')
.returns(Promise.resolve({authorization: 'accessToken'}));
});
teardown(() => {
sandbox.restore();
});
test('installChecksResult formats the CheckResult row', async () => {
const message = 'Run #1: unexpectedly failed.';
/*
* Recreate Gerrit endpoint DOM.
*
* <container>
* <shadow>
* <gr-endpoint-decorator name="check-result-expanded">
* <messageDiv class="message">
* message text
* </messageDiv>
* </gr-endpoint-decorator>
* </shadow>
* </container>
*
*/
const container = document.createElement('div');
const shadow = container.attachShadow({mode: 'open'});
const element = document.createElement('div');
(element as any).result = {message};
shadow.appendChild(element);
const messageDiv = document.createElement('div');
messageDiv.className = 'message';
messageDiv.textContent = message;
element.appendChild(messageDiv);
await installChecksResult(element);
// Check element is styled.
const style = (element.getRootNode() as any).querySelector(
'style'
).textContent;
assert.isTrue(style.includes('word-break: break-all;'));
assert.isTrue(style.includes('white-space: pre-wrap;'));
// Check original messageDiv removed.
assert.notStrictEqual(messageDiv.parentNode, container);
// Check element's data is used to format the row.
const fetchStub = sandbox.stub(window, 'fetch');
fetchStub.returns(
Promise.resolve({
ok: true,
text: async () => 'foo test failed!',
} as Response)
);
const artifactStub = sandbox.stub(
ResultDbV1Client.prototype,
'listArtifacts'
);
artifactStub.returns(
Promise.resolve({
artifacts: [
{
name: 'invocations/123/foo/artifacts/baz',
artifactId: 'foo',
fetchUrl: 'https://foo.com',
fetchUrlExpiration: '2017-12-15T01:30:15.05Z',
sizeBytes: 10,
},
],
})
);
(element as any).result[DATA_SYMBOL] = {
variant: {variant: {def: {builder: 'baz', os: 'foo-OS'}}},
testResults: [
{
result: {
name: 'invocations/123/foo',
status: TestStatus.FAIL,
summaryHtml: 'hello <text-artifact artifact-id="foo" />',
expected: false,
},
},
{
result: {
status: TestStatus.PASS,
summaryHtml: '',
expected: true,
},
},
],
plugin: {
checks: () => {
return {
updateResult: (_1: any, _2: any) => {},
};
},
},
rdbHost: 'host',
};
await installChecksResult(element);
assert.strictEqual(
element.innerHTML,
`<a ${sanitizedAttr(
defaultMetaAttr
)}><em>builder: baz, os: foo-OS</em></a>` +
'<ul>' +
'<li>' +
'<h3>' +
`<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` +
`<a ${sanitizedAttr(redMetaAttr)}>unexpectedly failed</a>` +
'</h3>' +
'hello foo test failed!' +
'</li>' +
'<li>' +
'<h3>' +
`<a ${sanitizedAttr(defaultMetaAttr)}>Run #2: </a>` +
`<a ${sanitizedAttr(greenMetaAttr)}>expectedly passed</a>` +
'</h3>' +
'</li>' +
'</ul>'
);
// Variants without definitions and artifacts without sizes are processed.
artifactStub.returns(
Promise.resolve({
artifacts: [
{
name: 'invocations/123/baz/artifacts/bar',
artifactId: 'baz',
fetchUrl: 'https://baz.com',
fetchUrlExpiration: '2017-12-15T01:30:15.05Z',
sizeBytes: 0,
},
],
})
);
(element as any).result[DATA_SYMBOL] = {
testResults: [
{
result: {
name: 'invocations/123/baz',
status: TestStatus.FAIL,
summaryHtml: 'bye <text-artifact artifact-id="baz" />',
},
},
],
plugin: {
checks: () => {
return {
updateResult: (_1: any, _2: any) => {},
};
},
},
rdbHost: 'host',
};
await installChecksResult(element);
assert.strictEqual(
element.innerHTML,
'<ul>' +
'<li>' +
'<h3>' +
`<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` +
`<a ${sanitizedAttr(redMetaAttr)}>unexpectedly failed</a>` +
'</h3>' +
`bye <a ${sanitizedAttr(defaultMetaAttr)}>baz artifact is empty</a>` +
'</li>' +
'</ul>'
);
// Exonerations are shown.
(element as any).result[DATA_SYMBOL] = {
testResults: [
{
result: {
name: 'invocations/123/baz',
status: TestStatus.FAIL,
summaryHtml: 'bye <text-artifact artifact-id="baz" />',
},
},
],
testExonerations: [
{explanationHtml: '<p>foo is exonerated</p>'},
{explanationHtml: '<p>bar is exonerated</p>'},
],
plugin: {
checks: () => {
return {
updateResult: (_1: any, _2: any) => {},
};
},
},
rdbHost: 'host',
variant: {
variant: {},
results: [],
exonerations: [],
status: TestVariantStatus.EXPECTED,
variantHash: '123',
faultAttribute: {
comparisonSnapshot: {
sourceBuildId: '8775275012462334081',
sourceStartedUnixTimestamp: '1689668613',
},
snapshotComparisonFaultAttribution:
FaultAttribute.MATCHING_FAILURE_FOUND,
testName: 'foo-test',
},
},
};
await installChecksResult(element);
assert.strictEqual(
element.innerHTML,
'<br><br>' +
'<p>foo is exonerated</p><br><p>bar is exonerated</p>' +
'<ul>' +
'<li>' +
'<h3>' +
`<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` +
`<a ${sanitizedAttr(
redMetaAttr
)}>unexpectedly failed as of </a><a rel="noopener" target="_blank" href="https://ci.chromium.org/ui/b/8775275012462334081/test-results?q=ExactID:foo-test+VHash:123">7/18/2023, 8:23:33 AM</a>` +
'</h3>' +
`bye <a ${sanitizedAttr(defaultMetaAttr)}>baz artifact is empty</a>` +
'</li>' +
'</ul>'
);
// Variant hash excluded from MILO URL if different model was used
// for fault attribution.
(element as any).result[DATA_SYMBOL] = {
testResults: [
{
result: {
name: 'invocations/123/baz',
status: TestStatus.FAIL,
summaryHtml: 'bye <text-artifact artifact-id="baz" />',
},
},
],
plugin: {
checks: () => {
return {
updateResult: (_1: any, _2: any) => {},
};
},
},
rdbHost: 'host',
variant: {
variant: {},
results: [],
exonerations: [],
status: TestVariantStatus.EXPECTED,
variantHash: '123',
faultAttribute: {
comparisonSnapshot: {
sourceBuildId: '8775275012462334081',
sourceStartedUnixTimestamp: '1689668613',
},
snapshotComparisonFaultAttribution:
FaultAttribute.MATCHING_FAILURE_FOUND,
testName: 'foo-test',
diffModelUsed: 'true',
},
},
};
await installChecksResult(element);
assert.strictEqual(
element.innerHTML,
'<ul>' +
'<li>' +
'<h3>' +
`<a ${sanitizedAttr(defaultMetaAttr)}>Run #1: </a>` +
`<a ${sanitizedAttr(
redMetaAttr
)}>unexpectedly failed as of </a><a rel="noopener" target="_blank" href="https://ci.chromium.org/ui/b/8775275012462334081/test-results?q=ExactID:foo-test">7/18/2023, 8:23:33 AM</a>` +
'</h3>' +
`bye <a ${sanitizedAttr(defaultMetaAttr)}>baz artifact is empty</a>` +
'</li>' +
'</ul>'
);
});
test('replaceTextArtifacts replaces <text-artifact/>', async () => {
const fetchStub = sandbox.stub(window, 'fetch');
fetchStub.withArgs('https://foo.com/?n=50000').returns(
Promise.resolve({
ok: true,
text: async () => 'foo test failed!',
} as Response)
);
fetchStub.withArgs('https://bar.org/?n=50000').returns(
Promise.resolve({
ok: true,
text: async () => 'bar test passed!',
} as Response)
);
fetchStub
.withArgs('https://baz.org/?n=50000')
.returns(Promise.reject(new Error('bad fetch')));
fetchStub.withArgs('https://boo.org/?n=50000').returns(
Promise.resolve({
ok: false,
status: 500,
text: async () => 'server error',
} as Response)
);
const resArtifacts = [
{
artifactId: 'foo',
name: 'foo-name',
fetchUrl: 'https://foo.com/',
sizeBytes: 10,
},
{
artifactId: 'baz',
name: 'baz-name',
fetchUrl: 'https://baz.org/',
sizeBytes: 10,
},
{
artifactId: 'boo',
name: 'boo-name',
fetchUrl: 'https://boo.org/',
sizeBytes: 10,
},
];
const invArtifacts = [
{
artifactId: 'bar',
name: 'bar-name',
fetchUrl: 'https://bar.org/',
sizeBytes: 10,
},
];
// Replace and fetch artifacts.
assert.strictEqual(
await replaceTextArtifacts(
'<p><text-artifact artifact-id="foo" /></p>' +
'<p><text-artifact artifact-id="bar" inv-level /></p>' +
'<p><text-artifact artifact-id="baz" /></p>' +
'<p><text-artifact artifact-id="boo" /></p>',
resArtifacts,
invArtifacts
),
'<p>foo test failed!</p><p>bar test passed!</p>' +
`<p><a ${defaultMetaAttr}>Failed to fetch baz artifact: bad fetch. See console logs.</a></p>` +
`<p><a ${defaultMetaAttr}>Failed to fetch boo artifact: server error. See console logs.</a></p>`
);
});
test('replaceTextArtifacts caches successful fetches', async () => {
const fetchStub = sandbox.stub(window, 'fetch');
fetchStub.onCall(0).returns(Promise.reject(new Error('bad fetch')));
fetchStub.onCall(1).returns(
Promise.resolve({
ok: true,
text: async () => 'foobar test failed!',
} as Response)
);
fetchStub.onCall(2).returns(Promise.reject(new Error('bad fetch')));
const artifacts = [
{
artifactId: 'foobar',
name: 'foobar-name',
fetchUrl: 'https://foobar.com/',
sizeBytes: 10,
},
];
// Replace and fetch.
const html = '<p><text-artifact artifact-id="foobar" /></p>';
assert.strictEqual(
await replaceTextArtifacts(html, artifacts, []),
`<p><a ${defaultMetaAttr}>Failed to fetch foobar artifact: bad fetch. See console logs.</a></p>`
);
assert.strictEqual(
await replaceTextArtifacts(html, artifacts, []),
'<p>foobar test failed!</p>'
);
sinon.assert.calledTwice(fetch as any);
// Check that fetches after the first successful fetch use cache.
assert.strictEqual(
await replaceTextArtifacts(html, artifacts, []),
'<p>foobar test failed!</p>'
);
assert.strictEqual(
await replaceTextArtifacts(html, artifacts, []),
'<p>foobar test failed!</p>'
);
sinon.assert.calledTwice(fetch as any);
});
test('replaceTextArtifacts links to large text artifacts', async () => {
const fetchStub = sandbox.stub(window, 'fetch');
const artifactOne = 'a'.repeat(50001);
fetchStub.withArgs('https://a1.com/?n=50000').returns(
Promise.resolve({
ok: true,
text: async () => artifactOne,
} as Response)
);
const artifactTwo = 'a'.repeat(50002);
fetchStub.withArgs('https://a2.com/?n=50000').returns(
Promise.resolve({
ok: true,
text: async () => artifactTwo,
} as Response)
);
const artifacts = [
{
artifactId: 'a1',
name: 'a1-name',
fetchUrl: 'https://a1.com/',
sizeBytes: artifactOne.length,
},
{
artifactId: 'a2',
name: 'a2-name',
fetchUrl: 'https://a2.com/',
sizeBytes: artifactTwo.length,
},
];
assert.strictEqual(
await replaceTextArtifacts(
'<p><text-artifact artifact-id="a1"/></p>',
artifacts,
[]
),
`<p>${artifactOne}<a ${defaultMetaAttr}>\n\n...1 more byte...\n` +
'More information in a1 artifact</a></p>'
);
assert.strictEqual(
await replaceTextArtifacts(
'<p><text-artifact artifact-id="a2"/></p>',
artifacts,
[]
),
`<p>${artifactTwo}<a ${defaultMetaAttr}>\n\n...2 more bytes...\n` +
'More information in a2 artifact</a></p>'
);
});
test('parseMarkdown replaces headers', () => {
assert.strictEqual(parseMarkdown('#foo'), '#foo');
assert.strictEqual(parseMarkdown('# foo'), '<h1>foo</h1>');
assert.strictEqual(parseMarkdown('## foo'), '<h2>foo</h2>');
assert.strictEqual(parseMarkdown('### foo'), '<h3>foo</h3>');
assert.strictEqual(parseMarkdown('#### foo'), '<h4>foo</h4>');
assert.strictEqual(parseMarkdown('##### foo'), '<h5>foo</h5>');
assert.strictEqual(parseMarkdown('###### foo'), '<h6>foo</h6>');
assert.strictEqual(parseMarkdown('####### foo'), '####### foo');
assert.strictEqual(
parseMarkdown(`# foo
### bar
## baz`),
'<h1>foo</h1><h3>bar</h3><h2>baz</h2>'
);
assert.strictEqual(
parseMarkdown('## foo __bar__'),
'<h2>foo <strong>bar</strong></h2>'
);
assert.strictEqual(
parseMarkdown('__## foo bar__'),
'<strong>## foo bar</strong>'
);
});
test('parseMarkdown adds strong', () => {
assert.strictEqual(parseMarkdown('_foo__'), '_foo__');
assert.strictEqual(parseMarkdown('__foo__'), '<strong>foo</strong>');
assert.strictEqual(parseMarkdown('**foo__'), '**foo__');
assert.strictEqual(parseMarkdown('** foo**'), '** foo**');
assert.strictEqual(parseMarkdown('**foo **'), '**foo **');
assert.strictEqual(parseMarkdown('**foo**'), '<strong>foo</strong>');
assert.strictEqual(parseMarkdown('**fo**'), '<strong>fo</strong>');
assert.strictEqual(parseMarkdown('**f**'), '<strong>f</strong>');
assert.strictEqual(
parseMarkdown('**space here**'),
'<strong>space here</strong>'
);
assert.strictEqual(parseMarkdown('**newline\nhere**'), '**newline\nhere**');
assert.strictEqual(
parseMarkdown('this __is__ **foo**'),
'this <strong>is</strong> <strong>foo</strong>'
);
assert.strictEqual(parseMarkdown('____'), '____');
});
test('parseMarkdown replaces links', () => {
assert.strictEqual(
parseMarkdown('[foo](bar.com)'),
"<a href='bar.com'>foo</a>"
);
assert.strictEqual(
parseMarkdown('[foo [baz]](bar.com)'),
"<a href='bar.com'>foo [baz]</a>"
);
assert.strictEqual(parseMarkdown('[](bar.com)'), "<a href='bar.com'></a>");
assert.strictEqual(
parseMarkdown('[a[c]](bar.com)[b[]](car.com)'),
"<a href='bar.com'>a[c]</a><a href='car.com'>b[]</a>"
);
});
test('parseMarkdown creates unordered lists', () => {
const md = `# recipe
- **bacon**
- __lettuce__
- tomatoes`;
assert.strictEqual(
parseMarkdown(md),
'<h1>recipe</h1><ul>' +
'<li><strong>bacon</strong></li>' +
'<li><strong>lettuce</strong></li>' +
'<li>tomatoes</li></ul>'
);
});
test('parseMarkdown ignores markdown in code blocks', () => {
const md = `\`\`\`syntax highlighting here
don't format __this__
# or even this!
\`\`\`
but this __should__ be okay
# and this __too__`;
assert.strictEqual(
parseMarkdown(md),
"<pre><code>don't format __this__\n# or even this!\n</code></pre>" +
'but this <strong>should</strong> be okay' +
'\n<h1>and this <strong>too</strong></h1>'
);
});
test('parseMarkdown ignores markdown in link URLs', () => {
const md = "__bold__. [Link](foo.com/__dont_bold__). **also bold**";
assert.strictEqual(
parseMarkdown(md),
"<strong>bold</strong>. <a href='foo.com/__dont_bold__'>Link</a>. <strong>also bold</strong>"
);
});
});