Skip to content

Commit de2a6cc

Browse files
author
Dave Bartolomeo
committed
Make QLTest use codeql test run instead of odasa qltest
1 parent 55d1a4a commit de2a6cc

9 files changed

Lines changed: 394 additions & 466 deletions

File tree

extensions/ql-vscode/package.json

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,13 @@
127127
"default": "[%t] %q on %d - %s",
128128
"description": "Default string for how to label query history items. %t is the time of the query, %q is the query name, %d is the database name, and %s is a status string."
129129
},
130-
"codeQL.tests.semmleCoreDistributionPath": {
131-
"scope": "window",
132-
"type": "string",
133-
"default": "",
134-
"description": "Location of the Semmle Core distribution"
135-
},
136-
"codeQL.tests.semmleCoreLicensePath": {
137-
"scope": "window",
138-
"type": "string",
139-
"description": "Location of the directory containing the Semmle Core license file"
140-
},
141-
"codeQL.tests.numberOfThreads": {
130+
"codeQL.runningTests.numberOfThreads": {
142131
"scope": "window",
143132
"type": "integer",
144-
"description": "Number of threads to use for CodeQL test execution"
133+
"default": 1,
134+
"minimum": 1,
135+
"maximum": 1024,
136+
"description": "Number of threads for running CodeQL tests."
145137
}
146138
}
147139
},

extensions/ql-vscode/src/cli.ts

Lines changed: 238 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import * as child_process from "child_process";
1+
import * as child_process from 'child_process';
2+
import * as cpp from 'child-process-promise';
23
import * as fs from 'fs-extra';
34
import * as path from 'path';
45
import * as sarif from 'sarif';
56
import * as util from 'util';
6-
import { Logger, ProgressReporter } from "./logging";
7-
import { Disposable } from "vscode";
8-
import { DistributionProvider } from "./distribution";
9-
import { SortDirection } from "./interface-types";
10-
import { assertNever } from "./helpers-pure";
7+
import { Logger, ProgressReporter } from './logging';
8+
import { Disposable } from 'vscode';
9+
import { DistributionProvider } from './distribution';
10+
import { SortDirection } from './interface-types';
11+
import { assertNever } from './helpers-pure';
12+
import { Readable } from 'stream';
13+
import { StringDecoder } from 'string_decoder';
1114

1215
/**
1316
* The version of the SARIF format that we are using.
@@ -74,6 +77,30 @@ export interface ResolvedQLPacks {
7477
[index: string]: string[];
7578
}
7679

80+
/**
81+
* The expected output of `codeql resolve tests`.
82+
*/
83+
export type ResolvedTests = string[];
84+
85+
/**
86+
* Options for `codeql test run`.
87+
*/
88+
export interface TestRunOptions {
89+
logger?: Logger;
90+
}
91+
92+
/**
93+
* Event fired by `codeql test run`.
94+
*/
95+
export interface TestCompleted {
96+
test: string;
97+
pass: boolean;
98+
messages: string[];
99+
compilationMs: number;
100+
evaluationMs: number;
101+
expected: string;
102+
}
103+
77104
/**
78105
* This class manages a cli server started by `codeql execute cli-server` to
79106
* run commands without the overhead of starting a new java
@@ -153,14 +180,22 @@ export class CodeQLCliServer implements Disposable {
153180

154181
}
155182

183+
/**
184+
* Get the path to the CodeQL CLI distribution, or throw an exception if not found.
185+
*/
186+
private async getCodeQlPath(): Promise<string> {
187+
const codeqlPath = await this.config.getCodeQlPathWithoutVersionCheck();
188+
if (!codeqlPath) {
189+
throw new Error('Failed to find CodeQL distribution.');
190+
}
191+
return codeqlPath;
192+
}
193+
156194
/**
157195
* Launch the cli server
158196
*/
159197
private async launchProcess(): Promise<child_process.ChildProcessWithoutNullStreams> {
160-
const config = await this.config.getCodeQlPathWithoutVersionCheck();
161-
if (!config) {
162-
throw new Error("Failed to find codeql distribution")
163-
}
198+
const config = await this.getCodeQlPath();
164199
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, _data => {})
165200
}
166201

@@ -245,6 +280,69 @@ export class CodeQLCliServer implements Disposable {
245280
}
246281
}
247282

283+
/**
284+
* Runs an asynchronous CodeQL CLI command without invoking the CLI server, returning any events
285+
* fired by the command as an asynchronous generator.
286+
*
287+
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
288+
* @param commandArgs The arguments to pass to the `codeql` command.
289+
* @param logger Logger to write text output from the command.
290+
* @returns The sequence of async events produced by the command.
291+
*/
292+
private async* runAsyncCodeQlCliCommandInternal(
293+
command: string[],
294+
commandArgs: string[],
295+
logger?: Logger
296+
): AsyncGenerator<string, void, unknown> {
297+
// Add format argument first, in case commandArgs contains positional parameters.
298+
const args = [
299+
...command,
300+
'--format', 'jsonz',
301+
...commandArgs
302+
];
303+
304+
// Spawn the CodeQL process
305+
const codeqlPath = await this.getCodeQlPath();
306+
const childPromise = cpp.spawn(codeqlPath, args);
307+
const child = childPromise.childProcess;
308+
309+
if (logger !== undefined) {
310+
// The human-readable output goes to stderr.
311+
logStream(child.stderr!, logger);
312+
}
313+
314+
for await (const event of await splitStreamAtSeparators(child.stdout!, ['\0'])) {
315+
yield event;
316+
}
317+
318+
await childPromise;
319+
}
320+
321+
/**
322+
* Runs an asynchronous CodeQL CLI command without invoking the CLI server, returning any events
323+
* fired by the command as an asynchronous generator.
324+
*
325+
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
326+
* @param commandArgs The arguments to pass to the `codeql` command.
327+
* @param description Description of the action being run, to be shown in log and error messages.
328+
* @param logger Logger to write text output from the command.
329+
* @returns The sequence of async events produced by the command.
330+
*/
331+
public async* runAsyncCodeQlCliCommand<EventType>(
332+
command: string[],
333+
commandArgs: string[],
334+
description: string,
335+
logger?: Logger
336+
): AsyncGenerator<EventType, void, unknown> {
337+
for await (const event of await this.runAsyncCodeQlCliCommandInternal(command, commandArgs, logger)) {
338+
try {
339+
yield JSON.parse(event) as EventType;
340+
} catch (err) {
341+
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`)
342+
}
343+
}
344+
}
345+
248346
/**
249347
* Runs a CodeQL CLI command on the server, returning the output as a string.
250348
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
@@ -325,6 +423,39 @@ export class CodeQLCliServer implements Disposable {
325423
return await this.runJsonCodeQlCliCommand<ResolvedQLPacks>(['resolve', 'qlpacks'], subcommandArgs, 'Resolving QL packs');
326424
}
327425

426+
/**
427+
* Finds all available QL tests in a given directory.
428+
* @param testPath Root of directory tree to search for tests.
429+
* @returns The list of tests that were found.
430+
*/
431+
public async resolveTests(testPath: string): Promise<ResolvedTests> {
432+
const subcommandArgs = [
433+
testPath
434+
];
435+
return await this.runJsonCodeQlCliCommand<ResolvedTests>(['resolve', 'tests'], subcommandArgs, 'Resolving tests');
436+
}
437+
438+
/**
439+
* Runs QL tests.
440+
* @param testPaths Full paths of the tests to run.
441+
* @param workspaces Workspace paths to use as search paths for QL packs.
442+
* @param options Additional options.
443+
*/
444+
public async* runTests(testPaths: string[], workspaces: string[], options: TestRunOptions):
445+
AsyncGenerator<TestCompleted, void, unknown> {
446+
447+
const subcommandArgs = [
448+
'--additional-packs', workspaces.join(path.delimiter),
449+
'--threads', '8',
450+
...testPaths
451+
];
452+
453+
for await (const event of await this.runAsyncCodeQlCliCommand<TestCompleted>(['test', 'run'],
454+
subcommandArgs, 'Run CodeQL Tests', options.logger)) {
455+
yield event;
456+
}
457+
}
458+
328459
/**
329460
* Gets the metadata for a query.
330461
* @param queryPath The path to the query.
@@ -516,3 +647,100 @@ export async function runCodeQlCliCommand(codeQlPath: string, command: string[],
516647
throw new Error(`${description} failed: ${err.stderr || err}`)
517648
}
518649
}
650+
651+
/**
652+
* Buffer to hold state used when splitting a text stream into lines.
653+
*/
654+
class SplitBuffer {
655+
private readonly decoder = new StringDecoder('utf8');
656+
private readonly maxSeparatorLength: number;
657+
private buffer = '';
658+
private searchIndex = 0;
659+
660+
constructor(private readonly separators: readonly string[]) {
661+
this.maxSeparatorLength = separators.map(s => s.length).reduce((a, b) => Math.max(a, b), 0);
662+
}
663+
664+
/**
665+
* Append new text data to the buffer.
666+
* @param chunk The chunk of data to append.
667+
*/
668+
public addChunk(chunk: Buffer): void {
669+
this.buffer += this.decoder.write(chunk);
670+
}
671+
672+
/**
673+
* Signal that the end of the input stream has been reached.
674+
*/
675+
public end(): void {
676+
this.buffer += this.decoder.end();
677+
this.buffer += this.separators[0]; // Append a separator to the end to ensure the last line is returned.
678+
}
679+
680+
/**
681+
* Extract the next full line from the buffer, if one is available.
682+
* @returns The text of the next available full line (without the separator), or `undefined` if no
683+
* line is available.
684+
*/
685+
public getNextLine(): string | undefined {
686+
while (this.searchIndex <= (this.buffer.length - this.maxSeparatorLength)) {
687+
for (const separator of this.separators) {
688+
if (this.buffer.startsWith(separator, this.searchIndex)) {
689+
const line = this.buffer.substr(0, this.searchIndex);
690+
this.buffer = this.buffer.substr(this.searchIndex + separator.length);
691+
this.searchIndex = 0;
692+
return line;
693+
}
694+
}
695+
this.searchIndex++;
696+
}
697+
698+
return undefined;
699+
}
700+
}
701+
702+
/**
703+
* Splits a text stream into lines based on a list of valid line separators.
704+
* @param stream The text stream to split. This stream will be fully consumed.
705+
* @param separators The list of strings that act as line separators.
706+
* @returns A sequence of lines (not including separators).
707+
*/
708+
async function* splitStreamAtSeparators(stream: Readable, separators: string[]):
709+
AsyncGenerator<string, void, unknown> {
710+
711+
const buffer = new SplitBuffer(separators);
712+
for await (const chunk of stream) {
713+
buffer.addChunk(chunk);
714+
let line: string | undefined;
715+
do {
716+
line = buffer.getNextLine();
717+
if (line !== undefined) {
718+
yield line;
719+
}
720+
} while (line !== undefined);
721+
}
722+
buffer.end();
723+
let line: string | undefined;
724+
do {
725+
line = buffer.getNextLine();
726+
if (line !== undefined) {
727+
yield line;
728+
}
729+
} while (line !== undefined);
730+
}
731+
732+
/**
733+
* Standard line endings for splitting human-readable text.
734+
*/
735+
const lineEndings = ['\r\n', '\r', '\n'];
736+
737+
/**
738+
* Log a text stream to a `Logger` interface.
739+
* @param stream The stream to log.
740+
* @param logger The logger that will consume the stream output.
741+
*/
742+
async function logStream(stream: Readable, logger: Logger): Promise<void> {
743+
for await (const line of await splitStreamAtSeparators(stream, lineEndings)) {
744+
logger.log(line);
745+
}
746+
}

extensions/ql-vscode/src/logging.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type ProgressReporter = Progress<{ message: string }>;
1212

1313
/** A logger that writes messages to an output channel in the Output tab. */
1414
export class OutputChannelLogger extends DisposableObject implements Logger {
15-
outputChannel: OutputChannel;
15+
public readonly outputChannel: OutputChannel;
1616

1717
constructor(title: string) {
1818
super();
@@ -27,7 +27,6 @@ export class OutputChannelLogger extends DisposableObject implements Logger {
2727
logWithoutTrailingNewline(message: string) {
2828
this.outputChannel.append(message);
2929
}
30-
3130
}
3231

3332
/** The global logger for the extension. */
@@ -38,3 +37,6 @@ export const queryServerLogger = new OutputChannelLogger('CodeQL Query Server');
3837

3938
/** The logger for messages from the language server. */
4039
export const ideServerLogger = new OutputChannelLogger('CodeQL Language Server');
40+
41+
/** The logger for messages from tests. */
42+
export const testLogger = new OutputChannelLogger('CodeQL Tests');

0 commit comments

Comments
 (0)