Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 15 additions & 24 deletions src/aspectValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,14 @@
import * as vscode from 'vscode';
import type {ExtensionLogger} from './outputChannel';

export const VALIDATE_DOCUMENT_REQUEST = 'turtle/aspectValidation/validateDocument';
export const VALIDATE_DOCUMENT_REQUEST = 'turtle/aspectValidation/validateDocumentV2';
export const VALIDATE_DOCUMENT_COMMAND = 'semantic-models.validateDocumentNow';
export const SUPPORTED_DOCUMENT_SCHEMES = ['file', 'untitled'];
const STATUS_MESSAGE_TIMEOUT_MS = 5000;

export type AspectValidationTrigger = 'manual' | 'save';

export interface TurtleDiagnostic {
code: string;
message: string;
}

export interface DiagnosticReport {
diagnostics: Array<TurtleDiagnostic>;
}
export type ValidationResult = 'validation_successful' | 'validation_failed';

export interface RequestClient {
sendRequest<R>(method: string, params?: unknown): Thenable<R>;
Expand Down Expand Up @@ -79,7 +72,7 @@ export class AspectValidationController {
async validateDocument(
document: Pick<vscode.TextDocument, 'languageId' | 'uri'> | undefined,
trigger: AspectValidationTrigger,
): Promise<DiagnosticReport | undefined> {
): Promise<ValidationResult | undefined> {
if (!document || document.languageId !== 'turtle' || !SUPPORTED_DOCUMENT_SCHEMES.includes(document.uri.scheme)) {
if (trigger === 'manual') {
await this.window.showWarningMessage('Open a Turtle file before running aspect validation.');
Expand All @@ -89,7 +82,7 @@ export class AspectValidationController {

const binding = this.binding;
const request = () =>
binding.client.sendRequest<DiagnosticReport>(VALIDATE_DOCUMENT_REQUEST, {
binding.client.sendRequest<ValidationResult>(VALIDATE_DOCUMENT_REQUEST, {
uri: document.uri.toString(),
reason: trigger,
});
Expand All @@ -101,8 +94,8 @@ export class AspectValidationController {
title: string,
trigger: AspectValidationTrigger,
binding: RequestClientBinding,
request: () => Thenable<DiagnosticReport>,
): Promise<DiagnosticReport | undefined> {
request: () => Thenable<ValidationResult>,
): Promise<ValidationResult | undefined> {
try {
const result = await this.runWithProgress(title, trigger, request);
if (this.binding !== binding) {
Expand All @@ -124,8 +117,8 @@ export class AspectValidationController {
private runWithProgress(
title: string,
trigger: AspectValidationTrigger,
request: () => Thenable<DiagnosticReport>,
): Promise<DiagnosticReport> {
request: () => Thenable<ValidationResult>,
): Promise<ValidationResult> {
if (trigger === 'save') {
const disposable = this.window.setStatusBarMessage(`${title} in progress...`, STATUS_MESSAGE_TIMEOUT_MS);
return Promise.resolve(request()).finally(() => disposable.dispose());
Expand All @@ -143,7 +136,7 @@ export class AspectValidationController {
);
}

private async showSummary(result: DiagnosticReport, trigger: AspectValidationTrigger): Promise<void> {
private async showSummary(result: ValidationResult, trigger: AspectValidationTrigger): Promise<void> {
const summary = this.formatSummary(result);
this.outputChannel.info(`[validation] ${summary}`);

Expand All @@ -152,11 +145,10 @@ export class AspectValidationController {
return;
}

const hasViolations = (result.diagnostics?.length ?? 0) > 0;
if (hasViolations) {
await this.window.showErrorMessage(summary);
} else {
if (result === 'validation_successful') {
await this.window.showInformationMessage(summary);
} else {
await this.window.showErrorMessage(summary);
}
}

Expand All @@ -172,12 +164,11 @@ export class AspectValidationController {
await this.window.showErrorMessage(summary);
}

private formatSummary(result: DiagnosticReport): string {
const violationCount = result.diagnostics?.length ?? 0;
if (violationCount === 0) {
private formatSummary(result: ValidationResult): string {
if (result === 'validation_successful') {
return 'Aspect validation completed without issues.';
}
return result.diagnostics.map(x => x.message).join(', ');
return 'Aspect validation failed. See the Problems view for details.';
}

private toFailureMessage(error: unknown): string {
Expand Down
41 changes: 31 additions & 10 deletions src/test/aspectValidationController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ async function withStubbedRegisterCommand(run: () => void | Promise<void>): Prom

suite('AspectValidationController', () => {
test('ignores a stale successful request after client replacement', async () => {
let resolveRequest: ((value: {diagnostics: []}) => void) | undefined;
const request = new Promise<{diagnostics: []}>(resolve => (resolveRequest = resolve));
let resolveRequest: ((value: 'validation_successful') => void) | undefined;
const request = new Promise<'validation_successful'>(resolve => (resolveRequest = resolve));
const harness = createValidationControllerHarness({request: async <R>() => request as Promise<R>});
const validation = harness.controller.validateDocument(createValidationDocument('/tmp/model.ttl'), 'manual');

harness.controller.setClient({sendRequest: async <R>() => ({diagnostics: []}) as R}, 2);
resolveRequest?.({diagnostics: []});
harness.controller.setClient({sendRequest: async <R>() => ('validation_successful') as R}, 2);
resolveRequest?.('validation_successful');

assert.strictEqual(await validation, undefined);
assert.deepStrictEqual(harness.window.infoMessages, []);
Expand All @@ -52,7 +52,7 @@ suite('AspectValidationController', () => {
const harness = createValidationControllerHarness({request: async <R>() => request as Promise<R>});
const validation = harness.controller.validateDocument(createValidationDocument('/tmp/model.ttl'), 'manual');

harness.controller.setClient({sendRequest: async <R>() => ({diagnostics: []}) as R}, 2);
harness.controller.setClient({sendRequest: async <R>() => ('validation_successful') as R}, 2);
rejectRequest?.(new Error('old connection failed'));

assert.strictEqual(await validation, undefined);
Expand All @@ -61,14 +61,35 @@ suite('AspectValidationController', () => {
});

test('reports the current successful request', async () => {
const harness = createValidationControllerHarness({response: {diagnostics: []}});
const harness = createValidationControllerHarness({response: 'validation_successful'});
const result = await harness.controller.validateDocument(createValidationDocument('/tmp/model.ttl'), 'manual');

assert.deepStrictEqual(result, {diagnostics: []});
assert.deepStrictEqual(result, 'validation_successful');
assert.deepStrictEqual(harness.window.infoMessages, ['Aspect validation completed without issues.']);
assert.strictEqual(harness.sentRequests[0].method, VALIDATE_DOCUMENT_REQUEST);
});

test('reports an Error result and points to the Problems view on manual validation', async () => {
const harness = createValidationControllerHarness({response: 'validation_failed'});
const result = await harness.controller.validateDocument(createValidationDocument('/tmp/model.ttl'), 'manual');

assert.strictEqual(result, 'validation_failed');
assert.deepStrictEqual(harness.window.errorMessages, ['Aspect validation failed. See the Problems view for details.']);
assert.deepStrictEqual(harness.window.infoMessages, []);
});

test('shows the summary in the status bar on save-triggered validation', async () => {
const success = createValidationControllerHarness({response: 'validation_successful'});
await success.controller.validateDocument(createValidationDocument('/tmp/model.ttl'), 'save');
assert.ok(success.window.statusMessages.includes('Aspect validation completed without issues.'));
assert.deepStrictEqual(success.window.infoMessages, []);

const failure = createValidationControllerHarness({response: 'validation_failed'});
await failure.controller.validateDocument(createValidationDocument('/tmp/model.ttl'), 'save');
assert.ok(failure.window.statusMessages.includes('Aspect validation failed. See the Problems view for details.'));
assert.deepStrictEqual(failure.window.errorMessages, []);
});

test('reports the current failed request', async () => {
const harness = createValidationControllerHarness({error: new Error('connection failed')});
const result = await harness.controller.validateDocument(createValidationDocument('/tmp/model.ttl'), 'manual');
Expand All @@ -78,7 +99,7 @@ suite('AspectValidationController', () => {
});

test('does not validate documents with an unsupported scheme', async () => {
const harness = createValidationControllerHarness({response: {diagnostics: []}});
const harness = createValidationControllerHarness({response: 'validation_successful'});
const virtualDoc = createValidationDocument(vscode.Uri.parse('chat-editing-text-model:/tmp/Model.ttl'));

const result = await harness.controller.validateDocument(virtualDoc, 'manual');
Expand All @@ -90,12 +111,12 @@ suite('AspectValidationController', () => {
});

test('validates untitled documents', async () => {
const harness = createValidationControllerHarness({response: {diagnostics: []}});
const harness = createValidationControllerHarness({response: 'validation_successful'});
const untitledDoc = createValidationDocument(vscode.Uri.parse('untitled:Untitled-1'));

const result = await harness.controller.validateDocument(untitledDoc, 'manual');

assert.deepStrictEqual(result, {diagnostics: []});
assert.deepStrictEqual(result, 'validation_successful');
assert.strictEqual(harness.sentRequests.length, 1);
});

Expand Down
6 changes: 3 additions & 3 deletions src/test/validationTestHarness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
import * as vscode from 'vscode';
import {
AspectValidationController,
DiagnosticReport,
ValidationResult,
RequestClient,
ValidationOutputChannel,
ValidationWindow,
} from '../aspectValidation';
import type {ExtensionLogger} from '../outputChannel';

type ValidationHarnessOptions = {
response?: DiagnosticReport;
response?: ValidationResult;
error?: Error;
request?: <R>(method: string, params?: unknown) => Promise<R>;
};
Expand Down Expand Up @@ -61,7 +61,7 @@ export function createValidationControllerHarness(options: ValidationHarnessOpti
throw options.error;
}

return (options.response ?? {valid: true}) as R;
return (options.response ?? 'validation_successful') as R;
},
};

Expand Down
Loading