Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions extensions/ql-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,12 @@ async function activateWithInstalledDistribution(
})
);

ctx.subscriptions.push(
commandRunner('codeQL.copyVariantAnalysisRepoList', async (variantAnalysisId: number) => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysisId);
})
);

ctx.subscriptions.push(
commandRunner('codeQL.monitorVariantAnalysis', async (
variantAnalysis: VariantAnalysis,
Expand Down
8 changes: 6 additions & 2 deletions extensions/ql-vscode/src/query-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1256,11 +1256,15 @@ export class QueryHistoryManager extends DisposableObject {
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);

// Remote queries only
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem || finalSingleItem.t !== 'remote') {
if (!this.assertSingleQuery(finalMultiSelect) || !finalSingleItem) {
return;
}

await commands.executeCommand('codeQL.copyRepoList', finalSingleItem.queryId);
if (finalSingleItem.t === 'remote') {
await commands.executeCommand('codeQL.copyRepoList', finalSingleItem.queryId);
} else if (finalSingleItem.t === 'variant-analysis') {
await commands.executeCommand('codeQL.copyVariantAnalysisRepoList', finalSingleItem.variantAnalysis.id);
}
}

async handleExportResults(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from 'path';

import * as ghApiClient from './gh-api/gh-api-client';
import { CancellationToken, commands, EventEmitter, ExtensionContext, Uri, window } from 'vscode';
import { CancellationToken, commands, env, EventEmitter, ExtensionContext, Uri, window } from 'vscode';
import { DisposableObject } from '../pure/disposable-object';
import { Credentials } from '../authentication';
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
Expand All @@ -28,6 +28,7 @@ import {
import PQueue from 'p-queue';
import { createTimestampFile, showAndLogErrorMessage, showAndLogInformationMessage } from '../helpers';
import * as fs from 'fs-extra';
import * as os from 'os';
import { cancelVariantAnalysis } from './gh-api/gh-actions-api-client';
import { ProgressCallback, UserCancellationException } from '../commandRunner';
import { CodeQLCliServer } from '../cli';
Expand Down Expand Up @@ -367,6 +368,27 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
await cancelVariantAnalysis(credentials, variantAnalysis);
}

public async copyRepoListToClipboard(variantAnalysisId: number) {
const variantAnalysis = this.variantAnalyses.get(variantAnalysisId);
if (!variantAnalysis) {
throw new Error(`No variant analysis with id: ${variantAnalysisId}`);
}

const fullNames = variantAnalysis.scannedRepos?.filter(a => a.resultCount && a.resultCount > 0).map(a => a.repository.fullName);
if (!fullNames || fullNames.length === 0) {
return;
}

const text = [
'"new-repo-list": [',
...fullNames.slice(0, -1).map(repo => ` "${repo}",`),
` "${fullNames[fullNames.length - 1]}"`,
']'
];

await env.clipboard.writeText(text.join(os.EOL));
}

private getRepoStatesStoragePath(variantAnalysisId: number): string {
return path.join(
this.getVariantAnalysisStorageLocation(variantAnalysisId),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as sinon from 'sinon';
import { assert, expect } from 'chai';
import { CancellationTokenSource, commands, extensions, QuickPickItem, Uri, window } from 'vscode';
import { CancellationTokenSource, commands, env, extensions, QuickPickItem, Uri, window } from 'vscode';
import { CodeQLExtensionInterface } from '../../../extension';
import { logger } from '../../../logging';
import * as config from '../../../config';
Expand All @@ -16,7 +16,10 @@ import { storagePath } from '../global.helper';
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
import * as VariantAnalysisModule from '../../../remote-queries/shared/variant-analysis';
import { createMockScannedRepos } from '../../factories/remote-queries/shared/scanned-repositories';
import {
createMockScannedRepo,
createMockScannedRepos
} from '../../factories/remote-queries/shared/scanned-repositories';
import {
VariantAnalysis,
VariantAnalysisScannedRepository,
Expand Down Expand Up @@ -252,7 +255,9 @@ describe('Variant Analysis Manager', async function() {
});

describe('when credentials are invalid', async () => {
beforeEach(async () => { sandbox.stub(Credentials, 'initialize').resolves(undefined); });
beforeEach(async () => {
sandbox.stub(Credentials, 'initialize').resolves(undefined);
});

it('should return early if credentials are wrong', async () => {
try {
Expand Down Expand Up @@ -695,4 +700,121 @@ describe('Variant Analysis Manager', async function() {
});
});
});

describe('copyRepoListToClipboard', async () => {
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 these tests be in no-workspace or minimal-workspace at all?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I think they could be moved to minimal-workspace. However, I don't really see what the advantage would be of splitting these tests over multiple files and test suites. I think it would actually make it harder where something is being tested, although I would also be happy to move them over if there are advantages to that.

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.

My understanding is that CLI tests are slower (and maybe flakier?) because they have to set up the whole CLI. We generally need to tidy up around the area so I'm happy for us to leave things where they are for now, but I was curious about whether there was a reason we chose to put them here. I guess the reason is that other VariantAnalysisManager tests are in CLI tests.

let variantAnalysis: VariantAnalysis;
let variantAnalysisStorageLocation: string;

let writeTextStub: sinon.SinonStub;

beforeEach(async () => {
variantAnalysis = createMockVariantAnalysis({});

variantAnalysisStorageLocation = variantAnalysisManager.getVariantAnalysisStorageLocation(variantAnalysis.id);
await createTimestampFile(variantAnalysisStorageLocation);
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);

writeTextStub = sinon.stub();
sinon.stub(env, 'clipboard').value({
writeText: writeTextStub,
});
});

afterEach(() => {
fs.rmSync(variantAnalysisStorageLocation, { recursive: true });
});

describe('when the variant analysis does not have any repositories', () => {
beforeEach(async () => {
await variantAnalysisManager.rehydrateVariantAnalysis({
...variantAnalysis,
scannedRepos: [],
});
});

it('should not copy any text', async () => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);

expect(writeTextStub).not.to.have.been.called;
});
});

describe('when the variant analysis does not have any repositories with results', () => {
beforeEach(async () => {
await variantAnalysisManager.rehydrateVariantAnalysis({
...variantAnalysis,
scannedRepos: [
{
...createMockScannedRepo(),
resultCount: 0,
},
{
...createMockScannedRepo(),
resultCount: undefined,
}
],
});
});

it('should not copy any text', async () => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);

expect(writeTextStub).not.to.have.been.called;
});
});

describe('when the variant analysis has repositories with results', () => {
const scannedRepos = [
{
...createMockScannedRepo(),
resultCount: 100,
},
{
...createMockScannedRepo(),
resultCount: 0,
},
{
...createMockScannedRepo(),
resultCount: 200,
},
{
...createMockScannedRepo(),
resultCount: undefined,
},
{
...createMockScannedRepo(),
resultCount: 5,
},
];

beforeEach(async () => {
await variantAnalysisManager.rehydrateVariantAnalysis({
...variantAnalysis,
scannedRepos,
});
});

it('should copy text', async () => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);

expect(writeTextStub).to.have.been.calledOnce;
});

it('should be valid JSON when put in object', async () => {
await variantAnalysisManager.copyRepoListToClipboard(variantAnalysis.id);

const text = writeTextStub.getCalls()[0].lastArg;

const parsed = JSON.parse('{' + text + '}');

expect(parsed).to.deep.eq({
'new-repo-list': [
scannedRepos[0].repository.fullName,
scannedRepos[2].repository.fullName,
scannedRepos[4].repository.fullName,
],
});
});
});
});
});