diff --git a/clients/vscode-hlasmplugin/CHANGELOG.md b/clients/vscode-hlasmplugin/CHANGELOG.md index ce34179a7..f37d72767 100644 --- a/clients/vscode-hlasmplugin/CHANGELOG.md +++ b/clients/vscode-hlasmplugin/CHANGELOG.md @@ -3,6 +3,7 @@ ## ****Unreleased**** #### Added +- Infrastructure for remote configuration #### Fixed - Bridge for Git configuration files may be ignored by the macro tracer diff --git a/clients/vscode-hlasmplugin/src/extension.ts b/clients/vscode-hlasmplugin/src/extension.ts index a11bdca0f..332729c5e 100644 --- a/clients/vscode-hlasmplugin/src/extension.ts +++ b/clients/vscode-hlasmplugin/src/extension.ts @@ -33,6 +33,7 @@ import { ConfigurationsHandler } from './configurationsHandler'; import { getLanguageClientMiddleware } from './languageClientMiddleware'; import { ClientInterface, ClientUriDetails, HLASMExternalFiles } from './hlasmExternalFiles'; import { HLASMExternalFilesFtp } from './hlasmExternalFilesFtp'; +import { HLASMExternalConfigurationProvider, HLASMExternalConfigurationProviderHandler } from './hlasmExternalConfigurationProvider'; export const EXTENSION_ID = "broadcommfd.hlasm-language-support"; @@ -109,6 +110,9 @@ export async function activate(context: vscode.ExtensionContext) { clientErrorHandler.defaultHandler = hlasmpluginClient.createDefaultErrorHandler(); + const extConfProvider = new HLASMExternalConfigurationProvider(hlasmpluginClient) + context.subscriptions.push(extConfProvider); + // The objectToString is necessary, because telemetry reporter only takes objects with // string properties and there are some boolean that we receive from the language server hlasmpluginClient.onTelemetry((object) => { telemetry.reportEvent(object.method_name, objectToString(object.properties), object.measurements) }); @@ -146,7 +150,10 @@ export async function activate(context: vscode.ExtensionContext) { }, registerExternalFileClient(service: string, client: ClientInterface) { extFiles.setClient(service, client); - } + }, + registerExternalConfigurationProvider(h: HLASMExternalConfigurationProviderHandler) { + return extConfProvider.addHandler(h); + }, }; return api; } diff --git a/clients/vscode-hlasmplugin/src/hlasmExternalConfigurationProvider.ts b/clients/vscode-hlasmplugin/src/hlasmExternalConfigurationProvider.ts new file mode 100644 index 000000000..53597a578 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/hlasmExternalConfigurationProvider.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +import * as vscode from 'vscode'; +import * as vscodelc from 'vscode-languageclient'; + +interface ExternalConfigurationRequest { + uri: string; +}; +interface ProcGrpsSchemaJson { }; +interface ExternalConfigurationResponse { + configuration: string | ProcGrpsSchemaJson; +}; + +export type HLASMExternalConfigurationProviderHandler = (uri: vscode.Uri) => PromiseLike | ExternalConfigurationResponse | null; + +function isExternalConfigurationRequest(p: any): p is ExternalConfigurationRequest { + return typeof p === 'object' && ('uri' in p); +} + +function isError(e: any): e is Error { + return e instanceof Error; +} + +export class HLASMExternalConfigurationProvider { + private toDispose: vscode.Disposable[] = []; + private requestHandlers: HLASMExternalConfigurationProviderHandler[] = []; + + constructor( + private channel: { + onRequest(method: string, handler: vscodelc.GenericRequestHandler): vscode.Disposable; + sendNotification(method: string, params: any): Promise; + }) { + this.toDispose.push(this.channel.onRequest('external_configuration_request', (...params: any[]) => this.handleRawMessage(...params))); + } + + dispose() { + this.toDispose.forEach(x => x.dispose()); + } + + private async handleRequest(uri: vscode.Uri): Promise { + for (const h of this.requestHandlers) { + try { + const resp = await h(uri); + if (resp) + return resp; + } + catch (e) { + return new vscodelc.ResponseError(-106, isError(e) ? e.message : 'Unknown error'); + } + } + + return new vscodelc.ResponseError(0, 'Not found'); + } + + public async handleRawMessage(...params: any[]): Promise { + if (params.length < 1 || !isExternalConfigurationRequest(params[0])) + return new vscodelc.ResponseError(-5, 'Invalid request'); + + try { + const uri = vscode.Uri.parse(params[0].uri); + return this.handleRequest(uri); + } catch (e) { + return new vscodelc.ResponseError(-5, 'Invalid request'); + } + } + + private invalidateConfiguration(uri: vscode.Uri | null) { + return this.channel.sendNotification('invalidate_external_configuration', uri ? { uri: uri.toString() } : {}); + } + + public addHandler(h: HLASMExternalConfigurationProviderHandler) { + this.requestHandlers.push(h); + + return { + dispose: () => { + const idx = this.requestHandlers.indexOf(h); + if (idx >= 0) + this.requestHandlers.splice(idx, 1); + return this.invalidateConfiguration(null); + }, + invalidate: (uri: vscode.Uri | null) => this.invalidateConfiguration(uri) + }; + } +} diff --git a/clients/vscode-hlasmplugin/src/test/suite/hlasmExternalConfigurationProvider.test.ts b/clients/vscode-hlasmplugin/src/test/suite/hlasmExternalConfigurationProvider.test.ts new file mode 100644 index 000000000..0d04581f6 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/test/suite/hlasmExternalConfigurationProvider.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2023 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +import * as vscode from 'vscode'; +import * as vscodelc from 'vscode-languageclient'; +import * as assert from 'assert'; +import { HLASMExternalConfigurationProvider } from '../../hlasmExternalConfigurationProvider'; + +suite('External configuration provider', () => { + test('Dispose', async () => { + let disposeCalled = 0; + const c = new HLASMExternalConfigurationProvider({ + onRequest: (method: string, handler: vscodelc.GenericRequestHandler): vscode.Disposable => { + return { dispose: () => { ++disposeCalled; } }; + }, + sendNotification: async (method: string, params: any): Promise => { } + }); + + const h = c.addHandler(async (uri) => null); + + assert.ok(h); + assert.strictEqual(typeof h, 'object'); + + c.dispose(); + + assert.strictEqual(disposeCalled, 1); + }); + + test('Query after dispose', async () => { + const c = new HLASMExternalConfigurationProvider({ + onRequest: (method: string, handler: vscodelc.GenericRequestHandler): vscode.Disposable => { + return { dispose: () => { } }; + }, + sendNotification: async (method: string, params: any): Promise => { } + }); + + const h = c.addHandler(async (uri) => { throw Error('Should not be called'); }); + + assert.ok(h); + assert.strictEqual(typeof h, 'object'); + + h.dispose(); + + const f = await c.handleRawMessage({ uri: '' }); + assert.ok('code' in f); + assert.deepStrictEqual(f.code, 0); + }); + + test('Not found', async () => { + const c = new HLASMExternalConfigurationProvider({ + onRequest: (method: string, handler: vscodelc.GenericRequestHandler): vscode.Disposable => { + return { dispose: () => { } }; + }, + sendNotification: async (method: string, params: any): Promise => { } + }); + + let calledWithUri: unknown; + c.addHandler(async (uri) => { + assert.ok(!calledWithUri); + calledWithUri = uri; + return null; + }); + + const f = await c.handleRawMessage({ uri: 'schema:path' }); + assert.ok('code' in f); + assert.deepStrictEqual(f.code, 0); + + assert.ok(calledWithUri instanceof vscode.Uri); + assert.deepStrictEqual(calledWithUri.toString(), 'schema:path'); + }); + + test('Return configuration', async () => { + const c = new HLASMExternalConfigurationProvider({ + onRequest: (method: string, handler: vscodelc.GenericRequestHandler): vscode.Disposable => { + return { dispose: () => { } }; + }, + sendNotification: async (method: string, params: any): Promise => { } + }); + + let calledWithUri: unknown; + c.addHandler(async (uri) => { + assert.ok(!calledWithUri); + calledWithUri = uri; + return { configuration: 'PROCGRP' }; + }); + + const f = await c.handleRawMessage({ uri: 'schema:path' }); + assert.deepStrictEqual(f, { configuration: 'PROCGRP' }); + + assert.ok(calledWithUri instanceof vscode.Uri); + assert.deepStrictEqual(calledWithUri.toString(), 'schema:path'); + }); + + test('Invalidation', async () => { + let notificationParam: unknown; + const c = new HLASMExternalConfigurationProvider({ + onRequest: (method: string, handler: vscodelc.GenericRequestHandler): vscode.Disposable => { + return { dispose: () => { } }; + }, + sendNotification: async (method: string, params: any): Promise => { + assert.deepEqual(method, 'invalidate_external_configuration'); + notificationParam = params; + } + }); + + const h = c.addHandler(async (_) => null); + + await h.invalidate(null); + assert.deepStrictEqual(notificationParam, {}); + + await h.invalidate(vscode.Uri.parse('schema:path')); + assert.deepStrictEqual(notificationParam, { uri: 'schema:path' }); + }); + + test('Throwing handler', async () => { + const c = new HLASMExternalConfigurationProvider({ + onRequest: (method: string, handler: vscodelc.GenericRequestHandler): vscode.Disposable => { + return { dispose: () => { } }; + }, + sendNotification: async (method: string, params: any): Promise => { } + }); + + const h = c.addHandler(async (_) => { throw Error('Error message') }); + + const f = await c.handleRawMessage({ uri: 'schema:path' }); + assert.ok('code' in f && 'message' in f); + assert.deepStrictEqual(f.code, -106); + assert.deepStrictEqual(f.message, 'Error message'); + }); +}); diff --git a/clients/vscode-hlasmplugin/src/test/suite/index.ts b/clients/vscode-hlasmplugin/src/test/suite/index.ts index c368154d0..583912d6c 100644 --- a/clients/vscode-hlasmplugin/src/test/suite/index.ts +++ b/clients/vscode-hlasmplugin/src/test/suite/index.ts @@ -64,6 +64,30 @@ async function primeExtension(): Promise { }; ext.registerExternalFileClient('TEST', fileClientMock); + ext.registerExternalConfigurationProvider((uri: vscode.Uri) => { + const uriString = uri.toString(); + if (uriString.includes("AAAAA")) + return { + configuration: { + name: "P1", + asm_options: { + SYSPARM: "AAAAA" + }, + libs: [ + { + path: "libs" + }, + "copy" + ] + } + }; + else if (uriString.includes("BBBBB")) + return { + configuration: 'P1' + }; + else + return null; + }); return [vscode.debug.registerDebugAdapterTrackerFactory('hlasm', { createDebugAdapterTracker: function (session: vscode.DebugSession): vscode.ProviderResult { diff --git a/clients/vscode-hlasmplugin/src/test/suite/integration.test.ts b/clients/vscode-hlasmplugin/src/test/suite/integration.test.ts index 82bf79189..627d63711 100644 --- a/clients/vscode-hlasmplugin/src/test/suite/integration.test.ts +++ b/clients/vscode-hlasmplugin/src/test/suite/integration.test.ts @@ -17,6 +17,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as helper from './testHelper'; import { waitForDiagnostics } from './testHelper'; +import { EXTENSION_ID, activate } from '../../extension'; suite('Integration Test Suite', () => { const workspace_file = 'open'; @@ -239,4 +240,50 @@ suite('Integration Test Suite', () => { assert.ok(diags2); assert.strictEqual(diags2.length, 0); }).timeout(10000).slow(2500); + + test('External configuration', async () => { + const testFile = (s: string) => helper.waitForDiagnosticsChange(s, async () => { await helper.showDocument(s); }) + const diagsA = await testFile('AAAAA.hlasm'); + assert.ok(diagsA); + assert.deepStrictEqual(diagsA.map(x => [x.code, x.message]), [['MNOTE', 'AAAAA']]); + + const diagsB = await testFile('BBBBB.hlasm'); + assert.ok(diagsB); + assert.deepStrictEqual(diagsB.map(x => [x.code, x.message]), [['MNOTE', 'DONE']]); + + const diagsC = await testFile('CCCCC.hlasm'); + assert.ok(diagsC); + assert.deepStrictEqual(diagsC.map(x => x.code), ['E049']); + + const ext = await vscode.extensions.getExtension>(EXTENSION_ID)!.activate(); + const tmp = ext.registerExternalConfigurationProvider((uri: vscode.Uri) => { + const uriString = uri.toString(); + if (uriString.includes("CCCCC")) + return { + configuration: { + name: "P1", + asm_options: { + SYSPARM: "AAAAA" + }, + libs: [ + { + path: "libs" + }, + "copy" + ] + } + }; + else + return null; + }); + + const newDiagsC = await helper.waitForDiagnosticsChange('CCCCC.hlasm', () => tmp.invalidate(null)); + assert.ok(newDiagsC); + assert.deepStrictEqual(newDiagsC.length, 0); + + const newNewDiagsC = await helper.waitForDiagnosticsChange('CCCCC.hlasm', () => tmp.dispose()); + assert.ok(newNewDiagsC); + assert.deepStrictEqual(newNewDiagsC.map(x => x.code), ['E049']); + + }).timeout(10000).slow(2500); }); diff --git a/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts b/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts index ecc6ce822..cc89ba823 100644 --- a/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts +++ b/clients/vscode-hlasmplugin/src/test/suite/testHelper.ts @@ -48,7 +48,7 @@ export function getWorkspacePath(): string { export async function getWorkspaceFile(workspace_file: string) { const files = await vscode.workspace.findFiles(workspace_file); - assert.ok(files && files[0]); + assert.ok(files && files[0], workspace_file); return files[0]; } diff --git a/clients/vscode-hlasmplugin/src/test/workspace/AAAAA.hlasm b/clients/vscode-hlasmplugin/src/test/workspace/AAAAA.hlasm new file mode 100644 index 000000000..05dbc1636 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/test/workspace/AAAAA.hlasm @@ -0,0 +1,14 @@ + MAC 1 + + MACRO + TEST + MNOTE 4,'&SYSPARM' + MEND + TEST + + + + + + + diff --git a/clients/vscode-hlasmplugin/src/test/workspace/BBBBB.hlasm b/clients/vscode-hlasmplugin/src/test/workspace/BBBBB.hlasm new file mode 100644 index 000000000..86d4c3915 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/test/workspace/BBBBB.hlasm @@ -0,0 +1,3 @@ + MAC 1 + + MNOTE 'DONE' diff --git a/clients/vscode-hlasmplugin/src/test/workspace/CCCCC.hlasm b/clients/vscode-hlasmplugin/src/test/workspace/CCCCC.hlasm new file mode 100644 index 000000000..d1fdc91f1 --- /dev/null +++ b/clients/vscode-hlasmplugin/src/test/workspace/CCCCC.hlasm @@ -0,0 +1 @@ + MAC 1 diff --git a/language_server/src/dap/dap_server.cpp b/language_server/src/dap/dap_server.cpp index 7de39aa0b..362224878 100644 --- a/language_server/src/dap/dap_server.cpp +++ b/language_server/src/dap/dap_server.cpp @@ -30,7 +30,10 @@ server::server(parser_library::debugger_configuration_provider& dc_provider, tel register_feature_methods(); } -void server::request(const std::string&, const nlohmann::json&, std::function) +void server::request(const std::string&, + const nlohmann::json&, + std::function, + std::function) { // Currently, there are no supported DAP requests from client to server /*send_message_->reply(nlohmann::json { diff --git a/language_server/src/dap/dap_server.h b/language_server/src/dap/dap_server.h index a6199b475..7dbe88191 100644 --- a/language_server/src/dap/dap_server.h +++ b/language_server/src/dap/dap_server.h @@ -35,7 +35,8 @@ class server final : public hlasm_plugin::language_server::server, public dap_di void request(const std::string& requested_method, const nlohmann::json& args, - std::function handler) override; + std::function handler, + std::function error_handler) override; void respond(const request_id& id, const std::string& requested_method, const nlohmann::json& args) override; diff --git a/language_server/src/feature.h b/language_server/src/feature.h index 804effb6e..dcc22ba5c 100644 --- a/language_server/src/feature.h +++ b/language_server/src/feature.h @@ -128,7 +128,8 @@ class response_provider public: virtual void request(const std::string& requested_method, const nlohmann::json& args, - std::function handler) = 0; + std::function handler, + std::function error_handler) = 0; virtual void respond(const request_id& id, const std::string& requested_method, const nlohmann::json& args) = 0; virtual void notify(const std::string& method, const nlohmann::json& args) = 0; virtual void respond_error(const request_id& id, diff --git a/language_server/src/lsp/feature_workspace_folders.cpp b/language_server/src/lsp/feature_workspace_folders.cpp index c15a6474a..2e00ec8cd 100644 --- a/language_server/src/lsp/feature_workspace_folders.cpp +++ b/language_server/src/lsp/feature_workspace_folders.cpp @@ -165,9 +165,13 @@ void feature_workspace_folders::send_configuration_request() { { "section", "hlasm" } }, }, } }; - response_->request("workspace/configuration", config_request_args, [this](const nlohmann::json& params) { - configuration(params); - }); + response_->request( + "workspace/configuration", + config_request_args, + [this](const nlohmann::json& params) { configuration(params); }, + [](int, [[maybe_unused]] const char* msg) { + LOG_WARNING("Unexpected error configuration response received: " + std::string(msg)); + }); } void feature_workspace_folders::configuration(const nlohmann::json& params) const diff --git a/language_server/src/lsp/lsp_server.cpp b/language_server/src/lsp/lsp_server.cpp index 2d4f9664f..09b2692f5 100644 --- a/language_server/src/lsp/lsp_server.cpp +++ b/language_server/src/lsp/lsp_server.cpp @@ -38,6 +38,7 @@ namespace hlasm_plugin::language_server::lsp { server::server(parser_library::workspace_manager& ws_mngr) : language_server::server(this) + , ws_mngr(ws_mngr) { features_.push_back(std::make_unique(ws_mngr, *this)); features_.push_back(std::make_unique(ws_mngr, *this)); @@ -65,6 +66,23 @@ void server::consume_parsing_metadata( }); } +namespace { +constexpr std::pair unknown_error { -1, "Unknown error" }; +std::pair extract_lsp_error(const nlohmann::json& errmsg) noexcept +{ + if (!errmsg.is_object()) + return unknown_error; + + auto code = errmsg.find("code"); + auto msg = errmsg.find("message"); + + if (code == errmsg.end() || msg == errmsg.end() || !code->is_number_integer() || !msg->is_string()) + return unknown_error; + + return { code->get(), msg->get()->c_str() }; +} +} // namespace + void server::message_received(const nlohmann::json& message) { std::optional id; @@ -75,7 +93,6 @@ void server::message_received(const nlohmann::json& message) return; } - if (auto result_found = message.find("result"); result_found != message.end()) { // we received a response to our request that was successful @@ -86,7 +103,7 @@ void server::message_received(const nlohmann::json& message) } else if (auto handler = request_handlers_.extract(*id)) { - handler.mapped()(result_found.value()); + handler.mapped().first(result_found.value()); } else { @@ -96,13 +113,25 @@ void server::message_received(const nlohmann::json& message) } else if (auto error_result_found = message.find("error"); error_result_found != message.end()) { - std::string warn_message; - if (auto message_found = error_result_found->find("message"); message_found != error_result_found->end()) - warn_message = message_found->dump(); + decltype(request_handlers_.extract(*id)) handler; + if (id) + handler = request_handlers_.extract(*id); + if (handler) + { + auto [err, msg] = extract_lsp_error(*error_result_found); + handler.mapped().second(err, msg); + } else - warn_message = "Request with id " + (id ? id->to_string() : "") + " returned with unspecified error."; - LOG_WARNING(warn_message); - send_telemetry_error("lsp_server/response_error_returned", warn_message); + { + std::string warn_message; + if (auto message_found = error_result_found->find("message"); message_found != error_result_found->end()) + warn_message = message_found->dump(); + else + warn_message = + "Request with id " + (id ? id->to_string() : "") + " returned with unspecified error."; + LOG_WARNING(warn_message); + send_telemetry_error("lsp_server/response_error_returned", warn_message); + } } else if (auto method_found = message.find("method"); method_found == message.end()) { @@ -128,7 +157,8 @@ void server::message_received(const nlohmann::json& message) void server::request(const std::string& requested_method, const nlohmann::json& args, - std::function handler) + std::function handler, + std::function error_handler) { auto id = request_id_counter++; nlohmann::json reply { @@ -137,7 +167,7 @@ void server::request(const std::string& requested_method, { "method", requested_method }, { "params", args }, }; - request_handlers_.emplace(id, std::move(handler)); + request_handlers_.try_emplace(request_id(id), std::move(handler), std::move(error_handler)); send_message_->reply(reply); } @@ -208,6 +238,9 @@ void server::register_methods() methods_.try_emplace("$/cancelRequest", method { [this](const nlohmann::json& args) { cancel_request_handler(args); }, telemetry_log_level::NO_TELEMETRY }); + methods_.try_emplace("invalidate_external_configuration", + method { + std::bind_front(&server::invalidate_external_configuration, this), telemetry_log_level::NO_TELEMETRY }); } void server::send_telemetry(const telemetry_message& message) { notify("telemetry/event", nlohmann::json(message)); } @@ -216,6 +249,10 @@ void empty_handler(const nlohmann::json&) { // Does nothing } +void empty_error_handler(int, const char*) +{ + // Does nothing +} void server::on_initialize(const request_id& id, const nlohmann::json& param) { @@ -255,7 +292,7 @@ void server::on_initialize(const request_id& id, const nlohmann::json& param) }, }; - request("client/registerCapability", register_configuration_changed_args, &empty_handler); + request("client/registerCapability", register_configuration_changed_args, &empty_handler, &empty_error_handler); for (auto& f : features_) { @@ -408,19 +445,48 @@ void server::consume_diagnostics( void server::request_workspace_configuration( const char* url, parser_library::workspace_manager_response> json_text) { - request("workspace/configuration", + request( + "workspace/configuration", nlohmann::json { { "items", nlohmann::json::array_t { { { "scopeUri", url } }, }, } }, - [json_text = std::move(json_text)](const nlohmann::json& params) { + [json_text](const nlohmann::json& params) { if (!params.is_array() || params.size() != 1) json_text.error(utils::error::invalid_conf_response); else json_text.provide(parser_library::sequence(params.at(0).dump())); - }); + }, + [json_text](int err, const char* msg) { json_text.error(err, msg); }); +} + +void server::request_file_configuration(parser_library::sequence uri, + parser_library::workspace_manager_response> json_text) +{ + request( + "external_configuration_request", + nlohmann::json { { + "uri", + std::string_view(uri), + } }, + [json_text](const nlohmann::json& params) { + if (!params.is_object() || !params.contains("configuration")) + json_text.error(utils::error::invalid_external_configuration); + else + json_text.provide(parser_library::sequence(params.at("configuration").dump())); + }, + [json_text](int err, const char* msg) { json_text.error(err, msg); }); +} + +void server::invalidate_external_configuration(const nlohmann::json& data) +{ + auto uri = data.find("uri"); + if (uri != data.end() && uri->is_string()) + ws_mngr.invalidate_external_configuration(parser_library::sequence(*uri->get_ptr())); + else + ws_mngr.invalidate_external_configuration({}); } } // namespace hlasm_plugin::language_server::lsp diff --git a/language_server/src/lsp/lsp_server.h b/language_server/src/lsp/lsp_server.h index 5fd8c687a..698b92183 100644 --- a/language_server/src/lsp/lsp_server.h +++ b/language_server/src/lsp/lsp_server.h @@ -51,7 +51,8 @@ class server final : public hlasm_plugin::language_server::server, // Sends request to LSP client using send_message_provider. void request(const std::string& requested_method, const nlohmann::json& args, - std::function handler) override; + std::function handler, + std::function error_handler) override; // Sends respond to request to LSP client using send_message_provider. void respond(const request_id& id, const std::string& requested_method, const nlohmann::json& args) override; // Sends notification to LSP client using send_message_provider. @@ -67,6 +68,7 @@ class server final : public hlasm_plugin::language_server::server, private: std::atomic request_id_counter = 0; + parser_library::workspace_manager& ws_mngr; // requests // Implements initialize request. @@ -103,6 +105,10 @@ class server final : public hlasm_plugin::language_server::server, void request_workspace_configuration( const char* url, parser_library::workspace_manager_response> json_text) override; + void request_file_configuration(parser_library::sequence url, + parser_library::workspace_manager_response> json_text) override; + + void invalidate_external_configuration(const nlohmann::json& error); }; } // namespace hlasm_plugin::language_server::lsp diff --git a/language_server/src/server.h b/language_server/src/server.h index 4f2bb91fd..09bc66d04 100644 --- a/language_server/src/server.h +++ b/language_server/src/server.h @@ -54,7 +54,9 @@ class server : public response_provider std::vector> features_; std::map methods_; - std::unordered_map> request_handlers_; + std::unordered_map, std::function>> + request_handlers_; struct method_telemetry_data { diff --git a/language_server/test/dap/dap_feature_test.cpp b/language_server/test/dap/dap_feature_test.cpp index 2ae814bd0..861c5f0fa 100644 --- a/language_server/test/dap/dap_feature_test.cpp +++ b/language_server/test/dap/dap_feature_test.cpp @@ -58,7 +58,10 @@ struct notif_mock struct response_provider_mock : public response_provider { - void request(const std::string&, const nlohmann::json&, std::function) override + void request(const std::string&, + const nlohmann::json&, + std::function, + std::function) override {} void respond(const request_id& id, const std::string& requested_method, const nlohmann::json& args) override { diff --git a/language_server/test/lsp/lsp_server_test.cpp b/language_server/test/lsp/lsp_server_test.cpp index 317e2717f..4320416cf 100644 --- a/language_server/test/lsp/lsp_server_test.cpp +++ b/language_server/test/lsp/lsp_server_test.cpp @@ -30,6 +30,7 @@ namespace nlohmann { inline void PrintTo(nlohmann::json const& json, std::ostream* os) { *os << json.dump(); } } // namespace nlohmann +using namespace ::testing; using namespace hlasm_plugin; using namespace language_server; @@ -38,7 +39,7 @@ TEST(lsp_server, initialize) // this is json params actually sent by vscode LSP client auto j = R"({"jsonrpc":"2.0","id":47,"method":"initialize","params":{"processId":5236,"rootPath":null,"rootUri":null,"capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true},"didChangeConfiguration":{"dynamicRegistration":true},"didChangeWatchedFiles":{"dynamicRegistration":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"executeCommand":{"dynamicRegistration":true},"configuration":true,"workspaceFolders":true},"textDocument":{"publishDiagnostics":{"relatedInformation":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true},"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"]}},"definition":{"dynamicRegistration":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]}},"codeAction":{"dynamicRegistration":true,"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}}},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true},"documentLink":{"dynamicRegistration":true},"typeDefinition":{"dynamicRegistration":true},"implementation":{"dynamicRegistration":true},"colorProvider":{"dynamicRegistration":true}}},"trace":"off","workspaceFolders":null}})"_json; - test::ws_mngr_mock ws_mngr; + NiceMock ws_mngr; send_message_provider_mock smpm; lsp::server s(ws_mngr); s.set_send_message_provider(&smpm); @@ -55,11 +56,11 @@ TEST(lsp_server, initialize) auto config_request_message = R"({"id":1,"jsonrpc":"2.0","method":"workspace/configuration","params":{"items":[{"section":"hlasm"},{}]}})"_json; - EXPECT_CALL(smpm, reply(::testing::_)).WillOnce(::testing::SaveArg<0>(&server_capab)); - EXPECT_CALL(smpm, reply(show_message)).Times(::testing::AtMost(1)); - EXPECT_CALL(smpm, reply(register_message)).Times(::testing::AtMost(1)); - EXPECT_CALL(smpm, reply(config_request_message)).Times(::testing::AtMost(1)); - EXPECT_CALL(smpm, reply(::testing::Truly([](const nlohmann::json& arg) { + EXPECT_CALL(smpm, reply(_)).WillOnce(SaveArg<0>(&server_capab)); + EXPECT_CALL(smpm, reply(show_message)).Times(AtMost(1)); + EXPECT_CALL(smpm, reply(register_message)).Times(AtMost(1)); + EXPECT_CALL(smpm, reply(config_request_message)).Times(AtMost(1)); + EXPECT_CALL(smpm, reply(Truly([](const nlohmann::json& arg) { return arg.count("method") && arg["method"] == "telemetry/event"; }))); @@ -92,7 +93,7 @@ TEST(lsp_server, initialize) TEST(lsp_server, not_implemented_method) { auto j = R"({"jsonrpc":"2.0","id":47,"method":"unknown_method","params":"A parameter"})"_json; - test::ws_mngr_mock ws_mngr; + NiceMock ws_mngr; send_message_provider_mock smpm; lsp::server s(ws_mngr); s.set_send_message_provider(&smpm); @@ -137,7 +138,7 @@ TEST(lsp_server, request_correct) EXPECT_CALL(message_provider, reply(expected_message)); rp.request( - "client_method", "a_json_parameter", [&handler](const nlohmann::json& params) { handler.handle(params); }); + "client_method", "a_json_parameter", [&handler](const nlohmann::json& params) { handler.handle(params); }, {}); auto request_response = R"({"id":0,"jsonrpc":"2.0","result":"response_result"})"_json; @@ -191,9 +192,7 @@ TEST(lsp_server, request_no_id) s.message_received(request_response); } - - -TEST(lsp_server, request_error) +TEST(lsp_server, request_error_unknown) { auto ws_mngr = parser_library::create_workspace_manager(); send_message_provider_mock message_provider; @@ -215,6 +214,25 @@ TEST(lsp_server, request_error) s.message_received(request_response); } +TEST(lsp_server, request_error) +{ + auto ws_mngr = parser_library::create_workspace_manager(); + NiceMock message_provider; + MockFunction error_handler; + lsp::server s(*ws_mngr); + response_provider& rp = s; + s.set_send_message_provider(&message_provider); + + auto request_response = R"({"id":0,"jsonrpc":"2.0","error":{"code":-123456,"message":"the_error_message"}})"_json; + + // Only telemetry expected + EXPECT_CALL(error_handler, Call(-123456, StrEq("the_error_message"))); + + rp.request("client_method", "args", {}, error_handler.AsStdFunction()); + + s.message_received(request_response); +} + TEST(lsp_server, request_error_no_message) { auto ws_mngr = parser_library::create_workspace_manager(); @@ -240,14 +258,95 @@ TEST(lsp_server, request_error_no_message) TEST(lsp_server_test, non_compliant_uri) { - test::ws_mngr_mock ws_mngr; - ::testing::NiceMock smpm; + NiceMock ws_mngr; + NiceMock smpm; lsp::server s(ws_mngr); s.set_send_message_provider(&smpm); - EXPECT_CALL( - ws_mngr, did_open_file(::testing::StrEq("user_storage:/user/storage/layout"), 4, ::testing::StrEq("sad"), 3)); + EXPECT_CALL(ws_mngr, did_open_file(StrEq("user_storage:/user/storage/layout"), 4, StrEq("sad"), 3)); s.message_received( R"({"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"user_storage:/user/storage/layout","languageId":"plaintext","version":4,"text":"sad"}}})"_json); } + +TEST(lsp_server_test, external_configuration_invalidation) +{ + NiceMock ws_mngr; + NiceMock smpm; + lsp::server s(ws_mngr); + s.set_send_message_provider(&smpm); + + EXPECT_CALL(ws_mngr, invalidate_external_configuration(StrEq("scheme:path"))); + + s.message_received( + R"({"jsonrpc":"2.0","method":"invalidate_external_configuration","params":{"uri":"scheme:path"}})"_json); +} + +TEST(lsp_server_test, external_configuration_request) +{ + parser_library::workspace_manager_requests* wmr = nullptr; + NiceMock ws_mngr; + EXPECT_CALL(ws_mngr, set_request_interface(_)).WillOnce(SaveArg<0>(&wmr)); + + NiceMock smpm; + lsp::server s(ws_mngr); + + ASSERT_TRUE(wmr); + + s.set_send_message_provider(&smpm); + + struct resp_t + { + std::variant> result; + + void provide(parser_library::sequence c) { result = std::string(c); } + void error(int err, const char* errmsg) noexcept { result = std::pair(err, errmsg); } + }; + + auto [c, i] = parser_library::make_workspace_manager_response(std::in_place_type); + + EXPECT_CALL(smpm, + reply( + R"({"jsonrpc":"2.0","id":0,"method":"external_configuration_request","params":{"uri":"scheme:path"}})"_json)); + + wmr->request_file_configuration(parser_library::sequence(std::string_view("scheme:path")), c); + + s.message_received(R"({"jsonrpc":"2.0","id":0,"result":{"configuration":"GRP1"}})"_json); + + EXPECT_EQ(i->result, (std::variant>(R"("GRP1")"))); +} + +TEST(lsp_server_test, external_configuration_request_error) +{ + parser_library::workspace_manager_requests* wmr = nullptr; + NiceMock ws_mngr; + EXPECT_CALL(ws_mngr, set_request_interface(_)).WillOnce(SaveArg<0>(&wmr)); + + NiceMock smpm; + lsp::server s(ws_mngr); + + ASSERT_TRUE(wmr); + + s.set_send_message_provider(&smpm); + + struct resp_t + { + std::variant> result; + + void provide(parser_library::sequence c) { result = std::string(c); } + void error(int err, const char* errmsg) noexcept { result = std::pair(err, errmsg); } + }; + + auto [c, i] = parser_library::make_workspace_manager_response(std::in_place_type); + + EXPECT_CALL(smpm, + reply( + R"({"jsonrpc":"2.0","id":0,"method":"external_configuration_request","params":{"uri":"scheme:path"}})"_json)); + + wmr->request_file_configuration(parser_library::sequence(std::string_view("scheme:path")), c); + + s.message_received(R"({"jsonrpc":"2.0","id":0,"error":{"code":123456, "message":"error message"}})"_json); + + EXPECT_EQ(i->result, + (std::variant>(std::pair(123456, "error message")))); +} diff --git a/language_server/test/lsp/workspace_folders_test.cpp b/language_server/test/lsp/workspace_folders_test.cpp index 931d0732c..8f83e01f3 100644 --- a/language_server/test/lsp/workspace_folders_test.cpp +++ b/language_server/test/lsp/workspace_folders_test.cpp @@ -89,7 +89,7 @@ TEST(workspace_folders, initialize_folders) response_provider_mock rpm; lsp::feature_workspace_folders f(ws_mngr, rpm); - EXPECT_CALL(rpm, request(std::string("workspace/configuration"), _, _)).Times(5); + EXPECT_CALL(rpm, request(std::string("workspace/configuration"), _, _, _)).Times(5); // workspace folders on, but no workspaces provided auto init1 = R"({"processId":5236, @@ -158,7 +158,7 @@ TEST(workspace_folders, initialize_folder_with_configuration) ws_mngr->set_request_interface(&req_mock); - EXPECT_CALL(rpm, request(std::string("workspace/configuration"), _, _)).Times(1); + EXPECT_CALL(rpm, request(std::string("workspace/configuration"), _, _, _)).Times(1); parser_library::workspace_manager_response> json_text; EXPECT_CALL(req_mock, request_workspace_configuration(StrEq(ws1_uri), _)) @@ -202,7 +202,7 @@ TEST(workspace_folders, did_change_configuration) }, }; - EXPECT_CALL(provider, request("workspace/configuration", config_request_args, ::testing::_)) + EXPECT_CALL(provider, request("workspace/configuration", config_request_args, ::testing::_, ::testing::_)) .WillOnce(::testing::SaveArg<2>(&handler)); methods["workspace/didChangeConfiguration"].as_notification_handler()("{}"_json); @@ -255,7 +255,7 @@ TEST(workspace_folders, did_change_configuration_empty_configuration_params) }, }; - EXPECT_CALL(provider, request("workspace/configuration", config_request_args, ::testing::_)) + EXPECT_CALL(provider, request("workspace/configuration", config_request_args, ::testing::_, ::testing::_)) .WillOnce(::testing::SaveArg<2>(&handler)); methods["workspace/didChangeConfiguration"].as_notification_handler()("{}"_json); diff --git a/language_server/test/response_provider_mock.h b/language_server/test/response_provider_mock.h index c7ece473e..5b9ddf432 100644 --- a/language_server/test/response_provider_mock.h +++ b/language_server/test/response_provider_mock.h @@ -21,10 +21,11 @@ namespace hlasm_plugin::language_server { class response_provider_mock : public response_provider { public: - MOCK_METHOD3(request, + MOCK_METHOD4(request, void(const std::string& requested_method, const nlohmann::json& args, - std::function handler)); + std::function handler, + std::function error_handler)); MOCK_METHOD3(respond, void(const request_id& id, const std::string& requested_method, const nlohmann::json& args)); MOCK_METHOD2(notify, void(const std::string& method, const nlohmann::json& args)); MOCK_METHOD5(respond_error, diff --git a/language_server/test/ws_mngr_mock.h b/language_server/test/ws_mngr_mock.h index 1a28bf97d..fd1608b1e 100644 --- a/language_server/test/ws_mngr_mock.h +++ b/language_server/test/ws_mngr_mock.h @@ -87,6 +87,8 @@ class ws_mngr_mock : public workspace_manager MOCK_METHOD(void, idle_handler, (const std::atomic* yield_indicator), (override)); MOCK_METHOD(debugger_configuration_provider&, get_debugger_configuration_provider, (), (override)); + + MOCK_METHOD(void, invalidate_external_configuration, (sequence uri), (override)); }; } // namespace hlasm_plugin::language_server::test diff --git a/language_server/test/ws_mngr_req_mock.h b/language_server/test/ws_mngr_req_mock.h index 56ed285a2..08ff3852a 100644 --- a/language_server/test/ws_mngr_req_mock.h +++ b/language_server/test/ws_mngr_req_mock.h @@ -28,6 +28,12 @@ class workspace_manager_requests_mock : public hlasm_plugin::parser_library::wor hlasm_plugin::parser_library::workspace_manager_response> json_text), (override)); + MOCK_METHOD(void, + request_file_configuration, + (hlasm_plugin::parser_library::sequence url, + hlasm_plugin::parser_library::workspace_manager_response> + json_text), + (override)); }; #endif diff --git a/parser_library/include/CMakeLists.txt b/parser_library/include/CMakeLists.txt index 4a2f452a5..5168ebac8 100644 --- a/parser_library/include/CMakeLists.txt +++ b/parser_library/include/CMakeLists.txt @@ -13,6 +13,7 @@ target_sources(parser_library PUBLIC debugger.h + external_configuration_requests.h lib_config.h message_consumer.h protocol.h diff --git a/parser_library/include/external_configuration_requests.h b/parser_library/include/external_configuration_requests.h new file mode 100644 index 000000000..54b96a258 --- /dev/null +++ b/parser_library/include/external_configuration_requests.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#ifndef HLASMPLUGIN_PARSERLIBRARY_EXTERNAL_CONFIGURATION_REQUESTS_H +#define HLASMPLUGIN_PARSERLIBRARY_EXTERNAL_CONFIGURATION_REQUESTS_H + +#include "sequence.h" +#include "workspace_manager_response.h" + +namespace hlasm_plugin::parser_library { + +class external_configuration_requests +{ +protected: + ~external_configuration_requests() = default; + +public: + virtual void read_external_configuration( + sequence url, workspace_manager_response> content) = 0; +}; + +} // namespace hlasm_plugin::parser_library + +#endif diff --git a/parser_library/include/workspace_manager.h b/parser_library/include/workspace_manager.h index 10cc56aa7..42b664017 100644 --- a/parser_library/include/workspace_manager.h +++ b/parser_library/include/workspace_manager.h @@ -32,6 +32,7 @@ namespace hlasm_plugin::parser_library { class workspace_manager_external_file_requests; +class external_configuration_requests; namespace debugging { struct debugger_configuration; } // namespace debugging @@ -140,6 +141,8 @@ class workspace_manager virtual void idle_handler(const std::atomic* yield_indicator = nullptr) = 0; virtual debugger_configuration_provider& get_debugger_configuration_provider() = 0; + + virtual void invalidate_external_configuration(sequence uri) = 0; }; workspace_manager* create_workspace_manager_impl(workspace_manager_external_file_requests* external_requests); diff --git a/parser_library/include/workspace_manager_requests.h b/parser_library/include/workspace_manager_requests.h index 0a9ca0869..01616a991 100644 --- a/parser_library/include/workspace_manager_requests.h +++ b/parser_library/include/workspace_manager_requests.h @@ -15,9 +15,9 @@ #ifndef HLASMPLUGIN_PARSERLIBRARY_WORKSPACE_MANAGER_REQUESTS_H #define HLASMPLUGIN_PARSERLIBRARY_WORKSPACE_MANAGER_REQUESTS_H +#include "sequence.h" #include "workspace_manager_response.h" - namespace hlasm_plugin::parser_library { class workspace_manager_requests @@ -28,6 +28,8 @@ class workspace_manager_requests public: virtual void request_workspace_configuration( const char* url, workspace_manager_response> json_text) = 0; + virtual void request_file_configuration( + sequence url, workspace_manager_response> json_text) = 0; }; } // namespace hlasm_plugin::parser_library diff --git a/parser_library/src/debugging/debugger.cpp b/parser_library/src/debugging/debugger.cpp index 1150ac6a9..2e4e9612f 100644 --- a/parser_library/src/debugging/debugger.cpp +++ b/parser_library/src/debugging/debugger.cpp @@ -35,6 +35,7 @@ #include "macro_param_variable.h" #include "ordinary_symbol_variable.h" #include "set_symbol_variable.h" +#include "utils/async_busy_wait.h" #include "utils/task.h" #include "variable.h" #include "virtual_file_monitor.h" @@ -159,18 +160,6 @@ class debugger::impl final : public processing::statement_analyzer public: impl() = default; - struct - { - template - utils::value_task operator()(Channel channel, T* result) const - { - while (!channel.resolved()) - co_await utils::task::suspend(); - - co_return std::move(*result); - } - } static constexpr async_busy_wait = {}; // clang 14 - void launch(std::string_view source, debugger_configuration_provider& dc_provider, bool stop_on_entry, @@ -191,7 +180,7 @@ class debugger::impl final : public processing::statement_analyzer auto [conf_resp, conf] = make_workspace_manager_response(std::in_place_type); dc_provider.provide_debugger_configuration(sequence(source), conf_resp); analyzer_task = - async_busy_wait(std::move(conf_resp), &conf->conf) + utils::async_busy_wait(std::move(conf_resp), &conf->conf) .then(std::bind_front( &impl::start_main_analyzer, this, utils::resource::resource_location(source), std::move(resp))); } diff --git a/parser_library/src/workspace_manager.cpp b/parser_library/src/workspace_manager.cpp index 10d5f6153..c15409869 100644 --- a/parser_library/src/workspace_manager.cpp +++ b/parser_library/src/workspace_manager.cpp @@ -32,10 +32,12 @@ #include #include "debugging/debugger_configuration.h" +#include "external_configuration_requests.h" #include "lsp/completion_item.h" #include "lsp/document_symbol_item.h" #include "nlohmann/json.hpp" #include "protocol.h" +#include "utils/async_busy_wait.h" #include "utils/content_loader.h" #include "utils/encoding.h" #include "utils/error_codes.h" @@ -58,7 +60,8 @@ namespace hlasm_plugin::parser_library { class workspace_manager_impl final : public workspace_manager, debugger_configuration_provider, public diagnosable_impl, - workspaces::external_file_reader + workspaces::external_file_reader, + external_configuration_requests { static constexpr lib_config supress_all { 0 }; using resource_location = utils::resource::resource_location; @@ -68,8 +71,9 @@ class workspace_manager_impl final : public workspace_manager, opened_workspace(const resource_location& location, const std::string& name, workspaces::file_manager& file_manager, - const lib_config& global_config) - : ws(location, name, file_manager, global_config, settings) + const lib_config& global_config, + external_configuration_requests* ecr) + : ws(location, name, file_manager, global_config, settings, ecr) {} opened_workspace(workspaces::file_manager& file_manager, const lib_config& global_config) : ws(file_manager, global_config, settings) @@ -839,18 +843,6 @@ class workspace_manager_impl final : public workspace_manager, static constexpr std::string_view hlasm_external_scheme = "hlasm-external://"; - struct - { - template - utils::value_task operator()(Channel channel, T* result) const - { - while (!channel.resolved()) - co_await utils::task::suspend(); - - co_return std::move(*result); - } - } static constexpr async_busy_wait = {}; // clang 14 - [[nodiscard]] utils::value_task> load_text_external( const utils::resource::resource_location& document_loc) const { @@ -864,7 +856,7 @@ class workspace_manager_impl final : public workspace_manager, auto [channel, data] = make_workspace_manager_response(std::in_place_type); m_external_file_requests->read_external_file(document_loc.get_uri().c_str(), channel); - return async_busy_wait(std::move(channel), &data->result); + return utils::async_busy_wait(std::move(channel), &data->result); } [[nodiscard]] utils::value_task> load_text( @@ -924,7 +916,7 @@ class workspace_manager_impl final : public workspace_manager, auto [channel, data] = make_workspace_manager_response(std::in_place_type, directory); m_external_file_requests->read_external_directory(data->dir.get_uri().c_str(), channel); - return async_busy_wait(std::move(channel), &data->result); + return utils::async_busy_wait(std::move(channel), &data->result); } [[nodiscard]] utils::value_task>, @@ -972,9 +964,14 @@ class workspace_manager_impl final : public workspace_manager, void add_workspace(const char* name, const char* uri) override { - auto& ows = - m_workspaces.try_emplace(name, resource_location(std::move(uri)), name, m_file_manager, m_global_config) - .first->second; + auto& ows = m_workspaces + .try_emplace(name, + resource_location(std::move(uri)), + name, + m_file_manager, + m_global_config, + static_cast(this)) + .first->second; ows.ws.set_message_consumer(m_message_consumer); auto& new_workspace = m_work_queue.emplace_back(work_item { @@ -1044,6 +1041,33 @@ class workspace_manager_impl final : public workspace_manager, else m_work_queue.push_front(std::move(wi)); } + + + void read_external_configuration(sequence uri, workspace_manager_response> content) override + { + if (!m_requests) + { + content.error(utils::error::not_found); + return; + } + + m_requests->request_file_configuration(uri, content); + } + + void invalidate_external_configuration(sequence uri) override + { + if (uri.size() == 0) + { + resource_location res; + for (auto& [_, ows] : m_workspaces) + ows.ws.invalidate_external_configuration(res); + m_implicit_workspace.ws.invalidate_external_configuration(res); + m_quiet_implicit_workspace.ws.invalidate_external_configuration(res); + } + else + ws_path_match(std::string_view(uri)) + .ws.invalidate_external_configuration(resource_location(std::string_view(uri))); + } }; workspace_manager* create_workspace_manager_impl(workspace_manager_external_file_requests* external_requests) diff --git a/parser_library/src/workspaces/workspace.cpp b/parser_library/src/workspaces/workspace.cpp index 52872027b..200fc2314 100644 --- a/parser_library/src/workspaces/workspace.cpp +++ b/parser_library/src/workspaces/workspace.cpp @@ -109,9 +109,10 @@ struct workspace_parse_lib_provider final : public parse_lib_provider std::unordered_map, resource_location_hasher, std::equal_to<>> current_file_map; - workspace_parse_lib_provider(workspace& ws, workspace::processor_file_compoments& pfc) + workspace_parse_lib_provider( + workspace& ws, std::vector> libraries, workspace::processor_file_compoments& pfc) : ws(ws) - , libraries(ws.get_proc_grp(pfc.m_file->get_location()).libraries()) + , libraries(std::move(libraries)) , pfc(pfc) {} @@ -255,28 +256,23 @@ workspace::workspace(const resource_location& location, const std::string& name, file_manager& file_manager, const lib_config& global_config, - const shared_json& global_settings) + const shared_json& global_settings, + external_configuration_requests* ecr) : name_(name) , location_(location.lexically_normal()) , file_manager_(file_manager) , fm_vfm_(file_manager_, location) , implicit_proc_grp("pg_implicit", {}, {}) , global_config_(global_config) - , m_configuration(file_manager, location_, global_settings) -{} - -workspace::workspace(const resource_location& location, - file_manager& file_manager, - const lib_config& global_config, - const shared_json& global_settings) - : workspace(location, location.get_uri(), file_manager, global_config, global_settings) + , m_configuration(file_manager, location_, global_settings, ecr) {} workspace::workspace(file_manager& file_manager, const lib_config& global_config, const shared_json& global_settings, - std::shared_ptr implicit_library) - : workspace(resource_location(""), file_manager, global_config, global_settings) + std::shared_ptr implicit_library, + external_configuration_requests* ecr) + : workspace(resource_location(""), "", file_manager, global_config, global_settings, ecr) { opened_ = true; if (implicit_library) @@ -560,8 +556,10 @@ utils::value_task workspace::parse_file(const resource_locati return [](processor_file_compoments& comp, workspace& self) -> utils::value_task { const auto& url = comp.m_file->get_location(); - comp.m_alternative_config = co_await self.m_configuration.load_alternative_config_if_needed(url); - workspace_parse_lib_provider ws_lib(self, comp); + auto config = co_await self.get_analyzer_configuration(url); + + comp.m_alternative_config = std::move(config.alternative_config_url); + workspace_parse_lib_provider ws_lib(self, std::move(config.libraries), comp); if (auto prefetch = ws_lib.prefetch_libraries(); prefetch.valid()) co_await std::move(prefetch); @@ -571,8 +569,8 @@ utils::value_task workspace::parse_file(const resource_locati auto results = co_await parse_one_file(comp.m_last_opencode_id_storage, comp.m_file, ws_lib, - self.get_asm_options(url), - self.get_preprocessor_options(url), + std::move(config.opts), + std::move(config.pp_opts), &self.fm_vfm_); results.hc_macro_map = std::move(comp.m_last_results->hc_macro_map); // save macro stuff results.macro_diagnostics = std::move(comp.m_last_results->macro_diagnostics); @@ -605,13 +603,24 @@ utils::value_task workspace::parse_file(const resource_locati utils::value_task workspace::get_debugger_configuration(resource_location url) { - co_await m_configuration.load_alternative_config_if_needed(url); - co_return debugging::debugger_configuration { - .fm = &file_manager_, + return get_analyzer_configuration(std::move(url)).then([this](auto c) { + return debugging::debugger_configuration { + .fm = &file_manager_, + .libraries = std::move(c.libraries), + .workspace_uri = location_, + .opts = std::move(c.opts), + .pp_opts = std::move(c.pp_opts), + }; + }); +} +utils::value_task workspace::get_analyzer_configuration(resource_location url) +{ + auto alt_config = co_await m_configuration.load_alternative_config_if_needed(url); + co_return analyzer_configuration { .libraries = get_libraries(url), - .workspace_uri = location_, .opts = get_asm_options(url), .pp_opts = get_preprocessor_options(url), + .alternative_config_url = std::move(alt_config), }; } @@ -656,6 +665,16 @@ utils::task workspace::mark_file_for_parsing( return {}; } + +void workspace::invalidate_external_configuration(const resource_location& url) +{ + m_configuration.prune_external_processor_groups(url); + if (url.empty()) + mark_all_opened_files(); + else if (auto it = m_processor_files.find(url); it != m_processor_files.end() && it->second.m_opened) + m_parsing_pending.emplace(url); +} + workspace_file_info workspace::parse_successful(processor_file_compoments& comp, workspace_parse_lib_provider libs) { workspace_file_info ws_file_info; @@ -704,6 +723,8 @@ utils::task workspace::did_open_file(resource_location file_location, file_conte utils::task workspace::did_close_file(resource_location file_location) { + m_configuration.prune_external_processor_groups(file_location); + auto fcomp = m_processor_files.find(file_location); if (fcomp == m_processor_files.end()) co_return; // this indicates some kind of double close or configuration file close diff --git a/parser_library/src/workspaces/workspace.h b/parser_library/src/workspaces/workspace.h index 0126febc6..cf9fdd6bc 100644 --- a/parser_library/src/workspaces/workspace.h +++ b/parser_library/src/workspaces/workspace.h @@ -39,6 +39,9 @@ #include "utils/task.h" #include "workspace_configuration.h" +namespace hlasm_plugin::parser_library { +class external_configuration_requests; +} // namespace hlasm_plugin::parser_library namespace hlasm_plugin::parser_library::workspaces { class file_manager; class library; @@ -68,16 +71,14 @@ class workspace : public diagnosable_impl workspace(file_manager& file_manager, const lib_config& global_config, const shared_json& global_settings, - std::shared_ptr implicit_library = nullptr); - workspace(const resource_location& location, - file_manager& file_manager, - const lib_config& global_config, - const shared_json& global_settings); + std::shared_ptr implicit_library = nullptr, + external_configuration_requests* ecr = nullptr); workspace(const resource_location& location, const std::string& name, file_manager& file_manager, const lib_config& global_config, - const shared_json& global_settings); + const shared_json& global_settings, + external_configuration_requests* ecr = nullptr); workspace(const workspace& ws) = delete; workspace& operator=(const workspace&) = delete; @@ -137,6 +138,8 @@ class workspace : public diagnosable_impl utils::value_task get_debugger_configuration(resource_location url); + void invalidate_external_configuration(const resource_location& url); + private: std::string name_; resource_location location_; @@ -209,6 +212,15 @@ class workspace : public diagnosable_impl std::vector find_related_opencodes(const resource_location& document_loc) const; void filter_and_close_dependencies(std::set files_to_close_candidates, const processor_file_compoments* file_to_ignore = nullptr); + + struct analyzer_configuration + { + std::vector> libraries; + asm_option opts; + std::vector pp_opts; + resource_location alternative_config_url; + }; + utils::value_task get_analyzer_configuration(resource_location url); }; } // namespace hlasm_plugin::parser_library::workspaces diff --git a/parser_library/src/workspaces/workspace_configuration.cpp b/parser_library/src/workspaces/workspace_configuration.cpp index 43567969e..843f19533 100644 --- a/parser_library/src/workspaces/workspace_configuration.cpp +++ b/parser_library/src/workspaces/workspace_configuration.cpp @@ -22,14 +22,17 @@ #include #include +#include "external_configuration_requests.h" #include "file_manager.h" #include "library_local.h" #include "nlohmann/json.hpp" +#include "utils/async_busy_wait.h" #include "utils/content_loader.h" #include "utils/encoding.h" #include "utils/path.h" #include "utils/path_conversions.h" #include "utils/platform.h" +#include "utils/string_operations.h" #include "wildcard.h" namespace hlasm_plugin::parser_library::workspaces { @@ -215,11 +218,14 @@ const std::regex json_settings_replacer::config_reference(R"(\$\{([^}]+)\})"); } // namespace -workspace_configuration::workspace_configuration( - file_manager& fm, utils::resource::resource_location location, const shared_json& global_settings) +workspace_configuration::workspace_configuration(file_manager& fm, + utils::resource::resource_location location, + const shared_json& global_settings, + external_configuration_requests* ecr) : m_file_manager(fm) , m_location(std::move(location)) , m_global_settings(global_settings) + , m_external_configuration_requests(ecr) { auto hlasm_folder = utils::resource::resource_location::join(m_location, HLASM_PLUGIN_FOLDER); m_proc_grps_loc = utils::resource::resource_location::join(hlasm_folder, FILENAME_PROC_GRPS); @@ -276,7 +282,10 @@ void workspace_configuration::process_processor_group(const config::processor_gr }, lib_or_dataset); } - m_proc_grps.try_emplace(std::make_pair(prc_grp.name(), alternative_root), std::move(prc_grp)); + if (alternative_root.empty()) + m_proc_grps.try_emplace(basic_conf { prc_grp.name() }, std::move(prc_grp)); + else + m_proc_grps.try_emplace(b4g_conf { prc_grp.name(), alternative_root }, std::move(prc_grp)); } constexpr std::string_view external_uri_scheme = "hlasm-external"; @@ -373,7 +382,7 @@ bool workspace_configuration::process_program(const config::program_mapping& pgm std::optional grp_id; if (pgm.pgroup != NOPROC_GROUP_ID) { - grp_id = proc_grp_id { pgm.pgroup, utils::resource::resource_location() }; + grp_id.emplace(basic_conf { pgm.pgroup }); if (!m_proc_grps.contains(*grp_id)) return false; } @@ -387,10 +396,10 @@ bool workspace_configuration::process_program(const config::program_mapping& pgm if (auto rl = transform_to_resource_location(*pgm_name, m_location); pgm_name->find_first_of("*?") == std::string::npos) - m_exact_pgm_conf.try_emplace(rl, tagged_program { program(rl, grp_id, pgm.opts) }); + m_exact_pgm_conf.try_emplace(rl, tagged_program { program(rl, grp_id, pgm.opts, false) }); else m_regex_pgm_conf.emplace_back( - tagged_program { program { rl, grp_id, pgm.opts } }, wildcard2regex(rl.get_uri())); + tagged_program { program { rl, grp_id, pgm.opts, false } }, wildcard2regex(rl.get_uri())); return true; } @@ -539,6 +548,13 @@ bool workspace_configuration::settings_updated() const return false; } +struct +{ + std::string_view operator()(const basic_conf& c) const noexcept { return c.name; } + std::string_view operator()(const b4g_conf& c) const noexcept { return c.name; } + std::string_view operator()(const external_conf&) const noexcept { return {}; } +} static constexpr proc_group_name; + std::optional> workspace_configuration::try_creating_rl_tagged_pgm_pair( std::unordered_set>& missing_pgroups, @@ -551,9 +567,9 @@ workspace_configuration::try_creating_rl_tagged_pgm_pair( std::optional grp_id_o; if (m_proc_grps.contains(grp_id)) grp_id_o = std::move(grp_id); - else if (grp_id.first != NOPROC_GROUP_ID) + else if (auto pg_name = std::visit(proc_group_name, grp_id); pg_name != NOPROC_GROUP_ID) { - missing_pgroups.emplace(grp_id.first); + missing_pgroups.emplace(pg_name); if (default_b4g_proc_group) return {}; @@ -568,6 +584,7 @@ workspace_configuration::try_creating_rl_tagged_pgm_pair( rl, std::move(grp_id_o), {}, + false, }, tag, }); @@ -585,7 +602,10 @@ utils::value_task workspace_configuration::parse_b4g_c { std::erase_if(m_exact_pgm_conf, [tag = std::to_address(it)](const auto& e) { return e.second.tag == tag; }); std::erase_if(m_regex_pgm_conf, [tag = std::to_address(it)](const auto& e) { return e.first.tag == tag; }); - std::erase_if(m_proc_grps, [&alternative_root](const auto& e) { return e.first.second == alternative_root; }); + std::erase_if(m_proc_grps, [&alternative_root](const auto& e) { + const auto* b4g = std::get_if(&e.first); + return b4g && b4g->bridge_json_uri == alternative_root; + }); it->second = {}; } @@ -614,7 +634,7 @@ utils::value_task workspace_configuration::parse_b4g_c for (const auto& [name, details] : conf.config.value().files) m_exact_pgm_conf.insert(*try_creating_rl_tagged_pgm_pair(missing_pgroups, false, - proc_grp_id { + b4g_conf { details.processor_group_name, alternative_root, }, @@ -626,7 +646,7 @@ utils::value_task workspace_configuration::parse_b4g_c { if (auto rl_tagged_pgm = try_creating_rl_tagged_pgm_pair(missing_pgroups, true, - proc_grp_id { + b4g_conf { def_grp, alternative_root, }, @@ -708,8 +728,10 @@ void workspace_configuration::copy_diagnostics(const diagnosable& target, const std::unordered_set& b4g_filter) const { - for (auto& [_, pg] : m_proc_grps) + for (auto& [key, pg] : m_proc_grps) { + if (const auto* e = std::get_if(&key); e && e->definition.use_count() <= 1) + continue; pg.collect_diags(); for (const auto& d : pg.diags()) target.add_diagnostic(d); @@ -821,30 +843,165 @@ const processor_group& workspace_configuration::get_proc_grp(const proc_grp_id& const program* workspace_configuration::get_program(const utils::resource::resource_location& file_location) const { - return get_program_normalized(file_location.lexically_normal()); + return get_program_normalized(file_location.lexically_normal()).first; } -const program* workspace_configuration::get_program_normalized( +std::pair workspace_configuration::get_program_normalized( const utils::resource::resource_location& file_location_normalized) const { // direct match if (auto program = m_exact_pgm_conf.find(file_location_normalized); program != m_exact_pgm_conf.cend()) - return &program->second.pgm; + return { &program->second.pgm, true }; for (const auto& [program, pattern] : m_regex_pgm_conf) { if (std::regex_match(file_location_normalized.get_uri(), pattern)) - return &program.pgm; + return { &program.pgm, false }; } - return nullptr; + return { nullptr, false }; +} + + +decltype(workspace_configuration::m_proc_grps)::iterator workspace_configuration::make_external_proc_group( + const utils::resource::resource_location& normalized_location, std::string group_json) +{ + config::processor_group pg; + global_settings_map utilized_settings_values; + + const auto current_settings = m_global_settings.load(); + json_settings_replacer json_visitor { *current_settings, utilized_settings_values, m_location }; + + std::vector diags; + + auto proc_json = nlohmann::json::parse(group_json); + json_visitor(proc_json); + proc_json.get_to(pg); + + if (!pg.asm_options.valid()) + diags.push_back(diagnostic_s::error_W0005(normalized_location, pg.name, "external processor group")); + for (const auto& p : pg.preprocessors) + { + if (!p.valid()) + diags.push_back(diagnostic_s::error_W0006(normalized_location, pg.name, p.type())); + } + + processor_group prc_grp("", pg.asm_options, pg.preprocessors); + + for (auto& lib_or_dataset : pg.libs) + { + std::visit( + [this, &diags, &prc_grp](const auto& lib) { + process_processor_group_library(lib, utils::resource::resource_location(), diags, {}, prc_grp); + }, + lib_or_dataset); + } + m_utilized_settings_values.merge(std::move(utilized_settings_values)); + + for (auto&& d : diags) + prc_grp.add_diagnostic(std::move(d)); + + return m_proc_grps + .try_emplace(external_conf { std::make_shared(std::move(group_json)) }, std::move(prc_grp)) + .first; +} + +void workspace_configuration::update_external_configuration( + const utils::resource::resource_location& normalized_location, std::string group_json) +{ + if (std::string_view group_name(group_json); utils::trim_left(group_name, " \t\n\r"), group_name.starts_with("\"")) + { + m_exact_pgm_conf.insert_or_assign(normalized_location, + tagged_program { + program { + normalized_location, + basic_conf { nlohmann::json::parse(group_json).get() }, + {}, + true, + }, + }); + return; + } + + auto pg = m_proc_grps.find(tagged_string_view { group_json }); + if (pg == m_proc_grps.end()) + { + pg = make_external_proc_group(normalized_location, std::move(group_json)); + } + + m_exact_pgm_conf.insert_or_assign(normalized_location, + tagged_program { + program { + normalized_location, + pg->first, + {}, + true, + }, + }); +} + + +void workspace_configuration::prune_external_processor_groups(const utils::resource::resource_location& location) +{ + if (!location.empty()) + { + if (auto p = m_exact_pgm_conf.find(location.lexically_normal()); + p != m_exact_pgm_conf.end() && p->second.pgm.external) + m_exact_pgm_conf.erase(p); + } + else + std::erase_if(m_exact_pgm_conf, [](const auto& p) { return p.second.pgm.external; }); + + std::erase_if(m_proc_grps, [](const auto& pg) { + const auto* e = std::get_if(&pg.first); + return e && e->definition.use_count() == 1; + }); } utils::value_task workspace_configuration::load_alternative_config_if_needed( const utils::resource::resource_location& file_location) { const auto rl = file_location.lexically_normal(); - if (auto pgm = get_program_normalized(rl); - pgm && pgm->pgroup.has_value() && pgm->pgroup.value().second == utils::resource::resource_location()) + auto [pgm, direct] = get_program_normalized(rl); + + if (direct && pgm && pgm->pgroup + && (std::holds_alternative(*pgm->pgroup) || std::holds_alternative(*pgm->pgroup))) + co_return utils::resource::resource_location(); + + if (m_external_configuration_requests) + { + struct resp + { + std::variant result; + void provide(sequence c) { result = std::string(c); } + void error(int err, const char*) noexcept { result = err; } + }; + auto [c, i] = make_workspace_manager_response(std::in_place_type); + m_external_configuration_requests->read_external_configuration(sequence(rl.get_uri()), c); + + auto json_data = co_await utils::async_busy_wait(std::move(c), &i->result); + if (std::holds_alternative(json_data)) + { + try + { + update_external_configuration(rl, std::move(std::get(json_data))); + co_return utils::resource::resource_location(); + } + catch (const nlohmann::json&) + { + // incompatible json in the response + json_data = -1; + } + } + + if (std::get(json_data) != 0) + { + // TODO: do we do something with the error? + // this basically indicates either an error on the client side, + // or an allocation failure while processing the response + } + } + + if (pgm && pgm->pgroup && std::holds_alternative(*pgm->pgroup)) co_return utils::resource::resource_location(); auto configuration_url = utils::resource::resource_location::replace_filename(rl, B4G_CONF_FILE); diff --git a/parser_library/src/workspaces/workspace_configuration.h b/parser_library/src/workspaces/workspace_configuration.h index d7f36cf9e..bb119942c 100644 --- a/parser_library/src/workspaces/workspace_configuration.h +++ b/parser_library/src/workspaces/workspace_configuration.h @@ -16,6 +16,7 @@ #define HLASMPLUGIN_PARSERLIBRARY_WORKSPACE_CONFIGURATION_H #include +#include #include #include #include @@ -39,26 +40,67 @@ #include "utils/resource_location.h" #include "utils/task.h" +namespace hlasm_plugin::parser_library { +class external_configuration_requests; +} // namespace hlasm_plugin::parser_library namespace hlasm_plugin::parser_library::workspaces { using program_id = utils::resource::resource_location; using global_settings_map = std::unordered_map, utils::hashers::string_hasher, std::equal_to<>>; -using proc_grp_id = std::pair; + +struct basic_conf +{ + std::string name; + + auto operator<=>(const basic_conf&) const = default; + + size_t hash() const noexcept { return std::hash()(name); } +}; + +struct b4g_conf +{ + std::string name; + utils::resource::resource_location bridge_json_uri; + + auto operator<=>(const b4g_conf&) const = default; + + size_t hash() const noexcept + { + return std::hash()(name) ^ utils::resource::resource_location_hasher()(bridge_json_uri); + } +}; + +struct external_conf +{ + std::shared_ptr definition; + + bool operator==(const external_conf& o) const { return *definition == *o.definition; } + auto operator<=>(const external_conf& o) const { return definition->compare(*o.definition) <=> 0; } // clang 14 + + bool operator==(std::string_view o) const { return *definition == o; } + auto operator<=>(std::string_view o) const { return definition->compare(o) <=> 0; } // clang 14 + + size_t hash() const noexcept { return std::hash()(*definition); } +}; + +using proc_grp_id = std::variant; class file_manager; struct library_local_options; // represents pair program => processor group - saves // information that a program uses certain processor group struct program { - program(program_id prog_id, std::optional pgroup, config::assembler_options asm_opts) + program(program_id prog_id, std::optional pgroup, config::assembler_options asm_opts, bool external) : prog_id(std::move(prog_id)) , pgroup(std::move(pgroup)) , asm_opts(std::move(asm_opts)) + , external(external) {} program_id prog_id; std::optional pgroup; config::assembler_options asm_opts; + bool external; }; enum class parse_config_file_result @@ -184,16 +226,43 @@ class workspace_configuration utils::resource::resource_location m_proc_grps_loc; utils::resource::resource_location m_pgm_conf_loc; + template + struct tagged_string_view + { + std::string_view value; + }; + struct proc_grp_id_hasher { - size_t operator()(const proc_grp_id& pgid) const + using is_transparent = void; + + size_t operator()(const proc_grp_id& pgid) const noexcept + { + return std::visit([](const auto& x) { return x.hash(); }, pgid); + } + + size_t operator()(const tagged_string_view& external_conf_candidate) const noexcept + { + return std::hash()(external_conf_candidate.value); + } + }; + struct proc_grp_id_equal + { + using is_transparent = void; + + bool operator()(const proc_grp_id& l, const proc_grp_id& r) const noexcept { return l == r; } + bool operator()(const proc_grp_id& l, const tagged_string_view& r) const noexcept { - return std::hash()(pgid.first) ^ utils::resource::resource_location_hasher()(pgid.second); + return std::holds_alternative(l) && *std::get(l).definition == r.value; + } + bool operator()(const tagged_string_view& l, const proc_grp_id& r) const noexcept + { + return std::holds_alternative(r) && l.value == *std::get(r).definition; } }; config::proc_grps m_proc_grps_source; - std::unordered_map m_proc_grps; + std::unordered_map m_proc_grps; struct tagged_program { @@ -224,6 +293,8 @@ class workspace_configuration std::less<>> m_libraries; + external_configuration_requests* m_external_configuration_requests; + std::shared_ptr get_local_library( const utils::resource::resource_location& url, const library_local_options& opts); @@ -252,7 +323,8 @@ class workspace_configuration bool is_config_file(const utils::resource::resource_location& file_location) const; bool is_b4g_config_file(const utils::resource::resource_location& file) const; - const program* get_program_normalized(const utils::resource::resource_location& file_location_normalized) const; + std::pair get_program_normalized( + const utils::resource::resource_location& file_location_normalized) const; std::optional> try_creating_rl_tagged_pgm_pair( @@ -281,8 +353,10 @@ class workspace_configuration std::vector& diags); public: - workspace_configuration( - file_manager& fm, utils::resource::resource_location location, const shared_json& global_settings); + workspace_configuration(file_manager& fm, + utils::resource::resource_location location, + const shared_json& global_settings, + external_configuration_requests* ecr); bool is_configuration_file(const utils::resource::resource_location& file) const; [[nodiscard]] utils::value_task parse_configuration_file( @@ -304,6 +378,13 @@ class workspace_configuration b4g_filter) const; const processor_group& get_proc_grp(const proc_grp_id& p) const; // test only + + void update_external_configuration( + const utils::resource::resource_location& normalized_location, std::string group_json); + decltype(m_proc_grps)::iterator make_external_proc_group( + const utils::resource::resource_location& normalized_location, std::string group_json); + + void prune_external_processor_groups(const utils::resource::resource_location& location); }; } // namespace hlasm_plugin::parser_library::workspaces diff --git a/parser_library/test/workspace/load_config_test.cpp b/parser_library/test/workspace/load_config_test.cpp index af9e2f4be..83ce0f9da 100644 --- a/parser_library/test/workspace/load_config_test.cpp +++ b/parser_library/test/workspace/load_config_test.cpp @@ -191,7 +191,7 @@ TEST(workspace, load_config_synthetic) ws.open().run(); // Check P1 - auto& pg = ws.get_proc_grp({ "P1", resource_location() }); + auto& pg = ws.get_proc_grp(basic_conf { "P1" }); EXPECT_EQ("P1", pg.name()); auto expected = []() -> std::array { if (is_windows()) @@ -210,7 +210,7 @@ TEST(workspace, load_config_synthetic) check_process_group(pg, expected); // Check P2 - auto& pg2 = ws.get_proc_grp({ "P2", resource_location() }); + auto& pg2 = ws.get_proc_grp(basic_conf { "P2" }); EXPECT_EQ("P2", pg2.name()); auto expected2 = []() -> std::array { @@ -448,7 +448,7 @@ TEST(workspace, proc_grps_with_substitutions) EXPECT_TRUE(ws.diags().empty()); - const auto& pg = ws.get_proc_grp({ "aproc_groupb", resource_location() }); + const auto& pg = ws.get_proc_grp(basic_conf { "aproc_groupb" }); using hlasm_plugin::utils::resource::resource_location; diff --git a/parser_library/test/workspace/workspace_configuration_test.cpp b/parser_library/test/workspace/workspace_configuration_test.cpp index 024754f21..977033fcf 100644 --- a/parser_library/test/workspace/workspace_configuration_test.cpp +++ b/parser_library/test/workspace/workspace_configuration_test.cpp @@ -18,12 +18,14 @@ #include "../common_testing.h" #include "empty_configs.h" +#include "external_configuration_requests.h" #include "file_manager_mock.h" #include "workspaces/workspace_configuration.h" using namespace ::testing; using namespace hlasm_plugin::parser_library; using namespace hlasm_plugin::parser_library::workspaces; +using namespace hlasm_plugin::utils; using namespace hlasm_plugin::utils::resource; namespace { @@ -87,7 +89,7 @@ TEST(workspace_configuration, refresh_needed) co_return std::nullopt; })); - workspace_configuration cfg(fm, resource_location("test://workspace"), global_settings); + workspace_configuration cfg(fm, resource_location("test://workspace"), global_settings, nullptr); EXPECT_TRUE(cfg.refresh_libraries({ resource_location("test://workspace/.hlasmplugin") }).run().value()); EXPECT_TRUE( @@ -96,3 +98,200 @@ TEST(workspace_configuration, refresh_needed) cfg.refresh_libraries({ resource_location("test://workspace/.hlasmplugin/pgm_conf.json") }).run().value()); EXPECT_FALSE(cfg.refresh_libraries({ resource_location("test://workspace/something/else") }).run().value()); } + +namespace { +class external_configuration_requests_mock : public hlasm_plugin::parser_library::external_configuration_requests +{ +public: + MOCK_METHOD(void, + read_external_configuration, + (hlasm_plugin::parser_library::sequence url, + hlasm_plugin::parser_library::workspace_manager_response> + content), + (override)); +}; +} // namespace + +TEST(workspace_configuration, external_configurations_group_name) +{ + NiceMock fm; + shared_json global_settings = make_empty_shared_json(); + NiceMock ext_confg; + + EXPECT_CALL(fm, get_file_content(resource_location("test://workspace/.hlasmplugin/proc_grps.json"))) + .WillOnce(Invoke([]() { + return value_task>::from_value(R"( +{ + "pgroups": [ + { + "name": "GRP1", + "libs": [], + "asm_options": {"SYSPARM": "PARM1"} + } + ] +} +)"); + })); + EXPECT_CALL(fm, get_file_content(resource_location("test://workspace/.hlasmplugin/pgm_conf.json"))) + .WillOnce(Invoke([]() { return value_task>::from_value(std::nullopt); })); + + workspace_configuration cfg(fm, resource_location("test://workspace"), global_settings, &ext_confg); + cfg.parse_configuration_file().run(); + + EXPECT_CALL(ext_confg, + read_external_configuration( + Truly([](sequence v) { return std::string_view(v) == "test://workspace/file1.hlasm"; }), _)) + .WillOnce(Invoke([](auto, auto channel) { channel.provide(sequence(std::string_view(R"("GRP1")"))); })); + + const resource_location pgm_loc("test://workspace/file1.hlasm"); + + cfg.load_alternative_config_if_needed(pgm_loc).run(); + + const auto* pgm = cfg.get_program(pgm_loc); + + ASSERT_TRUE(pgm); + EXPECT_TRUE(pgm->external); + EXPECT_EQ(pgm->pgroup, proc_grp_id(basic_conf { "GRP1" })); + + const auto& grp = cfg.get_proc_grp(pgm->pgroup.value()); + + asm_option opts; + grp.apply_options_to(opts); + EXPECT_EQ(opts, asm_option { .sysparm = "PARM1" }); +} + +TEST(workspace_configuration, external_configurations_group_inline) +{ + NiceMock fm; + shared_json global_settings = make_empty_shared_json(); + NiceMock ext_confg; + + EXPECT_CALL(fm, get_file_content(_)).WillRepeatedly(Invoke([]() { + return value_task>::from_value(std::nullopt); + })); + + workspace_configuration cfg(fm, resource_location("test://workspace"), global_settings, &ext_confg); + cfg.parse_configuration_file().run(); + + EXPECT_CALL(ext_confg, + read_external_configuration( + Truly([](sequence v) { return std::string_view(v) == "test://workspace/file1.hlasm"; }), _)) + .WillOnce(Invoke([](auto, auto channel) { + channel.provide(sequence(std::string_view(R"({ + "name": "GRP1", + "libs": [ + "path" + ], + "asm_options": {"SYSPARM": "PARM1"} + })"))); + })); + + const resource_location pgm_loc("test://workspace/file1.hlasm"); + + cfg.load_alternative_config_if_needed(pgm_loc).run(); + + const auto* pgm = cfg.get_program(pgm_loc); + ASSERT_TRUE(pgm); + EXPECT_TRUE(pgm->external); + EXPECT_TRUE(std::holds_alternative(pgm->pgroup.value())); + + const auto& grp = cfg.get_proc_grp(pgm->pgroup.value()); + + asm_option opts; + grp.apply_options_to(opts); + EXPECT_EQ(opts, asm_option { .sysparm = "PARM1" }); + EXPECT_EQ(grp.libraries().size(), 1); +} + +TEST(workspace_configuration, external_configurations_prune) +{ + NiceMock fm; + shared_json global_settings = make_empty_shared_json(); + NiceMock ext_confg; + + EXPECT_CALL(fm, get_file_content(_)).WillRepeatedly(Invoke([]() { + return value_task>::from_value(std::nullopt); + })); + + workspace_configuration cfg(fm, resource_location("test://workspace"), global_settings, &ext_confg); + cfg.parse_configuration_file().run(); + + static constexpr std::string_view grp_def(R"({ + "name": "GRP1", + "libs": [ + "path" + ], + "asm_options": {"SYSPARM": "PARM1"} + })"); + + EXPECT_CALL(ext_confg, + read_external_configuration( + Truly([](sequence v) { return std::string_view(v) == "test://workspace/file1.hlasm"; }), _)) + .WillOnce(Invoke([](auto, auto channel) { channel.provide(sequence(grp_def)); })); + + const resource_location pgm_loc("test://workspace/file1.hlasm"); + + cfg.load_alternative_config_if_needed(pgm_loc).run(); + + EXPECT_CALL(ext_confg, + read_external_configuration( + Truly([](sequence v) { return std::string_view(v) == "test://workspace/file2.hlasm"; }), _)) + .WillOnce(Invoke([](auto, auto channel) { channel.provide(sequence(grp_def)); })); + + const resource_location pgm_loc_2("test://workspace/file2.hlasm"); + + cfg.load_alternative_config_if_needed(pgm_loc_2).run(); + + cfg.prune_external_processor_groups(pgm_loc); + + EXPECT_EQ(cfg.get_program(pgm_loc), nullptr); + EXPECT_NE(cfg.get_program(pgm_loc_2), nullptr); + + EXPECT_NO_THROW(cfg.get_proc_grp(external_conf { std::make_shared(grp_def) })); + + cfg.prune_external_processor_groups(pgm_loc_2); + + EXPECT_EQ(cfg.get_program(pgm_loc), nullptr); + EXPECT_EQ(cfg.get_program(pgm_loc_2), nullptr); + + EXPECT_THROW(cfg.get_proc_grp(external_conf { std::make_shared(grp_def) }), std::out_of_range); +} + +TEST(workspace_configuration, external_configurations_prune_all) +{ + NiceMock fm; + shared_json global_settings = make_empty_shared_json(); + NiceMock ext_confg; + + EXPECT_CALL(fm, get_file_content(_)).WillRepeatedly(Invoke([]() { + return value_task>::from_value(std::nullopt); + })); + + workspace_configuration cfg(fm, resource_location("test://workspace"), global_settings, &ext_confg); + cfg.parse_configuration_file().run(); + + static constexpr std::string_view grp_def(R"({ + "name": "GRP1", + "libs": [ + "path" + ], + "asm_options": {"SYSPARM": "PARM1"} + })"); + + EXPECT_CALL(ext_confg, + read_external_configuration( + Truly([](sequence v) { return std::string_view(v) == "test://workspace/file1.hlasm"; }), _)) + .WillOnce(Invoke([](auto, auto channel) { channel.provide(sequence(grp_def)); })); + + const resource_location pgm_loc("test://workspace/file1.hlasm"); + + cfg.load_alternative_config_if_needed(pgm_loc).run(); + + cfg.prune_external_processor_groups(resource_location()); + + const auto* pgm = cfg.get_program(pgm_loc); + + EXPECT_EQ(pgm, nullptr); + + EXPECT_THROW(cfg.get_proc_grp(external_conf { std::make_shared(grp_def) }), std::out_of_range); +} diff --git a/utils/include/utils/CMakeLists.txt b/utils/include/utils/CMakeLists.txt index 24b12752b..fd1af5b89 100644 --- a/utils/include/utils/CMakeLists.txt +++ b/utils/include/utils/CMakeLists.txt @@ -11,6 +11,7 @@ # Broadcom, Inc. - initial API and implementation target_sources(hlasm_utils PUBLIC + async_busy_wait.h bk_tree.h content_loader.h encoding.h diff --git a/utils/include/utils/async_busy_wait.h b/utils/include/utils/async_busy_wait.h new file mode 100644 index 000000000..f87333c33 --- /dev/null +++ b/utils/include/utils/async_busy_wait.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Broadcom. + * The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Broadcom, Inc. - initial API and implementation + */ + +#ifndef HLASMPLUGIN_UTILS_ASYNC_BUSY_WAIT_H +#define HLASMPLUGIN_UTILS_ASYNC_BUSY_WAIT_H + +#include "task.h" + +namespace hlasm_plugin::utils { +struct +{ + template + value_task operator()(Channel channel, T* result) const + { + while (!channel.resolved()) + co_await task::suspend(); + + co_return std::move(*result); + } +} static constexpr async_busy_wait = {}; // clang 14 +} // namespace hlasm_plugin::utils + +#endif diff --git a/utils/include/utils/error_codes.h b/utils/include/utils/error_codes.h index ddb8efea6..0221a0a7d 100644 --- a/utils/include/utils/error_codes.h +++ b/utils/include/utils/error_codes.h @@ -36,11 +36,14 @@ constexpr error_code not_found { 0, "Not found" }; constexpr error_code allocation { -1, "Allocation failed" }; // constexpr error_code provide { -2, "Exception thrown while providing result" }; +// constexpr error_code invalid_request_sent { -5, "Invalid request" }; constexpr error_code invalid_json { -100, "Invalid JSON content" }; constexpr error_code message_send { -101, "Error occured while sending a message" }; constexpr error_code invalid_conf_response { -102, "Invalid response to 'workspace/configuration'" }; constexpr error_code invalid_request { -103, "Invalid request" }; constexpr error_code workspace_removed { -104, "Workspace removed" }; +constexpr error_code invalid_external_configuration { -105, "Invalid external configuration response" }; +// constexpr error_code external_configuration_handler_failed { -106, "" }; } // namespace hlasm_plugin::utils::error diff --git a/utils/include/utils/string_operations.h b/utils/include/utils/string_operations.h index 0b94a04b4..e2af865fc 100644 --- a/utils/include/utils/string_operations.h +++ b/utils/include/utils/string_operations.h @@ -23,7 +23,9 @@ namespace hlasm_plugin::utils { size_t trim_left(std::string_view& s); +size_t trim_left(std::string_view& s, std::string_view to_trim); size_t trim_right(std::string_view& s); +size_t trim_right(std::string_view& s, std::string_view to_trim); size_t consume(std::string_view& s, std::string_view lit); std::string_view next_nonblank_sequence(std::string_view s); diff --git a/utils/include/utils/task.h b/utils/include/utils/task.h index 62d15fbc8..1a2e67cc8 100644 --- a/utils/include/utils/task.h +++ b/utils/include/utils/task.h @@ -196,6 +196,7 @@ class task_base { assert(m_handle); m_handle.promise().yield_indicator = nullptr; + m_handle.promise().next_step.promise().top_waiter = m_handle; while (!m_handle.done()) m_handle.promise().next_step(); } diff --git a/utils/src/string_operations.cpp b/utils/src/string_operations.cpp index 8697b003d..23b035721 100644 --- a/utils/src/string_operations.cpp +++ b/utils/src/string_operations.cpp @@ -30,6 +30,20 @@ size_t trim_left(std::string_view& s) return to_trim; } +size_t trim_left(std::string_view& s, std::string_view to_trim) +{ + const auto to_trim_idx = s.find_first_not_of(to_trim); + if (to_trim_idx == std::string_view::npos) + { + auto s_length = s.length(); + s = {}; + return s_length; + } + + s.remove_prefix(to_trim_idx); + return to_trim_idx; +} + size_t trim_right(std::string_view& s) { const auto to_trim = s.find_last_not_of(' '); @@ -44,6 +58,20 @@ size_t trim_right(std::string_view& s) return to_trim; } +size_t trim_right(std::string_view& s, std::string_view to_trim) +{ + const auto to_trim_idx = s.find_last_not_of(to_trim); + if (to_trim_idx == std::string_view::npos) + { + auto s_length = s.length(); + s = {}; + return s_length; + } + + s = s.substr(0, to_trim_idx + 1); + return to_trim_idx; +} + size_t consume(std::string_view& s, std::string_view lit) { // case sensitive