[doclint] move doclint testing to golden

This patch:
- move doclint testing to golden
- refactor doclint/lint.js to drop specs and everything
This commit is contained in:
Andrey Lushnikov 2017-07-13 01:25:32 -07:00
parent 9b2e9ce6ca
commit c468c451c5
33 changed files with 190 additions and 261 deletions

1
utils/doclint/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
output/

View File

@ -1,4 +1,3 @@
const {describe, it, fail, runSpecs} = require('./specRunner');
const path = require('path');
const jsBuilder = require('./JSBuilder');
const mdBuilder = require('./MDBuilder');
@ -29,54 +28,54 @@ let EXCLUDE_METHODS = new Set([
'Response.constructor',
]);
const browser = new Browser({args: ['--no-sandbox']});
browser.newPage()
.then(initializeSpecs)
.then(runSpecs)
.catch(console.error)
.then(() => browser.close());
async function initializeSpecs(page) {
let jsResult = await jsBuilder(path.join(PROJECT_DIR, 'lib'));
let mdResult = await mdBuilder(page, path.join(PROJECT_DIR, 'docs'));
/**
* @param {!Page} page
* @param {string} docsFolderPath
* @param {string} jsFolderPath
* @return {!Promise<!Array<string>>}
*/
async function lint(page, docsFolderPath, jsFolderPath) {
let mdResult = await mdBuilder(page, docsFolderPath);
let jsResult = await jsBuilder(jsFolderPath);
let jsDocumentation = filterJSDocumentation(jsResult);
let mdDocumentation = mdResult.documentation;
let diff = Documentation.diff(mdDocumentation, jsDocumentation);
describe('JavaScript documentation parser', function() {
it('should not contain any duplicate classes (probably error in parsing!)', () => {
let jsErrors = [];
let mdErrors = [];
// Report all markdown parse errors.
mdErrors.push(...mdResult.errors);
{
// Report duplicate JavaScript classes.
let jsClasses = new Map();
for (let jsClass of jsDocumentation.classesArray) {
if (jsClasses.has(jsClass.name))
fail(`JavaScript has duplicate declaration of ${jsClass.name}. (This probably means that this linter has an error)`);
jsErrors.push(`Duplicate declaration of class ${jsClass.name}`);
jsClasses.set(jsClass.name, jsClass);
}
});
});
describe('Markdown Documentation', function() {
it('should not have any parse errors', () => {
for (let error of mdResult.errors)
fail(error);
});
it('should not contain any duplicate classes', () => {
}
{
// Report duplicate MarkDown classes.
let mdClasses = new Map();
for (let mdClass of mdDocumentation.classesArray) {
if (mdClasses.has(mdClass.name))
fail(`Documentation has duplicate declaration of class ${mdClass.name}`);
mdErrors.push(`Duplicate declaration of class ${mdClass.name}`);
mdClasses.set(mdClass.name, mdClass);
}
});
it('class constructors should be defined before other methods', () => {
}
{
// Make sure class constructors are defined before other methods.
for (let mdClass of mdDocumentation.classesArray) {
let constructorMethod = mdClass.methods.get('constructor');
if (!constructorMethod)
continue;
if (mdClass.methodsArray[0] !== constructorMethod)
fail(`Method 'new ${mdClass.name}' should go before other methods of class ${mdClass.name}`);
mdErrors.push(`Constructor of ${mdClass.name} should go before other methods`);
}
});
it('methods should be sorted alphabetically', () => {
}
{
// Methods should be sorted alphabetically.
for (let mdClass of mdDocumentation.classesArray) {
for (let i = 0; i < mdClass.methodsArray.length - 1; ++i) {
// Constructor should always go first.
@ -85,45 +84,32 @@ async function initializeSpecs(page) {
let method1 = mdClass.methodsArray[i];
let method2 = mdClass.methodsArray[i + 1];
if (method1.name > method2.name)
fail(`${mdClass.name}.${method1.name} breaks alphabetic sorting inside class ${mdClass.name}`);
mdErrors.push(`${mdClass.name}.${method1.name} breaks alphabetic sorting inside class ${mdClass.name}`);
}
}
});
it('should not contain any non-existing class', () => {
for (let className of diff.extraClasses)
fail(`Documentation describes non-existing class ${className}`);
});
it('should describe all existing classes', () => {
for (let className of diff.missingClasses)
fail(`Documentation lacks description of class ${className}`);
});
it('should not contain any non-existing methods', () => {
for (let methodName of diff.extraMethods)
fail(`Documentation describes non-existing method: ${methodName}`);
});
it('should describe all existing methods', () => {
for (let methodName of diff.missingMethods)
fail(`Documentation lacks method ${methodName}`);
});
it('should describe all arguments propertly', () => {
}
// Report non-existing and missing classes.
mdErrors.push(...diff.extraClasses.map(className => `Non-existing class found: ${className}`));
mdErrors.push(...diff.missingClasses.map(className => `Class not found: ${className}`));
mdErrors.push(...diff.extraMethods.map(methodName => `Non-existing method found: ${methodName}`));
mdErrors.push(...diff.missingMethods.map(methodName => `Method not found: ${methodName}`));
mdErrors.push(...diff.extraProperties.map(propertyName => `Non-existing property found: ${propertyName}`));
mdErrors.push(...diff.missingProperties.map(propertyName => `Property not found: ${propertyName}`));
{
// Report badly described arguments.
for (let badArgument of diff.badArguments) {
let text = [`Method ${badArgument.method} fails to describe its parameters:`];
for (let missing of badArgument.missingArgs)
text.push(`- Missing description for "${missing}"`);
for (let extra of badArgument.extraArgs)
text.push(`- Described non-existing parameter "${extra}"`);
fail(text.join('\n'));
mdErrors.push(text.join('\n'));
}
});
it('should not contain any non-existing properties', () => {
for (let propertyName of diff.extraProperties)
fail(`Documentation describes non-existing property: ${propertyName}`);
});
it('should describe all existing properties', () => {
for (let propertyName of diff.missingProperties)
fail(`Documentation lacks property ${propertyName}`);
});
});
}
// Push all errors with proper prefixes
let errors = jsErrors.map(error => '[JavaScript] ' + error);
errors.push(...mdErrors.map(error => '[MarkDown] ' + error));
return errors;
}
/**
@ -146,3 +132,30 @@ function filterJSDocumentation(jsDocumentation) {
}
return new Documentation(classes);
}
module.exports = lint;
const RED_COLOR = '\x1b[31m';
const RESET_COLOR = '\x1b[0m';
// Handle CLI invocation.
if (!module.parent) {
const startTime = Date.now();
const browser = new Browser({args: ['--no-sandbox']});
browser.newPage().then(async page => {
const errors = await lint(page, path.join(PROJECT_DIR, 'docs'), path.join(PROJECT_DIR, 'lib'));
await browser.close();
if (errors.length) {
console.log('Documentation Failures:');
for (let i = 0; i < errors.length; ++i) {
let error = errors[i];
error = error.split('\n').join('\n ');
console.log(`${i + 1}) ${RED_COLOR}${error}${RESET_COLOR}`);
}
}
console.log(`${errors.length} failures`);
const runningTime = Date.now() - startTime;
console.log(`Finished in ${runningTime / 1000} seconds`);
process.exit(errors.length > 0 ? 1 : 0);
});
}

View File

@ -1,85 +0,0 @@
const startTime = Date.now();
let allTests = [];
let titles = [];
let currentTest = null;
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
};
/**
* @param {string} title
* @param {function()} fun
*/
function describe(title, fun) {
titles.push(title);
fun();
titles.pop();
}
/**
* @param {string} title
* @param {function()} fun
*/
function it(title, fun) {
titles.push(title);
allTests.push({
errors: [],
title: titles.join(' '),
fun,
});
titles.pop();
}
/**
* @param {string} msg
*/
function fail(msg) {
currentTest.errors.push(msg);
}
function runSpecs() {
console.log('Started\n');
for (currentTest of allTests) {
currentTest.fun();
if (currentTest.errors.length)
process.stdout.write(`${colors.red}F${colors.reset}`);
else
process.stdout.write(`${colors.green}.${colors.reset}`);
}
console.log('\n');
reportErrors();
}
function reportErrors() {
let failedTests = allTests.filter(test => !!test.errors.length);
if (failedTests.length) {
console.log('Failures:');
for (let i = 0; i < failedTests.length; ++i) {
let test = failedTests[i];
console.log(`${i + 1}) ${test.title}`);
console.log(` Messages:`);
for (let error of test.errors) {
error = error.split('\n').join('\n ');
console.log(' * ' + colors.red + error + colors.reset);
}
}
console.log('');
}
console.log(`Ran ${allTests.length} specs`);
console.log(`${allTests.length} specs, ${failedTests.length} failures`);
const runningTime = Date.now() - startTime;
console.log(`Finished in ${runningTime / 1000} seconds`);
process.exit(failedTests.length > 0 ? 1 : 0);
}
module.exports = {
describe,
it,
fail,
runSpecs,
};

View File

@ -0,0 +1,3 @@
[MarkDown] Non-existing class found: Bar
[MarkDown] Non-existing class found: Baz
[MarkDown] Class not found: Other

View File

@ -1,3 +1,5 @@
### class: Foo
### class: Bar
### class: Baz

View File

@ -0,0 +1,2 @@
class Other {
}

View File

@ -0,0 +1,6 @@
### class: Foo
#### foo.proceed()
#### foo.start()

View File

@ -0,0 +1,7 @@
class Foo {
start() {
}
stop() {
}
}

View File

@ -1,2 +0,0 @@
### class: Foo

View File

@ -1,4 +0,0 @@
class Foo {
bar() {
}
}

View File

@ -0,0 +1,5 @@
### class: Foo
#### foo.a
#### foo.c

View File

@ -0,0 +1,7 @@
class Foo {
constructor() {
this.a = 42;
this.b = 'hello';
this.emit('done');
}
}

View File

@ -0,0 +1,7 @@
### class: Foo
#### new Foo(arg1, arg2)
- `arg1` <[string]>
- `arg2` <[string]>
#### foo.test(fileNames)
- `filePaths` <[Array]>

View File

@ -0,0 +1,7 @@
class Foo {
constructor(arg1, arg3) {
}
test(filePaths) {
}
}

View File

@ -1,4 +0,0 @@
### class: Foo
#### foo.bar()

View File

@ -1,2 +0,0 @@
class Foo {
}

View File

@ -1,3 +0,0 @@
### class: Foo
#### new Foo()

View File

@ -1,5 +0,0 @@
class Foo {
constructor() {
this.barProperty = 42;
}
}

View File

@ -1,3 +0,0 @@
### class: Foo
#### foo.bazProperty

View File

@ -1,4 +0,0 @@
class Foo {
constructor() {
}
}

View File

@ -1,4 +0,0 @@
### class: Foo
#### new Foo(arg2)
- `arg2` <[string]> Some arg.

View File

@ -1,4 +0,0 @@
class Foo {
constructor(arg1) {
}
}

View File

@ -1,2 +0,0 @@
class Foo {
}

View File

@ -0,0 +1,3 @@
[MarkDown] Non-existing class found: Bar
[MarkDown] Non-existing class found: Baz
[MarkDown] Class not found: Other

View File

@ -0,0 +1,2 @@
[MarkDown] Non-existing method found: Foo.proceed
[MarkDown] Method not found: Foo.stop

View File

@ -0,0 +1,3 @@
[MarkDown] Method not found: Foo.constructor
[MarkDown] Non-existing property found: Foo.c
[MarkDown] Property not found: Foo.b

View File

@ -0,0 +1,4 @@
[MarkDown] Heading arguments for "foo.test(fileNames)" do not match described ones, i.e. "fileNames" != "filePaths"
[MarkDown] Method Foo.constructor fails to describe its parameters:
- Missing description for "arg3"
- Described non-existing parameter "arg2"

View File

@ -0,0 +1 @@
[MarkDown] Markdown TOC is outdated, run `yarn generate-toc`

View File

@ -1,69 +1,47 @@
const fs = require('fs');
const rm = require('rimraf').sync;
const path = require('path');
const jsBuilder = require('../JSBuilder');
const mdBuilder = require('../MDBuilder');
const Documentation = require('../Documentation');
const Browser = require('../../../lib/Browser');
const doclint = require('../lint');
const GoldenUtils = require('../../../test/golden-utils');
const OUTPUT_DIR = path.join(__dirname, 'output');
const GOLDEN_DIR = path.join(__dirname, 'golden');
const browser = new Browser({args: ['--no-sandbox']});
let page;
let specName;
jasmine.getEnv().addReporter({
specStarted: result => specName = result.description
});
beforeAll(SX(async function() {
page = await browser.newPage();
if (fs.existsSync(OUTPUT_DIR))
rm(OUTPUT_DIR);
}));
afterAll(SX(async function() {
await browser.close();
}));
describe('doclint', function() {
test('01-missing-class', diff => {
expect(diff.missingClasses.length).toBe(1);
expect(diff.missingClasses[0]).toBe('Foo');
});
test('02-extra-class', diff => {
expect(diff.extraClasses.length).toBe(1);
expect(diff.extraClasses[0]).toBe('Bar');
});
test('03-missing-method', diff => {
expect(diff.missingMethods.length).toBe(1);
expect(diff.missingMethods[0]).toBe('Foo.bar');
});
test('04-extra-method', diff => {
expect(diff.extraMethods.length).toBe(1);
expect(diff.extraMethods[0]).toBe('Foo.bar');
});
test('05-missing-property', diff => {
expect(diff.missingProperties.length).toBe(1);
expect(diff.missingProperties[0]).toBe('Foo.barProperty');
});
test('06-extra-property', diff => {
expect(diff.extraProperties.length).toBe(1);
expect(diff.extraProperties[0]).toBe('Foo.bazProperty');
});
test('07-bad-arguments', diff => {
expect(diff.badArguments.length).toBe(1);
expect(diff.badArguments[0]).toEqual({
method: 'Foo.constructor',
missingArgs: ['arg1'],
extraArgs: ['arg2']
});
});
test('08-outdated-table-of-contents', (diff, mdErrors) => {
expect(mdErrors.length).toBe(1);
expect(mdErrors[0]).toBe('Markdown TOC is outdated, run `yarn generate-toc`');
});
beforeEach(function() {
GoldenUtils.addMatchers(jasmine, GOLDEN_DIR, OUTPUT_DIR);
});
async function test(folderName, func) {
it(folderName, SX(async () => {
const [jsResult, mdResult] = await Promise.all([
jsBuilder(path.join(__dirname, folderName)),
mdBuilder(page, path.join(__dirname, folderName))
]);
const jsDocumentation = jsResult;
const mdDocumentation = mdResult.documentation;
func(Documentation.diff(mdDocumentation, jsDocumentation), mdResult.errors);
}));
describe('doclint', function() {
it('01-class-errors', SX(test));
it('02-method-errors', SX(test));
it('03-property-errors', SX(test));
it('04-bad-arguments', SX(test));
it('05-outdated-toc', SX(test));
});
async function test() {
const filePath = path.join(__dirname, specName);
const errors = await doclint(page, filePath, filePath);
expect(errors.join('\n')).toBeGolden(specName + '.txt');
}
// Since Jasmine doesn't like async functions, they should be wrapped