Add disposeHandlers

These functions assist with object disposal. They add custom behaviour
during disposal. The primary usage of disposalHandlers is during testing
where some objects should not be disposed in order to avoid testing
errors.

Additionally, move DisposableObject to the pure folder and create unit
tests for it.

Also, add `--disable-gpu` to command line options when running tests.
It helps to avoid error messages like this:

```- [19141:19141:0425/011526.129520:ERROR:sandbox_linux.cc(374)] InitializeSandbox() called with multiple threads in process gpu-process.```

See also https://askubuntu.com/a/1288969
This commit is contained in:
Andrew Eisenberg
2021-02-10 15:30:34 -08:00
parent c5fe58db37
commit f2620c65af
25 changed files with 213 additions and 31 deletions

View File

@@ -19,7 +19,8 @@ import { UrlValue, BqrsId } from './pure/bqrs-cli-types';
import { showLocation } from './interface-utils';
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from './pure/bqrs-utils';
import { commandRunner } from './commandRunner';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { showAndLogErrorMessage } from './helpers';
export interface AstItem {
id: BqrsId;
@@ -129,8 +130,13 @@ export class AstViewer extends DisposableObject {
this.treeDataProvider.db = db;
this.treeDataProvider.refresh();
this.treeView.message = `AST for ${path.basename(fileName)}`;
this.treeView.reveal(roots[0], { focus: false });
this.currentFile = fileName;
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(roots[0], { focus: false })?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
);
}
private updateTreeSelection(e: TextEditorSelectionChangeEvent) {
@@ -178,7 +184,12 @@ export class AstViewer extends DisposableObject {
const targetItem = findBest(range, this.treeDataProvider.roots);
if (targetItem) {
this.treeView.reveal(targetItem);
// Handle error on reveal. This could happen if
// the tree view is disposed during the reveal.
this.treeView.reveal(targetItem)?.then(
() => { /**/ },
err => showAndLogErrorMessage(err)
);
}
}
}

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from '../vscode-utils/disposable-object';
import { DisposableObject } from '../pure/disposable-object';
import {
WebviewPanel,
ExtensionContext,

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { workspace, Event, EventEmitter, ConfigurationChangeEvent, ConfigurationTarget } from 'vscode';
import { DistributionManager } from './distribution';
import { logger } from './logging';

View File

@@ -1,5 +1,5 @@
import * as path from 'path';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import {
Event,
EventEmitter,

View File

@@ -15,7 +15,7 @@ import {
withProgress
} from './commandRunner';
import { zipArchiveScheme, encodeArchiveBasePath, decodeSourceArchiveUri, encodeSourceArchiveUri } from './archive-filesystem-provider';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { Logger, logger } from './logging';
import { registerDatabases, Dataset, deregisterDatabases } from './pure/messages';
import { QueryServerClient } from './queryserver-client';

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { logger } from './logging';
/**

View File

@@ -1,6 +1,6 @@
import * as path from 'path';
import * as Sarif from 'sarif';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import * as vscode from 'vscode';
import {
Diagnostic,

View File

@@ -1,5 +1,5 @@
import { window as Window, OutputChannel, Progress, Disposable } from 'vscode';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import * as fs from 'fs-extra';
import * as path from 'path';

View File

@@ -1,4 +1,11 @@
import { Disposable } from 'vscode';
// Avoid explicitly referencing Disposable type in vscode.
// This file cannot have dependencies on the vscode API.
interface Disposable {
dispose(): any;
}
export type DisposeHandler = (disposable: Disposable) => void;
/**
* Base class to make it easier to implement a `Disposable` that owns other disposable object.
@@ -40,21 +47,39 @@ export abstract class DisposableObject implements Disposable {
* @param obj The object to stop tracking.
*/
protected disposeAndStopTracking(obj: Disposable): void {
if (obj !== undefined) {
this.tracked!.delete(obj);
if (obj && this.tracked) {
this.tracked.delete(obj);
obj.dispose();
}
}
public dispose() {
/**
* Dispose this object and all contained objects
*
* @param disposeHandler An optional dispose handler that gets
* passed each element to dispose. The dispose handler
* can choose how (and if) to dispose the object. The
* primary usage is for tests that should not dispose
* all items of a disposable.
*/
public dispose(disposeHandler?: DisposeHandler) {
if (this.tracked !== undefined) {
for (const trackedObject of this.tracked.values()) {
trackedObject.dispose();
if (disposeHandler) {
disposeHandler(trackedObject);
} else {
trackedObject.dispose();
}
}
this.tracked = undefined;
}
while (this.disposables.length > 0) {
this.disposables.pop()!.dispose();
const disposable = this.disposables.pop()!;
if (disposeHandler) {
disposeHandler(disposable);
} else {
disposable.dispose();
}
}
}
}

View File

@@ -13,7 +13,7 @@ import {
import { logger } from './logging';
import { URLSearchParams } from 'url';
import { QueryServerClient } from './queryserver-client';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { commandRunner } from './commandRunner';
/**

View File

@@ -1,6 +1,6 @@
import * as cp from 'child_process';
import * as path from 'path';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { Disposable, CancellationToken, commands } from 'vscode';
import { createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
import * as cli from './cli';

View File

@@ -1,7 +1,7 @@
import { ConfigurationChangeEvent, StatusBarAlignment, StatusBarItem, window, workspace } from 'vscode';
import { CodeQLCliServer } from './cli';
import { CANARY_FEATURES, DistributionConfigListener } from './config';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
/**
* Creates and manages a status bar item for codeql. THis item contains

View File

@@ -15,7 +15,7 @@ import {
import { TestAdapterRegistrar } from 'vscode-test-adapter-util';
import { QLTestFile, QLTestNode, QLTestDirectory, QLTestDiscovery } from './qltest-discovery';
import { Event, EventEmitter, CancellationTokenSource, CancellationToken } from 'vscode';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { CodeQLCliServer } from './cli';
import { getOnDiskWorkspaceFolders } from './helpers';
import { testLogger } from './logging';

View File

@@ -13,7 +13,7 @@ import {
import { showAndLogWarningMessage } from './helpers';
import { TestTreeNode } from './test-tree-node';
import { DisposableObject } from './vscode-utils/disposable-object';
import { DisposableObject } from './pure/disposable-object';
import { UIService } from './vscode-utils/ui-service';
import { QLTestAdapter, getExpectedFile, getActualFile } from './test-adapter';
import { logger } from './logging';

View File

@@ -44,7 +44,6 @@ describe('Databases', function() {
afterEach(() => {
try {
// dispose();
sandbox.restore();
} catch (e) {
fail(e);

View File

@@ -20,6 +20,7 @@ import { registerDatabases } from '../../pure/messages';
import { ProgressCallback } from '../../commandRunner';
import { CodeQLCliServer } from '../../cli';
import { encodeArchiveBasePath, encodeSourceArchiveUri } from '../../archive-filesystem-provider';
import { testDisposeHandler } from '../test-dispose-handler';
describe('databases', () => {
@@ -85,7 +86,7 @@ describe('databases', () => {
afterEach(async () => {
dir.removeCallback();
databaseManager.dispose();
databaseManager.dispose(testDisposeHandler);
sandbox.restore();
});

View File

@@ -7,6 +7,7 @@ import * as yaml from 'js-yaml';
import { AstViewer, AstItem } from '../../astViewer';
import { commands, Range } from 'vscode';
import { DatabaseItem } from '../../databases';
import { testDisposeHandler } from '../test-dispose-handler';
chai.use(chaiAsPromised);
const expect = chai.expect;
@@ -31,7 +32,7 @@ describe('AstViewer', () => {
afterEach(() => {
sandbox.restore();
if (viewer) {
viewer.dispose();
viewer.dispose(testDisposeHandler);
viewer = undefined;
}
});

View File

@@ -37,7 +37,6 @@ describe('AstBuilder', () => {
let mockCli: CodeQLCliServer;
let overrides: Record<string, object | undefined>;
beforeEach(() => {
mockCli = {
bqrsDecode: sinon.stub().callsFake((_: string, resultSet: 'nodes' | 'edges' | 'graphProperties') => {

View File

@@ -7,6 +7,7 @@ import { expect } from 'chai';
import { Uri } from 'vscode';
import { DatabaseUI } from '../../databases-ui';
import { testDisposeHandler } from '../test-dispose-handler';
describe('databases-ui', () => {
describe('fixDbUri', () => {
@@ -89,7 +90,7 @@ describe('databases-ui', () => {
expect(fs.pathExistsSync(db4)).to.be.false;
expect(fs.pathExistsSync(db5)).to.be.false;
databaseUI.dispose();
databaseUI.dispose(testDisposeHandler);
});
function createDatabase(storageDir: string, dbName: string, language: string, extraFile?: string) {

View File

@@ -7,7 +7,14 @@ import * as path from 'path';
import * as fs from 'fs-extra';
import * as sinon from 'sinon';
import { getInitialQueryContents, InvocationRateLimiter, isLikelyDbLanguageFolder, showBinaryChoiceDialog, showBinaryChoiceWithUrlDialog, showInformationMessageWithAction } from '../../helpers';
import {
getInitialQueryContents,
InvocationRateLimiter,
isLikelyDbLanguageFolder,
showBinaryChoiceDialog,
showBinaryChoiceWithUrlDialog,
showInformationMessageWithAction
} from '../../helpers';
import { reportStreamProgress } from '../../commandRunner';
import Sinon = require('sinon');
import { fail } from 'assert';

View File

@@ -112,18 +112,21 @@ function getLaunchArgs(dir: TestDir) {
switch (dir) {
case TestDir.NoWorksspace:
return [
'--disable-extensions'
'--disable-extensions',
'--disable-gpu'
];
case TestDir.MinimalWorksspace:
return [
'--disable-extensions',
'--disable-gpu',
path.resolve(__dirname, '../../test/data')
];
case TestDir.CliIntegration:
// CLI integration tests requires a multi-root workspace so that the data and the QL sources are accessible.
return [
'--disable-gpu',
path.resolve(__dirname, '../../test/data'),
process.env.TEST_CODEQL_PATH!
];

View File

@@ -0,0 +1,14 @@
import { Disposable } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
export function testDisposeHandler(disposable: any & Disposable) {
if (disposable.onDidExpandElement && disposable.onDidCollapseElement && disposable.reveal) {
// This looks like a treeViewer. Don't dispose
return;
}
if (disposable instanceof DisposableObject) {
disposable.dispose(testDisposeHandler);
} else {
disposable.dispose();
}
}

View File

@@ -1,4 +1,4 @@
import { DisposableObject } from './disposable-object';
import { DisposableObject } from '../pure/disposable-object';
import { EventEmitter, Event, Uri, GlobPattern, workspace } from 'vscode';
/**
@@ -62,4 +62,3 @@ export class MultiFileSystemWatcher extends DisposableObject {
this._onDidChange.fire(uri);
}
}

View File

@@ -1,5 +1,5 @@
import { TreeDataProvider, window } from 'vscode';
import { DisposableObject } from './disposable-object';
import { DisposableObject } from '../pure/disposable-object';
import { commandRunner } from '../commandRunner';
/**

View File

@@ -0,0 +1,122 @@
import 'chai';
import 'chai/register-should';
import 'sinon-chai';
import * as sinon from 'sinon';
import 'mocha';
import { DisposableObject } from '../../src/pure/disposable-object';
import { expect } from 'chai';
describe('DisposableObject and DisposeHandler', () => {
let disposable1: { dispose: sinon.SinonSpy };
let disposable2: { dispose: sinon.SinonSpy };
let disposable3: { dispose: sinon.SinonSpy };
let disposable4: { dispose: sinon.SinonSpy };
let disposableObject: any;
let nestedDisposableObject: any;
const sandbox = sinon.createSandbox();
beforeEach(() => {
sandbox.restore();
disposable1 = { dispose: sandbox.spy() };
disposable2 = { dispose: sandbox.spy() };
disposable3 = { dispose: sandbox.spy() };
disposable4 = { dispose: sandbox.spy() };
disposableObject = new MyDisposableObject();
nestedDisposableObject = new MyDisposableObject();
});
afterEach(() => {
sandbox.restore();
});
it('should dispose tracked and pushed objects', () => {
disposableObject.push(disposable1);
disposableObject.push(disposable2);
disposableObject.track(nestedDisposableObject);
nestedDisposableObject.track(disposable3);
disposableObject.dispose();
expect(disposable1.dispose).to.have.been.called;
expect(disposable2.dispose).to.have.been.called;
expect(disposable3.dispose).to.have.been.called;
// pushed items must be called in reverse order
sinon.assert.callOrder(disposable2.dispose, disposable1.dispose);
// now that disposableObject has been disposed, subsequent disposals are
// no-ops
disposable1.dispose.resetHistory();
disposable2.dispose.resetHistory();
disposable3.dispose.resetHistory();
disposableObject.dispose();
expect(disposable1.dispose).not.to.have.been.called;
expect(disposable2.dispose).not.to.have.been.called;
expect(disposable3.dispose).not.to.have.been.called;
});
it('should dispose and stop tracking objects', () => {
disposableObject.track(disposable1);
disposableObject.disposeAndStopTracking(disposable1);
expect(disposable1.dispose).to.have.been.called;
disposable1.dispose.resetHistory();
disposableObject.dispose();
expect(disposable1.dispose).not.to.have.been.called;
});
it('should avoid disposing an object that is not tracked', () => {
disposableObject.push(disposable1);
disposableObject.disposeAndStopTracking(disposable1);
expect(disposable1.dispose).not.to.have.been.called;
disposableObject.dispose();
expect(disposable1.dispose).to.have.been.called;
});
it('ahould use a dispose handler', () => {
const handler = (d: any) => (d === disposable1 || d === disposable3 || d === nestedDisposableObject)
? d.dispose(handler)
: void (0);
disposableObject.push(disposable1);
disposableObject.push(disposable2);
disposableObject.track(nestedDisposableObject);
nestedDisposableObject.track(disposable3);
nestedDisposableObject.track(disposable4);
disposableObject.dispose(handler);
expect(disposable1.dispose).to.have.been.called;
expect(disposable2.dispose).not.to.have.been.called;
expect(disposable3.dispose).to.have.been.called;
expect(disposable4.dispose).not.to.have.been.called;
// now that disposableObject has been disposed, subsequent disposals are
// no-ops
disposable1.dispose.resetHistory();
disposable2.dispose.resetHistory();
disposable3.dispose.resetHistory();
disposable4.dispose.resetHistory();
disposableObject.dispose();
expect(disposable1.dispose).not.to.have.been.called;
expect(disposable2.dispose).not.to.have.been.called;
expect(disposable3.dispose).not.to.have.been.called;
expect(disposable4.dispose).not.to.have.been.called;
});
class MyDisposableObject extends DisposableObject {
constructor() {
super();
}
}
});