Skip to content

Commit

Permalink
feat: add command line interface (#94)
Browse files Browse the repository at this point in the history
Adds an `axe-sarif-converter` CLI tool to wrap the library and associated acceptance tests. Usage:

```
> axe-sarif-converter --help
axe-sarif-converter: Converts JSON files containing axe-core Result object(s)
into SARIF files

Options:
  --help             Show help                                         [boolean]
  --version          Show version number                               [boolean]
  --input-files, -i  Input JSON file(s) containing axe-core Result object(s).
                     Does not support globs. Each input file may consist of
                     either a single root-level axe-core Results object or a
                     root-level array of axe-core Results objects.
                                                              [array] [required]
  --output-file, -o  Output SARIF file. Multiple input files (or input files
                     containing multiple Result objects) will be combined into
                     one output file with a SARIF Run per axe-core Result.
                                                             [string] [required]
  --verbose, -v      Enables verbose console output.  [boolean] [default: false]
  --pretty, -p       Includes line breaks and indentation in the output.
                                                      [boolean] [default: false]
  --force, -f        Overwrites the output file if it already exists.
                                                      [boolean] [default: false]

Examples:
  axe-sarif-converter -i axe-results.json -o axe-results.sarif
```
  • Loading branch information
dbjorge authored Sep 9, 2019
1 parent 6ef4df9 commit 892ed0b
Show file tree
Hide file tree
Showing 9 changed files with 11,162 additions and 7 deletions.
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Licensed under the MIT License.
[![npm](https://img.shields.io/npm/v/axe-sarif-converter.svg)](https://www.npmjs.com/package/axe-sarif-converter)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)

Convert [axe-core](https://github.com/dequelabs/axe-core) accessibility scan results to the [SARIF format](http://sarifweb.azurewebsites.net/).
Convert [axe-core](https://github.com/dequelabs/axe-core) accessibility scan results to the [SARIF format](http://sarifweb.azurewebsites.net/). Provides both a TypeScript API and a CLI tool.

Use this with the [Sarif Viewer Build Tab Azure DevOps Extension](https://marketplace.visualstudio.com/items?itemName=sariftools.sarif-viewer-build-tab) to visualize accessibility scan results in the build results of an [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) build.

Expand Down Expand Up @@ -46,6 +46,18 @@ test('my accessibility test', async () => {
}
```
You can also use axe-sarif-converter as a command line tool:
```bash
# axe-cli is used here for example purposes only; you could also run axe-core
# using your library of choice and JSON.stringify the results.
npx axe-cli https://accessibilityinsights.io --save ./sample-axe-results.json

npx axe-sarif-converter --input-files ./sample-axe-results.json --output-file ./sample-axe-results.sarif
```
See `npx axe-sarif-converter --help` for full command line option details.
## Samples
The [microsoft/axe-pipelines-samples](https://github.com/microsoft/axe-pipelines-samples) project contains full sample code that walks you through integrating this library into your project, from writing a test to seeing results in Azure Pipelines.
Expand All @@ -61,6 +73,26 @@ Note that the SARIF format _does not use semantic versioning_, and there are bre
## Contributing
To get started working on the project:
1. Install dependencies:
- Install [Node.js](https://nodejs.org/en/download/) (LTS version)
- `npm install -g yarn`
- `yarn install`
1. Run all build, lint, and test steps:
- `yarn precheckin`
1. Run the CLI tool with your changes:
- `yarn build`
- `node dist/cli.js`
- Alternately, register a linked global `axe-sarif-converter` command with `npm install && npm link` (yarn doesn't work for this; see [yarnpkg/yarn#1585](https://github.com/yarnpkg/yarn/issues/1585))
### Contributor License Agreement
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
Expand All @@ -69,6 +101,8 @@ When you submit a pull request, a CLA-bot will automatically determine whether y
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
### Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [[email protected]](mailto:[email protected]) with any additional questions or comments.
6 changes: 3 additions & 3 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- script: yarn copyrightheaders
displayName: yarn copyrightheaders

- script: yarn build
displayName: yarn build

- script: yarn test -- --ci
displayName: yarn test

Expand All @@ -48,9 +51,6 @@ jobs:
condition: always()
displayName: publish sarif results

- script: yarn build
displayName: yarn build

- script: yarn semantic-release
displayName: yarn semantic-release (master branch only)
env:
Expand Down
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ module.exports = {
moduleFileExtensions: ['ts', 'js'],
rootDir: rootDir,
collectCoverage: true,
collectCoverageFrom: ['./**/*.ts', '!./**/*.test.ts'],
collectCoverageFrom: [
'<rootDir>/**/*.ts',
'!<rootDir>/**/*.test.ts',
// The CLI is tested via integration tests that spawn separate node
// processes, so coverage information on this file isn't accurate
'!<rootDir>/cli.ts',
],
coverageReporters: ['json', 'lcov', 'text', 'cobertura'],
testMatch: [`${currentDir}/**/*.test.(ts|js)`],
reporters: [
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.0.0-managed-by-semantic-release",
"description": "Convert axe-core accessibility scan results to the SARIF format",
"main": "dist/index.js",
"bin": "dist/cli.js",
"types": "dist/index.d.js",
"files": [
"dist/",
Expand All @@ -14,12 +15,14 @@
},
"dependencies": {
"@types/sarif": ">=2.1.1 <=2.1.2",
"axe-core": "^3.2.2"
"axe-core": "^3.2.2",
"yargs": "^14.0.0"
},
"devDependencies": {
"@types/jest": "^24.0.15",
"@types/lodash": "^4.14.136",
"@types/node": "^12.6.8",
"@types/yargs": "^13.0.2",
"jest": "^24.8.0",
"jest-circus": "^24.8.0",
"jest-junit": "^8.0.0",
Expand Down
28 changes: 28 additions & 0 deletions src/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`axe-sarif-converter CLI prints help info with --help: help_stdout 1`] = `
"axe-sarif-converter: Converts JSON files containing axe-core Result object(s)
into SARIF files
Options:
--help Show help [boolean]
--version Show version number [boolean]
--input-files, -i Input JSON file(s) containing axe-core Result object(s).
Does not support globs. Each input file may consist of
either a single root-level axe-core Results object or a
root-level array of axe-core Results objects.
[array] [required]
--output-file, -o Output SARIF file. Multiple input files (or input files
containing multiple Result objects) will be combined into
one output file with a SARIF Run per axe-core Result.
[string] [required]
--verbose, -v Enables verbose console output. [boolean] [default: false]
--pretty, -p Includes line breaks and indentation in the output.
[boolean] [default: false]
--force, -f Overwrites the output file if it already exists.
[boolean] [default: false]
Examples:
axe-sarif-converter -i axe-results.json -o axe-results.sarif
"
`;
203 changes: 203 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';

// tslint:disable: mocha-no-side-effect-code

describe('axe-sarif-converter CLI', () => {
beforeAll(async () => {
await ensureDirectoryExists(testResultsDir);
});

it('prints help info with --help', async () => {
const output = await invokeCliWith('--help');
expect(output.stderr).toBe('');
expect(output.stdout).toMatchSnapshot('help_stdout');
});

it('prints version number from package.json with --version', async () => {
const output = await invokeCliWith('--version');
expect(output.stderr).toBe('');
expect(output.stdout).toBe('0.0.0-managed-by-semantic-release\n');
});

it('requires the -i parameter', async () => {
try {
await invokeCliWith(`-o irrelevant.sarif`);
fail('Should have returned non-zero exit code');
} catch (e) {
expect(e.stderr).toMatch('Missing required argument: input-files');
}
});

it('requires the -o parameter', async () => {
try {
await invokeCliWith(`-i irrelevant.json`);
fail('Should have returned non-zero exit code');
} catch (e) {
expect(e.stderr).toMatch('Missing required argument: output-file');
}
});

it('supports conversion from axe-cli style list of results', async () => {
const outputFile = path.join(testResultsDir, 'axe-cli.sarif');
await deleteIfExists(outputFile);

const output = await invokeCliWith(`-i ${axeCliFile} -o ${outputFile}`);

expect(output.stderr).toBe('');
expect(output.stdout).toBe('');

const outputJson = JSON.parse((await readFile(outputFile)).toString());
expect(outputJson.runs.length).toBe(1);
});

it('supports basic conversion with short-form i/o args', async () => {
const outputFile = path.join(testResultsDir, 'basic_short.sarif');
await deleteIfExists(outputFile);

const output = await invokeCliWith(
`-i ${basicAxeV2File} -o ${outputFile}`,
);

expect(output.stderr).toBe('');
expect(output.stdout).toBe('');
expectSameJSONContent(outputFile, basicSarifFile);
});

it('supports basic conversion with long-form i/o args', async () => {
const outputFile = path.join(testResultsDir, 'basic_long.sarif');
await deleteIfExists(outputFile);

const output = await invokeCliWith(
`--input-files ${basicAxeV2File} --output-file ${outputFile}`,
);

expect(output.stderr).toBe('');
expect(output.stdout).toBe('');
expectSameJSONContent(outputFile, basicSarifFile);
});

it("doesn't overwrite existing files by default", async () => {
const outputFile = path.join(
testResultsDir,
'overwrite_no_force.sarif',
);
await writeFile(outputFile, 'preexisting content');

try {
await invokeCliWith(`-i ${basicAxeV2File} -o ${outputFile}`);
fail('Should have returned non-zero exit code');
} catch (e) {
expect(e.code).toBeGreaterThan(0);
expect(e.stderr).toMatch('Did you mean to use --force?');
}

const outputFileContent = (await readFile(outputFile)).toString();
expect(outputFileContent).toBe('preexisting content');
});

it.each(['-f', '--force'])('overwrites files with %s', async arg => {
const outputFile = path.join(testResultsDir, `overwrite_${arg}.sarif`);
await writeFile(outputFile, 'preexisting content');

await invokeCliWith(`-i ${basicAxeV2File} -o ${outputFile} ${arg}`);

expectSameJSONContent(outputFile, basicSarifFile);
});

it.each(['-v', '--verbose'])('emits verbose output', async verboseArg => {
const outputFile = path.join(
testResultsDir,
'emits_verbose_output.sarif',
);
await deleteIfExists(outputFile);

const output = await invokeCliWith(
`-i ${basicAxeV2File} -o ${outputFile} ${verboseArg}`,
);

expect(output.stderr).toBe('');
expect(output.stdout).toContain(basicAxeV2File);
expect(output.stdout).toContain(outputFile);

expectSameJSONContent(outputFile, basicSarifFile);
});

it.each(['-p', '--pretty'])('pretty-prints', async prettyArg => {
const outputFile = path.join(testResultsDir, 'pretty-prints.sarif');
await deleteIfExists(outputFile);

await invokeCliWith(
`-i ${basicAxeV2File} -o ${outputFile} ${prettyArg}`,
);

const outputContents = (await readFile(outputFile)).toString();

const lines = outputContents.split('\n');
expect(lines.length).toBeGreaterThan(100);
expect(lines.every(line => line.length < 200)).toBe(true);
});

async function invokeCliWith(
args: string,
): Promise<{ stderr: string; stdout: string }> {
return await exec(`node ${__dirname}/../dist/cli.js ${args}`);
}

const testResourcesDir = path.join(__dirname, 'test-resources');
const testResultsDir = path.join(__dirname, '..', 'test-results');
const basicAxeV2File = path.join(
testResourcesDir,
'basic-axe-v3.2.2-reporter-v2.json',
);
const basicSarifFile = path.join(
testResourcesDir,
'basic-axe-v3.2.2-sarif-v2.1.2.sarif',
);
const axeCliFile = path.join(testResourcesDir, 'axe-cli-v3.1.1.json');

const mkdir = promisify(fs.mkdir);
const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
const unlink = promisify(fs.unlink);
const exec = promisify(child_process.exec);

async function deleteIfExists(path: string): Promise<void> {
try {
await unlink(path);
} catch (e) {
if (e.code != 'ENOENT') {
throw e;
}
}
}

async function ensureDirectoryExists(path: string): Promise<void> {
try {
await mkdir(path);
} catch (e) {
if (e.code !== 'EEXIST') {
throw e;
}
}
}

async function expectSameJSONContent(
actualFile: string,
expectedFile: string,
) {
const actualContentBuffer = await readFile(actualFile);
const actualJSONContent = JSON.parse(actualContentBuffer.toString());

const expectedContentBuffer = await readFile(expectedFile);
const expectedJSONContent = JSON.parse(
expectedContentBuffer.toString(),
);

expect(actualJSONContent).toStrictEqual(expectedJSONContent);
}
});
Loading

0 comments on commit 892ed0b

Please sign in to comment.