Path: blob/main/extensions/copilot/test/simulation/outcomeValidators.ts
13389 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*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import { EXISTING_CODE_MARKER } from '../../src/extension/prompts/node/panel/codeBlockFormattingRules';6import { IAIEvaluationService } from '../../src/extension/testing/node/aiEvaluationService';7import { ITestingServicesAccessor } from '../../src/platform/test/node/services';8import { IFile, SimulationWorkspace } from '../../src/platform/test/node/simulationWorkspace';9import { CancellationToken } from '../../src/util/vs/base/common/cancellation';10import { basename } from '../../src/util/vs/base/common/resources';11import { splitLines } from '../../src/util/vs/base/common/strings';12import { URI } from '../../src/util/vs/base/common/uri';13import { getDiagnostics } from './diagnosticProviders';14import { DiagnosticsProvider, ITestDiagnostic } from './diagnosticProviders/diagnosticsProvider';15import { DiagnosticProviderId, IInlineEditOutcome, IOutcome, IWorkspaceEditOutcome, OutcomeAnnotation } from './types';1617export function assertLooksLikeJSDoc(text: string): void {18text = text.trim();19assert(text.startsWith('/**') && text.endsWith('*/'), `expected jsdoc, but got:\n${text}`);20}2122export function assertContainsAllSnippets(text: string, snippets: string[], dbgMsg: string = '') {23for (let i = 0, len = snippets.length; i < len; ++i) {24const snippet = snippets[i];25assert(text.indexOf(snippet) !== -1, `${dbgMsg} (contains snippet "${snippet}")`);26}27}2829/**30* Searches for `marker1`, and then for `marker2` after `marker1`.31*/32export function findTextBetweenMarkersFromTop(text: string, marker1: string, marker2: string): string | null {33const index1 = text.indexOf(marker1);34if (index1 === -1) {35return null;36}37const index2 = text.indexOf(marker2, index1 + 1);38if (index2 === -1) {39return null;40}41return text.substring(index1 + marker1.length, index2);42}434445/**46* Searches for `marker2`, and then for `marker1` before `marker2`.47*/48export function findTextBetweenMarkersFromBottom(text: string, marker1: string, marker2: string) {49const index2 = text.indexOf(marker2);50if (index2 === -1) {51return null;52}53const index1 = text.lastIndexOf(marker1, index2);54if (index1 === -1) {55return null;56}57return text.substring(index1 + marker1.length, index2);58}596061/**62* This method validates the outcome by finding if after the edit, there remain errors63*/64export async function assertNoDiagnosticsAsync(accessor: ITestingServicesAccessor, outcome: IOutcome, workspace: SimulationWorkspace, method: DiagnosticProviderId | DiagnosticsProvider) {65assert.strictEqual(outcome.type, 'inlineEdit');66const diagnostics = await getWorkspaceDiagnostics(accessor, workspace, method);67if (diagnostics.length > 0) {68for (const diagnostic of diagnostics) {69if (diagnostic.message.indexOf('indent') !== -1) {70outcome.annotations.push({ label: 'indentation', message: diagnostic.message, severity: 'warning' });71}72}73}74assert.deepStrictEqual(diagnostics.length, 0, JSON.stringify(diagnostics, undefined, 2));75}7677/**78* This method validates the outcome by finding if after the edit, there remain errors79*/80export async function assertNoSyntacticDiagnosticsAsync(accessor: ITestingServicesAccessor, outcome: IOutcome, workspace: SimulationWorkspace, method: DiagnosticProviderId | DiagnosticsProvider) {81assert.strictEqual(outcome.type, 'inlineEdit');82const diagnostics = await getWorkspaceDiagnostics(accessor, workspace, method);83const syntacticDiagnostics = diagnostics.filter(d => d.kind === 'syntactic');8485assert.strictEqual(syntacticDiagnostics.length, 0, JSON.stringify(syntacticDiagnostics, undefined, 2));86}8788/**89* This method validates the outcome by comparing the number of errors before and after90*/91export async function assertLessDiagnosticsAsync(accessor: ITestingServicesAccessor, outcome: IOutcome, workspace: SimulationWorkspace, method: DiagnosticProviderId) {92assert.strictEqual(outcome.type, 'inlineEdit');93const initialDiagnostics = outcome.initialDiagnostics;94assert.ok(initialDiagnostics);95let numberOfDiagnosticsInitially = 0;96for (const diagnostics of initialDiagnostics.values()) {97numberOfDiagnosticsInitially += diagnostics.length;98}99const diagnostics = await getWorkspaceDiagnostics(accessor, workspace, method);100const numberOfDiagnosticsAfter = diagnostics.length;101assert.ok(numberOfDiagnosticsAfter < numberOfDiagnosticsInitially);102}103104/**105* Returns the diagnostics in all workspace files106*/107export async function getWorkspaceDiagnostics(accessor: ITestingServicesAccessor, workspace: SimulationWorkspace, method: DiagnosticProviderId | DiagnosticsProvider): Promise<ITestDiagnostic[]> {108const files = workspace.documents.map(doc => ({ fileName: workspace.getFilePath(doc.document.uri), fileContents: doc.document.getText() }));109if (typeof method === 'string') {110return await getDiagnostics(accessor, files, method);111} else {112return await method.getDiagnostics(accessor, files);113}114}115116export function assertFileContent(files: IFile[] | Array<{ srcUri: string; post: string }>, fileName: string): string {117const existing = [];118for (const file of files) {119// Handle new format120if ('srcUri' in file && 'post' in file) {121// Convert string to URI if needed122const uri = typeof file.srcUri === 'string'123? URI.parse(file.srcUri)124: file.srcUri;125const name = basename(uri);126if (name === fileName) {127return file.post;128}129existing.push(name);130}131// Handle old format132else {133const name = file.kind === 'relativeFile' ? file.fileName : basename(file.uri);134if (name === fileName) {135return file.fileContents;136}137existing.push(name);138}139}140assert.fail(`Expected to find file ${fileName}. Files available: ${existing.join(', ')}`);141}142143export function assertJSON(content: string): any {144try {145return JSON.parse(content);146} catch (e) {147assert.fail(`Expected JSON, but got: ${e.message}, ${content}`);148}149}150151/**152* Helper function to get file content regardless of file format (old or new)153*/154export function getFileContent(file: IFile | { srcUri: string; post: string }): string {155if ('srcUri' in file && 'post' in file) {156// New format157return file.post;158} else if ('kind' in file && (file.kind === 'relativeFile' || file.kind === 'qualifiedFile')) {159// Old format160return file.fileContents;161} else {162throw new Error(`Unknown file format: ${JSON.stringify(file)}`);163}164}165166export function assertNoElidedCodeComments(outcome: IInlineEditOutcome | IWorkspaceEditOutcome | string): void {167if (typeof outcome === 'string') {168assert.ok(outcome.indexOf(EXISTING_CODE_MARKER) === -1, 'Expected no elided code comments');169} else if (outcome.type === 'inlineEdit') {170assertNoElidedCodeComments(outcome.fileContents);171} else if (outcome.type === 'workspaceEdit') {172for (const file of outcome.files) {173// Use the helper function174assertNoElidedCodeComments(getFileContent(file));175}176}177}178179export async function assertCriteriaMetAsync(accessor: ITestingServicesAccessor, response: string, criteria: string): Promise<void> {180const evaluationService = accessor.get(IAIEvaluationService);181const result = await evaluationService.evaluate(response, criteria, CancellationToken.None);182assert.ok(result.errorMessage === undefined, `Error: ${result.errorMessage}`);183}184185export function validateConsistentIndentation(newText: string, insertSpaces: boolean, annotations: OutcomeAnnotation[]): void {186const indentationRegex = insertSpaces ? /^[ ]*[\S$]/ : /^\t*(\S|$|( \*))/; // special handling for Doc comments that start with ` *187const lines = splitLines(newText);188for (let i = 0; i < lines.length; i++) {189const line = lines[i];190if (line.length > 0 && !indentationRegex.test(line)) {191const message = `Expected line ${i} to start with ${insertSpaces ? 'spaces' : 'tabs'}: ${line}`;192annotations.push({ label: 'indentation', message, severity: 'warning' });193//assert.fail(message);194}195}196}197198199