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
4 changes: 3 additions & 1 deletion .github/skills/integration-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ Filters which **test cases** run by matching against their test titles (e.g. `de

Runs only the extension host test suites whose name matches the pattern. Supports comma-separated values and shell glob patterns (on macOS/Linux). Node.js integration tests are skipped.

Available suite names: `api-folder`, `api-workspace`, `colorize`, `terminal-suggest`, `typescript`, `markdown`, `emmet`, `git`, `git-base`, `ipynb`, `notebook-renderers`, `configuration-editing`, `github-authentication`, `css`, `html`.
Available suite names: `api-folder`, `api-workspace`, `colorize`, `terminal-suggest`, `typescript`, `markdown`, `emmet`, `git`, `git-base`, `ipynb`, `notebook-renderers`, `configuration-editing`, `github-authentication`, `copilot`, `css`, `html`, `json`.

The `css`, `html`, and `json` suites are standalone extension tests that run in Electron's Node.js mode without opening a workbench. They share the same suite selection and grep filtering as extension host tests.

```bash
# Run only Git extension tests
Expand Down
58 changes: 41 additions & 17 deletions extensions/json-language-features/client/src/jsonClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
import { hash } from './utils/hash';
import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem, createSchemaLoadIssueItem, createSchemaLoadStatusItem } from './languageStatus';
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
import { matchesUrlPattern } from './utils/urlMatch';
import { getSchemaRequestUrl, matchesUrlPattern } from './utils/urlMatch';

namespace VSCodeContentRequest {
export const type: RequestType<string, string, any> = new RequestType('vscode/content');
Expand Down Expand Up @@ -226,6 +226,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
toDispose.push(schemaResolutionErrorStatusBarItem);

const fileSchemaErrors = new Map<string, string>();
const schemaRequestAliases = new Map<string, { readonly requestUrl: string }>();
let schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload);
let trustedDomains = workspace.getConfiguration().get<Record<string, boolean>>(SettingIds.trustedDomains, {});

Expand All @@ -239,8 +240,19 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP

toDispose.push(commands.registerCommand(CommandIds.clearCacheCommandId, async () => {
if (isClientReady && runtime.schemaRequests.clearCache) {
// Preserve aliases recorded by requests that finish while the cache is being cleared.
const aliasesToClear = new Map(schemaRequestAliases);
const cachedSchemas = await runtime.schemaRequests.clearCache();
await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas);
const schemaIds = new Set(cachedSchemas);
for (const [schemaId, alias] of schemaRequestAliases) {
if (schemaIds.has(alias.requestUrl)) {
schemaIds.add(schemaId);
}
if (aliasesToClear.get(schemaId) === alias) {
schemaRequestAliases.delete(schemaId);
}
}
await client.sendNotification(SchemaContentChangeNotification.type, [...schemaIds]);
}
window.showInformationMessage(l10n.t('JSON schema cache cleared.'));
}));
Expand Down Expand Up @@ -419,21 +431,31 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
if (!workspace.isTrusted) {
throw new ResponseError(SchemaRequestServiceErrors.UntrustedWorkspaceError, l10n.t('Downloading schemas is disabled in untrusted workspaces'));
}
if (!await isTrusted(uri)) {
throw new ResponseError(SchemaRequestServiceErrors.UntrustedSchemaError, l10n.t('Location {0} is untrusted', uriString));
let requestUrl: URL;
try {
requestUrl = getSchemaRequestUrl(uri);
} catch (e) {
throw new ResponseError(SchemaRequestServiceErrors.HTTPError, e.toString(), e);
}
if (!await isTrusted(requestUrl)) {
throw new ResponseError(SchemaRequestServiceErrors.UntrustedSchemaError, l10n.t('Location {0} is untrusted', requestUrl.href));
}
if (runtime.telemetry && uri.authority === 'schema.management.azure.com') {
if (runtime.telemetry && requestUrl.host === 'schema.management.azure.com') {
/* __GDPR__
"json.schema" : {
"owner": "aeschli",
"comment": "Measure the use of the Azure resource manager schemas",
"schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The azure schema URL that was requested." }
}
*/
runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: uriString });
runtime.telemetry.sendTelemetryEvent('json.schema', { schemaURL: requestUrl.href });
}
try {
return await runtime.schemaRequests.getContent(uriString);
const content = await runtime.schemaRequests.getContent(requestUrl.href);
if (runtime.schemaRequests.clearCache && requestUrl.href !== uriPath) {
schemaRequestAliases.set(uriPath, { requestUrl: requestUrl.href });
}
return content;
} catch (e) {
throw new ResponseError(SchemaRequestServiceErrors.HTTPError, e.toString(), e);
}
Expand Down Expand Up @@ -689,28 +711,31 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP
return schemaAssociationsCache;
}

async function isTrusted(uri: Uri): Promise<boolean> {
if (uri.scheme !== 'http' && uri.scheme !== 'https') {
return true;
}
const uriString = uri.toString(true);

async function isTrusted(url: URL): Promise<boolean> {
// Check against trustedDomains setting
if (matchesUrlPattern(uri, trustedDomains)) {
if (matchesUrlPattern(url, trustedDomains)) {
return true;
}

const matchesSchemaUri = (uri: string): boolean => {
try {
return getSchemaRequestUrl(Uri.parse(uri)).href === url.href;
} catch {
return false;
}
};

const knownAssociations = await getSchemaAssociations(false);
for (const association of knownAssociations) {
if (association.uri === uriString) {
if (matchesSchemaUri(association.uri)) {
return true;
}
}
const settingsCache = getSettings(false);
if (settingsCache.json && settingsCache.json.schemas) {
for (const schemaSetting of settingsCache.json.schemas) {
const schemaUri = schemaSetting.url;
if (schemaUri === uriString) {
if (schemaUri && matchesSchemaUri(schemaUri)) {
return true;
}
}
Expand Down Expand Up @@ -996,4 +1021,3 @@ export namespace ErrorCodes {
export function isSchemaResolveError(d: Diagnostic) {
return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError;
}

41 changes: 41 additions & 0 deletions extensions/json-language-features/client/src/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as glob from 'glob';
import Mocha from 'mocha';

const options: Mocha.MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000,
grep: process.env.MOCHA_GREP,
};

const ciOutputDir = process.env.BUILD_ARTIFACTSTAGINGDIRECTORY || process.env.GITHUB_WORKSPACE;
if (ciOutputDir) {
const suite = 'Integration JSON Extension Tests';
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(ciOutputDir, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}

const files = glob.sync('**/*.test.js', { cwd: __dirname });
if (files.length === 0) {
throw new Error('No compiled JSON extension tests found.');
}

const mocha = new Mocha(options);
for (const file of files) {
mocha.addFile(path.join(__dirname, file));
}
mocha.run(failures => {
process.exitCode = failures ? 1 : 0;
});
Loading
Loading