[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:
parent
9b2e9ce6ca
commit
c468c451c5
1
utils/doclint/.gitignore
vendored
Normal file
1
utils/doclint/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
output/
|
@ -1,4 +1,3 @@
|
||||
const {describe, it, fail, runSpecs} = require('./specRunner');
|
||||
const path = require('path');
|
||||
const jsBuilder = require('./JSBuilder');
|
||||
const mdBuilder = require('./MDBuilder');
|
||||
@ -29,101 +28,88 @@ 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 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)`);
|
||||
jsClasses.set(jsClass.name, jsClass);
|
||||
}
|
||||
});
|
||||
});
|
||||
let jsErrors = [];
|
||||
let mdErrors = [];
|
||||
|
||||
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', () => {
|
||||
let mdClasses = new Map();
|
||||
for (let mdClass of mdDocumentation.classesArray) {
|
||||
if (mdClasses.has(mdClass.name))
|
||||
fail(`Documentation has duplicate declaration of class ${mdClass.name}`);
|
||||
mdClasses.set(mdClass.name, mdClass);
|
||||
}
|
||||
});
|
||||
it('class constructors should be defined before other methods', () => {
|
||||
for (let mdClass of mdDocumentation.classesArray) {
|
||||
let constructorMethod = mdClass.methods.get('constructor');
|
||||
if (!constructorMethod)
|
||||
// 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))
|
||||
jsErrors.push(`Duplicate declaration of class ${jsClass.name}`);
|
||||
jsClasses.set(jsClass.name, jsClass);
|
||||
}
|
||||
}
|
||||
{
|
||||
// Report duplicate MarkDown classes.
|
||||
let mdClasses = new Map();
|
||||
for (let mdClass of mdDocumentation.classesArray) {
|
||||
if (mdClasses.has(mdClass.name))
|
||||
mdErrors.push(`Duplicate declaration of class ${mdClass.name}`);
|
||||
mdClasses.set(mdClass.name, mdClass);
|
||||
}
|
||||
}
|
||||
{
|
||||
// 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)
|
||||
mdErrors.push(`Constructor of ${mdClass.name} should go before other methods`);
|
||||
}
|
||||
}
|
||||
{
|
||||
// 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.
|
||||
if (mdClass.methodsArray[i].name === 'constructor')
|
||||
continue;
|
||||
if (mdClass.methodsArray[0] !== constructorMethod)
|
||||
fail(`Method 'new ${mdClass.name}' should go before other methods of class ${mdClass.name}`);
|
||||
let method1 = mdClass.methodsArray[i];
|
||||
let method2 = mdClass.methodsArray[i + 1];
|
||||
if (method1.name > method2.name)
|
||||
mdErrors.push(`${mdClass.name}.${method1.name} breaks alphabetic sorting inside class ${mdClass.name}`);
|
||||
}
|
||||
});
|
||||
it('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.
|
||||
if (mdClass.methodsArray[i].name === 'constructor')
|
||||
continue;
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
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', () => {
|
||||
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'));
|
||||
}
|
||||
});
|
||||
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}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
// 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}"`);
|
||||
mdErrors.push(text.join('\n'));
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
3
utils/doclint/test/01-class-errors/01-class-errors.txt
Normal file
3
utils/doclint/test/01-class-errors/01-class-errors.txt
Normal file
@ -0,0 +1,3 @@
|
||||
[MarkDown] Non-existing class found: Bar
|
||||
[MarkDown] Non-existing class found: Baz
|
||||
[MarkDown] Class not found: Other
|
@ -1,3 +1,5 @@
|
||||
### class: Foo
|
||||
|
||||
### class: Bar
|
||||
|
||||
### class: Baz
|
2
utils/doclint/test/01-class-errors/other.js
Normal file
2
utils/doclint/test/01-class-errors/other.js
Normal file
@ -0,0 +1,2 @@
|
||||
class Other {
|
||||
}
|
6
utils/doclint/test/02-method-errors/doc.md
Normal file
6
utils/doclint/test/02-method-errors/doc.md
Normal file
@ -0,0 +1,6 @@
|
||||
### class: Foo
|
||||
|
||||
#### foo.proceed()
|
||||
|
||||
#### foo.start()
|
||||
|
7
utils/doclint/test/02-method-errors/foo.js
Normal file
7
utils/doclint/test/02-method-errors/foo.js
Normal file
@ -0,0 +1,7 @@
|
||||
class Foo {
|
||||
start() {
|
||||
}
|
||||
|
||||
stop() {
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
### class: Foo
|
||||
|
@ -1,4 +0,0 @@
|
||||
class Foo {
|
||||
bar() {
|
||||
}
|
||||
}
|
5
utils/doclint/test/03-property-errors/doc.md
Normal file
5
utils/doclint/test/03-property-errors/doc.md
Normal file
@ -0,0 +1,5 @@
|
||||
### class: Foo
|
||||
|
||||
#### foo.a
|
||||
|
||||
#### foo.c
|
7
utils/doclint/test/03-property-errors/foo.js
Normal file
7
utils/doclint/test/03-property-errors/foo.js
Normal file
@ -0,0 +1,7 @@
|
||||
class Foo {
|
||||
constructor() {
|
||||
this.a = 42;
|
||||
this.b = 'hello';
|
||||
this.emit('done');
|
||||
}
|
||||
}
|
7
utils/doclint/test/04-bad-arguments/doc.md
Normal file
7
utils/doclint/test/04-bad-arguments/doc.md
Normal file
@ -0,0 +1,7 @@
|
||||
### class: Foo
|
||||
#### new Foo(arg1, arg2)
|
||||
- `arg1` <[string]>
|
||||
- `arg2` <[string]>
|
||||
|
||||
#### foo.test(fileNames)
|
||||
- `filePaths` <[Array]>
|
7
utils/doclint/test/04-bad-arguments/foo.js
Normal file
7
utils/doclint/test/04-bad-arguments/foo.js
Normal file
@ -0,0 +1,7 @@
|
||||
class Foo {
|
||||
constructor(arg1, arg3) {
|
||||
}
|
||||
|
||||
test(filePaths) {
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
### class: Foo
|
||||
|
||||
#### foo.bar()
|
||||
|
@ -1,2 +0,0 @@
|
||||
class Foo {
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
### class: Foo
|
||||
|
||||
#### new Foo()
|
@ -1,5 +0,0 @@
|
||||
class Foo {
|
||||
constructor() {
|
||||
this.barProperty = 42;
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
### class: Foo
|
||||
|
||||
#### foo.bazProperty
|
@ -1,4 +0,0 @@
|
||||
class Foo {
|
||||
constructor() {
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
### class: Foo
|
||||
|
||||
#### new Foo(arg2)
|
||||
- `arg2` <[string]> Some arg.
|
@ -1,4 +0,0 @@
|
||||
class Foo {
|
||||
constructor(arg1) {
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
class Foo {
|
||||
}
|
3
utils/doclint/test/golden/01-class-errors.txt
Normal file
3
utils/doclint/test/golden/01-class-errors.txt
Normal file
@ -0,0 +1,3 @@
|
||||
[MarkDown] Non-existing class found: Bar
|
||||
[MarkDown] Non-existing class found: Baz
|
||||
[MarkDown] Class not found: Other
|
2
utils/doclint/test/golden/02-method-errors.txt
Normal file
2
utils/doclint/test/golden/02-method-errors.txt
Normal file
@ -0,0 +1,2 @@
|
||||
[MarkDown] Non-existing method found: Foo.proceed
|
||||
[MarkDown] Method not found: Foo.stop
|
3
utils/doclint/test/golden/03-property-errors.txt
Normal file
3
utils/doclint/test/golden/03-property-errors.txt
Normal file
@ -0,0 +1,3 @@
|
||||
[MarkDown] Method not found: Foo.constructor
|
||||
[MarkDown] Non-existing property found: Foo.c
|
||||
[MarkDown] Property not found: Foo.b
|
4
utils/doclint/test/golden/04-bad-arguments.txt
Normal file
4
utils/doclint/test/golden/04-bad-arguments.txt
Normal 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"
|
1
utils/doclint/test/golden/05-outdated-toc.txt
Normal file
1
utils/doclint/test/golden/05-outdated-toc.txt
Normal file
@ -0,0 +1 @@
|
||||
[MarkDown] Markdown TOC is outdated, run `yarn generate-toc`
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user