From 87a702621505a71e9c297ca731b7ea1cd40e276d Mon Sep 17 00:00:00 2001 From: Kris De Volder Date: Wed, 12 Apr 2017 14:53:48 -0700 Subject: [PATCH] Add some tests for quickfixes --- .../quickfix/QuickfixResolveParams.java | 9 ++ .../vscode/commons/util/CollectionUtil.java | 15 +++ .../yaml/reconcile/YamlSchemaProblems.java | 6 +- .../testharness/CodeAction.java | 44 +++++++++ .../languageserver/testharness/Editor.java | 20 ++++ .../testharness/LanguageServerHarness.java | 94 ++++++++++++++++++- .../vscode/concourse/ConcourseEditorTest.java | 49 ++++++++++ .../vscode/concourse/TimeOfDayParserTest.java | 3 +- 8 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/CodeAction.java diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixResolveParams.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixResolveParams.java index 10ef8711f..b0ede4654 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixResolveParams.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/quickfix/QuickfixResolveParams.java @@ -16,6 +16,15 @@ public class QuickfixResolveParams { private Object params; + public QuickfixResolveParams(String type, Object params) { + super(); + this.type = type; + this.params = params; + } + + public QuickfixResolveParams() { + } + public String getType() { return type; } diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/CollectionUtil.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/CollectionUtil.java index c9137ec50..6266d2199 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/CollectionUtil.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/CollectionUtil.java @@ -18,6 +18,21 @@ import java.util.Collection; */ public class CollectionUtil { + /** + * Get some element of the collection (will be the first one + * found by its iterator, or null if the collection is empty). + *

+ * Note that unless the collection is ordered, or has at most + * one element, then it may be unpredictable which element + * you will get. + */ + public static E getAny(Collection elements) { + for (E e : elements) { + return e; + } + return null; + } + public static boolean hasElements(Collection c) { return c!=null && !c.isEmpty(); } diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java index 3e719e77c..1ecd8979e 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/reconcile/YamlSchemaProblems.java @@ -22,6 +22,7 @@ import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemTy import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblemImpl; import org.springframework.ide.vscode.commons.languageserver.util.DocumentRegion; +import org.springframework.ide.vscode.commons.util.CollectionUtil; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; import org.springframework.ide.vscode.commons.yaml.path.YamlPath; @@ -140,6 +141,9 @@ public class YamlSchemaProblems { .map(YamlPathSegment::encode) .collect(Collectors.toList()); + String fixTitle = missingProps.size()==1 + ? "Add property '"+CollectionUtil.getAny(missingProps)+"'" + : "Add properties: "+missingProps; QuickfixData fix = new QuickfixData( quickfixType, new MissingPropertiesData( @@ -147,7 +151,7 @@ public class YamlSchemaProblems { segments, ImmutableList.copyOf(missingProps) ), - "Add properties: "+missingProps + fixTitle ); return missingProperty(msg, dc.getDocument(), parent, map) diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/CodeAction.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/CodeAction.java new file mode 100644 index 000000000..2ca1f2483 --- /dev/null +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/CodeAction.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * Copyright (c) 2017 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.languageserver.testharness; + +import org.eclipse.lsp4j.Command; + +/** + * Wrapper for the test harness to refer to and manipulate a + * CodeAction. + */ +public class CodeAction { + + private final Command command; + private LanguageServerHarness harness; + + public CodeAction(LanguageServerHarness harness, Command command) { + super(); + this.harness = harness; + + this.command = command; + } + + @Override + public String toString() { + return command.toString(); + } + + public String getLabel() { + return command.getTitle(); + } + + public void perform() throws Exception { + harness.perform(command); + } + +} diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index a50bbe0db..78edaad4b 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -24,6 +24,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import javax.swing.text.BadLocationException; @@ -42,6 +43,7 @@ import org.eclipse.lsp4j.TextDocumentPositionParams; import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.junit.Assert; +import org.springframework.ide.vscode.commons.util.CollectionUtil; import com.google.common.collect.ImmutableList; @@ -627,4 +629,22 @@ public class Editor { return languageId; } + public List getCodeActions(Diagnostic problem) throws Exception { + return harness.getCodeActions(document, problem); + } + + public CodeAction assertCodeAction(Diagnostic problem) throws Exception { + List actions = getCodeActions(problem); + assertEquals("Number of codeActions", 1, actions.size()); + return actions.get(0); + } + + public String getUri() { + return document.getUri(); + } + + public void assertRawText(String expectedText) throws Exception { + assertEquals(expectedText, getRawText()); + } + } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 71afd265f..302c1055c 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -18,17 +18,23 @@ import static org.junit.Assert.assertNotNull; import java.io.File; import java.nio.charset.Charset; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import org.assertj.core.api.Condition; import org.eclipse.lsp4j.ClientCapabilities; +import org.eclipse.lsp4j.CodeActionContext; +import org.eclipse.lsp4j.CodeActionParams; +import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.Diagnostic; @@ -50,13 +56,25 @@ import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.TextDocumentPositionParams; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.eclipse.lsp4j.TextDocumentSyncOptions; +import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; +import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageClientAware; import org.springframework.ide.vscode.commons.languageserver.LanguageIds; import org.springframework.ide.vscode.commons.languageserver.ProgressParams; import org.springframework.ide.vscode.commons.languageserver.STS4LanguageClient; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.TextReplace; +import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixResolveParams; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; +import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.text.IDocument; +import org.springframework.ide.vscode.commons.util.text.IRegion; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; public class LanguageServerHarness { @@ -73,6 +91,7 @@ public class LanguageServerHarness { private Map documents = new HashMap<>(); private Map diagnostics = new HashMap<>(); + private List activeEditors = new ArrayList<>(); public LanguageServerHarness(Callable factory, String defaultLanguageId) { @@ -340,11 +359,13 @@ public class LanguageServerHarness { } public Editor newEditor(String contents) throws Exception { - return new Editor(this, contents, getDefaultLanguageId()); + return newEditor(getDefaultLanguageId(), contents); } - public Editor newEditor(String languageId, String contents) throws Exception { - return new Editor(this, contents, languageId); + public synchronized Editor newEditor(String languageId, String contents) throws Exception { + Editor editor = new Editor(this, contents, languageId); + activeEditors.add(editor); + return editor; } public synchronized TextDocumentInfo createWorkingCopy(String contents, String languageId) throws Exception { @@ -406,4 +427,71 @@ public class LanguageServerHarness { return server.getTextDocumentService().definition(params).get(); } + public List getCodeActions(TextDocumentInfo doc, Diagnostic problem) throws Exception { + CodeActionContext context = new CodeActionContext(ImmutableList.of(problem)); + List actions = + server.getTextDocumentService().codeAction(new CodeActionParams(doc.getId(), problem.getRange(), context)).get(); + return actions.stream() + .map((command) -> new CodeAction(this, command)) + .collect(Collectors.toList()); + } + + ObjectMapper mapper = new ObjectMapper(); + + public void perform(Command command) throws Exception { + switch (command.getCommand()) { + case "sts.quickfix": + List args = command.getArguments(); + assertEquals(2, args.size()); + //Note convert the value to a 'typeless' Object becaus that is more representative on how it will be + // received when we get it in a real client/server setting (i.e. parsed from json). + Object untypedParams = mapper.convertValue(args.get(1), Object.class); + perform(server.quickfixResolve(new QuickfixResolveParams((String)args.get(0), untypedParams)).get()); + return; + default: + throw new IllegalArgumentException("Unknown command: "+command); + } + } + + private void perform(WorkspaceEdit workspaceEdit) throws Exception { + Assert.isNull("Versioned WorkspaceEdits not supported", workspaceEdit.getDocumentChanges()); + for (Entry> entry : workspaceEdit.getChanges().entrySet()) { + String uri = entry.getKey(); + TextDocumentInfo document = documents.get(uri); + assertNotNull("Can't apply edits to non-existing document: "+uri, document); + + TextDocument workingDocument = new TextDocument(uri, document.getLanguageId()); + workingDocument.setText(document.getText()); + DocumentEdits edits = new DocumentEdits(workingDocument); + for (TextEdit edit : entry.getValue()) { + Range range = edit.getRange(); + edits.replace(document.toOffset(range.getStart()), document.toOffset(range.getEnd()), edit.getNewText()); + } + edits.apply(workingDocument); + Editor editor = getOpenEditor(uri); + if (editor!=null) { + editor.setRawText(workingDocument.get()); + } else { + changeDocument(uri, workingDocument.get()); + } + } + } + + private Editor getOpenEditor(String uri) { + List editors = getOpenEditors(uri); + if (editors.isEmpty()) { + return null; + } else if (editors.size()>1) { + throw new IllegalStateException("Multiple active editors on the same uri. The harness doesn't handle that yet!"); + } + return editors.get(0); + } + + private synchronized List getOpenEditors(String uri) { + return activeEditors.stream() + .filter((editor) -> uri.equals(editor.getUri())) + .collect(Collectors.toList()); + } + + } diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java index c3885ae1f..67b4828c7 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/ConcourseEditorTest.java @@ -24,6 +24,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.ide.vscode.commons.languageserver.LanguageIds; import org.springframework.ide.vscode.commons.util.IOUtil; +import org.springframework.ide.vscode.languageserver.testharness.CodeAction; import org.springframework.ide.vscode.languageserver.testharness.Editor; import org.springframework.ide.vscode.languageserver.testharness.LanguageServerHarness; @@ -42,6 +43,54 @@ public class ConcourseEditorTest { harness.intialize(null); } + @Test public void addSingleRequiredPropertiesQuickfix() throws Exception { + Editor editor = harness.newEditor( + "resources:\n" + + "- name: foo\n" + + " source:\n" + + " username: someone\n" + + "# Confuse" + ); + Diagnostic problem = editor.assertProblems("-|'type' is required").get(0); + CodeAction quickfix = editor.assertCodeAction(problem); + assertEquals("Add property 'type'", quickfix.getLabel()); + quickfix.perform(); + + editor.assertRawText( + "resources:\n" + + "- name: foo\n" + + " source:\n" + + " username: someone\n" + + " type: \n" + + "# Confuse" + ); + } + + @Test public void addMultipleRequiredPropertiesQuickfix() throws Exception { + Editor editor = harness.newEditor( + "resources:\n" + + "- name: foo\n" + + " type: pool\n" + + " source:\n" + + " username: someone\n" + ); + Diagnostic problem = editor.assertProblems("source|[branch, pool, uri] are required").get(0); + CodeAction quickfix = editor.assertCodeAction(problem); + assertEquals("Add properties: [branch, pool, uri]", quickfix.getLabel()); + quickfix.perform(); + + editor.assertRawText( + "resources:\n" + + "- name: foo\n" + + " type: pool\n" + + " source:\n" + + " username: someone\n" + + " branch: \n" + + " pool: \n" + + " uri: \n" + ); + } + @Test public void testReconcileCatchesParseError() throws Exception { Editor editor = harness.newEditor( "somemap: val\n"+ diff --git a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/TimeOfDayParserTest.java b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/TimeOfDayParserTest.java index cd2f83154..446dce071 100644 --- a/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/TimeOfDayParserTest.java +++ b/headless-services/concourse-language-server/src/test/java/org/springframework/ide/vscode/concourse/TimeOfDayParserTest.java @@ -69,7 +69,6 @@ public class TimeOfDayParserTest { "2359" }; for (String string : examples) { - System.out.println(string); parser.parse(string); } } @@ -78,6 +77,8 @@ public class TimeOfDayParserTest { public void badExamples() { does_not_parse("arbirary garbage"); does_not_parse("3:04 PM -0700 extra"); + does_not_parse("extra 3:04 PM -0700 extra"); + does_not_parse("extra 3:04 PM -0700"); } private void does_not_parse(String string) {