Skip to content

Commit

Permalink
Updated these tests to be more clear about what they're doing.
Browse files Browse the repository at this point in the history
  • Loading branch information
SCWells72 committed Dec 10, 2024
1 parent 4abc152 commit 6fdf7fc
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,27 @@

package com.redhat.devtools.lsp4ij.features.foldingRange;

import com.intellij.openapi.util.TextRange;
import com.redhat.devtools.lsp4ij.fixtures.LSPFoldingRangeFixtureTestCase;

/**
* Selection range tests by emulating LSP 'textDocument/foldingRange' responses from the typescript-language-server.
*/
public class TypeScriptFoldingRangeTest extends LSPFoldingRangeFixtureTestCase {

private static final String DEMO_TS_FILE_NAME = "demo.ts";
// language=typescript
private static final String DEMO_TS_FILE_BODY = """
export class Demo {
demo() {
console.log('demo');
}
}
""";

// Demo class braced block exclusive of braces
private static final TextRange DEMO_CLASS_BODY_TEXT_RANGE = TextRange.create(afterFirst(DEMO_TS_FILE_BODY, "{", 1), beforeLast(DEMO_TS_FILE_BODY, "}", 1));
// demo() function braced block exclusive of braces
private static final TextRange DEMO_METHOD_BODY_TEXT_RANGE = TextRange.create(afterFirst(DEMO_TS_FILE_BODY, "{", 2), beforeLast(DEMO_TS_FILE_BODY, "}", 2));

public TypeScriptFoldingRangeTest() {
super("*.ts");
}

public void testFoldingRanges() {
assertFoldingRanges(
DEMO_TS_FILE_NAME,
DEMO_TS_FILE_BODY,
"demo.ts",
"""
export class Demo {<start1>
demo() {<start2>
console.log('demo');
<end2>}
<end1>}
""",
// language=json
"""
[
Expand All @@ -54,16 +44,20 @@ public void testFoldingRanges() {
"endLine": 2
}
]
""",
DEMO_CLASS_BODY_TEXT_RANGE,
DEMO_METHOD_BODY_TEXT_RANGE
"""
);
}

public void testFoldingRanges_collapsedText() {
assertFoldingRanges(
DEMO_TS_FILE_NAME,
DEMO_TS_FILE_BODY,
"demo.ts",
"""
export class Demo {<start1>
demo() {<start2>
console.log('demo');
<end2>}
<end1>}
""",
// language=json
"""
[
Expand All @@ -78,9 +72,7 @@ public void testFoldingRanges_collapsedText() {
"collapsedText": "methodBody"
}
]
""",
DEMO_CLASS_BODY_TEXT_RANGE,
DEMO_METHOD_BODY_TEXT_RANGE
"""
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import com.redhat.devtools.lsp4ij.launching.ServerMappingSettings;
import com.redhat.devtools.lsp4ij.mock.MockLanguageServer;
import com.redhat.devtools.lsp4ij.mock.MockLanguageServerDefinition;
import org.jetbrains.annotations.NotNull;

import java.util.List;

Expand Down Expand Up @@ -73,85 +72,4 @@ private void registerServer() {
private void unregisterServer() {
LanguageServersRegistry.getInstance().removeServerDefinition(myFixture.getProject(), serverDefinition);
}

// Utility methods for getting the before/after index of the n'th occurrence of a substring of the test file body
// from the beginning or end as appropriate. These methods help to avoid hard-coding offsets into the file.

/**
* Returns the index immediately <i>before</i> the {@code count} occurrence of {@code snippet} in {@code fileBody}
* searching from the beginning.
*
* @param fileBody the file body
* @param snippet the search snippet
* @param count the occurrence count
* @return the index immediately before the occurrence; fails fast if not found
*/
protected static int beforeFirst(@NotNull String fileBody, @NotNull String snippet, int count) {
int fromIndex = 0;
for (int i = 0; i < count; i++) {
int index = fileBody.indexOf(snippet, fromIndex);
assertFalse("Failed to find occurrence " + (i + 1) + " of '" + snippet + "'.", index == -1);
if (count == (i + 1)) {
return index;
} else {
fromIndex = index + 1;
if (fromIndex == fileBody.length()) {
fail("Failed to find occurrence " + (i + 1) + " of '" + snippet + "'.");
}
}
}
return fromIndex;
}

/**
* Returns the index immediately <i>after</i> the {@code count} occurrence of {@code snippet} in {@code fileBody}
* searching from the beginning.
*
* @param fileBody the file body
* @param snippet the search snippet
* @param count the occurrence count
* @return the index immediately after the occurrence; fails fast if not found
*/
protected static int afterFirst(@NotNull String fileBody, @NotNull String snippet, int count) {
return beforeFirst(fileBody, snippet, count) + snippet.length();
}

/**
* Returns the index immediately <i>before</i> the {@code count} occurrence of {@code snippet} in {@code fileBody}
* searching from the end.
*
* @param fileBody the file body
* @param snippet the search snippet
* @param count the occurrence count
* @return the index immediately before the last occurrence; fails fast if not found
*/
protected static int beforeLast(@NotNull String fileBody, @NotNull String snippet, int count) {
int fromIndex = fileBody.length() - 1;
for (int i = 0; i < count; i++) {
int index = fileBody.lastIndexOf(snippet, fromIndex);
assertFalse("Failed to find last occurrence " + (i + 1) + " of '" + snippet + "'.", index == -1);
if (count == (i + 1)) {
return index;
} else {
fromIndex = index - 1;
if (fromIndex == 0) {
fail("Failed to find occurrence " + (i + 1) + " of '" + snippet + "'.");
}
}
}
return fromIndex;
}

/**
* Returns the index immediately <i>after</i> the {@code count} occurrence of {@code snippet} in {@code fileBody}
* searching from the end.
*
* @param fileBody the file body
* @param snippet the search snippet
* @param count the occurrence count
* @return the index immediately after the last occurrence; fails fast if not found
*/
protected static int afterLast(@NotNull String fileBody, @NotNull String snippet, int count) {
return beforeLast(fileBody, snippet, count) + snippet.length();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,42 @@
import org.eclipse.lsp4j.FoldingRange;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Base class test case to test LSP 'textDocument/foldingRange' feature.
*/
public abstract class LSPFoldingRangeFixtureTestCase extends LSPCodeInsightFixtureTestCase {

private static final String START_TOKEN_TEXT = "start";
private static final String END_TOKEN_TEXT = "end";
// For simplicity's sake, we only support up to 10 start/end token pairs
private static final Pattern TOKEN_PATTERN = Pattern.compile("(?ms)<(" + START_TOKEN_TEXT + "|" + END_TOKEN_TEXT + ")(\\d)>");
private static final int START_TOKEN_LENGTH = START_TOKEN_TEXT.length() + 3;
private static final int END_TOKEN_LENGTH = END_TOKEN_TEXT.length() + 3;

protected LSPFoldingRangeFixtureTestCase(String... fileNamePatterns) {
super(fileNamePatterns);
}

protected void assertFoldingRanges(@NotNull String fileName,
@NotNull String fileBody,
@NotNull String mockFoldingRangesJson,
@NotNull TextRange... expectedFoldingTextRanges) {
@NotNull String mockFoldingRangesJson) {
// Derive the expected text ranges from the tokenized file body
List<TextRange> expectedTextRanges = getExpectedTextRanges(fileBody);

MockLanguageServer.INSTANCE.setTimeToProceedQueries(100);
List<FoldingRange> mockFoldingRanges = JSONUtils.getLsp4jGson().fromJson(mockFoldingRangesJson, new TypeToken<List<FoldingRange>>() {
}.getType());
MockLanguageServer.INSTANCE.setFoldingRanges(mockFoldingRanges);

PsiFile file = myFixture.configureByText(fileName, fileBody);
PsiFile file = myFixture.configureByText(fileName, removeTokens(fileBody));
Editor editor = myFixture.getEditor();

// Initialize the language server
Expand All @@ -61,19 +75,100 @@ protected void assertFoldingRanges(@NotNull String fileName,
FoldingModel foldingModel = editor.getFoldingModel();
FoldRegion[] foldRegions = foldingModel.getAllFoldRegions();

assertEquals(expectedFoldingTextRanges.length, foldRegions.length);
// We only need to check against start because we confirmed that start and end are the same length above
assertEquals(expectedTextRanges.size(), foldRegions.length);

for (int i = 0; i < foldRegions.length; i++) {
FoldRegion actualFoldRegion = foldRegions[i];

// Check the text range
TextRange expectedFoldingTextRange = expectedFoldingTextRanges[i];
assertEquals(expectedFoldingTextRange, actualFoldRegion.getTextRange());
TextRange expectedTextRange = expectedTextRanges.get(i);
TextRange actualTextRange = actualFoldRegion.getTextRange();
assertEquals(expectedTextRange, actualTextRange);

// Check the placeholder text
FoldingRange expectedFoldingRange = mockFoldingRanges.get(i);
String expectedPlaceholderText = StringUtil.isNotEmpty(expectedFoldingRange.getCollapsedText()) ? expectedFoldingRange.getCollapsedText() : "...";
assertEquals(expectedPlaceholderText, actualFoldRegion.getPlaceholderText());
FoldingRange mockFoldingRange = mockFoldingRanges.get(i);
String mockCollapsedText = mockFoldingRange.getCollapsedText();
String expectedPlaceholderText = StringUtil.isNotEmpty(mockCollapsedText) ? mockCollapsedText : "...";
String actualPlaceholderText = actualFoldRegion.getPlaceholderText();
assertEquals(expectedPlaceholderText, actualPlaceholderText);
}
}

@NotNull
private static List<TextRange> getExpectedTextRanges(@NotNull String fileBody) {
// Gather raw start and end token offsets
Map<Integer, Integer> rawStartOffsetsByIndex = new LinkedHashMap<>();
Map<Integer, Integer> rawEndOffsetsByIndex = new LinkedHashMap<>();
Matcher tokenMatcher = TOKEN_PATTERN.matcher(fileBody);
while (tokenMatcher.find()) {
String tokenText = tokenMatcher.group(1);
int tokenIndex = Integer.parseInt(tokenMatcher.group(2));
int rawStartOffset = tokenMatcher.start();
if (tokenText.contains(START_TOKEN_TEXT)) {
if (rawStartOffsetsByIndex.containsKey(tokenIndex)) {
fail("Multiple start tokens were found for index " + tokenIndex + ".");
}
rawStartOffsetsByIndex.put(tokenIndex, rawStartOffset);
} else {
if (rawEndOffsetsByIndex.containsKey(tokenIndex)) {
fail("Multiple end tokens were found for index " + tokenIndex + ".");
}
rawEndOffsetsByIndex.put(tokenIndex, rawStartOffset);
}
}
assertFalse("No start tokens found.", rawStartOffsetsByIndex.isEmpty());
assertFalse("No end tokens found.", rawEndOffsetsByIndex.isEmpty());
assertEquals("Start and end tokens do not match in length.", rawStartOffsetsByIndex.size(), rawEndOffsetsByIndex.size());
assertEquals("Start and end tokens do not have paired indexes.", rawStartOffsetsByIndex.keySet(), rawEndOffsetsByIndex.keySet());

// Align the raw start and end offset collections
List<Integer> rawStartOffsets = new ArrayList<>(rawStartOffsetsByIndex.values());
List<Integer> rawEndOffsets = new ArrayList<>(rawStartOffsetsByIndex.size());
for (Integer rawStartOffsetIndex : rawStartOffsetsByIndex.keySet()) {
Integer rawEndOffset = rawEndOffsetsByIndex.get(rawStartOffsetIndex);
assertNotNull("Failed to find the end offset with index " + rawStartOffsetIndex + ".", rawEndOffset);
rawEndOffsets.add(rawEndOffset);
}

// Compute final offsets as appropriate based on relative token positioning
List<Integer> startOffsets = new ArrayList<>(rawStartOffsets.size());
for (int i = 0; i < rawStartOffsets.size(); i++) {
int rawStartOffset = rawStartOffsets.get(i);
int startOffset = rawStartOffset;
for (Integer otherRawStartOffset : rawStartOffsets) {
if (rawStartOffset > otherRawStartOffset) startOffset -= START_TOKEN_LENGTH;
}
for (int rawEndOffset : rawEndOffsets) {
if (rawStartOffset > rawEndOffset) startOffset -= END_TOKEN_LENGTH;
}
startOffsets.add(startOffset);
}
List<Integer> endOffsets = new ArrayList<>(rawEndOffsets.size());
for (int i = 0; i < rawEndOffsets.size(); i++) {
int rawEndOffset = rawEndOffsets.get(i);
int endOffset = rawEndOffset;
for (int rawStartOffset : rawStartOffsets) {
if (rawEndOffset > rawStartOffset) endOffset -= START_TOKEN_LENGTH;
}
for (Integer otherRawEndOffset : rawEndOffsets) {
if (rawEndOffset > otherRawEndOffset) endOffset -= END_TOKEN_LENGTH;
}
endOffsets.add(endOffset);
}

// Create text ranges from the start and end offset pairs
List<TextRange> expectedTextRanges = new ArrayList<>(startOffsets.size());
for (int i = 0; i < startOffsets.size(); i++) {
int startOffset = startOffsets.get(i);
int endOffset = endOffsets.get(i);
expectedTextRanges.add(TextRange.create(startOffset, endOffset));
}
return expectedTextRanges;
}

@NotNull
private static String removeTokens(@NotNull String fileBody) {
return fileBody.replaceAll(TOKEN_PATTERN.pattern(), "");
}
}

0 comments on commit 6fdf7fc

Please sign in to comment.