jones-test/lib/DocsTest.js (203 lines of code) (raw):

/* Copyright (c) 2012, 2015 Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ "use strict"; var fs = require("fs"), path = require("path"), Test = require("./Test"), unified_debug = require("unified_debug"), udebug = unified_debug.getLogger("DocsTest.js"); function DocumentedFunction(className, functionName) { this.className = className; this.functionName = functionName; } /* Extract code from markdown. Only extract code that is delimited above and below by ``` */ function extractCodeFromMarkdown(text) { var result = ""; var lines = text.split("\n"); var isCodeBlock = false; var i, line; for(i = 0; i < lines.length ; i++) { line = lines[i]; if(isCodeBlock) { result += line; } if(line.match(/^```/)) { isCodeBlock = ! isCodeBlock; } } return result; } /* Returns a list of function definitions from JavaScript code */ function scan(text) { var i = 0; // the index of the current character var c = text.charAt(i); // the current character var list = []; // functions found in the file var constructor = 0; // constructor function found in file var tok; // the current token function isUpper(c) { return (c >= 'A' && c <= 'Z'); } function isLower(c) { return (c >= 'a' && c <= 'z'); } function isAlpha(c) { return (isUpper(c) || isLower(c)); } function isNumeric(c) { return (c >= '0' && c <= '9'); } function isJsFunctionName(c) { return( isAlpha(c) || isNumeric(c) || (c == '_')); } function peek() { return text.charAt(i + 1); } function advance(n) { // Advance to next character var amt = n || 1; if(i + amt >= text.length) { i = text.length; c = ''; } else { i += amt; c = text.charAt(i); } } function Token() { this.str = c; advance(); } Token.prototype.consume = function() { this.str += c; advance(); }; Token.prototype.commit = function() { var docFunction; if(isUpper(this.str.charAt(0))) { constructor = this.str; } else { docFunction = new DocumentedFunction(constructor, this.str); list.push(docFunction); } }; // Start scanning while(c) { while(c != '' && c <= ' ') { advance(); } // whitespace if(c == '/' && peek() == '/') { // comment to EOL advance(2); while(c !== '\n' && c !== '\r' && c !== '') { advance(); } } else if (c === '/' && peek() === '*') { // comment to */ advance(2); while(! (c == '*' && peek() == '/')) { advance(); } if(c === '') { throw new Error("Unterminated comment"); } advance(2); } else if(isAlpha(c)) { // candidate functions tok = new Token(); while(isJsFunctionName(c)) { tok.consume(); } if(c == '(') { // IT WAS A FUNCTION tok.commit(); advance(); /* Now, there may be more functions (callbacks) defined as arguments, so we skip to the next semicolon */ while(c && c !== ';') { advance(); } } // delete tok; } else { advance(); } } return list; } /// PUBLIC API: function DocsTest(docFileName) { this.phase = 1; // ConcurrentTest this.name = "Documentation: " + path.basename(docFileName); this.docFileName = docFileName; this.testClassMap = {}; this.undocMap = {}; this.hasTests = false; this.isMarkdown = (docFileName.match(/\.md$/)); this.topLevel = false; } DocsTest.prototype = new Test.Test(); DocsTest.prototype.fullName = function() { return this.suite.name + " " + this.name; }; DocsTest.prototype.addTestObject = function(testObject, className, undocFlag) { this.hasTests = true; if(className === undefined) { className = 0; this.topLevel = true; } this.testClassMap[className] = testObject; if(undocFlag) { this.undocMap[className] = testObject; } }; DocsTest.prototype.testObjectsVsFunctionList = function(functionList) { var docFunction, testObject, func, name, msg, _class; var verified = {}; var missing = 0; var firstMissing = null; var i; function verify(docFunc) { if(! verified[docFunc.className]) { verified[docFunc.className] = {}; } verified[docFunc.className][docFunc.functionName] = true; udebug.log_detail("verified %s.%s", docFunc.className, docFunc.functionName); } /* Iterate the functions found in the documentation. For each one, try to find a corresponding member of the testObject. */ for(i = 0 ; i < functionList.length ; i++) { docFunction = functionList[i]; if(this.topLevel) { testObject = this.testClassMap[0]; } else { testObject = this.testClassMap[docFunction.className]; } if(testObject) { func = testObject[docFunction.functionName]; if(typeof func === 'function') { verify(docFunction); } else { udebug.log_detail("Missing", docFunction); if(! firstMissing) { firstMissing = name; } missing += 1; } } else { udebug.log("No %s in object", name); missing += 1; } } if(missing) { msg = "Missing " + firstMissing; if(missing > 1) { msg += " and " + (missing-1) + " other function"; } if(missing > 2) { msg += "s"; } this.appendErrorMessage(msg); } // Test undocumented functions for(_class in this.undocMap) { if(this.undocMap.hasOwnProperty(_class)) { testObject = this.undocMap[_class]; for(name in testObject) { if(testObject.hasOwnProperty(name)) { if(typeof testObject[name] === 'function') { if((! verified[_class]) || (!verified[_class][name])) { this.appendErrorMessage(_class + "." + name + " undocumented"); } } } } } } }; DocsTest.prototype.runDocsTest = function() { var file = path.resolve(this.suite.driver.baseDirectory, this.docFileName); var text = fs.readFileSync(file, 'utf8'); if(this.isMarkdown){ text = extractCodeFromMarkdown(text); } var functionList = scan(text); if(this.hasTests) { this.testObjectsVsFunctionList(functionList); } else { this.appendErrorMessage("Use DocsTest.addTestObject to supply a testObject"); } return true; // Synchronous tests return true from run() }; // Run can be overriden by a particular test if needed; // the test should call runDocsTest() DocsTest.prototype.run = function() { return this.runDocsTest(); }; module.exports = DocsTest;