Merge remote-tracking branch 'origin/main' into koesie10/fix-duplicate-downloads
This commit is contained in:
@@ -15,7 +15,8 @@ const packageFiles = [
|
||||
'snippets.json',
|
||||
'media',
|
||||
'node_modules',
|
||||
'out'
|
||||
'out',
|
||||
'workspace-databases-schema.json'
|
||||
];
|
||||
|
||||
async function copyPackage(sourcePath: string, destPath: string): Promise<void> {
|
||||
|
||||
@@ -84,6 +84,12 @@
|
||||
"editor.wordBasedSuggestions": false
|
||||
}
|
||||
},
|
||||
"jsonValidation": [
|
||||
{
|
||||
"fileMatch": "workspace-databases.json",
|
||||
"url": "./workspace-databases-schema.json"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "ql",
|
||||
|
||||
@@ -12,6 +12,7 @@ export class DbConfigStore extends DisposableObject {
|
||||
|
||||
public constructor(workspaceStoragePath: string) {
|
||||
super();
|
||||
|
||||
this.configPath = path.join(workspaceStoragePath, 'workspace-databases.json');
|
||||
|
||||
this.config = this.createEmptyConfig();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { logger } from '../../logging';
|
||||
import { ProviderResult, TreeDataProvider, TreeItem } from 'vscode';
|
||||
import { DbTreeViewItem } from './db-tree-view-item';
|
||||
import { createDbTreeViewItemWarning, DbTreeViewItem } from './db-tree-view-item';
|
||||
import { DbManager } from '../db-manager';
|
||||
|
||||
export class DbTreeDataProvider implements TreeDataProvider<DbTreeViewItem> {
|
||||
@@ -41,6 +41,12 @@ export class DbTreeDataProvider implements TreeDataProvider<DbTreeViewItem> {
|
||||
// This will be fleshed out in a future change.
|
||||
void logger.log(`Creating database tree with ${dbItems.length} items`);
|
||||
|
||||
return [];
|
||||
// Add a sample warning as a proof of concept.
|
||||
const warningTreeViewItem = createDbTreeViewItemWarning(
|
||||
'There was an error',
|
||||
'Fix it'
|
||||
);
|
||||
|
||||
return [warningTreeViewItem];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import * as vscode from 'vscode';
|
||||
import { DbItem } from '../db-item';
|
||||
|
||||
/**
|
||||
* Represents an item in the database tree view.
|
||||
* Represents an item in the database tree view. This item could be
|
||||
* representing an actual database item or a warning.
|
||||
*/
|
||||
export class DbTreeViewItem extends vscode.TreeItem {
|
||||
constructor(
|
||||
public readonly dbItem: DbItem,
|
||||
public readonly dbItem: DbItem | undefined,
|
||||
public readonly iconPath: vscode.ThemeIcon,
|
||||
public readonly label: string,
|
||||
public readonly tooltip: string,
|
||||
public readonly collapsibleState: vscode.TreeItemCollapsibleState,
|
||||
@@ -15,3 +17,14 @@ export class DbTreeViewItem extends vscode.TreeItem {
|
||||
super(label, collapsibleState);
|
||||
}
|
||||
}
|
||||
|
||||
export function createDbTreeViewItemWarning(label: string, tooltip: string): DbTreeViewItem {
|
||||
return new DbTreeViewItem(
|
||||
undefined,
|
||||
new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground')),
|
||||
label,
|
||||
tooltip,
|
||||
vscode.TreeItemCollapsibleState.None,
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,11 +109,7 @@ import { NewQueryRunner } from './query-server/query-runner';
|
||||
import { QueryRunner } from './queryRunner';
|
||||
import { VariantAnalysisView } from './remote-queries/variant-analysis-view';
|
||||
import { VariantAnalysisViewSerializer } from './remote-queries/variant-analysis-view-serializer';
|
||||
import { VariantAnalysis } from './remote-queries/shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
|
||||
} from './remote-queries/gh-api/variant-analysis';
|
||||
import { VariantAnalysis, VariantAnalysisScannedRepository } from './remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisManager } from './remote-queries/variant-analysis-manager';
|
||||
import { createVariantAnalysisContentProvider } from './remote-queries/variant-analysis-content-provider';
|
||||
import { VSCodeMockGitHubApiServer } from './mocks/vscode-mock-gh-api-server';
|
||||
@@ -949,8 +945,8 @@ async function activateWithInstalledDistribution(
|
||||
|
||||
ctx.subscriptions.push(
|
||||
commandRunner('codeQL.autoDownloadVariantAnalysisResult', async (
|
||||
scannedRepo: ApiVariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysis,
|
||||
token: CancellationToken
|
||||
) => {
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepo, variantAnalysisSummary, token);
|
||||
|
||||
@@ -40,7 +40,7 @@ import * as fs from 'fs-extra';
|
||||
import { CliVersionConstraint } from './cli';
|
||||
import { HistoryItemLabelProvider } from './history-item-label-provider';
|
||||
import { Credentials } from './authentication';
|
||||
import { cancelRemoteQuery } from './remote-queries/gh-api/gh-actions-api-client';
|
||||
import { cancelRemoteQuery, cancelVariantAnalysis } from './remote-queries/gh-api/gh-actions-api-client';
|
||||
import { RemoteQueriesManager } from './remote-queries/remote-queries-manager';
|
||||
import { RemoteQueryHistoryItem } from './remote-queries/remote-query-history-item';
|
||||
import { ResultsView } from './interface';
|
||||
@@ -1109,6 +1109,7 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
const { finalSingleItem, finalMultiSelect } = this.determineSelection(singleItem, multiSelect);
|
||||
|
||||
const selected = finalMultiSelect || [finalSingleItem];
|
||||
|
||||
const results = selected.map(async item => {
|
||||
if (item.status === QueryStatus.InProgress) {
|
||||
if (item.t === 'local') {
|
||||
@@ -1117,6 +1118,10 @@ export class QueryHistoryManager extends DisposableObject {
|
||||
void showAndLogInformationMessage('Cancelling variant analysis. This may take a while.');
|
||||
const credentials = await this.getCredentials();
|
||||
await cancelRemoteQuery(credentials, item.remoteQuery);
|
||||
} else if (item.t === 'variant-analysis') {
|
||||
void showAndLogInformationMessage('Cancelling variant analysis. This may take a while.');
|
||||
const credentials = await this.getCredentials();
|
||||
await cancelVariantAnalysis(credentials, item.variantAnalysis);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { RemoteQuery } from '../remote-query';
|
||||
import { RemoteQueryFailureIndexItem, RemoteQueryResultIndex, RemoteQuerySuccessIndexItem } from '../remote-query-result-index';
|
||||
import { getErrorMessage } from '../../pure/helpers-pure';
|
||||
import { unzipFile } from '../../pure/zip';
|
||||
import { VariantAnalysis } from '../shared/variant-analysis';
|
||||
|
||||
export const RESULT_INDEX_ARTIFACT_NAME = 'result-index';
|
||||
|
||||
@@ -94,6 +95,18 @@ export async function cancelRemoteQuery(
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelVariantAnalysis(
|
||||
credentials: Credentials,
|
||||
variantAnalysis: VariantAnalysis
|
||||
): Promise<void> {
|
||||
const octokit = await credentials.getOctokit();
|
||||
const { actionsWorkflowRunId, controllerRepo: { fullName } } = variantAnalysis;
|
||||
const response = await octokit.request(`POST /repos/${fullName}/actions/runs/${actionsWorkflowRunId}/cancel`);
|
||||
if (response.status >= 300) {
|
||||
throw new Error(`Error cancelling variant analysis: ${response.status} ${response?.data?.message || ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadArtifactFromLink(
|
||||
credentials: Credentials,
|
||||
storagePath: string,
|
||||
|
||||
@@ -76,6 +76,17 @@ export interface VariantAnalysisScannedRepository {
|
||||
failureMessage?: string
|
||||
}
|
||||
|
||||
export interface VariantAnalysisRepositoryTask {
|
||||
repository: Repository,
|
||||
analysisStatus: VariantAnalysisRepoStatus,
|
||||
resultCount?: number,
|
||||
artifactSizeInBytes?: number,
|
||||
failureMessage?: string,
|
||||
databaseCommitSha?: string,
|
||||
sourceLocationPrefix?: string,
|
||||
artifactUrl?: string,
|
||||
}
|
||||
|
||||
export interface VariantAnalysisSkippedRepositories {
|
||||
accessMismatchRepos?: VariantAnalysisSkippedRepositoryGroup,
|
||||
notFoundRepos?: VariantAnalysisSkippedRepositoryGroup,
|
||||
|
||||
@@ -5,14 +5,11 @@ import { CancellationToken, commands, EventEmitter, ExtensionContext, window } f
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
import { Credentials } from '../authentication';
|
||||
import { VariantAnalysisMonitor } from './variant-analysis-monitor';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisRepoTask,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
|
||||
} from './gh-api/variant-analysis';
|
||||
import {
|
||||
isVariantAnalysisComplete,
|
||||
VariantAnalysis, VariantAnalysisQueryLanguage,
|
||||
VariantAnalysis,
|
||||
VariantAnalysisQueryLanguage,
|
||||
VariantAnalysisRepositoryTask,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
@@ -23,7 +20,7 @@ import { VariantAnalysisView } from './variant-analysis-view';
|
||||
import { VariantAnalysisViewManager } from './variant-analysis-view-manager';
|
||||
import { VariantAnalysisResultsManager } from './variant-analysis-results-manager';
|
||||
import { getControllerRepo } from './run-remote-query';
|
||||
import { processUpdatedVariantAnalysis } from './variant-analysis-processor';
|
||||
import { processUpdatedVariantAnalysis, processVariantAnalysisRepositoryTask } from './variant-analysis-processor';
|
||||
import PQueue from 'p-queue';
|
||||
import { createTimestampFile, showAndLogErrorMessage } from '../helpers';
|
||||
import * as fs from 'fs-extra';
|
||||
@@ -206,11 +203,11 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
}
|
||||
|
||||
public async autoDownloadVariantAnalysisResult(
|
||||
scannedRepo: ApiVariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
cancellationToken: CancellationToken
|
||||
): Promise<void> {
|
||||
if (this.repoStates.get(variantAnalysisSummary.id)?.[scannedRepo.repository.id]?.downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Succeeded) {
|
||||
if (this.repoStates.get(variantAnalysis.id)?.[scannedRepo.repository.id]?.downloadStatus === VariantAnalysisScannedRepositoryDownloadStatus.Succeeded) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,56 +216,58 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Pending,
|
||||
};
|
||||
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
|
||||
const credentials = await Credentials.initialize(this.ctx);
|
||||
if (!credentials) { throw Error('Error authenticating with GitHub'); }
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
return;
|
||||
}
|
||||
|
||||
let repoTask: VariantAnalysisRepoTask;
|
||||
let repoTask: VariantAnalysisRepositoryTask;
|
||||
try {
|
||||
repoTask = await ghApiClient.getVariantAnalysisRepo(
|
||||
const repoTaskResponse = await ghApiClient.getVariantAnalysisRepo(
|
||||
credentials,
|
||||
variantAnalysisSummary.controller_repo.id,
|
||||
variantAnalysisSummary.id,
|
||||
variantAnalysis.controllerRepo.id,
|
||||
variantAnalysis.id,
|
||||
scannedRepo.repository.id
|
||||
);
|
||||
|
||||
repoTask = processVariantAnalysisRepositoryTask(repoTaskResponse);
|
||||
} catch (e) {
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysisSummary.id}. Error: ${getErrorMessage(e)}`);
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysis.id}. Error: ${getErrorMessage(e)}`);
|
||||
}
|
||||
|
||||
if (repoTask.artifact_url) {
|
||||
if (repoTask.artifactUrl) {
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.InProgress;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
|
||||
try {
|
||||
await this.variantAnalysisResultsManager.download(credentials, variantAnalysisSummary.id, repoTask, this.getVariantAnalysisStorageLocation(variantAnalysisSummary.id));
|
||||
await this.variantAnalysisResultsManager.download(credentials, variantAnalysis.id, repoTask, this.getVariantAnalysisStorageLocation(variantAnalysis.id));
|
||||
} catch (e) {
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Failed;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysisSummary.id}. Error: ${getErrorMessage(e)}`);
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
throw new Error(`Could not download the results for variant analysis with id: ${variantAnalysis.id}. Error: ${getErrorMessage(e)}`);
|
||||
}
|
||||
}
|
||||
|
||||
repoState.downloadStatus = VariantAnalysisScannedRepositoryDownloadStatus.Succeeded;
|
||||
await this.onRepoStateUpdated(variantAnalysisSummary.id, repoState);
|
||||
await this.onRepoStateUpdated(variantAnalysis.id, repoState);
|
||||
|
||||
await fs.outputJson(this.getRepoStatesStoragePath(variantAnalysisSummary.id), this.repoStates.get(variantAnalysisSummary.id));
|
||||
await fs.outputJson(this.getRepoStatesStoragePath(variantAnalysis.id), this.repoStates.get(variantAnalysis.id));
|
||||
}
|
||||
|
||||
public async enqueueDownload(
|
||||
scannedRepo: ApiVariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysis: VariantAnalysis,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
await this.queue.add(() => this.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysisSummary, token));
|
||||
await this.queue.add(() => this.autoDownloadVariantAnalysisResult(scannedRepo, variantAnalysis, token));
|
||||
}
|
||||
|
||||
public downloadsQueueSize(): number {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ExtensionContext, CancellationToken, commands, EventEmitter } from 'vscode';
|
||||
import { CancellationToken, commands, EventEmitter, ExtensionContext } from 'vscode';
|
||||
import { Credentials } from '../authentication';
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
|
||||
import { isFinalVariantAnalysisStatus, VariantAnalysis } from './shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
isFinalVariantAnalysisStatus,
|
||||
VariantAnalysis,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisScannedRepository
|
||||
} from './gh-api/variant-analysis';
|
||||
} from './shared/variant-analysis';
|
||||
import { VariantAnalysisMonitorResult } from './shared/variant-analysis-monitor-result';
|
||||
import { processUpdatedVariantAnalysis } from './variant-analysis-processor';
|
||||
import { DisposableObject } from '../pure/disposable-object';
|
||||
@@ -57,7 +58,7 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
|
||||
this._onVariantAnalysisChange.fire(variantAnalysis);
|
||||
|
||||
const downloadedRepos = this.downloadVariantAnalysisResults(variantAnalysisSummary, scannedReposDownloaded);
|
||||
const downloadedRepos = this.downloadVariantAnalysisResults(variantAnalysis, scannedReposDownloaded);
|
||||
scannedReposDownloaded.push(...downloadedRepos);
|
||||
|
||||
if (isFinalVariantAnalysisStatus(variantAnalysis.status) || variantAnalysis.failureReason) {
|
||||
@@ -72,7 +73,7 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
|
||||
private scheduleForDownload(
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse
|
||||
variantAnalysisSummary: VariantAnalysis
|
||||
) {
|
||||
void commands.executeCommand('codeQL.autoDownloadVariantAnalysisResult', scannedRepo, variantAnalysisSummary);
|
||||
}
|
||||
@@ -81,22 +82,22 @@ export class VariantAnalysisMonitor extends DisposableObject {
|
||||
scannedRepo: VariantAnalysisScannedRepository,
|
||||
alreadyDownloaded: number[]
|
||||
): boolean {
|
||||
return !alreadyDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysis_status === 'succeeded';
|
||||
return !alreadyDownloaded.includes(scannedRepo.repository.id) && scannedRepo.analysisStatus === VariantAnalysisRepoStatus.Succeeded;
|
||||
}
|
||||
|
||||
private getReposToDownload(
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
variantAnalysisSummary: VariantAnalysis,
|
||||
alreadyDownloaded: number[]
|
||||
): VariantAnalysisScannedRepository[] {
|
||||
if (variantAnalysisSummary.scanned_repositories) {
|
||||
return variantAnalysisSummary.scanned_repositories.filter(scannedRepo => this.shouldDownload(scannedRepo, alreadyDownloaded));
|
||||
if (variantAnalysisSummary.scannedRepos) {
|
||||
return variantAnalysisSummary.scannedRepos.filter(scannedRepo => this.shouldDownload(scannedRepo, alreadyDownloaded));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private downloadVariantAnalysisResults(
|
||||
variantAnalysisSummary: VariantAnalysisApiResponse,
|
||||
variantAnalysisSummary: VariantAnalysis,
|
||||
scannedReposDownloaded: number[]
|
||||
): number[] {
|
||||
const repoResultsToDownload = this.getReposToDownload(variantAnalysisSummary, scannedReposDownloaded);
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
VariantAnalysisFailureReason as ApiVariantAnalysisFailureReason,
|
||||
VariantAnalysisStatus as ApiVariantAnalysisStatus,
|
||||
VariantAnalysisSkippedRepositoryGroup as ApiVariantAnalysisSkippedRepositoryGroup,
|
||||
VariantAnalysisNotFoundRepositoryGroup as ApiVariantAnalysisNotFoundRepositoryGroup
|
||||
VariantAnalysisNotFoundRepositoryGroup as ApiVariantAnalysisNotFoundRepositoryGroup,
|
||||
VariantAnalysisRepoTask as ApiVariantAnalysisRepoTask,
|
||||
} from './gh-api/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
@@ -16,7 +17,8 @@ import {
|
||||
VariantAnalysisStatus,
|
||||
VariantAnalysisRepoStatus,
|
||||
VariantAnalysisSubmission,
|
||||
VariantAnalysisSkippedRepositoryGroup
|
||||
VariantAnalysisSkippedRepositoryGroup,
|
||||
VariantAnalysisRepositoryTask
|
||||
} from './shared/variant-analysis';
|
||||
|
||||
export function processVariantAnalysis(
|
||||
@@ -76,10 +78,28 @@ export function processUpdatedVariantAnalysis(
|
||||
return variantAnalysis;
|
||||
}
|
||||
|
||||
function processScannedRepositories(
|
||||
scannedRepos: ApiVariantAnalysisScannedRepository[]
|
||||
): VariantAnalysisScannedRepository[] {
|
||||
return scannedRepos.map(scannedRepo => {
|
||||
export function processVariantAnalysisRepositoryTask(
|
||||
response: ApiVariantAnalysisRepoTask
|
||||
): VariantAnalysisRepositoryTask {
|
||||
return {
|
||||
repository: {
|
||||
id: response.repository.id,
|
||||
fullName: response.repository.full_name,
|
||||
private: response.repository.private,
|
||||
},
|
||||
analysisStatus: processApiRepoStatus(response.analysis_status),
|
||||
resultCount: response.result_count,
|
||||
artifactSizeInBytes: response.artifact_size_in_bytes,
|
||||
failureMessage: response.failure_message,
|
||||
databaseCommitSha: response.database_commit_sha,
|
||||
sourceLocationPrefix: response.source_location_prefix,
|
||||
artifactUrl: response.artifact_url,
|
||||
};
|
||||
}
|
||||
|
||||
export function processScannedRepository(
|
||||
scannedRepo: ApiVariantAnalysisScannedRepository
|
||||
): VariantAnalysisScannedRepository {
|
||||
return {
|
||||
repository: {
|
||||
id: scannedRepo.repository.id,
|
||||
@@ -93,7 +113,12 @@ function processScannedRepositories(
|
||||
artifactSizeInBytes: scannedRepo.artifact_size_in_bytes,
|
||||
failureMessage: scannedRepo.failure_message
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function processScannedRepositories(
|
||||
scannedRepos: ApiVariantAnalysisScannedRepository[]
|
||||
): VariantAnalysisScannedRepository[] {
|
||||
return scannedRepos.map(scannedRepo => processScannedRepository(scannedRepo));
|
||||
}
|
||||
|
||||
function processSkippedRepositories(
|
||||
|
||||
@@ -9,9 +9,12 @@ import { sarifParser } from '../sarif-parser';
|
||||
import { extractAnalysisAlerts } from './sarif-processing';
|
||||
import { CodeQLCliServer } from '../cli';
|
||||
import { extractRawResults } from './bqrs-processing';
|
||||
import { VariantAnalysis, VariantAnalysisScannedRepositoryResult } from './shared/variant-analysis';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisRepositoryTask,
|
||||
VariantAnalysisScannedRepositoryResult
|
||||
} from './shared/variant-analysis';
|
||||
import { DisposableObject, DisposeHandler } from '../pure/disposable-object';
|
||||
import { VariantAnalysisRepoTask } from './gh-api/variant-analysis';
|
||||
import * as ghApiClient from './gh-api/gh-api-client';
|
||||
import { EventEmitter } from 'vscode';
|
||||
import { unzipFile } from '../pure/zip';
|
||||
@@ -22,7 +25,7 @@ const createCacheKey = (variantAnalysisId: number, repositoryFullName: string):
|
||||
|
||||
export type ResultDownloadedEvent = {
|
||||
variantAnalysisId: number;
|
||||
repoTask: VariantAnalysisRepoTask;
|
||||
repoTask: VariantAnalysisRepositoryTask;
|
||||
}
|
||||
|
||||
export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
@@ -48,18 +51,18 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
public async download(
|
||||
credentials: Credentials,
|
||||
variantAnalysisId: number,
|
||||
repoTask: VariantAnalysisRepoTask,
|
||||
repoTask: VariantAnalysisRepositoryTask,
|
||||
variantAnalysisStoragePath: string,
|
||||
): Promise<void> {
|
||||
if (!repoTask.artifact_url) {
|
||||
if (!repoTask.artifactUrl) {
|
||||
throw new Error('Missing artifact URL');
|
||||
}
|
||||
|
||||
const resultDirectory = this.getRepoStorageDirectory(variantAnalysisStoragePath, repoTask.repository.full_name);
|
||||
const resultDirectory = this.getRepoStorageDirectory(variantAnalysisStoragePath, repoTask.repository.fullName);
|
||||
|
||||
const result = await ghApiClient.getVariantAnalysisRepoResult(
|
||||
credentials,
|
||||
repoTask.artifact_url
|
||||
repoTask.artifactUrl
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(resultDirectory))) {
|
||||
@@ -112,13 +115,13 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
|
||||
const storageDirectory = this.getRepoStorageDirectory(variantAnalysisStoragePath, repositoryFullName);
|
||||
|
||||
const repoTask: VariantAnalysisRepoTask = await fs.readJson(path.join(storageDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME));
|
||||
const repoTask: VariantAnalysisRepositoryTask = await fs.readJson(path.join(storageDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME));
|
||||
|
||||
if (!repoTask.database_commit_sha || !repoTask.source_location_prefix) {
|
||||
if (!repoTask.databaseCommitSha || !repoTask.sourceLocationPrefix) {
|
||||
throw new Error('Missing database commit SHA');
|
||||
}
|
||||
|
||||
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(repoTask.repository.full_name, repoTask.database_commit_sha);
|
||||
const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(repoTask.repository.fullName, repoTask.databaseCommitSha);
|
||||
|
||||
const resultsDirectory = path.join(storageDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY);
|
||||
const sarifPath = path.join(resultsDirectory, 'results.sarif');
|
||||
@@ -134,7 +137,7 @@ export class VariantAnalysisResultsManager extends DisposableObject {
|
||||
}
|
||||
|
||||
if (await fs.pathExists(bqrsPath)) {
|
||||
const rawResults = await this.readBqrsResults(bqrsPath, fileLinkPrefix, repoTask.source_location_prefix);
|
||||
const rawResults = await this.readBqrsResults(bqrsPath, fileLinkPrefix, repoTask.sourceLocationPrefix);
|
||||
|
||||
return {
|
||||
variantAnalysisId,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ComponentMeta } from '@storybook/react';
|
||||
import RepositoriesSearchComponent from '../../view/remote-queries/RepositoriesSearch';
|
||||
|
||||
export default {
|
||||
title: 'Repositories Search',
|
||||
title: 'MRVA/Repositories Search',
|
||||
component: RepositoriesSearchComponent,
|
||||
argTypes: {
|
||||
filterValue: {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { RepositoriesSearch as RepositoriesSearchComponent } from '../../view/variant-analysis/RepositoriesSearch';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Repositories Search',
|
||||
component: RepositoriesSearchComponent,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof RepositoriesSearchComponent>;
|
||||
|
||||
export const RepositoriesSearch = () => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<RepositoriesSearchComponent value={value} onChange={setValue} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { RepositoriesSearchSortRow as RepositoriesSearchSortRowComponent } from '../../view/variant-analysis/RepositoriesSearchSortRow';
|
||||
import { defaultFilterSortState } from '../../view/variant-analysis/filterSort';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Repositories Search and Sort Row',
|
||||
component: RepositoriesSearchSortRowComponent,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof RepositoriesSearchSortRowComponent>;
|
||||
|
||||
export const RepositoriesSearchSortRow = () => {
|
||||
const [value, setValue] = useState(defaultFilterSortState);
|
||||
|
||||
return (
|
||||
<RepositoriesSearchSortRowComponent value={value} onChange={setValue} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { RepositoriesSort as RepositoriesSortComponent } from '../../view/variant-analysis/RepositoriesSort';
|
||||
import { SortKey } from '../../view/variant-analysis/filterSort';
|
||||
|
||||
export default {
|
||||
title: 'Variant Analysis/Repositories Sort',
|
||||
component: RepositoriesSortComponent,
|
||||
argTypes: {
|
||||
value: {
|
||||
control: {
|
||||
disable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} as ComponentMeta<typeof RepositoriesSortComponent>;
|
||||
|
||||
export const RepositoriesSort = () => {
|
||||
const [value, setValue] = useState(SortKey.Name);
|
||||
|
||||
return (
|
||||
<RepositoriesSortComponent value={value} onChange={setValue} />
|
||||
);
|
||||
};
|
||||
@@ -246,3 +246,13 @@ FullExample.args = {
|
||||
repoStates,
|
||||
repoResults,
|
||||
};
|
||||
|
||||
export const FullExampleWithoutSkipped = Template.bind({});
|
||||
FullExampleWithoutSkipped.args = {
|
||||
variantAnalysis: {
|
||||
...variantAnalysis,
|
||||
skippedRepos: {},
|
||||
},
|
||||
repoStates,
|
||||
repoResults,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react';
|
||||
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { VariantAnalysisContainer } from '../../view/variant-analysis/VariantAnalysisContainer';
|
||||
import { VariantAnalysisAnalyzedRepos } from '../../view/variant-analysis/VariantAnalysisAnalyzedRepos';
|
||||
import {
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
import { AnalysisAlert } from '../../remote-queries/shared/analysis-result';
|
||||
import { createMockVariantAnalysis } from '../../vscode-tests/factories/remote-queries/shared/variant-analysis';
|
||||
import { createMockRepositoryWithMetadata } from '../../vscode-tests/factories/remote-queries/shared/repository';
|
||||
import { createMockScannedRepo } from '../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
|
||||
import analysesResults from '../remote-queries/data/analysesResultsMessage.json';
|
||||
|
||||
@@ -114,5 +117,40 @@ Example.args = {
|
||||
interpretedResults: interpretedResultsForRepo('expressjs/express'),
|
||||
}
|
||||
]
|
||||
}
|
||||
;
|
||||
};
|
||||
|
||||
faker.seed(42);
|
||||
const uniqueStore = {};
|
||||
|
||||
const manyScannedRepos = Array.from({ length: 1000 }, (_, i) => {
|
||||
const mockedScannedRepo = createMockScannedRepo();
|
||||
|
||||
return {
|
||||
...mockedScannedRepo,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
resultCount: faker.datatype.number({ min: 0, max: 1000 }),
|
||||
repository: {
|
||||
...mockedScannedRepo.repository,
|
||||
// We need to ensure the ID is unique for React keys
|
||||
id: faker.helpers.unique(faker.datatype.number, [], {
|
||||
store: uniqueStore,
|
||||
}),
|
||||
fullName: `octodemo/${faker.helpers.unique(faker.random.word, [], {
|
||||
store: uniqueStore,
|
||||
})}`,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const PerformanceExample = Template.bind({});
|
||||
PerformanceExample.args = {
|
||||
variantAnalysis: {
|
||||
...createMockVariantAnalysis(VariantAnalysisStatus.Succeeded, manyScannedRepos),
|
||||
id: 1,
|
||||
},
|
||||
repositoryResults: manyScannedRepos.map(repoTask => ({
|
||||
variantAnalysisId: 1,
|
||||
repositoryId: repoTask.repository.id,
|
||||
interpretedResults: interpretedResultsForRepo('facebook/create-react-app'),
|
||||
}))
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ type Props = {
|
||||
name: string;
|
||||
label: string;
|
||||
className?: string;
|
||||
slot?: string;
|
||||
};
|
||||
|
||||
const CodiconIcon = styled.span`
|
||||
@@ -15,5 +16,6 @@ const CodiconIcon = styled.span`
|
||||
export const Codicon = ({
|
||||
name,
|
||||
label,
|
||||
className
|
||||
}: Props) => <CodiconIcon role="img" aria-label={label} className={classNames('codicon', `codicon-${name}`, className)} />;
|
||||
className,
|
||||
slot,
|
||||
}: Props) => <CodiconIcon role="img" aria-label={label} className={classNames('codicon', `codicon-${name}`, className)} slot={slot} />;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react';
|
||||
import { Codicon } from '../common';
|
||||
|
||||
const TextField = styled(VSCodeTextField)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RepositoriesSearch = ({ value, onChange, className }: Props) => {
|
||||
const handleInput = useCallback((e: InputEvent) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
onChange(target.value);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<TextField
|
||||
placeholder='Filter by repository owner/name'
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
className={className}
|
||||
>
|
||||
<Codicon name="search" label="Search..." slot="start" />
|
||||
</TextField>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { RepositoriesFilterSortState, SortKey } from './filterSort';
|
||||
import { RepositoriesSearch } from './RepositoriesSearch';
|
||||
import { RepositoriesSort } from './RepositoriesSort';
|
||||
|
||||
type Props = {
|
||||
value: RepositoriesFilterSortState;
|
||||
onChange: Dispatch<SetStateAction<RepositoriesFilterSortState>>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
`;
|
||||
|
||||
const RepositoriesSearchColumn = styled(RepositoriesSearch)`
|
||||
flex: 3;
|
||||
`;
|
||||
|
||||
const RepositoriesSortColumn = styled(RepositoriesSort)`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const RepositoriesSearchSortRow = ({ value, onChange }: Props) => {
|
||||
const handleSearchValueChange = useCallback((searchValue: string) => {
|
||||
onChange(oldValue => ({
|
||||
...oldValue,
|
||||
searchValue,
|
||||
}));
|
||||
}, [onChange]);
|
||||
|
||||
const handleSortKeyChange = useCallback((sortKey: SortKey) => {
|
||||
onChange(oldValue => ({
|
||||
...oldValue,
|
||||
sortKey,
|
||||
}));
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<RepositoriesSearchColumn value={value.searchValue} onChange={handleSearchValueChange} />
|
||||
<RepositoriesSortColumn value={value.sortKey} onChange={handleSortKeyChange} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeDropdown, VSCodeOption } from '@vscode/webview-ui-toolkit/react';
|
||||
import { SortKey } from './filterSort';
|
||||
import { Codicon } from '../common';
|
||||
|
||||
const Dropdown = styled(VSCodeDropdown)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
value: SortKey;
|
||||
onChange: (value: SortKey) => void;
|
||||
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const RepositoriesSort = ({ value, onChange, className }: Props) => {
|
||||
const handleInput = useCallback((e: InputEvent) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
|
||||
onChange(target.value as SortKey);
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
className={className}
|
||||
>
|
||||
<Codicon name="sort-precedence" label="Sort..." slot="indicator" />
|
||||
<VSCodeOption value={SortKey.Name}>Name</VSCodeOption>
|
||||
<VSCodeOption value={SortKey.ResultsCount}>Results</VSCodeOption>
|
||||
<VSCodeOption value={SortKey.Stars}>Stars</VSCodeOption>
|
||||
<VSCodeOption value={SortKey.LastUpdated}>Last commit</VSCodeOption>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { RepoRow } from './RepoRow';
|
||||
import {
|
||||
@@ -6,7 +7,7 @@ import {
|
||||
VariantAnalysisScannedRepositoryResult,
|
||||
VariantAnalysisScannedRepositoryState
|
||||
} from '../../remote-queries/shared/variant-analysis';
|
||||
import { useMemo } from 'react';
|
||||
import { compareWithResults, matchesFilter, RepositoriesFilterSortState } from './filterSort';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
@@ -19,12 +20,15 @@ export type VariantAnalysisAnalyzedReposProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
repositoryStates?: VariantAnalysisScannedRepositoryState[];
|
||||
repositoryResults?: VariantAnalysisScannedRepositoryResult[];
|
||||
|
||||
filterSortState?: RepositoriesFilterSortState;
|
||||
}
|
||||
|
||||
export const VariantAnalysisAnalyzedRepos = ({
|
||||
variantAnalysis,
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
filterSortState,
|
||||
}: VariantAnalysisAnalyzedReposProps) => {
|
||||
const repositoryStateById = useMemo(() => {
|
||||
const map = new Map<number, VariantAnalysisScannedRepositoryState>();
|
||||
@@ -42,9 +46,15 @@ export const VariantAnalysisAnalyzedRepos = ({
|
||||
return map;
|
||||
}, [repositoryResults]);
|
||||
|
||||
const repositories = useMemo(() => {
|
||||
return variantAnalysis.scannedRepos?.filter((repoTask) => {
|
||||
return matchesFilter(repoTask.repository, filterSortState);
|
||||
})?.sort(compareWithResults(filterSortState));
|
||||
}, [filterSortState, variantAnalysis.scannedRepos]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{variantAnalysis.scannedRepos?.map(repository => {
|
||||
{repositories?.map(repository => {
|
||||
const state = repositoryStateById.get(repository.repository.id);
|
||||
const results = repositoryResultsById.get(repository.repository.id);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VSCodeBadge, VSCodePanels, VSCodePanelTab, VSCodePanelView } from '@vscode/webview-ui-toolkit/react';
|
||||
import { formatDecimal } from '../../pure/number';
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
import { VariantAnalysisAnalyzedRepos } from './VariantAnalysisAnalyzedRepos';
|
||||
import { Alert } from '../common';
|
||||
import { VariantAnalysisSkippedRepositoriesTab } from './VariantAnalysisSkippedRepositoriesTab';
|
||||
import { defaultFilterSortState, RepositoriesFilterSortState } from './filterSort';
|
||||
import { RepositoriesSearchSortRow } from './RepositoriesSearchSortRow';
|
||||
|
||||
export type VariantAnalysisOutcomePanelProps = {
|
||||
variantAnalysis: VariantAnalysis;
|
||||
@@ -42,6 +45,8 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
repositoryStates,
|
||||
repositoryResults,
|
||||
}: VariantAnalysisOutcomePanelProps) => {
|
||||
const [filterSortState, setFilterSortState] = useState<RepositoriesFilterSortState>(defaultFilterSortState);
|
||||
|
||||
const noCodeqlDbRepos = variantAnalysis.skippedRepos?.noCodeqlDbRepos;
|
||||
const notFoundRepos = variantAnalysis.skippedRepos?.notFoundRepos;
|
||||
const overLimitRepositoryCount = variantAnalysis.skippedRepos?.overLimitRepos?.repositoryCount ?? 0;
|
||||
@@ -70,10 +75,12 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
return (
|
||||
<>
|
||||
{warnings}
|
||||
<RepositoriesSearchSortRow value={filterSortState} onChange={setFilterSortState} />
|
||||
<VariantAnalysisAnalyzedRepos
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -82,6 +89,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
return (
|
||||
<>
|
||||
{warnings}
|
||||
<RepositoriesSearchSortRow value={filterSortState} onChange={setFilterSortState} />
|
||||
<VSCodePanels>
|
||||
<Tab>
|
||||
Analyzed
|
||||
@@ -104,6 +112,7 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
variantAnalysis={variantAnalysis}
|
||||
repositoryStates={repositoryStates}
|
||||
repositoryResults={repositoryResults}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</VSCodePanelView>
|
||||
{notFoundRepos?.repositoryCount &&
|
||||
@@ -111,14 +120,18 @@ export const VariantAnalysisOutcomePanels = ({
|
||||
<VariantAnalysisSkippedRepositoriesTab
|
||||
alertTitle='No access'
|
||||
alertMessage='The following repositories could not be scanned because you do not have read access.'
|
||||
skippedRepositoryGroup={notFoundRepos} />
|
||||
skippedRepositoryGroup={notFoundRepos}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</VSCodePanelView>}
|
||||
{noCodeqlDbRepos?.repositoryCount &&
|
||||
<VSCodePanelView>
|
||||
<VariantAnalysisSkippedRepositoriesTab
|
||||
alertTitle='No database'
|
||||
alertMessage='The following repositories could not be scanned because they do not have an available CodeQL database.'
|
||||
skippedRepositoryGroup={noCodeqlDbRepos} />
|
||||
skippedRepositoryGroup={noCodeqlDbRepos}
|
||||
filterSortState={filterSortState}
|
||||
/>
|
||||
</VSCodePanelView>}
|
||||
</VSCodePanels>
|
||||
</>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { VariantAnalysisSkippedRepositoryGroup } from '../../remote-queries/shared/variant-analysis';
|
||||
import { Alert } from '../common';
|
||||
import { RepoRow } from './RepoRow';
|
||||
import { compareRepository, matchesFilter, RepositoriesFilterSortState } from './filterSort';
|
||||
|
||||
export type VariantAnalysisSkippedRepositoriesTabProps = {
|
||||
alertTitle: string,
|
||||
alertMessage: string,
|
||||
skippedRepositoryGroup: VariantAnalysisSkippedRepositoryGroup,
|
||||
|
||||
filterSortState?: RepositoriesFilterSortState,
|
||||
};
|
||||
|
||||
function getSkipReasonAlert(
|
||||
@@ -39,11 +43,18 @@ export const VariantAnalysisSkippedRepositoriesTab = ({
|
||||
alertTitle,
|
||||
alertMessage,
|
||||
skippedRepositoryGroup,
|
||||
filterSortState,
|
||||
}: VariantAnalysisSkippedRepositoriesTabProps) => {
|
||||
const repositories = useMemo(() => {
|
||||
return skippedRepositoryGroup.repositories?.filter((repo) => {
|
||||
return matchesFilter(repo, filterSortState);
|
||||
})?.sort(compareRepository(filterSortState));
|
||||
}, [filterSortState, skippedRepositoryGroup.repositories]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{getSkipReasonAlert(alertTitle, alertMessage, skippedRepositoryGroup)}
|
||||
{skippedRepositoryGroup.repositories.map((repo) =>
|
||||
{repositories.map((repo) =>
|
||||
<RepoRow key={`repo/${repo.fullName}`} repository={repo} />
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { VariantAnalysisAnalyzedRepos, VariantAnalysisAnalyzedReposProps } from
|
||||
import { createMockVariantAnalysis } from '../../../vscode-tests/factories/remote-queries/shared/variant-analysis';
|
||||
import { createMockRepositoryWithMetadata } from '../../../vscode-tests/factories/remote-queries/shared/repository';
|
||||
import { createMockScannedRepo } from '../../../vscode-tests/factories/remote-queries/shared/scanned-repositories';
|
||||
import { defaultFilterSortState, SortKey } from '../filterSort';
|
||||
|
||||
describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
const defaultVariantAnalysis = createMockVariantAnalysis({
|
||||
@@ -22,7 +23,9 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 1,
|
||||
fullName: 'octodemo/hello-world-1',
|
||||
private: false,
|
||||
stargazersCount: 5_000,
|
||||
},
|
||||
resultCount: undefined,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
},
|
||||
{
|
||||
@@ -32,7 +35,9 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 2,
|
||||
fullName: 'octodemo/hello-world-2',
|
||||
private: false,
|
||||
stargazersCount: 20_000,
|
||||
},
|
||||
resultCount: 200,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
@@ -42,7 +47,9 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 3,
|
||||
fullName: 'octodemo/hello-world-3',
|
||||
private: true,
|
||||
stargazersCount: 20,
|
||||
},
|
||||
resultCount: undefined,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Failed,
|
||||
},
|
||||
{
|
||||
@@ -52,9 +59,35 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
id: 4,
|
||||
fullName: 'octodemo/hello-world-4',
|
||||
private: false,
|
||||
stargazersCount: 8_000,
|
||||
},
|
||||
resultCount: undefined,
|
||||
analysisStatus: VariantAnalysisRepoStatus.InProgress,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 5,
|
||||
fullName: 'octodemo/hello-world-5',
|
||||
private: false,
|
||||
stargazersCount: 50_000,
|
||||
},
|
||||
resultCount: 55_323,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
{
|
||||
...createMockScannedRepo(),
|
||||
repository: {
|
||||
...createMockRepositoryWithMetadata(),
|
||||
id: 6,
|
||||
fullName: 'octodemo/hello-world-6',
|
||||
private: false,
|
||||
stargazersCount: 1,
|
||||
},
|
||||
resultCount: 10_000,
|
||||
analysisStatus: VariantAnalysisRepoStatus.Succeeded,
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
@@ -117,4 +150,56 @@ describe(VariantAnalysisAnalyzedRepos.name, () => {
|
||||
}));
|
||||
expect(screen.getByText('This is an empty block.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the search value', () => {
|
||||
render({
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
searchValue: 'world-2',
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.queryByText('octodemo/hello-world-1')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('octodemo/hello-world-2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('octodemo/hello-world-3')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('octodemo/hello-world-4')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the sort key', async () => {
|
||||
render({
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(6);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-world-5');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-world-2');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-world-4');
|
||||
expect(rows[3]).toHaveTextContent('octodemo/hello-world-1');
|
||||
expect(rows[4]).toHaveTextContent('octodemo/hello-world-3');
|
||||
expect(rows[5]).toHaveTextContent('octodemo/hello-world-6');
|
||||
});
|
||||
|
||||
it('uses the results count sort key', async () => {
|
||||
render({
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(6);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-world-5');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-world-6');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-world-2');
|
||||
expect(rows[3]).toHaveTextContent('octodemo/hello-world-1');
|
||||
expect(rows[4]).toHaveTextContent('octodemo/hello-world-3');
|
||||
expect(rows[5]).toHaveTextContent('octodemo/hello-world-4');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { render as reactRender, screen } from '@testing-library/react';
|
||||
import { VariantAnalysisSkippedRepositoriesTab, VariantAnalysisSkippedRepositoriesTabProps } from '../VariantAnalysisSkippedRepositoriesTab';
|
||||
import { defaultFilterSortState, SortKey } from '../filterSort';
|
||||
|
||||
describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
|
||||
const render = (props: VariantAnalysisSkippedRepositoriesTabProps) =>
|
||||
@@ -97,4 +98,100 @@ describe(VariantAnalysisSkippedRepositoriesTab.name, () => {
|
||||
expect(screen.getByText('octodemo/hello-galaxy')).toBeInTheDocument();
|
||||
expect(screen.getByText('octodemo/hello-universe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the search value', async () => {
|
||||
render({
|
||||
alertTitle: 'No database',
|
||||
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-galaxy',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-universe',
|
||||
},
|
||||
],
|
||||
},
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
searchValue: 'world',
|
||||
}
|
||||
});
|
||||
|
||||
expect(screen.getByText('octodemo/hello-world')).toBeInTheDocument();
|
||||
expect(screen.queryByText('octodemo/hello-galaxy')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('octodemo/hello-universe')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the sort key', async () => {
|
||||
render({
|
||||
alertTitle: 'No database',
|
||||
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
stargazersCount: 300,
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-galaxy',
|
||||
stargazersCount: 50,
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-universe',
|
||||
stargazersCount: 500,
|
||||
},
|
||||
],
|
||||
},
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-universe');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-world');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-galaxy');
|
||||
});
|
||||
|
||||
it('does not use the result count sort key', async () => {
|
||||
render({
|
||||
alertTitle: 'No database',
|
||||
alertMessage: 'The following repositories could not be scanned because they do not have an available CodeQL database.',
|
||||
skippedRepositoryGroup: {
|
||||
repositoryCount: 1,
|
||||
repositories: [
|
||||
{
|
||||
fullName: 'octodemo/hello-world',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-galaxy',
|
||||
},
|
||||
{
|
||||
fullName: 'octodemo/hello-universe',
|
||||
},
|
||||
],
|
||||
},
|
||||
filterSortState: {
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
}
|
||||
});
|
||||
|
||||
const rows = screen.queryAllByRole('button');
|
||||
|
||||
expect(rows).toHaveLength(3);
|
||||
expect(rows[0]).toHaveTextContent('octodemo/hello-galaxy');
|
||||
expect(rows[1]).toHaveTextContent('octodemo/hello-universe');
|
||||
expect(rows[2]).toHaveTextContent('octodemo/hello-world');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import { compareRepository, compareWithResults, defaultFilterSortState, matchesFilter, SortKey } from '../filterSort';
|
||||
|
||||
describe(matchesFilter.name, () => {
|
||||
const repository = {
|
||||
fullName: 'github/codeql'
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
{ searchValue: '', matches: true },
|
||||
{ searchValue: 'github/codeql', matches: true },
|
||||
{ searchValue: 'github', matches: true },
|
||||
{ searchValue: 'git', matches: true },
|
||||
{ searchValue: 'codeql', matches: true },
|
||||
{ searchValue: 'code', matches: true },
|
||||
{ searchValue: 'ql', matches: true },
|
||||
{ searchValue: '/', matches: true },
|
||||
{ searchValue: 'gothub/codeql', matches: false },
|
||||
{ searchValue: 'hello', matches: false },
|
||||
{ searchValue: 'cod*ql', matches: false },
|
||||
{ searchValue: 'cod?ql', matches: false },
|
||||
];
|
||||
|
||||
test.each(testCases)('returns $matches if searching for $searchValue', ({ searchValue, matches }) => {
|
||||
expect(matchesFilter(repository, {
|
||||
...defaultFilterSortState,
|
||||
searchValue,
|
||||
})).toBe(matches);
|
||||
});
|
||||
});
|
||||
|
||||
describe(compareRepository.name, () => {
|
||||
describe('when sort key is undefined', () => {
|
||||
const sorter = compareRepository(undefined);
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy'
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world'
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is name', () => {
|
||||
const sorter = compareRepository({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Name,
|
||||
});
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy'
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world'
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is stars', () => {
|
||||
const sorter = compareRepository({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
});
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy',
|
||||
stargazersCount: 1,
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world',
|
||||
stargazersCount: 10,
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
|
||||
it('compares equal single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
stargazersCount: left.stargazersCount,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares missing single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
stargazersCount: undefined,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is last updated', () => {
|
||||
const sorter = compareRepository({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.LastUpdated,
|
||||
});
|
||||
|
||||
const left = {
|
||||
fullName: 'github/galaxy',
|
||||
updatedAt: '2020-01-01T00:00:00Z',
|
||||
};
|
||||
const right = {
|
||||
fullName: 'github/world',
|
||||
updatedAt: '2021-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
|
||||
it('compares equal single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
updatedAt: left.updatedAt,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares missing single values correctly', () => {
|
||||
expect(sorter({
|
||||
...left,
|
||||
updatedAt: undefined,
|
||||
}, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(compareWithResults.name, () => {
|
||||
describe('when sort key is undefined', () => {
|
||||
const sorter = compareWithResults(undefined);
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
},
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is stars', () => {
|
||||
const sorter = compareWithResults({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.Stars,
|
||||
});
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
stargazersCount: 1,
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
stargazersCount: 10,
|
||||
},
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is last updated', () => {
|
||||
const sorter = compareWithResults({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.LastUpdated,
|
||||
});
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
updatedAt: '2020-01-01T00:00:00Z',
|
||||
},
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
updatedAt: '2021-01-01T00:00:00Z',
|
||||
},
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sort key is results count', () => {
|
||||
const sorter = compareWithResults({
|
||||
...defaultFilterSortState,
|
||||
sortKey: SortKey.ResultsCount,
|
||||
});
|
||||
|
||||
const left = {
|
||||
repository: {
|
||||
fullName: 'github/galaxy',
|
||||
},
|
||||
resultCount: 10,
|
||||
};
|
||||
const right = {
|
||||
repository: {
|
||||
fullName: 'github/world',
|
||||
},
|
||||
resultCount: 100,
|
||||
};
|
||||
|
||||
it('compares correctly', () => {
|
||||
expect(sorter(left, right)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('compares the inverse correctly', () => {
|
||||
expect(sorter(right, left)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares equal values correctly', () => {
|
||||
expect(sorter(left, left)).toBe(0);
|
||||
});
|
||||
|
||||
it('compares equal single values correctly', () => {
|
||||
expect(sorter(left, {
|
||||
...right,
|
||||
resultCount: left.resultCount,
|
||||
})).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('compares missing single values correctly', () => {
|
||||
expect(sorter({
|
||||
...left,
|
||||
resultCount: undefined,
|
||||
}, right)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
73
extensions/ql-vscode/src/view/variant-analysis/filterSort.ts
Normal file
73
extensions/ql-vscode/src/view/variant-analysis/filterSort.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Repository, RepositoryWithMetadata } from '../../remote-queries/shared/repository';
|
||||
import { parseDate } from '../../pure/date';
|
||||
|
||||
export enum SortKey {
|
||||
Name = 'name',
|
||||
Stars = 'stars',
|
||||
LastUpdated = 'lastUpdated',
|
||||
ResultsCount = 'resultsCount',
|
||||
}
|
||||
|
||||
export type RepositoriesFilterSortState = {
|
||||
searchValue: string;
|
||||
sortKey: SortKey;
|
||||
}
|
||||
|
||||
export const defaultFilterSortState: RepositoriesFilterSortState = {
|
||||
searchValue: '',
|
||||
sortKey: SortKey.Name,
|
||||
};
|
||||
|
||||
export function matchesFilter(repo: Pick<Repository, 'fullName'>, filterSortState: RepositoriesFilterSortState | undefined): boolean {
|
||||
if (!filterSortState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return repo.fullName.toLowerCase().includes(filterSortState.searchValue.toLowerCase());
|
||||
}
|
||||
|
||||
type SortableRepository = Pick<Repository, 'fullName'> & Partial<Pick<RepositoryWithMetadata, 'stargazersCount' | 'updatedAt'>>;
|
||||
|
||||
export function compareRepository(filterSortState: RepositoriesFilterSortState | undefined): (left: SortableRepository, right: SortableRepository) => number {
|
||||
return (left: SortableRepository, right: SortableRepository) => {
|
||||
// Highest to lowest
|
||||
if (filterSortState?.sortKey === SortKey.Stars) {
|
||||
const stargazersCount = (right.stargazersCount ?? 0) - (left.stargazersCount ?? 0);
|
||||
if (stargazersCount !== 0) {
|
||||
return stargazersCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Newest to oldest
|
||||
if (filterSortState?.sortKey === SortKey.LastUpdated) {
|
||||
const lastUpdated = (parseDate(right.updatedAt)?.getTime() ?? 0) - (parseDate(left.updatedAt)?.getTime() ?? 0);
|
||||
if (lastUpdated !== 0) {
|
||||
return lastUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back on name compare
|
||||
return left.fullName.localeCompare(right.fullName, undefined, { sensitivity: 'base' });
|
||||
};
|
||||
}
|
||||
|
||||
type SortableResult = {
|
||||
repository: SortableRepository;
|
||||
resultCount?: number;
|
||||
}
|
||||
|
||||
export function compareWithResults(filterSortState: RepositoriesFilterSortState | undefined): (left: SortableResult, right: SortableResult) => number {
|
||||
const fallbackSort = compareRepository(filterSortState);
|
||||
|
||||
return (left: SortableResult, right: SortableResult) => {
|
||||
// Highest to lowest
|
||||
if (filterSortState?.sortKey === SortKey.ResultsCount) {
|
||||
const resultCount = (right.resultCount ?? 0) - (left.resultCount ?? 0);
|
||||
if (resultCount !== 0) {
|
||||
return resultCount;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackSort(left.repository, right.repository);
|
||||
};
|
||||
}
|
||||
@@ -10,24 +10,21 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
|
||||
import {
|
||||
VariantAnalysis as VariantAnalysisApiResponse,
|
||||
VariantAnalysisRepoTask,
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
|
||||
} from '../../../remote-queries/gh-api/variant-analysis';
|
||||
import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
|
||||
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
|
||||
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
|
||||
import { CodeQLCliServer } from '../../../cli';
|
||||
import { storagePath } from '../global.helper';
|
||||
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
|
||||
import {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus
|
||||
} from '../../../remote-queries/shared/variant-analysis';
|
||||
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 {
|
||||
VariantAnalysis,
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisScannedRepositoryDownloadStatus,
|
||||
VariantAnalysisStatus,
|
||||
} from '../../../remote-queries/shared/variant-analysis';
|
||||
import { createTimestampFile } from '../../../helpers';
|
||||
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
|
||||
import { VariantAnalysisRepoTask } from '../../../remote-queries/gh-api/variant-analysis';
|
||||
|
||||
describe('Variant Analysis Manager', async function() {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
@@ -37,8 +34,8 @@ describe('Variant Analysis Manager', async function() {
|
||||
let cli: CodeQLCliServer;
|
||||
let cancellationTokenSource: CancellationTokenSource;
|
||||
let variantAnalysisManager: VariantAnalysisManager;
|
||||
let variantAnalysisApiResponse: VariantAnalysisApiResponse;
|
||||
let scannedRepos: ApiVariantAnalysisScannedRepository[];
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
let scannedRepos: VariantAnalysisScannedRepository[];
|
||||
let getVariantAnalysisRepoStub: sinon.SinonStub;
|
||||
let getVariantAnalysisRepoResultStub: sinon.SinonStub;
|
||||
let variantAnalysisResultsManager: VariantAnalysisResultsManager;
|
||||
@@ -56,7 +53,10 @@ describe('Variant Analysis Manager', async function() {
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
scannedRepos = createMockScannedRepos();
|
||||
variantAnalysisApiResponse = createMockApiResponse('in_progress', scannedRepos);
|
||||
variantAnalysis = createMockVariantAnalysis({
|
||||
status: VariantAnalysisStatus.InProgress,
|
||||
scannedRepos,
|
||||
});
|
||||
|
||||
try {
|
||||
const extension = await extensions.getExtension<CodeQLExtensionInterface | Record<string, never>>('GitHub.vscode-codeql')!.activate();
|
||||
@@ -147,7 +147,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
try {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
} catch (error: any) {
|
||||
@@ -184,7 +184,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
it('should not try to download the result', async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
@@ -208,7 +208,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
@@ -218,7 +218,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
it('should fetch a repo task', async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
@@ -228,7 +228,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
it('should fetch a repo result', async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
// First, do a download so it is downloaded. This avoids having to mock the repo states.
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
@@ -247,7 +247,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
@@ -257,11 +257,11 @@ describe('Variant Analysis Manager', async function() {
|
||||
it('should write the repo state when the download is successful', async () => {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysisApiResponse.id.toString(), 'repo_states.json'), {
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
|
||||
[scannedRepos[0].repository.id]: {
|
||||
repositoryId: scannedRepos[0].repository.id,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
@@ -275,7 +275,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
try {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
fail('Expected an error to be thrown');
|
||||
@@ -292,7 +292,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
try {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
fail('Expected an error to be thrown');
|
||||
@@ -304,11 +304,11 @@ describe('Variant Analysis Manager', async function() {
|
||||
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[1],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysisApiResponse.id.toString(), 'repo_states.json'), {
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
|
||||
[scannedRepos[0].repository.id]: {
|
||||
repositoryId: scannedRepos[0].repository.id,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
|
||||
@@ -326,7 +326,7 @@ describe('Variant Analysis Manager', async function() {
|
||||
try {
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
fail('Expected an error to be thrown');
|
||||
@@ -338,11 +338,11 @@ describe('Variant Analysis Manager', async function() {
|
||||
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[1],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysisApiResponse.id.toString(), 'repo_states.json'), {
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
|
||||
[scannedRepos[0].repository.id]: {
|
||||
repositoryId: scannedRepos[0].repository.id,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Failed,
|
||||
@@ -359,9 +359,9 @@ describe('Variant Analysis Manager', async function() {
|
||||
// The actual tests for these are in rehydrateVariantAnalysis, so we can just mock them here and test that
|
||||
// the methods are called.
|
||||
|
||||
pathExistsStub.withArgs(path.join(storagePath, variantAnalysisApiResponse.id.toString())).resolves(true);
|
||||
pathExistsStub.withArgs(path.join(storagePath, variantAnalysis.id.toString())).resolves(true);
|
||||
// This will read in the correct repo states
|
||||
readJsonStub.withArgs(path.join(storagePath, variantAnalysisApiResponse.id.toString(), 'repo_states.json')).resolves({
|
||||
readJsonStub.withArgs(path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json')).resolves({
|
||||
[scannedRepos[1].repository.id]: {
|
||||
repositoryId: scannedRepos[1].repository.id,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
@@ -372,19 +372,16 @@ describe('Variant Analysis Manager', async function() {
|
||||
},
|
||||
});
|
||||
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis({
|
||||
...createMockVariantAnalysis({}),
|
||||
id: variantAnalysisApiResponse.id,
|
||||
});
|
||||
sinon.assert.calledWith(readJsonStub, path.join(storagePath, variantAnalysisApiResponse.id.toString(), 'repo_states.json'));
|
||||
await variantAnalysisManager.rehydrateVariantAnalysis(variantAnalysis);
|
||||
sinon.assert.calledWith(readJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'));
|
||||
|
||||
await variantAnalysisManager.autoDownloadVariantAnalysisResult(
|
||||
scannedRepos[0],
|
||||
variantAnalysisApiResponse,
|
||||
variantAnalysis,
|
||||
cancellationTokenSource.token
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysisApiResponse.id.toString(), 'repo_states.json'), {
|
||||
sinon.assert.calledWith(outputJsonStub, path.join(storagePath, variantAnalysis.id.toString(), 'repo_states.json'), {
|
||||
[scannedRepos[1].repository.id]: {
|
||||
repositoryId: scannedRepos[1].repository.id,
|
||||
downloadStatus: VariantAnalysisScannedRepositoryDownloadStatus.Succeeded,
|
||||
@@ -405,9 +402,9 @@ describe('Variant Analysis Manager', async function() {
|
||||
it('should pop download tasks off the queue', async () => {
|
||||
const getResultsSpy = sandbox.spy(variantAnalysisManager, 'autoDownloadVariantAnalysisResult');
|
||||
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[0], variantAnalysisApiResponse, cancellationTokenSource.token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[1], variantAnalysisApiResponse, cancellationTokenSource.token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[2], variantAnalysisApiResponse, cancellationTokenSource.token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[0], variantAnalysis, cancellationTokenSource.token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[1], variantAnalysis, cancellationTokenSource.token);
|
||||
await variantAnalysisManager.enqueueDownload(scannedRepos[2], variantAnalysis, cancellationTokenSource.token);
|
||||
|
||||
expect(variantAnalysisManager.downloadsQueueSize()).to.equal(0);
|
||||
expect(getResultsSpy).to.have.been.calledThrice;
|
||||
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
import { createFailedMockApiResponse, createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response';
|
||||
import { VariantAnalysis, VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis';
|
||||
import { createMockScannedRepos } from '../../factories/remote-queries/gh-api/scanned-repositories';
|
||||
import { processFailureReason } from '../../../remote-queries/variant-analysis-processor';
|
||||
import {
|
||||
processFailureReason,
|
||||
processScannedRepository,
|
||||
processUpdatedVariantAnalysis,
|
||||
} from '../../../remote-queries/variant-analysis-processor';
|
||||
import { Credentials } from '../../../authentication';
|
||||
import { createMockVariantAnalysis } from '../../factories/remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager';
|
||||
@@ -143,8 +147,8 @@ describe('Variant Analysis Monitor', async function() {
|
||||
|
||||
succeededRepos.forEach((succeededRepo, index) => {
|
||||
expect(commandSpy.getCall(index).args[0]).to.eq('codeQL.autoDownloadVariantAnalysisResult');
|
||||
expect(commandSpy.getCall(index).args[1]).to.eq(succeededRepo);
|
||||
expect(commandSpy.getCall(index).args[2]).to.eq(mockApiResponse);
|
||||
expect(commandSpy.getCall(index).args[1]).to.deep.eq(processScannedRepository(succeededRepo));
|
||||
expect(commandSpy.getCall(index).args[2]).to.deep.eq(processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,8 +158,8 @@ describe('Variant Analysis Monitor', async function() {
|
||||
expect(mockGetDownloadResult).to.have.callCount(succeededRepos.length);
|
||||
|
||||
succeededRepos.forEach((succeededRepo, index) => {
|
||||
expect(mockGetDownloadResult.getCall(index).args[0]).to.eq(succeededRepo);
|
||||
expect(mockGetDownloadResult.getCall(index).args[1]).to.eq(mockApiResponse);
|
||||
expect(mockGetDownloadResult.getCall(index).args[0]).to.deep.eq(processScannedRepository(succeededRepo));
|
||||
expect(mockGetDownloadResult.getCall(index).args[1]).to.deep.eq(processUpdatedVariantAnalysis(variantAnalysis, mockApiResponse));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,11 +8,14 @@ import * as fs from 'fs-extra';
|
||||
import * as path from 'path';
|
||||
|
||||
import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager';
|
||||
import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task';
|
||||
import { CodeQLCliServer } from '../../../cli';
|
||||
import { storagePath } from '../global.helper';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client';
|
||||
import {
|
||||
createMockVariantAnalysisRepositoryTask
|
||||
} from '../../factories/remote-queries/shared/variant-analysis-repo-tasks';
|
||||
import { VariantAnalysisRepositoryTask } from '../../../remote-queries/shared/variant-analysis';
|
||||
|
||||
describe(VariantAnalysisResultsManager.name, function() {
|
||||
this.timeout(10000);
|
||||
@@ -51,15 +54,15 @@ describe(VariantAnalysisResultsManager.name, function() {
|
||||
request: getOctokitStub
|
||||
})
|
||||
} as unknown as Credentials;
|
||||
let dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
let dummyRepoTask: VariantAnalysisRepositoryTask;
|
||||
let variantAnalysisStoragePath: string;
|
||||
let repoTaskStorageDirectory: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
dummyRepoTask = createMockVariantAnalysisRepositoryTask();
|
||||
|
||||
variantAnalysisStoragePath = path.join(storagePath, variantAnalysisId.toString());
|
||||
repoTaskStorageDirectory = variantAnalysisResultsManager.getRepoStorageDirectory(variantAnalysisStoragePath, dummyRepoTask.repository.full_name);
|
||||
repoTaskStorageDirectory = variantAnalysisResultsManager.getRepoStorageDirectory(variantAnalysisStoragePath, dummyRepoTask.repository.fullName);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -70,14 +73,14 @@ describe(VariantAnalysisResultsManager.name, function() {
|
||||
|
||||
describe('isVariantAnalysisRepoDownloaded', () => {
|
||||
it('should return false when no results are downloaded', async () => {
|
||||
expect(await variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(variantAnalysisStoragePath, dummyRepoTask.repository.full_name)).to.equal(false);
|
||||
expect(await variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(variantAnalysisStoragePath, dummyRepoTask.repository.fullName)).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the artifact_url is missing', async () => {
|
||||
it('should not try to download the result', async () => {
|
||||
const dummyRepoTask = createMockVariantAnalysisRepoTask();
|
||||
delete dummyRepoTask.artifact_url;
|
||||
const dummyRepoTask = createMockVariantAnalysisRepositoryTask();
|
||||
delete dummyRepoTask.artifactUrl;
|
||||
|
||||
try {
|
||||
await variantAnalysisResultsManager.download(
|
||||
@@ -103,7 +106,7 @@ describe(VariantAnalysisResultsManager.name, function() {
|
||||
|
||||
getVariantAnalysisRepoResultStub = sandbox
|
||||
.stub(ghApiClient, 'getVariantAnalysisRepoResult')
|
||||
.withArgs(mockCredentials, dummyRepoTask.artifact_url as string)
|
||||
.withArgs(mockCredentials, dummyRepoTask.artifactUrl as string)
|
||||
.resolves(arrayBuffer);
|
||||
});
|
||||
|
||||
@@ -149,7 +152,7 @@ describe(VariantAnalysisResultsManager.name, function() {
|
||||
variantAnalysisStoragePath
|
||||
);
|
||||
|
||||
expect(await variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(variantAnalysisStoragePath, dummyRepoTask.repository.full_name)).to.equal(true);
|
||||
expect(await variantAnalysisResultsManager.isVariantAnalysisRepoDownloaded(variantAnalysisStoragePath, dummyRepoTask.repository.fullName)).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ export function createMockLocalQueryInfo({
|
||||
const localQuery = new LocalQueryInfo(initialQueryInfo, cancellationToken);
|
||||
|
||||
localQuery.failureReason = failureReason;
|
||||
localQuery.cancel = () => { /**/ };
|
||||
|
||||
if (queryWithResults) {
|
||||
localQuery.completeThisQuery(queryWithResults);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import {
|
||||
VariantAnalysisRepositoryTask,
|
||||
VariantAnalysisRepoStatus,
|
||||
} from '../../../../remote-queries/shared/variant-analysis';
|
||||
import { createMockRepositoryWithMetadata } from './repository';
|
||||
|
||||
export function createMockVariantAnalysisRepositoryTask(data?: Partial<VariantAnalysisRepositoryTask>): VariantAnalysisRepositoryTask {
|
||||
return {
|
||||
repository: createMockRepositoryWithMetadata(),
|
||||
analysisStatus: VariantAnalysisRepoStatus.Pending,
|
||||
resultCount: faker.datatype.number(),
|
||||
artifactSizeInBytes: faker.datatype.number(),
|
||||
databaseCommitSha: faker.git.commitSha(),
|
||||
sourceLocationPrefix: faker.system.filePath(),
|
||||
artifactUrl: faker.internet.url(),
|
||||
...data,
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function createMockVariantAnalysis({
|
||||
skippedRepos?: VariantAnalysisSkippedRepositories,
|
||||
executionStartTime?: number | undefined
|
||||
}): VariantAnalysis {
|
||||
const variantAnalysis: VariantAnalysis = {
|
||||
return {
|
||||
id: faker.datatype.number(),
|
||||
controllerRepo: {
|
||||
...createMockRepository(),
|
||||
@@ -46,6 +46,4 @@ export function createMockVariantAnalysis({
|
||||
scannedRepos: scannedRepos,
|
||||
skippedRepos: skippedRepos
|
||||
};
|
||||
|
||||
return variantAnalysis;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ import { createMockVariantAnalysisHistoryItem } from '../factories/remote-querie
|
||||
import { VariantAnalysisHistoryItem } from '../../remote-queries/variant-analysis-history-item';
|
||||
import { QueryStatus } from '../../query-status';
|
||||
import { VariantAnalysisStatus } from '../../remote-queries/shared/variant-analysis';
|
||||
import * as ghActionsApiClient from '../../remote-queries/gh-api/gh-actions-api-client';
|
||||
import { Credentials } from '../../authentication';
|
||||
|
||||
describe('query-history', () => {
|
||||
const mockExtensionLocation = path.join(tmpDir.name, 'mock-extension-location');
|
||||
@@ -92,10 +94,18 @@ describe('query-history', () => {
|
||||
} as any as VariantAnalysisManager;
|
||||
|
||||
localQueryHistory = [
|
||||
// completed
|
||||
createMockLocalQueryInfo({ dbName: 'a', queryWithResults: createMockQueryWithResults({ sandbox, didRunSuccessfully: true }) }),
|
||||
// completed
|
||||
createMockLocalQueryInfo({ dbName: 'b', queryWithResults: createMockQueryWithResults({ sandbox, didRunSuccessfully: true }) }),
|
||||
// failed
|
||||
createMockLocalQueryInfo({ dbName: 'a', queryWithResults: createMockQueryWithResults({ sandbox, didRunSuccessfully: false }) }),
|
||||
// completed
|
||||
createMockLocalQueryInfo({ dbName: 'a', queryWithResults: createMockQueryWithResults({ sandbox, didRunSuccessfully: true }) }),
|
||||
// in progress
|
||||
createMockLocalQueryInfo({ resultCount: 0 }),
|
||||
// in progress
|
||||
createMockLocalQueryInfo({ resultCount: 0 })
|
||||
];
|
||||
remoteQueryHistory = [
|
||||
createMockRemoteQueryHistoryItem({ status: QueryStatus.Completed }),
|
||||
@@ -325,6 +335,168 @@ describe('query-history', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCancel', () => {
|
||||
let mockCredentials: Credentials;
|
||||
let mockCancelRemoteQuery: sinon.SinonStub;
|
||||
let mockCancelVariantAnalysis: sinon.SinonStub;
|
||||
let getOctokitStub: sinon.SinonStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockCredentials = {
|
||||
getOctokit: () => Promise.resolve({
|
||||
request: getOctokitStub
|
||||
})
|
||||
} as unknown as Credentials;
|
||||
sandbox.stub(Credentials, 'initialize').resolves(mockCredentials);
|
||||
mockCancelRemoteQuery = sandbox.stub(ghActionsApiClient, 'cancelRemoteQuery');
|
||||
mockCancelVariantAnalysis = sandbox.stub(ghActionsApiClient, 'cancelVariantAnalysis');
|
||||
});
|
||||
|
||||
describe('if the item is in progress', async () => {
|
||||
it('should cancel a single local query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const inProgress1 = localQueryHistory[4];
|
||||
const cancelSpy = sandbox.spy(inProgress1, 'cancel');
|
||||
|
||||
await queryHistoryManager.handleCancel(inProgress1, [inProgress1]);
|
||||
expect(cancelSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should cancel multiple local queries', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const inProgress1 = localQueryHistory[4];
|
||||
const inProgress2 = localQueryHistory[5];
|
||||
|
||||
const cancelSpy1 = sandbox.spy(inProgress1, 'cancel');
|
||||
const cancelSpy2 = sandbox.spy(inProgress2, 'cancel');
|
||||
|
||||
await queryHistoryManager.handleCancel(inProgress1, [inProgress1, inProgress2]);
|
||||
expect(cancelSpy1).to.have.been.called;
|
||||
expect(cancelSpy2).to.have.been.called;
|
||||
});
|
||||
|
||||
it('should cancel a single remote query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const inProgress1 = remoteQueryHistory[2];
|
||||
|
||||
await queryHistoryManager.handleCancel(inProgress1, [inProgress1]);
|
||||
expect(mockCancelRemoteQuery).to.have.been.calledWith(mockCredentials, inProgress1.remoteQuery);
|
||||
});
|
||||
|
||||
it('should cancel multiple remote queries', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const inProgress1 = remoteQueryHistory[2];
|
||||
const inProgress2 = remoteQueryHistory[3];
|
||||
|
||||
await queryHistoryManager.handleCancel(inProgress1, [inProgress1, inProgress2]);
|
||||
expect(mockCancelRemoteQuery).to.have.been.calledWith(mockCredentials, inProgress1.remoteQuery);
|
||||
expect(mockCancelRemoteQuery).to.have.been.calledWith(mockCredentials, inProgress2.remoteQuery);
|
||||
});
|
||||
|
||||
it('should cancel a single variant analysis', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const inProgress1 = variantAnalysisHistory[1];
|
||||
|
||||
await queryHistoryManager.handleCancel(inProgress1, [inProgress1]);
|
||||
expect(mockCancelVariantAnalysis).to.have.been.calledWith(mockCredentials, inProgress1.variantAnalysis);
|
||||
});
|
||||
|
||||
it('should cancel multiple variant analyses', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const inProgress1 = variantAnalysisHistory[1];
|
||||
const inProgress2 = variantAnalysisHistory[3];
|
||||
|
||||
await queryHistoryManager.handleCancel(inProgress1, [inProgress1, inProgress2]);
|
||||
expect(mockCancelVariantAnalysis).to.have.been.calledWith(mockCredentials, inProgress1.variantAnalysis);
|
||||
expect(mockCancelVariantAnalysis).to.have.been.calledWith(mockCredentials, inProgress2.variantAnalysis);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the item is not in progress', async () => {
|
||||
it('should not cancel a single local query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const completed = localQueryHistory[0];
|
||||
const cancelSpy = sandbox.spy(completed, 'cancel');
|
||||
|
||||
await queryHistoryManager.handleCancel(completed, [completed]);
|
||||
expect(cancelSpy).to.not.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should not cancel multiple local queries', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(localQueryHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const completed = localQueryHistory[0];
|
||||
const failed = localQueryHistory[2];
|
||||
|
||||
const cancelSpy = sandbox.spy(completed, 'cancel');
|
||||
const cancelSpy2 = sandbox.spy(failed, 'cancel');
|
||||
|
||||
await queryHistoryManager.handleCancel(completed, [completed, failed]);
|
||||
expect(cancelSpy).to.not.have.been.calledOnce;
|
||||
expect(cancelSpy2).to.not.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should not cancel a single remote query', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const completed = remoteQueryHistory[0];
|
||||
|
||||
await queryHistoryManager.handleCancel(completed, [completed]);
|
||||
expect(mockCancelRemoteQuery).to.not.have.been.calledWith(mockCredentials, completed.remoteQuery);
|
||||
});
|
||||
|
||||
it('should not cancel multiple remote queries', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const completed = remoteQueryHistory[0];
|
||||
const failed = remoteQueryHistory[1];
|
||||
|
||||
await queryHistoryManager.handleCancel(completed, [completed, failed]);
|
||||
expect(mockCancelRemoteQuery).to.not.have.been.calledWith(mockCredentials, completed.remoteQuery);
|
||||
expect(mockCancelRemoteQuery).to.not.have.been.calledWith(mockCredentials, failed.remoteQuery);
|
||||
});
|
||||
|
||||
it('should not cancel a single variant analysis', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const completedVariantAnalysis = variantAnalysisHistory[0];
|
||||
|
||||
await queryHistoryManager.handleCancel(completedVariantAnalysis, [completedVariantAnalysis]);
|
||||
expect(mockCancelVariantAnalysis).to.not.have.been.calledWith(mockCredentials, completedVariantAnalysis.variantAnalysis);
|
||||
});
|
||||
|
||||
it('should not cancel multiple variant analyses', async () => {
|
||||
queryHistoryManager = await createMockQueryHistory(allHistory);
|
||||
|
||||
// cancelling the selected item
|
||||
const completedVariantAnalysis = variantAnalysisHistory[0];
|
||||
const failedVariantAnalysis = variantAnalysisHistory[2];
|
||||
|
||||
await queryHistoryManager.handleCancel(completedVariantAnalysis, [completedVariantAnalysis, failedVariantAnalysis]);
|
||||
expect(mockCancelVariantAnalysis).to.not.have.been.calledWith(mockCredentials, completedVariantAnalysis.variantAnalysis);
|
||||
expect(mockCancelVariantAnalysis).to.not.have.been.calledWith(mockCredentials, failedVariantAnalysis.variantAnalysis);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('determineSelection', () => {
|
||||
const singleItem = 'a';
|
||||
const multipleItems = ['b', 'c', 'd'];
|
||||
|
||||
@@ -2,8 +2,14 @@ import { fail } from 'assert';
|
||||
import { expect } from 'chai';
|
||||
import * as sinon from 'sinon';
|
||||
import { Credentials } from '../../../../authentication';
|
||||
import { cancelRemoteQuery, getRepositoriesMetadata } from '../../../../remote-queries/gh-api/gh-actions-api-client';
|
||||
import {
|
||||
cancelRemoteQuery,
|
||||
cancelVariantAnalysis,
|
||||
getRepositoriesMetadata
|
||||
} from '../../../../remote-queries/gh-api/gh-actions-api-client';
|
||||
import { RemoteQuery } from '../../../../remote-queries/remote-query';
|
||||
import { createMockVariantAnalysis } from '../../../factories/remote-queries/shared/variant-analysis';
|
||||
import { VariantAnalysis } from '../../../../remote-queries/shared/variant-analysis';
|
||||
|
||||
describe('gh-actions-api-client mock responses', () => {
|
||||
let sandbox: sinon.SinonSandbox;
|
||||
@@ -50,6 +56,29 @@ describe('gh-actions-api-client mock responses', () => {
|
||||
} as unknown as RemoteQuery;
|
||||
}
|
||||
});
|
||||
|
||||
describe('cancelVariantAnalysis', () => {
|
||||
let variantAnalysis: VariantAnalysis;
|
||||
before(() => {
|
||||
variantAnalysis = createMockVariantAnalysis({});
|
||||
});
|
||||
|
||||
it('should cancel a variant analysis', async () => {
|
||||
mockResponse = sinon.stub().resolves({ status: 202 });
|
||||
await cancelVariantAnalysis(mockCredentials, variantAnalysis);
|
||||
|
||||
expect(mockResponse.calledOnce).to.be.true;
|
||||
expect(mockResponse.firstCall.args[0]).to.equal(`POST /repos/${variantAnalysis.controllerRepo.fullName}/actions/runs/${variantAnalysis.actionsWorkflowRunId}/cancel`);
|
||||
});
|
||||
|
||||
it('should fail to cancel a variant analysis', async () => {
|
||||
mockResponse = sinon.stub().resolves({ status: 409, data: { message: 'Uh oh!' } });
|
||||
|
||||
await expect(cancelVariantAnalysis(mockCredentials, variantAnalysis)).to.be.rejectedWith(/Error cancelling variant analysis: 409 Uh oh!/);
|
||||
expect(mockResponse.calledOnce).to.be.true;
|
||||
expect(mockResponse.firstCall.args[0]).to.equal(`POST /repos/${variantAnalysis.controllerRepo.fullName}/actions/runs/${variantAnalysis.actionsWorkflowRunId}/cancel`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('gh-actions-api-client real responses', function() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from 'chai';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import {
|
||||
VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository
|
||||
} from '../../../src/remote-queries/gh-api/variant-analysis';
|
||||
@@ -7,8 +8,15 @@ import {
|
||||
VariantAnalysisScannedRepository,
|
||||
VariantAnalysisRepoStatus
|
||||
} from '../../../src/remote-queries/shared/variant-analysis';
|
||||
import { processVariantAnalysis } from '../../../src/remote-queries/variant-analysis-processor';
|
||||
import { createMockScannedRepos } from '../../../src/vscode-tests/factories/remote-queries/gh-api/scanned-repositories';
|
||||
import {
|
||||
processScannedRepository,
|
||||
processVariantAnalysis,
|
||||
processVariantAnalysisRepositoryTask
|
||||
} from '../../../src/remote-queries/variant-analysis-processor';
|
||||
import {
|
||||
createMockScannedRepo,
|
||||
createMockScannedRepos
|
||||
} from '../../../src/vscode-tests/factories/remote-queries/gh-api/scanned-repositories';
|
||||
import { createMockSkippedRepos } from '../../../src/vscode-tests/factories/remote-queries/gh-api/skipped-repositories';
|
||||
import {
|
||||
createMockApiResponse
|
||||
@@ -16,8 +24,11 @@ import {
|
||||
import {
|
||||
createMockSubmission
|
||||
} from '../../../src/vscode-tests/factories/remote-queries/shared/variant-analysis-submission';
|
||||
import {
|
||||
createMockVariantAnalysisRepoTask
|
||||
} from '../../../src/vscode-tests/factories/remote-queries/gh-api/variant-analysis-repo-task';
|
||||
|
||||
describe('Variant Analysis processor', function() {
|
||||
describe(processVariantAnalysis.name, function() {
|
||||
const scannedRepos = createMockScannedRepos();
|
||||
const skippedRepos = createMockSkippedRepos();
|
||||
const mockApiResponse = createMockApiResponse('completed', scannedRepos, skippedRepos);
|
||||
@@ -147,3 +158,44 @@ describe('Variant Analysis processor', function() {
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe(processVariantAnalysisRepositoryTask.name, () => {
|
||||
const mockApiResponse = createMockVariantAnalysisRepoTask();
|
||||
|
||||
it('should return the correct result', () => {
|
||||
expect(processVariantAnalysisRepositoryTask(mockApiResponse)).to.deep.eq({
|
||||
repository: {
|
||||
id: mockApiResponse.repository.id,
|
||||
fullName: mockApiResponse.repository.full_name,
|
||||
private: mockApiResponse.repository.private,
|
||||
},
|
||||
analysisStatus: 'succeeded',
|
||||
resultCount: mockApiResponse.result_count,
|
||||
artifactSizeInBytes: mockApiResponse.artifact_size_in_bytes,
|
||||
failureMessage: mockApiResponse.failure_message,
|
||||
databaseCommitSha: mockApiResponse.database_commit_sha,
|
||||
sourceLocationPrefix: mockApiResponse.source_location_prefix,
|
||||
artifactUrl: mockApiResponse.artifact_url,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(processScannedRepository.name, () => {
|
||||
const mockApiResponse = createMockScannedRepo(faker.random.word(), faker.datatype.boolean(), VariantAnalysisRepoStatus.Pending);
|
||||
|
||||
it('should return the correct result', () => {
|
||||
expect(processScannedRepository(mockApiResponse)).to.deep.eq({
|
||||
repository: {
|
||||
id: mockApiResponse.repository.id,
|
||||
fullName: mockApiResponse.repository.full_name,
|
||||
private: mockApiResponse.repository.private,
|
||||
stargazersCount: mockApiResponse.repository.stargazers_count,
|
||||
updatedAt: mockApiResponse.repository.updated_at,
|
||||
},
|
||||
analysisStatus: 'pending',
|
||||
resultCount: mockApiResponse.result_count,
|
||||
artifactSizeInBytes: mockApiResponse.artifact_size_in_bytes,
|
||||
failureMessage: mockApiResponse.failure_message,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
49
extensions/ql-vscode/workspace-databases-schema.json
Normal file
49
extensions/ql-vscode/workspace-databases-schema.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repositoryLists": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"repositories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "repositories"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"owners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9-_\\.]+$"
|
||||
}
|
||||
},
|
||||
"repositories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["repositoryLists", "owners", "repositories"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user