blob: d946b35cec9b71ff646d30dacffe04d408c8a43d [file] [log] [blame]
/*global add_completion_callback, setup */
/*
* This file is intended for vendors to implement
* code needed to integrate testharness.js tests with their own test systems.
*
* The default implementation extracts metadata from the tests and validates
* it against the cached version that should be present in the test source
* file. If the cache is not found or is out of sync, source code suitable for
* caching the metadata is optionally generated.
*
* The cached metadata is present for extraction by test processing tools that
* are unable to execute javascript.
*
* Metadata is attached to tests via the properties parameter in the test
* constructor. See testharness.js for details.
*
* Typically test system integration will attach callbacks when each test has
* run, using add_result_callback(callback(test)), or when the whole test file
* has completed, using
* add_completion_callback(callback(tests, harness_status)).
*
* For more documentation about the callback functions and the
* parameters they are called with see testharness.js
*/
var metadata_generator = {
currentMetadata: {},
cachedMetadata: false,
metadataProperties: ['help', 'assert', 'author'],
error: function(message) {
var messageElement = document.createElement('p');
messageElement.setAttribute('class', 'error');
this.appendText(messageElement, message);
var summary = document.getElementById('summary');
if (summary) {
summary.parentNode.insertBefore(messageElement, summary);
}
else {
document.body.appendChild(messageElement);
}
},
/**
* Ensure property value has contact information
*/
validateContact: function(test, propertyName) {
var result = true;
var value = test.properties[propertyName];
var values = Array.isArray(value) ? value : [value];
for (var index = 0; index < values.length; index++) {
value = values[index];
var re = /(\S+)(\s*)<(.*)>(.*)/;
if (! re.test(value)) {
re = /(\S+)(\s+)(http[s]?:\/\/)(.*)/;
if (! re.test(value)) {
this.error('Metadata property "' + propertyName +
'" for test: "' + test.name +
'" must have name and contact information ' +
'("name <email>" or "name http(s)://")');
result = false;
}
}
}
return result;
},
/**
* Extract metadata from test object
*/
extractFromTest: function(test) {
var testMetadata = {};
// filter out metadata from other properties in test
for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
metaIndex++) {
var meta = this.metadataProperties[metaIndex];
if (test.properties.hasOwnProperty(meta)) {
if ('author' == meta) {
this.validateContact(test, meta);
}
testMetadata[meta] = test.properties[meta];
}
}
return testMetadata;
},
/**
* Compare cached metadata to extracted metadata
*/
validateCache: function() {
for (var testName in this.currentMetadata) {
if (! this.cachedMetadata.hasOwnProperty(testName)) {
return false;
}
var testMetadata = this.currentMetadata[testName];
var cachedTestMetadata = this.cachedMetadata[testName];
delete this.cachedMetadata[testName];
for (var metaIndex = 0; metaIndex < this.metadataProperties.length;
metaIndex++) {
var meta = this.metadataProperties[metaIndex];
if (cachedTestMetadata.hasOwnProperty(meta) &&
testMetadata.hasOwnProperty(meta)) {
if (Array.isArray(cachedTestMetadata[meta])) {
if (! Array.isArray(testMetadata[meta])) {
return false;
}
if (cachedTestMetadata[meta].length ==
testMetadata[meta].length) {
for (var index = 0;
index < cachedTestMetadata[meta].length;
index++) {
if (cachedTestMetadata[meta][index] !=
testMetadata[meta][index]) {
return false;
}
}
}
else {
return false;
}
}
else {
if (Array.isArray(testMetadata[meta])) {
return false;
}
if (cachedTestMetadata[meta] != testMetadata[meta]) {
return false;
}
}
}
else if (cachedTestMetadata.hasOwnProperty(meta) ||
testMetadata.hasOwnProperty(meta)) {
return false;
}
}
}
for (var testName in this.cachedMetadata) {
return false;
}
return true;
},
appendText: function(elemement, text) {
elemement.appendChild(document.createTextNode(text));
},
jsonifyArray: function(arrayValue, indent) {
var output = '[';
if (1 == arrayValue.length) {
output += JSON.stringify(arrayValue[0]);
}
else {
for (var index = 0; index < arrayValue.length; index++) {
if (0 < index) {
output += ',\n ' + indent;
}
output += JSON.stringify(arrayValue[index]);
}
}
output += ']';
return output;
},
jsonifyObject: function(objectValue, indent) {
var output = '{';
var value;
var count = 0;
for (var property in objectValue) {
++count;
if (Array.isArray(objectValue[property]) ||
('object' == typeof(value))) {
++count;
}
}
if (1 == count) {
for (var property in objectValue) {
output += ' "' + property + '": ' +
JSON.stringify(objectValue[property]) +
' ';
}
}
else {
var first = true;
for (var property in objectValue) {
if (! first) {
output += ',';
}
first = false;
output += '\n ' + indent + '"' + property + '": ';
value = objectValue[property];
if (Array.isArray(value)) {
output += this.jsonifyArray(value, indent +
' '.substr(0, 5 + property.length));
}
else if ('object' == typeof(value)) {
output += this.jsonifyObject(value, indent + ' ');
}
else {
output += JSON.stringify(value);
}
}
if (1 < output.length) {
output += '\n' + indent;
}
}
output += '}';
return output;
},
/**
* Generate javascript source code for captured metadata
* Metadata is in pretty-printed JSON format
*/
generateSource: function() {
/* "\/" is used instead of a plain forward slash so that the contents
of testharnessreport.js can (for convenience) be copy-pasted into a
script tag without issue. Otherwise, the HTML parser would think that
the script ended in the middle of that string literal. */
var source =
'<script id="metadata_cache">/*\n' +
this.jsonifyObject(this.currentMetadata, '') + '\n' +
'*/<\/script>\n';
return source;
},
/**
* Add element containing metadata source code
*/
addSourceElement: function(event) {
var sourceWrapper = document.createElement('div');
sourceWrapper.setAttribute('id', 'metadata_source');
var instructions = document.createElement('p');
if (this.cachedMetadata) {
this.appendText(instructions,
'Replace the existing <script id="metadata_cache"> element ' +
'in the test\'s <head> with the following:');
}
else {
this.appendText(instructions,
'Copy the following into the <head> element of the test ' +
'or the test\'s metadata sidecar file:');
}
sourceWrapper.appendChild(instructions);
var sourceElement = document.createElement('pre');
this.appendText(sourceElement, this.generateSource());
sourceWrapper.appendChild(sourceElement);
var messageElement = document.getElementById('metadata_issue');
messageElement.parentNode.insertBefore(sourceWrapper,
messageElement.nextSibling);
messageElement.parentNode.removeChild(messageElement);
(event.preventDefault) ? event.preventDefault() :
event.returnValue = false;
},
/**
* Extract the metadata cache from the cache element if present
*/
getCachedMetadata: function() {
var cacheElement = document.getElementById('metadata_cache');
if (cacheElement) {
var cacheText = cacheElement.firstChild.nodeValue;
var openBrace = cacheText.indexOf('{');
var closeBrace = cacheText.lastIndexOf('}');
if ((-1 < openBrace) && (-1 < closeBrace)) {
cacheText = cacheText.slice(openBrace, closeBrace + 1);
try {
this.cachedMetadata = JSON.parse(cacheText);
}
catch (exc) {
this.cachedMetadata = 'Invalid JSON in Cached metadata. ';
}
}
else {
this.cachedMetadata = 'Metadata not found in cache element. ';
}
}
},
/**
* Main entry point, extract metadata from tests, compare to cached version
* if present.
* If cache not present or differs from extrated metadata, generate an error
*/
process: function(tests) {
for (var index = 0; index < tests.length; index++) {
var test = tests[index];
if (this.currentMetadata.hasOwnProperty(test.name)) {
this.error('Duplicate test name: ' + test.name);
}
else {
this.currentMetadata[test.name] = this.extractFromTest(test);
}
}
this.getCachedMetadata();
var message = null;
var messageClass = 'warning';
var showSource = false;
if (0 === tests.length) {
if (this.cachedMetadata) {
message = 'Cached metadata present but no tests. ';
}
}
else if (1 === tests.length) {
if (this.cachedMetadata) {
message = 'Single test files should not have cached metadata. ';
}
else {
var testMetadata = this.currentMetadata[tests[0].name];
for (var meta in testMetadata) {
if (testMetadata.hasOwnProperty(meta)) {
message = 'Single tests should not have metadata. ' +
'Move metadata to <head>. ';
break;
}
}
}
}
else {
if (this.cachedMetadata) {
messageClass = 'error';
if ('string' == typeof(this.cachedMetadata)) {
message = this.cachedMetadata;
showSource = true;
}
else if (! this.validateCache()) {
message = 'Cached metadata out of sync. ';
showSource = true;
}
}
}
if (message) {
var messageElement = document.createElement('p');
messageElement.setAttribute('id', 'metadata_issue');
messageElement.setAttribute('class', messageClass);
this.appendText(messageElement, message);
if (showSource) {
var link = document.createElement('a');
this.appendText(link, 'Click for source code.');
link.setAttribute('href', '#');
link.setAttribute('onclick',
'metadata_generator.addSourceElement(event)');
messageElement.appendChild(link);
}
var summary = document.getElementById('summary');
if (summary) {
summary.parentNode.insertBefore(messageElement, summary);
}
else {
var log = document.getElementById('log');
if (log) {
log.appendChild(messageElement);
}
}
}
},
setup: function() {
add_start_callback(
function (properties) {
if (window.testRunner) {
window.testRunner.waitUntilDone();
}
});
add_completion_callback(
function (tests, harness_status) {
metadata_generator.process(tests, harness_status);
dump_test_results(tests, harness_status);
});
}
};
function dump_test_results(tests, status) {
var results_element = document.createElement("script");
results_element.type = "text/json";
results_element.id = "__testharness__results__";
var test_results = tests.map(function(x) {
return {name:x.name, status:x.status, message:x.message, stack:x.stack}
});
data = {test:window.location.href,
tests:test_results,
status: status.status,
message: status.message,
stack: status.stack};
results_element.textContent = JSON.stringify(data);
document.documentElement.lastChild.appendChild(results_element);
if (window.testRunner) {
window.testRunner.notifyDone();
window.close();
}
}
metadata_generator.setup();
/* If the parent window has a testharness_properties object,
* we use this to provide the test settings. This is used by the
* default in-browser runner to configure the timeout and the
* rendering of results
*/
try {
if (window.opener && "testharness_properties" in window.opener) {
/* If we pass the testharness_properties object as-is here without
* JSON stringifying and reparsing it, IE fails & emits the message
* "Could not complete the operation due to error 80700019".
*/
setup(JSON.parse(JSON.stringify(window.opener.testharness_properties)));
}
} catch (e) {
}
// vim: set expandtab shiftwidth=4 tabstop=4: