diff --git a/plugin-core/src/main/java/appland/actions/AddNavieContextFilesAction.java b/plugin-core/src/main/java/appland/actions/AddNavieContextFilesAction.java index cb4c5511..76b165e2 100644 --- a/plugin-core/src/main/java/appland/actions/AddNavieContextFilesAction.java +++ b/plugin-core/src/main/java/appland/actions/AddNavieContextFilesAction.java @@ -65,7 +65,7 @@ private boolean hasSelectedFiles(@NotNull AnActionEvent e) { private @Nullable NavieEditor findActiveNavieEditor(@NotNull AnActionEvent e) { var editorManager = FileEditorManager.getInstance(Objects.requireNonNull(e.getProject())); - var editor = (editorManager).getSelectedEditor(); + var editor = editorManager.getSelectedEditor(); // If invoked with the context menu of an editor tab, then "editor" equals the clicked tab's editor and not the // visible Navie editor tab. We're attempting a fallback in such a case. diff --git a/plugin-core/src/main/java/appland/notifications/AppMapNotifications.java b/plugin-core/src/main/java/appland/notifications/AppMapNotifications.java index 9feb8b28..e62e2a6f 100644 --- a/plugin-core/src/main/java/appland/notifications/AppMapNotifications.java +++ b/plugin-core/src/main/java/appland/notifications/AppMapNotifications.java @@ -255,6 +255,11 @@ public static boolean isWebviewTextInputBroken() { */ @SuppressWarnings("removal") public static void showWebviewTextInputBrokenMessage(@NotNull Project project, boolean forNavie) { + // don't show in our unit tests because there's no user interaction + if (ApplicationManager.getApplication().isUnitTestMode()) { + return; + } + var properties = PropertiesComponent.getInstance(); var hideMessagePropertyKey = forNavie ? "appmap.navie.hideIsBrokenMessage" : "appmap.signin.hideIsBrokenMessage"; if (properties.getBoolean(hideMessagePropertyKey, false)) { diff --git a/plugin-core/src/main/java/appland/webviews/WebviewEditorProvider.java b/plugin-core/src/main/java/appland/webviews/WebviewEditorProvider.java index fd82b9b5..53bbcad9 100644 --- a/plugin-core/src/main/java/appland/webviews/WebviewEditorProvider.java +++ b/plugin-core/src/main/java/appland/webviews/WebviewEditorProvider.java @@ -1,6 +1,7 @@ package appland.webviews; import com.google.common.base.Predicates; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.FileEditorPolicy; @@ -106,6 +107,10 @@ public boolean isWebViewFile(@NotNull VirtualFile file) { public LightVirtualFile createVirtualFile(@NotNull String title) { var file = new LightVirtualFile(title); WEBVIEW_EDITOR_KEY.set(file, webviewTypeId); + // TestEditorManagerImpl is used in tests and only uses custom editor provider is set via KEY :( + if (ApplicationManager.getApplication().isUnitTestMode()) { + FileEditorProvider.KEY.set(file, this); + } return file; } diff --git a/plugin-core/src/test/java/appland/AppMapBaseTest.java b/plugin-core/src/test/java/appland/AppMapBaseTest.java index 42f5f4d7..3bd3a70a 100644 --- a/plugin-core/src/test/java/appland/AppMapBaseTest.java +++ b/plugin-core/src/test/java/appland/AppMapBaseTest.java @@ -5,6 +5,8 @@ import appland.config.AppMapConfigFile; import appland.files.AppMapFiles; import appland.problemsView.TestFindingsManager; +import appland.rpcService.AppLandJsonRpcListener; +import appland.rpcService.AppLandJsonRpcListenerAdapter; import appland.rpcService.AppLandJsonRpcService; import appland.settings.AppMapApplicationSettings; import appland.settings.AppMapApplicationSettingsService; @@ -33,6 +35,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public abstract class AppMapBaseTest extends LightPlatformCodeInsightFixture4TestCase { @@ -186,6 +189,72 @@ protected void waitUntilIndexesAreReady() { IndexTestUtils.waitUntilIndexesAreReady(getProject()); } + protected void waitForJsonRpcServer() { + var service = AppLandJsonRpcService.getInstance(getProject()); + if (service.isServerRunning()) { + return; + } + + var latch = createWaitForJsonRpcServerRestartCondition(true); + ApplicationManager.getApplication().executeOnPooledThread(service::startServer); + try { + assertTrue("The AppMap JSON-RPC server must launch", latch.await(30, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + addSuppressedException(e); + } + } + + protected void waitForJsonRpcServerPort() { + var service = AppLandJsonRpcService.getInstance(getProject()); + if (service.isServerRunning()) { + return; + } + + var latch = createWaitForJsonRpcServerPortCondition(); + ApplicationManager.getApplication().executeOnPooledThread(service::startServer); + try { + assertTrue("The AppMap JSON-RPC server must launch", latch.await(30, TimeUnit.SECONDS)); + } catch (InterruptedException e) { + addSuppressedException(e); + } + } + + protected @NotNull CountDownLatch createWaitForJsonRpcServerRestartCondition(boolean allowStart) { + var latch = new CountDownLatch(1); + getProject().getMessageBus() + .connect(getTestRootDisposable()) + .subscribe(AppLandJsonRpcListener.TOPIC, new AppLandJsonRpcListenerAdapter() { + @Override + public void serverStarted() { + if (allowStart) { + latch.countDown(); + } + } + + @Override + public void serverRestarted() { + latch.countDown(); + } + }); + return latch; + } + + protected @NotNull CountDownLatch createWaitForJsonRpcServerPortCondition() { + var latch = new CountDownLatch(1); + getProject().getMessageBus() + .connect(getTestRootDisposable()) + .subscribe(AppLandJsonRpcListener.TOPIC, new AppLandJsonRpcListenerAdapter() { + @Override + public void serverConfigurationUpdated(@NotNull Collection contentRoots, + @NotNull Collection appMapConfigFiles) { + if (AppLandJsonRpcService.getInstance(getProject()).getServerPort() != null) { + latch.countDown(); + } + } + }); + return latch; + } + private String createAppMapEvents(int requestCount, int queryCount) { var json = new StringBuilder(); json.append("["); diff --git a/plugin-core/src/test/java/appland/actions/AddNavieContextFilesActionTest.java b/plugin-core/src/test/java/appland/actions/AddNavieContextFilesActionTest.java new file mode 100644 index 00000000..c5fcf40c --- /dev/null +++ b/plugin-core/src/test/java/appland/actions/AddNavieContextFilesActionTest.java @@ -0,0 +1,114 @@ +package appland.actions; + +import appland.AppMapBaseTest; +import appland.cli.TestAppLandDownloadService; +import appland.utils.DataContexts; +import appland.webviews.navie.NavieEditor; +import appland.webviews.navie.NavieEditorProvider; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.ex.ActionUtil; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.testFramework.EdtTestUtil; +import com.intellij.testFramework.TestActionEvent; +import com.intellij.testFramework.fixtures.TempDirTestFixture; +import com.intellij.testFramework.fixtures.impl.TempDirTestFixtureImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.nio.file.Path; +import java.util.concurrent.TimeUnit; + +public class AddNavieContextFilesActionTest extends AppMapBaseTest { + @Override + protected TempDirTestFixture createTempDirTestFixture() { + return new TempDirTestFixtureImpl(); + } + + @Before + public void ensureToolsDownloaded() { + TestAppLandDownloadService.ensureDownloaded(); + } + + @Test + public void enabledForNavieEditorAndContextFile() throws Exception { + // must be created first because the Navie editor must be the selected editor + var contextFile = myFixture.configureByText("test.txt", ""); + + var tempDir = myFixture.createFile("test.txt", "").getParent(); + withContentRoot(tempDir, () -> { + openNavieEditor(tempDir); + + var presentation = updateAction(createActionContext(contextFile.getVirtualFile())); + Assert.assertTrue("Action must be enabled for a file", presentation.isEnabledAndVisible()); + }); + } + + @Test + public void disabledWithoutNavieEditor() throws Exception { + var contextFile = myFixture.configureByText("test.txt", ""); + + var tempDir = myFixture.createFile("test.txt", "").getParent(); + withContentRoot(tempDir, () -> { + var presentation = updateAction(createActionContext(contextFile.getVirtualFile())); + Assert.assertFalse("Action must be unavailable without selected Navie editor", presentation.isEnabledAndVisible()); + }); + } + + @Test + public void disabledWithoutContextFile() throws Exception { + var contextDir = myFixture.configureByText("test.txt", "").getVirtualFile().getParent(); + + var tempDir = myFixture.createFile("test.txt", "").getParent(); + withContentRoot(tempDir, () -> { + openNavieEditor(tempDir); + + var presentation = updateAction(createActionContext(contextDir)); + Assert.assertFalse("Action must be unavailable for a selected directory", presentation.isEnabledAndVisible()); + }); + } + + private void openNavieEditor(@NotNull VirtualFile tempDir) throws Exception { + createAppMapConfig(tempDir, Path.of("tmp", "appmap")); + waitForJsonRpcServerPort(); + + edt(() -> NavieEditorProvider.openEditor(getProject(), DataContext.EMPTY_CONTEXT)); + + // wait until the Navie editor is the selected editor + var deadline = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(30); + while (System.currentTimeMillis() <= deadline) { + var editor = EdtTestUtil.runInEdtAndGet(() -> FileEditorManager.getInstance(getProject()).getSelectedEditor()); + if (editor instanceof NavieEditor) { + break; + } + Thread.sleep(500); + } + + var editor = EdtTestUtil.runInEdtAndGet(() -> FileEditorManager.getInstance(getProject()).getSelectedEditor()); + assertTrue("Navie editor must be open", editor instanceof NavieEditor); + } + + private @NotNull DataContext createActionContext(@Nullable VirtualFile contextFile) { + return DataContexts.createCustomContext(dataId -> { + if (contextFile != null && PlatformDataKeys.VIRTUAL_FILE_ARRAY.is(dataId)) { + return new VirtualFile[]{contextFile}; + } + if (PlatformDataKeys.PROJECT.is(dataId)) { + return myFixture.getProject(); + } + return null; + }); + } + + private static @NotNull Presentation updateAction(@NotNull DataContext context) { + var action = ActionManager.getInstance().getAction("appmap.navie.pinContextFile"); + assertNotNull(action); + + var e = TestActionEvent.createFromAnAction(action, null, ActionPlaces.MAIN_MENU, context); + ActionUtil.performDumbAwareUpdate(action, e, false); + return e.getPresentation(); + } +} \ No newline at end of file diff --git a/plugin-core/src/test/java/appland/rpcService/DefaultAppLandJsonRpcServiceTest.java b/plugin-core/src/test/java/appland/rpcService/DefaultAppLandJsonRpcServiceTest.java index fc6e826e..1b43efa8 100644 --- a/plugin-core/src/test/java/appland/rpcService/DefaultAppLandJsonRpcServiceTest.java +++ b/plugin-core/src/test/java/appland/rpcService/DefaultAppLandJsonRpcServiceTest.java @@ -2,12 +2,13 @@ import appland.AppMapBaseTest; import appland.cli.AppLandCommandLineService; +import appland.cli.TestAppLandDownloadService; import appland.settings.AppMapApplicationSettingsService; import com.intellij.openapi.application.ApplicationInfo; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import org.junit.Assume; +import org.junit.Before; import org.junit.Test; import java.util.Collection; @@ -27,6 +28,11 @@ protected boolean runInDispatchThread() { return false; } + @Before + public void ensureToolsDownloaded() { + TestAppLandDownloadService.ensureDownloaded(); + } + @Test public void launchedWithProject() { waitForJsonRpcServer(); @@ -65,7 +71,7 @@ public void serverConfigurationUpdated(@NotNull Collection contentR public void serverRestartAfterTermination() throws Exception { waitForJsonRpcServer(); - var latch = createWaitForRestartCondition(false); + var latch = createWaitForJsonRpcServerRestartCondition(false); // kill and wait for restart TestAppLandJsonRpcService.killJsonRpcProcess(getProject()); @@ -97,7 +103,7 @@ public void restartAfterApiKeyChange() throws Exception { var appMapSettings = AppMapApplicationSettingsService.getInstance(); appMapSettings.setApiKey("dummy"); try { - var latch = createWaitForRestartCondition(false); + var latch = createWaitForJsonRpcServerRestartCondition(false); // change API key and wait for restart appMapSettings.setApiKeyNotifying("new-api-key"); @@ -119,39 +125,4 @@ public void codeEditorInfo() { var matches = editorInfo.matches("IntelliJ IDEA [0-9.]+( EAP | Beta)? by JetBrains s\\.r\\.o\\."); assertTrue("Code editor info must match: " + editorInfo, matches); } - - private void waitForJsonRpcServer() { - var service = AppLandJsonRpcService.getInstance(getProject()); - if (service.isServerRunning()) { - return; - } - - var latch = createWaitForRestartCondition(true); - ApplicationManager.getApplication().executeOnPooledThread(service::startServer); - try { - assertTrue("The AppMap JSON-RPC server must launch", latch.await(30, TimeUnit.SECONDS)); - } catch (InterruptedException e) { - addSuppressedException(e); - } - } - - private @NotNull CountDownLatch createWaitForRestartCondition(boolean allowStart) { - var latch = new CountDownLatch(1); - getProject().getMessageBus() - .connect(getTestRootDisposable()) - .subscribe(AppLandJsonRpcListener.TOPIC, new AppLandJsonRpcListenerAdapter() { - @Override - public void serverStarted() { - if (allowStart) { - latch.countDown(); - } - } - - @Override - public void serverRestarted() { - latch.countDown(); - } - }); - return latch; - } } \ No newline at end of file diff --git a/plugin-core/src/test/java/appland/settings/AppMapApplicationSettingsTest.java b/plugin-core/src/test/java/appland/settings/AppMapApplicationSettingsTest.java index e79e20e1..daa7b874 100644 --- a/plugin-core/src/test/java/appland/settings/AppMapApplicationSettingsTest.java +++ b/plugin-core/src/test/java/appland/settings/AppMapApplicationSettingsTest.java @@ -3,6 +3,7 @@ import appland.AppMapBaseTest; import com.intellij.configurationStore.XmlSerializer; import com.intellij.openapi.util.JDOMUtil; +import org.jetbrains.annotations.NotNull; import org.junit.Assert; import org.junit.Test; @@ -11,12 +12,7 @@ public class AppMapApplicationSettingsTest extends AppMapBaseTest { @Test public void xmlSerialization() { - var settings = new AppMapApplicationSettings(); - settings.setCliEnvironmentNotifying(Map.of("name1", "value1", "name2", "value2")); - settings.setApiKey("my-appmap-api-key"); - settings.setInstallInstructionsViewed(true); - settings.setFirstStart(true); - settings.setCliPassParentEnv(true); + var settings = createSettings(); var serialized = XmlSerializer.serialize(settings); Assert.assertNotNull(serialized); @@ -24,16 +20,37 @@ public void xmlSerialization() { var deserialized = XmlSerializer.deserialize(serialized, AppMapApplicationSettings.class); Assert.assertEquals(settings, deserialized); - var expectedXML = "\n" + - " \n" + - " "; + var expectedXML = """ + + + """; Assert.assertEquals(expectedXML, JDOMUtil.write(serialized)); } + + @Test + public void copy() { + var settings = createSettings(); + var copiedSettings = new AppMapApplicationSettings(settings); + Assert.assertEquals("Copy constructor must copy all settings", settings, copiedSettings); + } + + @NotNull + private static AppMapApplicationSettings createSettings() { + var settings = new AppMapApplicationSettings(); + settings.setCliEnvironmentNotifying(Map.of("name1", "value1", "name2", "value2")); + settings.setApiKey("my-appmap-api-key"); + settings.setInstallInstructionsViewed(true); + settings.setFirstStart(true); + settings.setCliPassParentEnv(true); + settings.setMaxPinnedFileSizeKB(40); + return settings; + } } \ No newline at end of file diff --git a/plugin-core/src/test/java/appland/webviews/navie/NaviePinFileRequestTest.java b/plugin-core/src/test/java/appland/webviews/navie/NaviePinFileRequestTest.java new file mode 100644 index 00000000..b8b362a4 --- /dev/null +++ b/plugin-core/src/test/java/appland/webviews/navie/NaviePinFileRequestTest.java @@ -0,0 +1,15 @@ +package appland.webviews.navie; + +import appland.AppMapBaseTest; +import appland.utils.GsonUtils; +import org.junit.Test; + +public class NaviePinFileRequestTest extends AppMapBaseTest { + @Test + public void jsonSerialization() { + // language=JSON + var expected = "{\"name\":\"request-name\",\"uri\":\"file:///dir/subdir/file.txt\",\"content\":\"my content\"}"; + var request = new NaviePinFileRequest("request-name", "file:///dir/subdir/file.txt", "my content"); + assertEquals(expected, GsonUtils.GSON.toJson(request)); + } +} \ No newline at end of file