-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add command line interface (#94)
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
Showing
9 changed files
with
11,162 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
||
|
@@ -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. | ||
|
@@ -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. | ||
|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
" | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); |
Oops, something went wrong.