Skip to content

Commit 25ecbbb

Browse files
FTMahringerCopilot
andcommitted
test(plugins): add real jar fixture builder
Add backend test-side fixture plumbing for building real plugin JARs from synapse-plugin-template, isolate integration test storage under a temporary SYNAPSE_HOME, and wire explicit Failsafe execution for *IntegrationTest classes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7e3455e commit 25ecbbb

4 files changed

Lines changed: 369 additions & 1 deletion

File tree

packages/core/pom.xml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,24 @@
185185
</excludes>
186186
</configuration>
187187
</plugin>
188+
<plugin>
189+
<groupId>org.apache.maven.plugins</groupId>
190+
<artifactId>maven-failsafe-plugin</artifactId>
191+
<version>3.5.1</version>
192+
<configuration>
193+
<includes>
194+
<include>**/*IntegrationTest.java</include>
195+
</includes>
196+
</configuration>
197+
<executions>
198+
<execution>
199+
<goals>
200+
<goal>integration-test</goal>
201+
<goal>verify</goal>
202+
</goals>
203+
</execution>
204+
</executions>
205+
</plugin>
188206
</plugins>
189207
</build>
190208
</project>

packages/core/src/test/java/dev/synapse/core/BaseIntegrationTest.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package dev.synapse.core;
22

3+
import java.io.IOException;
4+
import java.nio.file.Files;
5+
import java.nio.file.Path;
36
import org.junit.jupiter.api.BeforeAll;
47
import org.springframework.boot.test.context.SpringBootTest;
58
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
@@ -19,6 +22,8 @@
1922
@Testcontainers
2023
public abstract class BaseIntegrationTest {
2124

25+
protected static Path synapseTestHome;
26+
2227
static boolean dockerAvailable() {
2328
try {
2429
DockerClientFactory.instance().client();
@@ -31,7 +36,14 @@ static boolean dockerAvailable() {
3136
static PostgreSQLContainer<?> postgres;
3237

3338
@BeforeAll
34-
static void beforeAll() {
39+
static void beforeAll() throws IOException {
40+
if (synapseTestHome == null) {
41+
synapseTestHome = Files.createTempDirectory("synapse-it-home");
42+
synapseTestHome.toFile().deleteOnExit();
43+
System.setProperty("synapse.test.home", synapseTestHome.toString());
44+
System.setProperty("user.home", synapseTestHome.toString());
45+
}
46+
3547
if (!dockerAvailable()) {
3648
return;
3749
}
@@ -53,6 +65,7 @@ static void configureProperties(DynamicPropertyRegistry registry) {
5365
registry.add("jwt.secret", () ->
5466
"CHANGE_ME_IN_PRODUCTION_THIS_MUST_BE_AT_LEAST_256_BITS_LONG_FOR_HS256"
5567
);
68+
registry.add("synapse.test.home", () -> synapseTestHome.toString());
5669
registry.add("secrets.encryption.key", () ->
5770
"dev_key_32_bytes_change_me_now!!"
5871
);
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
package dev.synapse.core.plugins;
2+
3+
import java.io.IOException;
4+
import java.io.UncheckedIOException;
5+
import java.nio.charset.StandardCharsets;
6+
import java.nio.file.FileVisitResult;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.SimpleFileVisitor;
10+
import java.nio.file.StandardCopyOption;
11+
import java.nio.file.attribute.BasicFileAttributes;
12+
import java.time.Duration;
13+
import java.util.LinkedHashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.concurrent.atomic.AtomicReference;
17+
import java.util.stream.Stream;
18+
19+
/**
20+
* Builds real plugin JAR fixtures from synapse-plugin-template for integration tests.
21+
*/
22+
public final class PluginFixtureBuilder {
23+
24+
private static final Duration MAVEN_TIMEOUT = Duration.ofMinutes(3);
25+
private static final AtomicReference<PluginFixtureSet> CACHED_FIXTURES = new AtomicReference<>();
26+
27+
private PluginFixtureBuilder() {}
28+
29+
public static PluginFixtureSet buildFixtures() {
30+
PluginFixtureSet cached = CACHED_FIXTURES.get();
31+
if (cached != null && allExist(cached)) {
32+
return cached;
33+
}
34+
35+
synchronized (CACHED_FIXTURES) {
36+
cached = CACHED_FIXTURES.get();
37+
if (cached != null && allExist(cached)) {
38+
return cached;
39+
}
40+
41+
try {
42+
Path repoRoot = findRepoRoot();
43+
Path fixtureDir = Files.createTempDirectory("synapse-plugin-fixtures-");
44+
fixtureDir.toFile().deleteOnExit();
45+
46+
ensurePluginApiPackaged(repoRoot);
47+
48+
Path channelJar = buildTemplateFixture(
49+
repoRoot,
50+
"channel",
51+
"fixture-channel-plugin",
52+
"Fixture Channel Plugin",
53+
"1.0.0",
54+
Map.of()
55+
);
56+
Path modelProviderJar = buildTemplateFixture(
57+
repoRoot,
58+
"model-provider",
59+
"fixture-model-provider-plugin",
60+
"Fixture Model Provider Plugin",
61+
"1.0.0",
62+
Map.of()
63+
);
64+
Path dependencyJar = buildTemplateFixture(
65+
repoRoot,
66+
"channel",
67+
"fixture-channel-plugin-dependent",
68+
"Fixture Channel Plugin Dependent",
69+
"1.0.0",
70+
Map.of(
71+
"requires:\n synapse_version: \">=2.6.0\"\n api_version: \">=1.0.0\"\n java: \">=25\"",
72+
"""
73+
requires:
74+
synapse_version: ">=2.6.0"
75+
api_version: ">=1.0.0"
76+
java: ">=25"
77+
plugins:
78+
- id: fixture-model-provider-plugin
79+
version: ">=1.0.0"
80+
"""
81+
)
82+
);
83+
Path conflictJar = buildTemplateFixture(
84+
repoRoot,
85+
"channel",
86+
"fixture-channel-plugin-conflict",
87+
"Fixture Channel Plugin Conflict",
88+
"1.0.0",
89+
Map.of(
90+
"requires:\n synapse_version: \">=2.6.0\"\n api_version: \">=1.0.0\"\n java: \">=25\"",
91+
"""
92+
requires:
93+
synapse_version: ">=2.6.0"
94+
api_version: ">=1.0.0"
95+
java: ">=25"
96+
plugins:
97+
- id: fixture-channel-plugin
98+
version: "<1.0.0"
99+
"""
100+
)
101+
);
102+
103+
PluginFixtureSet fixtures = new PluginFixtureSet(
104+
fixtureDir,
105+
copyToFixtureDir(channelJar, fixtureDir, "fixture-channel-plugin-1.0.0.jar"),
106+
copyToFixtureDir(modelProviderJar, fixtureDir, "fixture-model-provider-plugin-1.0.0.jar"),
107+
copyToFixtureDir(dependencyJar, fixtureDir, "fixture-channel-plugin-dependent-1.0.0.jar"),
108+
copyToFixtureDir(conflictJar, fixtureDir, "fixture-channel-plugin-conflict-1.0.0.jar"),
109+
"fixture-channel-plugin",
110+
"fixture-model-provider-plugin",
111+
"fixture-channel-plugin-dependent",
112+
"fixture-channel-plugin-conflict"
113+
);
114+
CACHED_FIXTURES.set(fixtures);
115+
return fixtures;
116+
} catch (IOException e) {
117+
throw new UncheckedIOException("Failed to build plugin fixtures", e);
118+
} catch (InterruptedException e) {
119+
Thread.currentThread().interrupt();
120+
throw new IllegalStateException("Interrupted while building plugin fixtures", e);
121+
}
122+
}
123+
}
124+
125+
private static boolean allExist(PluginFixtureSet fixtures) {
126+
return Stream.of(
127+
fixtures.channelJar(),
128+
fixtures.modelProviderJar(),
129+
fixtures.dependencyJar(),
130+
fixtures.conflictJar()
131+
).allMatch(Files::isRegularFile);
132+
}
133+
134+
private static Path findRepoRoot() throws IOException {
135+
Path current = Path.of("").toAbsolutePath().normalize();
136+
while (current != null) {
137+
if (
138+
Files.isDirectory(current.resolve("synapse-plugin-template")) &&
139+
Files.isDirectory(current.resolve("synapse-plugin-api")) &&
140+
Files.isDirectory(current.resolve("packages/core"))
141+
) {
142+
return current;
143+
}
144+
current = current.getParent();
145+
}
146+
throw new IOException("Could not locate SYNAPSE repo root from test runtime");
147+
}
148+
149+
private static void ensurePluginApiPackaged(Path repoRoot)
150+
throws IOException, InterruptedException {
151+
runMaven(repoRoot.resolve("synapse-plugin-api"), "package", "-DskipTests");
152+
}
153+
154+
private static Path buildTemplateFixture(
155+
Path repoRoot,
156+
String templateName,
157+
String pluginId,
158+
String pluginName,
159+
String version,
160+
Map<String, String> extraReplacements
161+
) throws IOException, InterruptedException {
162+
Path sourceTemplate = repoRoot.resolve("synapse-plugin-template").resolve(templateName);
163+
Path workspace = Files.createTempDirectory("synapse-plugin-template-" + templateName + "-");
164+
workspace.toFile().deleteOnExit();
165+
copyDirectory(sourceTemplate, workspace);
166+
deletePathIfExists(workspace.resolve("target"));
167+
168+
Map<String, String> replacements = new LinkedHashMap<>();
169+
String pluginApiJar = repoRoot
170+
.resolve("synapse-plugin-api")
171+
.resolve("target")
172+
.resolve("synapse-plugin-api-1.0.0.jar")
173+
.toAbsolutePath()
174+
.normalize()
175+
.toString()
176+
.replace("\\", "/");
177+
if ("channel".equals(templateName)) {
178+
replacements.put("fixture-channel-plugin", pluginId);
179+
replacements.put("Fixture Channel Plugin", pluginName);
180+
replacements.put("channel-plugin-fixture", pluginId);
181+
} else {
182+
replacements.put("fixture-model-provider-plugin", pluginId);
183+
replacements.put("Fixture Model Provider Plugin", pluginName);
184+
replacements.put("model-provider-plugin-fixture", pluginId);
185+
}
186+
replacements.put("1.0.0", version);
187+
replacements.put(
188+
"${project.basedir}/../../synapse-plugin-api/target/synapse-plugin-api-${synapse.plugin.api.version}.jar",
189+
pluginApiJar
190+
);
191+
replacements.putAll(extraReplacements);
192+
193+
rewriteFile(workspace.resolve("manifest.yml"), replacements);
194+
rewriteFile(workspace.resolve("pom.xml"), replacements);
195+
rewriteJavaSources(workspace.resolve("src/main/java"), replacements);
196+
197+
runMaven(workspace, "package", "-DskipTests");
198+
199+
try (Stream<Path> jars = Files.list(workspace.resolve("target"))) {
200+
return jars
201+
.filter(path -> path.getFileName().toString().endsWith(".jar"))
202+
.filter(path -> !path.getFileName().toString().startsWith("original-"))
203+
.filter(path -> !path.getFileName().toString().contains("-shaded"))
204+
.findFirst()
205+
.orElseThrow(() ->
206+
new IOException("No packaged plugin JAR found in " + workspace.resolve("target"))
207+
);
208+
}
209+
}
210+
211+
private static void rewriteJavaSources(Path sourceRoot, Map<String, String> replacements)
212+
throws IOException {
213+
if (!Files.isDirectory(sourceRoot)) {
214+
return;
215+
}
216+
try (Stream<Path> files = Files.walk(sourceRoot)) {
217+
for (Path file : files.filter(Files::isRegularFile).toList()) {
218+
String name = file.getFileName().toString();
219+
if (name.endsWith(".java")) {
220+
rewriteFile(file, replacements);
221+
}
222+
}
223+
}
224+
}
225+
226+
private static void rewriteFile(Path file, Map<String, String> replacements)
227+
throws IOException {
228+
String content = Files.readString(file, StandardCharsets.UTF_8);
229+
for (Map.Entry<String, String> entry : replacements.entrySet()) {
230+
content = content.replace(entry.getKey(), entry.getValue());
231+
}
232+
Files.writeString(file, content, StandardCharsets.UTF_8);
233+
}
234+
235+
private static Path copyToFixtureDir(Path sourceJar, Path fixtureDir, String fileName)
236+
throws IOException {
237+
Path target = fixtureDir.resolve(fileName);
238+
Files.copy(sourceJar, target, StandardCopyOption.REPLACE_EXISTING);
239+
target.toFile().deleteOnExit();
240+
return target;
241+
}
242+
243+
private static void runMaven(Path workingDir, String... args)
244+
throws IOException, InterruptedException {
245+
List<String> command = new java.util.ArrayList<>();
246+
command.add(resolveMavenCommand());
247+
command.add("-q");
248+
command.addAll(List.of(args));
249+
250+
Process process = new ProcessBuilder(command)
251+
.directory(workingDir.toFile())
252+
.redirectErrorStream(true)
253+
.start();
254+
255+
boolean finished = process.waitFor(MAVEN_TIMEOUT.toMillis(), java.util.concurrent.TimeUnit.MILLISECONDS);
256+
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
257+
if (!finished) {
258+
process.destroyForcibly();
259+
throw new IOException("Maven command timed out in " + workingDir + ": " + String.join(" ", command));
260+
}
261+
if (process.exitValue() != 0) {
262+
throw new IOException(
263+
"Maven command failed in " + workingDir + ": " + String.join(" ", command) + System.lineSeparator() + output
264+
);
265+
}
266+
}
267+
268+
private static String resolveMavenCommand() {
269+
return System.getProperty("os.name").toLowerCase().contains("win")
270+
? "mvn.cmd"
271+
: "mvn";
272+
}
273+
274+
private static void copyDirectory(Path source, Path target) throws IOException {
275+
Files.walkFileTree(
276+
source,
277+
new SimpleFileVisitor<>() {
278+
@Override
279+
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
280+
throws IOException {
281+
Files.createDirectories(target.resolve(source.relativize(dir)));
282+
return FileVisitResult.CONTINUE;
283+
}
284+
285+
@Override
286+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
287+
throws IOException {
288+
Path destination = target.resolve(source.relativize(file));
289+
Files.copy(file, destination, StandardCopyOption.REPLACE_EXISTING);
290+
return FileVisitResult.CONTINUE;
291+
}
292+
}
293+
);
294+
}
295+
296+
private static void deletePathIfExists(Path path) throws IOException {
297+
if (!Files.exists(path)) {
298+
return;
299+
}
300+
Files.walkFileTree(
301+
path,
302+
new SimpleFileVisitor<>() {
303+
@Override
304+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
305+
throws IOException {
306+
Files.deleteIfExists(file);
307+
return FileVisitResult.CONTINUE;
308+
}
309+
310+
@Override
311+
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
312+
throws IOException {
313+
Files.deleteIfExists(dir);
314+
return FileVisitResult.CONTINUE;
315+
}
316+
}
317+
);
318+
}
319+
}

0 commit comments

Comments
 (0)