Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c13319d
Use sarif parser for reopened results
angelapwen Aug 9, 2022
b3d7e78
Bump unit test timeout to 3 min
angelapwen Aug 15, 2022
c37c5bf
Bump unit test timeout to 5 min
angelapwen Aug 15, 2022
f0d0017
Merge branch 'main' into handle-sarif-reopen
angelapwen Oct 11, 2022
c5a816d
Use sarif parser for reopened results
angelapwen Aug 9, 2022
c1b3ee1
Remove extra import statement
angelapwen Oct 11, 2022
2b6dd6b
Exit parser when invalid SARIF is parsed
angelapwen Oct 11, 2022
169a88a
Reset test timeout to 2 min
angelapwen Oct 11, 2022
fc32971
Add tests for valid and invalid small SARIF files
angelapwen Oct 11, 2022
ca8f930
Add large SARIF file tests
angelapwen Oct 12, 2022
8c92a19
Delete large valid SARIF before invalid test
angelapwen Oct 13, 2022
438607d
Use `del` rather than `unlink`
angelapwen Oct 13, 2022
a07e549
Update imports
angelapwen Oct 13, 2022
9fe8fd8
Close write stream
angelapwen Oct 13, 2022
d656db0
Improve SARIF parser and interpreter tests
angelapwen Oct 19, 2022
dcc51cc
Move interpretedResultsPath to before hook
angelapwen Oct 19, 2022
badbed9
Use safeDel rather than del
angelapwen Oct 19, 2022
6bd649a
Write valid JSON to large valid SARIF test
angelapwen Oct 20, 2022
05c6efe
Remove file deletion in after block
angelapwen Oct 20, 2022
f75f0b7
Use `safeDel` in after hook
angelapwen Oct 20, 2022
f7dc7b7
Attempt 1000 iterations for large tests
angelapwen Oct 21, 2022
50b3109
Sleep 10ms in tests on Windows
angelapwen Oct 21, 2022
fbe0b98
Add missing import
angelapwen Oct 21, 2022
93bd94c
Change stream listeners to close rather than finish
angelapwen Oct 21, 2022
86eaf9d
Write 1mil times for SARIF files
angelapwen Oct 21, 2022
b849fa9
Write to a new path for last test
angelapwen Oct 21, 2022
262fbee
Refactor timeout to before hook
angelapwen Oct 21, 2022
75c8aa3
Revert "Refactor timeout to before hook"
angelapwen Oct 21, 2022
f37bf65
Rename invalid results path
angelapwen Oct 21, 2022
ca0c863
Update results path var name
angelapwen Oct 24, 2022
671f0e2
Add comment explanation
angelapwen Oct 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions extensions/ql-vscode/src/query-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { QueryStatus } from './query-status';
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
import { QueryEvaluationInfo, QueryWithResults } from './run-queries-shared';
import { formatLegacyMessage } from './legacy-query-server/run-queries';
import { sarifParser } from './sarif-parser';

/**
* query-results.ts
Expand Down Expand Up @@ -160,10 +161,12 @@ export async function interpretResultsSarif(
sourceInfo?: cli.SourceInfo
): Promise<SarifInterpretationData> {
const { resultsPath, interpretedResultsPath } = resultsPaths;
let res;
if (await fs.pathExists(interpretedResultsPath)) {
return { ...JSON.parse(await fs.readFile(interpretedResultsPath, 'utf8')), t: 'SarifInterpretationData' };
res = await sarifParser(interpretedResultsPath);
Comment thread
angelapwen marked this conversation as resolved.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought....if this function throws an error because the file is not parseable, should we delete the file and re-run cli.interpretBqrsSarif?

Possibly, we can do this as a followup.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that's a good point 👍 I'll leave this comment up for now, while we work out the last invalid SARIF test issue..

} else {
res = await cli.interpretBqrsSarif(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
}
const res = await cli.interpretBqrsSarif(ensureMetadataIsComplete(metadata), resultsPath, interpretedResultsPath, sourceInfo);
return { ...res, t: 'SarifInterpretationData' };
}

Expand Down
26 changes: 14 additions & 12 deletions extensions/ql-vscode/src/sarif-parser.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import * as Sarif from 'sarif';
import * as fs from 'fs-extra';
import { parser } from 'stream-json';
import { pick } from 'stream-json/filters/Pick';
import Assembler = require('stream-json/Assembler');
import { chain } from 'stream-chain';
import { connectTo } from 'stream-json/Assembler';
import { getErrorMessage } from './pure/helpers-pure';
import { withParser } from 'stream-json/filters/Pick';

const DUMMY_TOOL: Sarif.Tool = { driver: { name: '' } };

export async function sarifParser(interpretedResultsPath: string): Promise<Sarif.Log> {
try {
// Parse the SARIF file into token streams, filtering out only the results array.
const p = parser();
const pipeline = chain([
fs.createReadStream(interpretedResultsPath),
p,
pick({ filter: 'runs.0.results' })
]);
const pipeline = fs.createReadStream(interpretedResultsPath).pipe(withParser({ filter: 'runs.0.results' }));

// Creates JavaScript objects from the token stream
const asm = Assembler.connectTo(pipeline);
const asm = connectTo(pipeline);

// Returns a constructed Log object with the results or an empty array if no results were found.
// Returns a constructed Log object with the results of an empty array if no results were found.
// If the parser fails for any reason, it will reject the promise.
return await new Promise((resolve, reject) => {
let alreadyDone = false;
pipeline.on('error', (error) => {
reject(error);
});

// If the parser pipeline completes before the assembler, we've reached end of file and have not found any results.
pipeline.on('end', () => {
if (!alreadyDone) {
reject(new Error('Invalid SARIF file: expecting at least one run with result.'));
}
});

asm.on('done', (asm) => {

const log: Sarif.Log = {
Expand All @@ -41,6 +42,7 @@ export async function sarifParser(interpretedResultsPath: string): Promise<Sarif
};

resolve(log);
alreadyDone = true;
});
});
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from 'chai';
import * as path from 'path';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as sinon from 'sinon';
import { LocalQueryInfo, InitialQueryInfo, interpretResultsSarif } from '../../query-results';
import { QueryWithResults } from '../../run-queries-shared';
Expand All @@ -11,6 +12,7 @@ import { tmpDir } from '../../helpers';
import { slurpQueryHistory, splatQueryHistory } from '../../query-serialization';
import { formatLegacyMessage, QueryInProgress } from '../../legacy-query-server/run-queries';
import { EvaluationResult, QueryResultType } from '../../pure/legacy-messages';
import Sinon = require('sinon');

describe('query-results', () => {
let disposeSpy: sinon.SinonSpy;
Expand Down Expand Up @@ -155,68 +157,213 @@ describe('query-results', () => {
});
});

it('should interpretResultsSarif', async () => {
const spy = sandbox.mock();
spy.returns({ a: '1234' });
const mockServer = {
interpretBqrsSarif: spy
} as unknown as CodeQLCliServer;

const interpretedResultsPath = path.join(tmpDir.name, 'interpreted.json');
const resultsPath = '123';
const sourceInfo = {};
describe('interpretResultsSarif', () => {
let mockServer: CodeQLCliServer;
let spy: Sinon.SinonExpectation;
const metadata = {
kind: 'my-kind',
id: 'my-id' as string | undefined,
scored: undefined
};
const results1 = await interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
const resultsPath = '123';
const interpretedResultsPath = path.join(tmpDir.name, 'interpreted.json');
const sourceInfo = {};

expect(results1).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
expect(spy).to.have.been.calledWith(
metadata,
resultsPath, interpretedResultsPath, sourceInfo
);
beforeEach(() => {
spy = sandbox.mock();
spy.returns({ a: '1234' });

// Try again, but with no id
spy.reset();
spy.returns({ a: '1234' });
delete metadata.id;
const results2 = await interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
expect(results2).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
expect(spy).to.have.been.calledWith(
{ kind: 'my-kind', id: 'dummy-id', scored: undefined },
resultsPath, interpretedResultsPath, sourceInfo
);
mockServer = {
interpretBqrsSarif: spy
} as unknown as CodeQLCliServer;
});

// try a third time, but this time we get from file
spy.reset();
fs.writeFileSync(interpretedResultsPath, JSON.stringify({
a: 6
}), 'utf8');
const results3 = await interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
expect(results3).to.deep.eq({ a: 6, t: 'SarifInterpretationData' });
afterEach(async () => {
sandbox.restore();
safeDel(interpretedResultsPath);
});

it('should interpretResultsSarif', async function() {
// up to 2 minutes per test
this.timeout(2 * 60 * 1000);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this (and the other) timeout to the describe block above? You only need to set the timeout once up there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get an Object is possibly undefined error when I move it to the describe block or the beforeEach block — is there a way to get around that error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Figured out how move this to the beforeEach hook and pushed up.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, that didn't work... had to revert


const results = await interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);

expect(results).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
expect(spy).to.have.been.calledWith(
metadata,
resultsPath, interpretedResultsPath, sourceInfo
);
});

it('should interpretBqrsSarif without ID', async function() {
// up to 2 minutes per test
this.timeout(2 * 60 * 1000);

delete metadata.id;
const results = await interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
expect(results).to.deep.eq({ a: '1234', t: 'SarifInterpretationData' });
expect(spy).to.have.been.calledWith(
{ kind: 'my-kind', id: 'dummy-id', scored: undefined },
resultsPath, interpretedResultsPath, sourceInfo
);
});

it('should use sarifParser on a valid small SARIF file', async function() {
// up to 2 minutes per test
this.timeout(2 * 60 * 1000);

fs.writeFileSync(interpretedResultsPath, JSON.stringify({
runs: [{ results: [] }] // A run needs results to succeed.
}), 'utf8');
const results = await interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
// We do not re-interpret if we are reading from a SARIF file.
expect(spy).to.not.have.been.called;

expect(results).to.have.property('t', 'SarifInterpretationData');
expect(results).to.have.nested.property('runs[0].results');
});

it('should throw an error on an invalid small SARIF file', async function() {
// up to 2 minutes per test
this.timeout(2 * 60 * 1000);

fs.writeFileSync(interpretedResultsPath, JSON.stringify({
a: '6' // Invalid: no runs or results
}), 'utf8');

await expect(
interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo)
).to.be.rejectedWith('Parsing output of interpretation failed: Invalid SARIF file: expecting at least one run with result.');

// We do not attempt to re-interpret if we are reading from a SARIF file.
expect(spy).to.not.have.been.called;
});

it('should use sarifParser on a valid large SARIF file', async function() {
// up to 2 minutes per test
this.timeout(2 * 60 * 1000);

const validSarifStream = fs.createWriteStream(interpretedResultsPath, { flags: 'w' });

const finished = new Promise((res, rej) => {
validSarifStream.addListener('close', res);
validSarifStream.addListener('error', rej);
});

validSarifStream.write(JSON.stringify({
runs: [{ results: [] }] // A run needs results to succeed.
}), 'utf8');

validSarifStream.write('[', 'utf8');
const iterations = 1_000_000;
for (let i = 0; i < iterations; i++) {
validSarifStream.write(JSON.stringify({
a: '6'
}), 'utf8');
if (i < iterations - 1) {
validSarifStream.write(',');
}
}
validSarifStream.write(']', 'utf8');
validSarifStream.end();
await finished;

// We need to sleep to wait for MSFT Defender to scan the file
// so that it can be read by our test.
if (os.platform() === 'win32') {
await sleep(10_000);
}

const results = await interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo
);
// We do not re-interpret if we are reading from a SARIF file.
expect(spy).to.not.have.been.called;

expect(results).to.have.property('t', 'SarifInterpretationData');
expect(results).to.have.nested.property('runs[0].results');
});

it('should throw an error on an invalid large SARIF file', async function() {
// up to 2 minutes per test
this.timeout(2 * 60 * 1000);

// There is a problem on Windows where the file at the prior path isn't able
// to be deleted or written to, so we rename the path for this last test.
const interpretedResultsPath = path.join(tmpDir.name, 'interpreted-invalid.json');
const invalidSarifStream = fs.createWriteStream(interpretedResultsPath, { flags: 'w' });

const finished = new Promise((res, rej) => {
invalidSarifStream.addListener('close', res);
invalidSarifStream.addListener('error', rej);
});

invalidSarifStream.write('[', 'utf8');
const iterations = 1_000_000;
for (let i = 0; i < iterations; i++) {
invalidSarifStream.write(JSON.stringify({
a: '6'
}), 'utf8');
if (i < iterations - 1) {
invalidSarifStream.write(',');
}
}
invalidSarifStream.write(']', 'utf8');
invalidSarifStream.end();
await finished;

// We need to sleep to wait for MSFT Defender to scan the file
// so that it can be read by our test.
if (os.platform() === 'win32') {
await sleep(10_000);
}

await expect(
interpretResultsSarif(
mockServer,
metadata,
{
resultsPath, interpretedResultsPath
},
sourceInfo as SourceInfo)
).to.be.rejectedWith('Parsing output of interpretation failed: Invalid SARIF file: expecting at least one run with result.');

// We do not attempt to re-interpret if we are reading from a SARIF file.
expect(spy).to.not.have.been.called;
});
});

describe('splat and slurp', () => {
Expand Down Expand Up @@ -300,6 +447,18 @@ describe('query-results', () => {
});
});

function safeDel(file: string) {
try {
fs.unlinkSync(file);
} catch (e) {
// ignore
}
}

async function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

function createMockQueryWithResults(
queryPath: string,
didRunSuccessfully = true,
Expand Down