Path: blob/main/extensions/copilot/test/simulation/diagnosticProviders/utils.ts
13394 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as cp from 'child_process';6import { spawnSync } from 'child_process';7import * as fs from 'fs';8import * as path from 'path';9import type * as vscode from 'vscode';10import { ITestingServicesAccessor } from '../../../src/platform/test/node/services';11import { ResourceMap } from '../../../src/util/vs/base/common/map';12import { Diagnostic, DiagnosticRelatedInformation, Location, Range } from '../../../src/vscodeTypes';13import { computeSHA256 } from '../../base/hash';14import { CacheScope, ICachingResourceFetcher } from '../../base/simulationContext';15import { CACHING_DIAGNOSTICS_PROVIDER_CACHE_SALT } from '../../cacheSalt';16import { cleanTempDirWithRetry, createTempDir } from '../stestUtil';17import { DiagnosticsProvider, IFile, ITestDiagnostic } from './diagnosticsProvider';1819/**20* Abstract class which finds diagnostics for a set of files and stores them in a cache path21*/22export abstract class CachingDiagnosticsProvider extends DiagnosticsProvider {23protected readonly id = this.constructor.name;2425abstract readonly cacheSalt: string;26abstract readonly cacheScope: CacheScope;2728protected get cacheVersion(): number { return CACHING_DIAGNOSTICS_PROVIDER_CACHE_SALT; }2930override async getDiagnostics(accessor: ITestingServicesAccessor, files: IFile[]): Promise<ITestDiagnostic[]> {31// Always use / as separators in file names to avoid cache misses on Windows32files = files.map(f => ({ ...f, fileName: f.fileName.replace(/\\/g, '/') }));3334// Keep files stable and maximize cache hits by sorting them by file name35files.sort((a, b) => a.fileName.localeCompare(b.fileName));3637const cacheKey = computeSHA256(`${this.id}-v${this.cacheVersion}-${JSON.stringify(files)}`);3839return await accessor.get(ICachingResourceFetcher).invokeWithCache(40this.cacheScope,41files,42this.cacheSalt,43cacheKey,44this.computeDiagnostics.bind(this)45);46}4748protected abstract computeDiagnostics(files: IFile[]): Promise<ITestDiagnostic[]>;49}5051/**52* Abstract class for defining diagnostics provider which provide linting errors53*/54export abstract class LintingDiagnosticsProvider extends CachingDiagnosticsProvider {5556protected override async computeDiagnostics(_files: IFile[]): Promise<ITestDiagnostic[]> {57const temporaryDirectory = await createTempDir();58const diagnostics: ITestDiagnostic[] = [];59const files = await setupTemporaryWorkspace(temporaryDirectory, _files);60for (const file of files) {61const command = await this.fetchCommand(temporaryDirectory, file.filePath);62const spawnResult = spawnSync(command.command, command.arguments, { shell: true, encoding: 'utf-8', env: command.env });63const processedDiagnostics = this.processDiagnostics(file.fileName, JSON.parse(spawnResult.stdout || '{}'));64diagnostics.push(...processedDiagnostics);65}66await cleanTempDirWithRetry(temporaryDirectory);67return diagnostics;68}6970protected abstract fetchCommand(temporaryDirectory: string, filePath: string): Promise<{ command: string; arguments: string[]; env?: NodeJS.ProcessEnv }>;7172protected abstract processDiagnostics(fileName: string, stdoutResult: any): ITestDiagnostic[];73}7475export async function setupTemporaryWorkspace(workspacePath: string, _files: IFile[]): Promise<{ filePath: string; fileName: string; fileContents: string }[]> {76const files = _files.map((file) => {77return {78filePath: path.join(workspacePath, file.fileName),79fileName: file.fileName,80fileContents: file.fileContents81};82});83await fs.promises.rm(workspacePath, { recursive: true, force: true });84await fs.promises.mkdir(workspacePath, { recursive: true });85for (const file of files) {86await fs.promises.mkdir(path.dirname(file.filePath), { recursive: true });87await fs.promises.writeFile(file.filePath, file.fileContents);88}89return files;90}9192export function convertTestToVSCodeDiagnostics(diagnostics: ITestDiagnostic[], pathToUri: (path: string) => vscode.Uri): ResourceMap<vscode.Diagnostic[]> {93const result = new ResourceMap<vscode.Diagnostic[]>();94for (const d of diagnostics) {95const diagnostic = new Diagnostic(new Range(d.startLine, d.startCharacter, d.endLine, d.endCharacter), d.message);96diagnostic.code = d.code;97diagnostic.source = d.source;98diagnostic.relatedInformation = d.relatedInformation?.map(r => {99const range = new Range(r.location.startLine, r.location.startCharacter, r.location.endLine, r.location.endCharacter);100const relatedLocation = new Location(pathToUri(r.location.file), range);101return new DiagnosticRelatedInformation(relatedLocation, r.message);102});103const uri = pathToUri(d.file);104if (!result.has(uri)) {105result.set(uri, []);106}107result.get(uri)!.push(diagnostic);108}109return result;110}111112export function findIfInstalled(verificationCommand: { command: string; arguments: string[] }, verificationRegex: RegExp): boolean {113const spawnResult = cp.spawnSync(verificationCommand.command, verificationCommand.arguments, { shell: true, encoding: 'utf-8' });114const regexMatch = spawnResult.stdout.match(verificationRegex);115if (!regexMatch || regexMatch.length === 0) {116return false;117}118return true;119}120121122