diff --git a/package.json b/package.json index 183b8201997b..df8434f94099 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "oav": "^0.4.1", "js-yaml": "^3.8.2", "azure-storage": "^2.1.0", + "@microsoft.azure/literate":"^1.0.19", + "@microsoft.azure/async-io":"^1.0.21", + "@microsoft.azure/polyfill":"^1.0.17", "oad": "^0.1.6" }, "homepage": "https://github.com/azure/azure-rest-api-specs", diff --git a/test/linter.js b/test/linter.js index 46f081943df7..e652bc28d40b 100644 --- a/test/linter.js +++ b/test/linter.js @@ -4,23 +4,135 @@ 'use strict'; var execSync = require('child_process').execSync, - utils = require('./util/utils'); + utils = require('./util/utils'), + fs = require('fs'), + literate = require('@microsoft.azure/literate'), + async_io_1 = require("@microsoft.azure/async-io"), + path = require('path'); -describe('AutoRest Linter validation:', function () { - let configsToProcess = utils.getConfigFilesChangedInPR(); - // Useful when debugging a test for a particular swagger. - // Just update the regex. That will return an array of filtered items. - // configsToProcess = ['specification/sql/resource-manager/readme.md']; - for (const config of configsToProcess) { - it(config + ' should honor linter validation rules.', async function () { - var cmd = `autorest --validation --azure-validator ${config} --message-format=json`; - console.log(`Executing: ${cmd}`); - - try { - let result = execSync(cmd, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 64 }); - } catch (err) { - throw new Error('Linter validation contains error(s)'); - } +async function readConfigFile(file, tag) { + // autorest configuration type + const msgs = new literate.MessageEmitter(); + msgs.Message.Subscribe((src, each) => console.log(each.Text)); + const cfg = new literate.Configuration("\n> see https://aka.ms/autorest", new literate.DiskFileSystem("readme.md"), async_io_1.ResolveUri(async_io_1.CreateFolderUri(process.cwd()), async_io_1.CreateFileUri(path.resolve(file)) || "."), { + "input-file": [], + }); + return await cfg.CreateView(msgs, true, { tag: tag }); +} + +async function getTagsMapFromConfig(args) { + if (!fs.existsSync(path.resolve(args.file))) { + console.error('config file invalid. cannot read tags from file ' + config); + return null; + } + + const allTags = scanForTags(fs.readFileSync(args.file)); + const result = {}; + for (const each of allTags) { + result[each] = (await readConfigFile(args.file, each))["input-file"]; + } + return result; +} + +function scanForTags(content) { + const result = new Array(); + const rx = /\$\(tag\)(.*)/g; + let match = rx.exec(content); + while (match) { + const vrx = /['"](.*?)['"]/g; + let v = vrx.exec(match[1]); + if (v && v.length && result.indexOf(v[1]) == -1) { + result.push(v[1]); + } + match = rx.exec(content); + } + return result; +} + +async function getTagsFromConfig(config) { + // get hold of all tags and their corresponding input files using the literate config tool + const tagsMap = await getTagsMapFromConfig({ file: config }); + if (tagsMap === null) { + return null; + } + const tags = Object.keys(tagsMap); + + // filter the tags + if (utils.prOnly) { + // get path to the modified files relative to their corresponding md file, need to do this since + // config files have relative paths to the input files + let allModifiedFiles = utils.getFilesChangedInPR(); + allModifiedFiles = allModifiedFiles.map(mfile => { + return mfile.replace(path.dirname(config) + '/', ''); }); + + // for each tag->files, find if there are any modified files and select those tags + return tags.filter(tag => { + + const tagFiles = (String(tagsMap[tag])).split(','); + // find intersection with the modified files + return tagFiles.filter(tagFile => { + return allModifiedFiles.indexOf(tagFile) > -1; + }).length > 0; + }); + } + return tags; +} + +function execLinterCommand(args) { + var cmd = `autorest --validation --azure-validator --message-format=json ${args}`.trim(); + console.log(`Executing: ${cmd}`); + try { + let result = execSync(cmd, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 64 }); + } catch (err) { + throw new Error('Linter validation contains error(s)'); + } + +} + +describe('AutoRest Linter validation:', function () { + if (utils.prOnly) { + // Useful when debugging a test for a particular swagger. + // Just update the regex. That will return an array of filtered items. + // configsToProcess = ['specification/sql/resource-manager/readme.md']; + let configsToProcess = utils.getConfigFilesChangedInPR(); + for (const config of configsToProcess) { + it(config + ' should honor linter validation rules.', async function () { + + // find all tags in the config file + const tagsToProcess = await getTagsFromConfig(config); + // if no tags found to process, run with the defaults + if (tagsToProcess === null || tagsToProcess.length === 0) { + // no tags found + // this means we need to run validator against the individual + // json files included in the PR + // but in the same directory tree as the config file + const filesChangedInPR = utils.getFilesChangedInPR(); + const configDir = path.dirname(config); + filesChangedInPR.filter(prfile => { + // set any type to string + prFile += ''; + return prFile.startsWith(configDir) && prfile.indexOf('examples') === -1 && prFile.endsWith('.json'); + }).forEach(prFileInConfigFile => { + console.warn(`Configuration file not found for file: ${prFileInConfigFile}, running validation rules against it in individual context.`); + execLinterCommand(`--input-file=${prFileInConfigFile}`); + }); + } + else { + // if tags found, run linter against every single tag + tagsToProcess.forEach((tagToProcess) => { + execLinterCommand(`${config} --tag=${tagToProcess}`); + }, this); + } + }); + } + } + else { + // we are not handling pr_only=false case today, + // to enable, we need to write logic to calculate + // all config files in the repo and run linter with + // every tag in the md file; we can get tags each + // config file from getTagsFromCinfig file + console.warn('Cannot run linter in pr_only false mode'); } });