diff --git a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClientTests.java b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClientTests.java index 52e9f3c7..114d526c 100644 --- a/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClientTests.java +++ b/com.microsoft.copilot.eclipse.core.test/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClientTests.java @@ -6,16 +6,32 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.ILogListener; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.lsp4e.LanguageServerPlugin; +import org.eclipse.lsp4e.LanguageServerWrapper; +import org.eclipse.lsp4e.LanguageServersRegistry.LanguageServerDefinition; +import org.eclipse.lsp4e.client.DefaultLanguageClient; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.MessageType; +import org.eclipse.osgi.service.debug.DebugOptions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,6 +39,9 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; import com.microsoft.copilot.eclipse.core.CopilotCore; import com.microsoft.copilot.eclipse.core.FeatureFlags; @@ -48,9 +67,46 @@ class CopilotLanguageClientTests { @Mock private IReferencedFileService fileService; + /** Captures log entries written to LanguageServerPlugin's ILog during each test. */ + private final List loggedStatuses = new ArrayList<>(); + private ILogListener logListener; + private ILog lsp4eLog; + @BeforeEach - void setUp() { + void setUp() throws Exception { client = new CopilotLanguageClient(); + setupWrapperOnClient(client); + loggedStatuses.clear(); + logListener = (status, pluginId) -> { + if (LanguageServerPlugin.PLUGIN_ID.equals(status.getPlugin())) { + loggedStatuses.add(status); + } + }; + lsp4eLog = LanguageServerPlugin.getDefault().getLog(); + lsp4eLog.addLogListener(logListener); + } + + @AfterEach + void tearDown() { + lsp4eLog.removeLogListener(logListener); + loggedStatuses.clear(); + } + + /** + * Uses reflection to inject a LanguageServerWrapper with a valid serverDefinition into the client, so that + * DefaultLanguageClient.logMessage() can run end-to-end without NPE on wrapper.serverDefinition.label. + */ + private static void setupWrapperOnClient(CopilotLanguageClient client) throws Exception { + LanguageServerDefinition serverDef = mock(LanguageServerDefinition.class); + + LanguageServerWrapper mockWrapper = mock(LanguageServerWrapper.class); + Field serverDefField = LanguageServerWrapper.class.getDeclaredField("serverDefinition"); + serverDefField.setAccessible(true); + serverDefField.set(mockWrapper, serverDef); + + Field wrapperField = DefaultLanguageClient.class.getDeclaredField("wrapper"); + wrapperField.setAccessible(true); + wrapperField.set(client, mockWrapper); } @Test @@ -131,4 +187,143 @@ void testOnDidChangeFeatureFlagsWithEmptyFeatureFlags() { verify(mockFeatureFlags).setByokEnabled(true); } } + + // ------------------------------------------------------------------------- + // logMessage tests + // ------------------------------------------------------------------------- + + @Test + void testLogMessage_nullMessage_doesNotLog() throws Exception { + loggedStatuses.clear(); + client.logMessage(null); + + TimeUnit.MILLISECONDS.sleep(200); + assertTrue(loggedStatuses.isEmpty(), "No log entry expected for null message"); + } + + @Test + void testLogMessage_nullType_doesNotLog() throws Exception { + MessageParams params = mock(MessageParams.class); + when(params.getType()).thenReturn(null); + + loggedStatuses.clear(); + client.logMessage(params); + + TimeUnit.MILLISECONDS.sleep(200); + assertTrue(loggedStatuses.isEmpty(), "No log entry expected when MessageType is null"); + } + + @Test + void testLogMessage_errorType_alwaysLogs() throws Exception { + MessageParams params = new MessageParams(MessageType.Error, "something went wrong"); + + loggedStatuses.clear(); + client.logMessage(params); + + TimeUnit.MILLISECONDS.sleep(200); + assertEquals(1, loggedStatuses.size(), "Expected exactly one log entry for Error"); + assertEquals(IStatus.ERROR, loggedStatuses.get(0).getSeverity()); + assertTrue(loggedStatuses.get(0).getMessage().contains("something went wrong")); + } + + @Test + void testLogMessage_warningType_alwaysLogs() throws Exception { + MessageParams params = new MessageParams(MessageType.Warning, "watch out"); + + loggedStatuses.clear(); + client.logMessage(params); + + TimeUnit.MILLISECONDS.sleep(200); + assertEquals(1, loggedStatuses.size(), "Expected exactly one log entry for Warning"); + assertEquals(IStatus.WARNING, loggedStatuses.get(0).getSeverity()); + assertTrue(loggedStatuses.get(0).getMessage().contains("watch out")); + } + + @Test + void testLogMessage_infoType_logsOnlyWhenTraceEnabled() throws Exception { + MessageParams params = new MessageParams(MessageType.Info, "info message"); + + BundleContext ctx = FrameworkUtil.getBundle(getClass()).getBundleContext(); + ServiceReference ref = ctx.getServiceReference(DebugOptions.class); + DebugOptions opts = ctx.getService(ref); + boolean wasDebugEnabled = opts.isDebugEnabled(); + String prevTraceOption = opts.getOption("org.eclipse.lsp4e/trace"); + try { + opts.setDebugEnabled(true); + opts.setOption("org.eclipse.lsp4e/trace", "true"); + + loggedStatuses.clear(); + + client.logMessage(params); + + TimeUnit.MILLISECONDS.sleep(200); + assertEquals(1, loggedStatuses.size(), "Expected one log entry for Info when trace is enabled"); + } finally { + if (prevTraceOption != null) { + opts.setOption("org.eclipse.lsp4e/trace", prevTraceOption); + } else { + opts.removeOption("org.eclipse.lsp4e/trace"); + } + opts.setDebugEnabled(wasDebugEnabled); + ctx.ungetService(ref); + } + } + + @Test + void testLogMessage_infoType_doesNotLogWhenTraceDisabled() throws Exception { + MessageParams params = new MessageParams(MessageType.Info, "info message"); + + BundleContext ctx = FrameworkUtil.getBundle(getClass()).getBundleContext(); + ServiceReference ref = ctx.getServiceReference(DebugOptions.class); + DebugOptions opts = ctx.getService(ref); + boolean wasDebugEnabled = opts.isDebugEnabled(); + String prevTraceOption = opts.getOption("org.eclipse.lsp4e/trace"); + try { + opts.setDebugEnabled(true); + opts.setOption("org.eclipse.lsp4e/trace", "false"); + + loggedStatuses.clear(); + client.logMessage(params); + + TimeUnit.MILLISECONDS.sleep(200); + assertTrue(loggedStatuses.isEmpty(), "No log entry expected for Info when trace is disabled"); + } finally { + if (prevTraceOption != null) { + opts.setOption("org.eclipse.lsp4e/trace", prevTraceOption); + } else { + opts.removeOption("org.eclipse.lsp4e/trace"); + } + opts.setDebugEnabled(wasDebugEnabled); + ctx.ungetService(ref); + } + } + + @Test + void testLogMessage_logType_doesNotLogWhenTraceDisabled() throws Exception { + MessageParams params = new MessageParams(MessageType.Log, "log message"); + + BundleContext ctx = FrameworkUtil.getBundle(getClass()).getBundleContext(); + ServiceReference ref = ctx.getServiceReference(DebugOptions.class); + DebugOptions opts = ctx.getService(ref); + boolean wasDebugEnabled = opts.isDebugEnabled(); + String prevTraceOption = opts.getOption("org.eclipse.lsp4e/trace"); + try { + opts.setDebugEnabled(true); + opts.setOption("org.eclipse.lsp4e/trace", "false"); + + loggedStatuses.clear(); + client.logMessage(params); + + TimeUnit.MILLISECONDS.sleep(200); + assertTrue(loggedStatuses.isEmpty(), "No log entry expected for Log when trace is disabled"); + } finally { + if (prevTraceOption != null) { + opts.setOption("org.eclipse.lsp4e/trace", prevTraceOption); + } else { + opts.removeOption("org.eclipse.lsp4e/trace"); + } + opts.setDebugEnabled(wasDebugEnabled); + ctx.ungetService(ref); + } + } } diff --git a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java index da7aaee9..cb74a915 100644 --- a/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java +++ b/com.microsoft.copilot.eclipse.core/src/com/microsoft/copilot/eclipse/core/lsp/CopilotLanguageClient.java @@ -13,6 +13,7 @@ import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.e4.core.contexts.EclipseContextFactory; @@ -20,6 +21,8 @@ import org.eclipse.lsp4e.LanguageClientImpl; import org.eclipse.lsp4j.ConfigurationItem; import org.eclipse.lsp4j.ConfigurationParams; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.MessageType; import org.eclipse.lsp4j.ProgressParams; import org.eclipse.lsp4j.ShowDocumentParams; import org.eclipse.lsp4j.ShowDocumentResult; @@ -458,4 +461,34 @@ protected IStatus run(IProgressMonitor monitor) { return super.showDocument(params); } } + + @Override + public void logMessage(MessageParams message) { + if (message == null) { + return; + } + + MessageType type = message.getType(); + if (type == null) { + return; + } + + switch (type) { + case Error: + case Warning: + super.logMessage(message); + break; + case Info: + case Log: + default: + if (isLsp4eLogTraceEnabled()) { + super.logMessage(message); + } + break; + } + } + + private boolean isLsp4eLogTraceEnabled() { + return Platform.getDebugBoolean("org.eclipse.lsp4e/trace"); + } } \ No newline at end of file