Skip to content

Commit b8ae866

Browse files
rcosta358codex
andauthored
Add language server unit tests (#141)
Adds 24 JUnit tests for source-folder selection, Windows-style file URIs, source positions, diagnostic DTOs, and context conversion. Runs server tests in CI. Validated with Java 21: Maven tests/package, client lint, TypeScript checks, and extension installation. Closes #129. Depends on #142 for the combined client/server test workflow. Generated by Codex. --------- Co-authored-by: Codex <noreply@openai.com>
1 parent c140f12 commit b8ae866

5 files changed

Lines changed: 365 additions & 1 deletion

File tree

‎.github/workflows/test.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
- name: Type-check tests
5555
run: npm run test:typecheck
5656

57-
- name: Build server
57+
- name: Build and test server
5858
working-directory: server
5959
run: |
6060
mvn -B package
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package dtos.diagnostics;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import spoon.reflect.cu.SourcePosition;
8+
9+
class SourcePositionDTOTest {
10+
@Test
11+
void convertsStringRangeToZeroBasedLinesAndExclusiveEndColumn() {
12+
assertEquals(new SourcePositionDTO(null, 1, 4, 3, 18), SourcePositionDTO.from("2:5-4:18"));
13+
assertEquals(new SourcePositionDTO(null, 0, 0, 0, 1), SourcePositionDTO.from("1:1-1:1"));
14+
}
15+
16+
@Test
17+
void rejectsMalformedRanges() {
18+
for (String invalid : new String[] { "", "2:5", "2:5-4", " 2:5-4:18", "2:a-4:18", "prefix 2:5-4:18" }) {
19+
assertNull(SourcePositionDTO.from(invalid));
20+
}
21+
}
22+
23+
@Test
24+
void toleratesMissingAndUnavailablePositions() {
25+
assertNull(SourcePositionDTO.from((String) null));
26+
assertNull(SourcePositionDTO.from((SourcePosition) null));
27+
assertNull(SourcePositionDTO.from(SourcePosition.NOPOSITION));
28+
}
29+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package utils;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.List;
6+
import java.util.Set;
7+
8+
import org.junit.jupiter.api.AfterEach;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
12+
import dtos.context.ContextHistoryDTO;
13+
import dtos.diagnostics.SourcePositionDTO;
14+
import liquidjava.processor.context.ContextHistory;
15+
import liquidjava.processor.context.Variable;
16+
import liquidjava.rj_language.Predicate;
17+
import spoon.Launcher;
18+
19+
class ContextHistoryConverterTest {
20+
private final ContextHistory history = ContextHistory.getInstance();
21+
22+
@BeforeEach
23+
@AfterEach
24+
void clearHistory() {
25+
history.clearHistory();
26+
}
27+
28+
@Test
29+
void convertsEmptyHistoryToEmptyCollections() {
30+
ContextHistoryDTO dto = ContextHistoryConverter.convertToDTO(history);
31+
assertTrue(dto.localVars().isEmpty());
32+
assertTrue(dto.globalVars().isEmpty());
33+
assertTrue(dto.ghosts().isEmpty());
34+
assertTrue(dto.aliases().isEmpty());
35+
assertTrue(dto.methods().isEmpty());
36+
assertTrue(dto.fileScopes().isEmpty());
37+
}
38+
39+
@Test
40+
void convertsScopesPerFileWithoutDependingOnSetOrder() {
41+
history.getFileScopes().put("Example.java", Set.of("2:5-4:18", "1:1-1:1"));
42+
history.getFileScopes().put("Other.java", Set.of("8:3-9:12"));
43+
ContextHistoryDTO dto = ContextHistoryConverter.convertToDTO(history);
44+
assertEquals(Set.of("Example.java", "Other.java"), dto.fileScopes().keySet());
45+
assertEquals(Set.of(new SourcePositionDTO(null, 1, 4, 3, 18), new SourcePositionDTO(null, 0, 0, 0, 1)),
46+
Set.copyOf(dto.fileScopes().get("Example.java")));
47+
assertEquals(List.of(new SourcePositionDTO(null, 7, 2, 8, 12)), dto.fileScopes().get("Other.java"));
48+
}
49+
50+
@Test
51+
void omitsVariablesWithoutCodePlacement() {
52+
Variable generated = new Variable("generated", new Launcher().getFactory().Type().INTEGER_PRIMITIVE,
53+
new Predicate());
54+
history.getLocalVars().add(generated);
55+
history.getGlobalVars().add(generated);
56+
ContextHistoryDTO dto = ContextHistoryConverter.convertToDTO(history);
57+
assertTrue(dto.localVars().isEmpty());
58+
assertTrue(dto.globalVars().isEmpty());
59+
}
60+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package utils;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.util.List;
8+
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.io.TempDir;
12+
13+
import dtos.diagnostics.LJDiagnosticDTO;
14+
import dtos.diagnostics.SourcePositionDTO;
15+
import dtos.errors.*;
16+
import dtos.warnings.*;
17+
import liquidjava.diagnostics.TranslationTable;
18+
import liquidjava.diagnostics.errors.*;
19+
import liquidjava.diagnostics.warnings.*;
20+
import liquidjava.processor.VCImplication;
21+
import liquidjava.processor.context.PlacementInCode;
22+
import liquidjava.rj_language.Predicate;
23+
import liquidjava.rj_language.ast.LiteralBoolean;
24+
import liquidjava.rj_language.opt.VCSimplificationResult;
25+
import spoon.Launcher;
26+
import spoon.reflect.cu.SourcePosition;
27+
import spoon.reflect.declaration.CtField;
28+
29+
class DiagnosticConverterTest {
30+
@TempDir
31+
Path workspace;
32+
33+
private CtField<?> field;
34+
private SourcePosition position;
35+
36+
@BeforeEach
37+
void createSourcePosition() throws Exception {
38+
Path file = workspace.resolve("Example.java");
39+
Files.writeString(file, "class Example {\n int value = 0;\n}\n");
40+
Launcher launcher = new Launcher();
41+
launcher.getEnvironment().setNoClasspath(true);
42+
launcher.addInputResource(file.toString());
43+
launcher.buildModel();
44+
field = launcher.getFactory().Class().get("Example").getField("value");
45+
position = field.getPosition();
46+
}
47+
48+
@Test
49+
void preservesCommonDiagnosticFields() throws Exception {
50+
CustomError error = new CustomError("verification failed", position);
51+
error.setHint("check the refinement");
52+
LJDiagnosticDTO dto = (LJDiagnosticDTO) DiagnosticConverter.convertToDTO(error);
53+
assertEquals("error", dto.category);
54+
assertEquals("custom-error", dto.type);
55+
assertEquals("Error", dto.title);
56+
assertEquals("verification failed", dto.message);
57+
assertEquals("check the refinement", dto.hint);
58+
assertEquals(workspace.resolve("Example.java").toRealPath().toString(), dto.file);
59+
assertEquals(new SourcePositionDTO(dto.file, 1, 8, 1, 18), dto.position);
60+
}
61+
62+
@Test
63+
void convertsIllegalConstructorTransitionToAnError() {
64+
LJDiagnosticDTO dto = (LJDiagnosticDTO) DiagnosticConverter.convertToDTO(
65+
new IllegalConstructorTransitionError(position));
66+
assertEquals("error", dto.category);
67+
assertEquals("illegal-constructor-transition-error", dto.type);
68+
}
69+
70+
@Test
71+
void convertsCustomWarningWithoutTreatingItAsAnError() {
72+
LJDiagnosticDTO dto = (LJDiagnosticDTO) DiagnosticConverter.convertToDTO(new CustomWarning("custom warning"));
73+
assertEquals("warning", dto.category);
74+
assertEquals("custom-warning", dto.type);
75+
assertEquals("custom warning", dto.message);
76+
}
77+
78+
@Test
79+
void preservesErrorSpecificDetails() {
80+
SyntaxErrorDTO syntax = (SyntaxErrorDTO) DiagnosticConverter.convertToDTO(new SyntaxError("invalid syntax", "_ >"));
81+
assertEquals("error", syntax.category);
82+
assertEquals("syntax-error", syntax.type);
83+
assertEquals("_ >", syntax.refinement);
84+
assertNull(syntax.file);
85+
assertNull(syntax.position);
86+
assertTrue(syntax.translationTable.isEmpty());
87+
88+
InvalidRefinementErrorDTO invalid = (InvalidRefinementErrorDTO) DiagnosticConverter.convertToDTO(
89+
new InvalidRefinementError(position, "not boolean", "42"));
90+
assertEquals("error", invalid.category);
91+
assertEquals("invalid-refinement-error", invalid.type);
92+
assertEquals("42", invalid.refinement);
93+
94+
NotFoundErrorDTO missing = (NotFoundErrorDTO) DiagnosticConverter.convertToDTO(
95+
new NotFoundError(position, "missing", NotFoundError.Kind.GHOST, List.of()));
96+
assertEquals("error", missing.category);
97+
assertEquals("not-found-error", missing.type);
98+
assertEquals("missing", missing.name);
99+
assertEquals("Ghost", missing.kind);
100+
101+
StateConflictErrorDTO conflict = (StateConflictErrorDTO) DiagnosticConverter.convertToDTO(
102+
new StateConflictError(position, new LiteralBoolean(false), null));
103+
assertEquals("error", conflict.category);
104+
assertEquals("state-conflict-error", conflict.type);
105+
assertEquals("false", conflict.state);
106+
}
107+
108+
@Test
109+
void preservesWarningSpecificDetailsAndOverloadHint() {
110+
ExternalClassNotFoundWarningDTO missingClass = (ExternalClassNotFoundWarningDTO) DiagnosticConverter.convertToDTO(
111+
new ExternalClassNotFoundWarning(position, "missing class", "example.External"));
112+
assertEquals("warning", missingClass.category);
113+
assertEquals("external-class-not-found-warning", missingClass.type);
114+
assertEquals("example.External", missingClass.className);
115+
116+
ExternalMethodNotFoundWarningDTO missingMethod = (ExternalMethodNotFoundWarningDTO) DiagnosticConverter.convertToDTO(
117+
new ExternalMethodNotFoundWarning(position, "missing method", "run()", "example.External",
118+
new String[] { "run(int)", "run(String)" }));
119+
assertEquals("warning", missingMethod.category);
120+
assertEquals("external-method-not-found-warning", missingMethod.type);
121+
assertEquals("run()", missingMethod.signature);
122+
assertEquals("example.External", missingMethod.className);
123+
assertArrayEquals(new String[] { "run(int)", "run(String)" }, missingMethod.overloads);
124+
assertEquals("Available overloads:\n run(int)\n run(String)", missingMethod.hint);
125+
126+
UnsatisfiableRefinementWarningDTO unsatisfiable = (UnsatisfiableRefinementWarningDTO) DiagnosticConverter.convertToDTO(
127+
new UnsatisfiableRefinementWarning(position, "_ > 0 && _ < 0"));
128+
assertEquals("warning", unsatisfiable.category);
129+
assertEquals("unsatisfiable-refinement-warning", unsatisfiable.type);
130+
assertEquals("_ > 0 && _ < 0", unsatisfiable.refinement);
131+
}
132+
133+
@Test
134+
void preservesRefinementDetailsAndSimplificationHistory() {
135+
VCSimplificationResult origin = new VCSimplificationResult(new VCImplication(new Predicate()));
136+
VCSimplificationResult found = new VCSimplificationResult(
137+
new VCImplication(new Predicate(new LiteralBoolean(false))), origin, "constant folding");
138+
RefinementErrorDTO dto = (RefinementErrorDTO) DiagnosticConverter.convertToDTO(
139+
new RefinementError(position, position, new Predicate(), found, null, null, "expected true"));
140+
assertEquals("error", dto.category);
141+
assertEquals("refinement-error", dto.type);
142+
assertEquals("true", dto.expected);
143+
assertEquals("expected true", dto.customMessage);
144+
assertEquals(dto.position, dto.declarationPosition);
145+
assertEquals("false", dto.found.implication().predicate());
146+
assertEquals("constant folding", dto.found.simplification());
147+
assertEquals("true", dto.found.origin().implication().predicate());
148+
assertNull(dto.found.origin().origin());
149+
assertNull(dto.found.origin().simplification());
150+
assertTrue(dto.counterexample.assignments().isEmpty());
151+
}
152+
153+
@Test
154+
void preservesStateRefinementDetailsWithoutDeclarationFile() {
155+
StateRefinementErrorDTO dto = (StateRefinementErrorDTO) DiagnosticConverter.convertToDTO(
156+
new StateRefinementError(position, null, new Predicate(new LiteralBoolean(false)),
157+
new VCSimplificationResult(new VCImplication(new Predicate())), null, "expected closed"));
158+
assertEquals("error", dto.category);
159+
assertEquals("state-refinement-error", dto.type);
160+
assertEquals("false", dto.expected);
161+
assertEquals("true", dto.found.implication().predicate());
162+
assertEquals("expected closed", dto.customMessage);
163+
assertNull(dto.declarationPosition);
164+
assertNull(dto.stateMachine);
165+
}
166+
167+
@Test
168+
void convertsTranslationTablePlacementsAndDisplayNames() {
169+
TranslationTable table = new TranslationTable();
170+
table.put("#value_12", PlacementInCode.createPlacement(field));
171+
ArgumentMismatchErrorDTO dto = (ArgumentMismatchErrorDTO) DiagnosticConverter.convertToDTO(
172+
new ArgumentMismatchError("wrong arguments", position, table));
173+
assertEquals("error", dto.category);
174+
assertEquals("argument-mismatch-error", dto.type);
175+
assertEquals(1, dto.translationTable.size());
176+
assertFalse(dto.translationTable.containsKey("#value_12"));
177+
assertEquals("int value = 0;", dto.translationTable.get("value¹²").text());
178+
assertEquals(dto.position, dto.translationTable.get("value¹²").position());
179+
}
180+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package utils;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.io.File;
6+
import java.nio.file.Path;
7+
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.api.io.TempDir;
10+
11+
class PathUtilsTest {
12+
@TempDir
13+
Path workspace;
14+
15+
@Test
16+
void extractsSourceFolderAtDifferentDepths() {
17+
for (String parent : new String[] { "", "project", "projects/example/module" }) {
18+
Path sourceRoot = workspace.resolve(parent).resolve("src/main");
19+
assertEquals(sourceRoot.toString(),
20+
PathUtils.extractBasePath(sourceRoot.resolve("java/Example.java").toUri().toString()));
21+
}
22+
}
23+
24+
@Test
25+
void usesFirstSourceFolderAndOneFollowingSegment() {
26+
Path sourceRoot = workspace.resolve("src/generated");
27+
assertEquals(sourceRoot.toString(),
28+
PathUtils.extractBasePath(sourceRoot.resolve("src/main/Example.java").toUri().toString()));
29+
}
30+
31+
@Test
32+
void retainsFullPathWithoutSourceFolder() {
33+
Path file = workspace.resolve("sources/Example.java");
34+
assertEquals(file.toString(), PathUtils.extractBasePath(file.toUri().toString()));
35+
}
36+
37+
@Test
38+
void retainsPathEndingAtSourceFolder() {
39+
Path source = workspace.resolve("src");
40+
assertEquals(source.toString(), PathUtils.extractBasePath(source.toUri().toString()));
41+
}
42+
43+
@Test
44+
void decodesEscapedSourcePath() {
45+
Path source = workspace.resolve("project with spaces/src/main");
46+
assertEquals(source.toString(),
47+
PathUtils.extractBasePath(source.resolve("Example.java").toUri().toString()));
48+
}
49+
50+
@Test
51+
void handlesWindowsDriveUrisUsingHostPathSemantics() {
52+
// a windows file uri has a drive root on windows, and /C:/ on unix.
53+
String source = File.separatorChar == '\\' ? "C:\\Users\\user\\project\\src\\main"
54+
: "/C:/Users/user/project/src/main";
55+
assertEquals(source, PathUtils.extractBasePath("file:///C:/Users/user/project/src/main/java/Example.java"));
56+
assertTrue(PathUtils.isFileInDirectory("file:///C:/Users/user/project/src/main/java/Example.java",
57+
"file:///C:/Users/user/project"));
58+
assertFalse(PathUtils.isFileInDirectory("file:///D:/Users/user/project/Example.java",
59+
"file:///C:/Users/user/project"));
60+
}
61+
62+
@Test
63+
void matchesDirectorySegmentsRatherThanStringPrefixes() {
64+
Path directory = workspace.resolve("project");
65+
assertTrue(PathUtils.isFileInDirectory(directory.resolve("src/main/Example.java").toUri().toString(),
66+
directory.toUri().toString()));
67+
assertFalse(PathUtils.isFileInDirectory(workspace.resolve("project-other/Example.java").toUri().toString(),
68+
directory.toUri().toString()));
69+
assertFalse(PathUtils.isFileInDirectory(workspace.resolve("Elsewhere.java").toUri().toString(),
70+
directory.toUri().toString()));
71+
}
72+
73+
@Test
74+
void rejectsInvalidAndNonFileUrisForDirectoryMembership() {
75+
String directory = workspace.toUri().toString();
76+
for (String invalid : new String[] { null, "not a uri", "https://example.com/Example.java", "file://host/path" }) {
77+
assertFalse(PathUtils.isFileInDirectory(invalid, directory));
78+
assertFalse(PathUtils.isFileInDirectory(directory, invalid));
79+
}
80+
}
81+
82+
@Test
83+
void convertsFilePathsToEscapedUris() {
84+
Path file = workspace.resolve("project with spaces/Example.java");
85+
String uri = PathUtils.toFileUri(file.toString());
86+
assertTrue(uri.startsWith("file:"));
87+
assertTrue(uri.contains("project%20with%20spaces"));
88+
assertEquals(file, Path.of(java.net.URI.create(uri)));
89+
}
90+
91+
@Test
92+
void convertsNullPathToEmptyUri() {
93+
assertEquals("", PathUtils.toFileUri(null));
94+
}
95+
}

0 commit comments

Comments
 (0)