Skip to content

Commit b8452e4

Browse files
committed
Build manifestmergetool for integration testing. Tune timeouts for remote builder tests
1 parent b579486 commit b8452e4

5 files changed

Lines changed: 80 additions & 8 deletions

File tree

server/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ Test can be run from the root directory with
114114
```sh
115115
./gradlew server:test
116116
```
117+
Unless `-PexcludeTags=integration` is passed, this first builds `server:bootJar` and
118+
`manifestmergetool:mainJar` into `server/app/` - the directory the test containers bind-mount.
119+
Both are needed: the containers run `extender.jar`, and `manifestmergetool.jar` is invoked as a
120+
subprocess by the sdk's `manifestMergeCmd` (see `IntegrationTest.buildAndroidLocalAar`). Those two
121+
jars are declared outputs of their tasks, so deleting one from `server/app/` re-runs the task
122+
rather than being silently reported as `UP-TO-DATE`.
123+
117124
During the testing local servers will be run. That's why it necessary to have prebuild docker images. There are two set of services that run:
118125
* **test** - run for integration testing (see *IntegrationTest.java*)
119126
* **auth-test** - run for authentication testing (see *AuthenticationTest.java*)

server/build.gradle

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ compileJava {
154154
bootJar {
155155
archiveBaseName = 'extender'
156156
version = project.ext.extenderVersion
157+
// See the note on :manifestmergetool:mainJar - server/app/extender.jar is what the docker
158+
// containers run, so it is declared as an output and restored if deleted.
159+
outputs.file("${project.projectDir}/app/extender.jar")
157160
doLast {
158161
copy {
159162
from "${project.projectDir}/build/libs/extender-${project.ext.extenderVersion}.jar"
@@ -201,12 +204,14 @@ task buildStandalone() {
201204
}
202205
into "build/standalone"
203206
}
207+
// .archiveFile, not the task itself: a task reference resolves to *all* of its outputs,
208+
// which now includes the server/app/ copy declared above.
204209
copy {
205-
from bootJar
210+
from bootJar.archiveFile
206211
into "build/standalone"
207212
}
208213
copy {
209-
from project(':manifestmergetool').collect { it.tasks.withType(Jar) }
214+
from project(':manifestmergetool').tasks.withType(Jar).collect { it.archiveFile }
210215
into "build/standalone"
211216
}
212217
}
@@ -218,6 +223,14 @@ test {
218223
excludeTags project.getProperty("excludeTags")
219224
}
220225
}
226+
// The integration and auth tests boot the docker stack, which bind-mounts server/app/ into
227+
// every container: extender.jar is what they run, and manifestmergetool.jar is invoked as a
228+
// subprocess by the sdk's manifestMergeCmd (e.g. IntegrationTest.buildAndroidLocalAar).
229+
// CI's coverage job passes -PexcludeTags=integration and skips the jar build entirely.
230+
def excludedTags = (project.findProperty("excludeTags") ?: "").toString().split(",")*.trim()
231+
if (!excludedTags.contains("integration")) {
232+
dependsOn 'bootJar', ':manifestmergetool:mainJar'
233+
}
221234
// Opt-in knobs to shrink a local integration test run. All absent in CI, where they are no-ops.
222235
// -PtargetPlatforms=x86_64-linux,js-web only build these targets, and only boot the
223236
// builders they need (see docker-compose.yml profiles)

server/manifestmergetool/build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ task mainJar(type: Jar) {
4545
}
4646
duplicatesStrategy = DuplicatesStrategy.WARN
4747
with jar
48+
// The copy below is a real output of this task: server/app/ is what the docker containers
49+
// bind-mount, and the sdk's manifestMergeCmd runs this jar from there. Declaring it means a
50+
// deleted server/app/manifestmergetool.jar re-runs the task instead of reporting UP-TO-DATE
51+
// and leaving it missing.
52+
outputs.file("${project.projectDir}/../app/manifestmergetool.jar")
4853
doLast {
4954
copy {
5055
from "${project.projectDir}/build/libs/manifestmergetool-${project.ext.manifestMergetoolVersion}.jar"

server/scripts/start-test-server.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ echo "Using PORT: ${PORT}"
3838

3939
URL=http://localhost:${PORT}
4040

41+
# server/app/ is bind-mounted into every container: extender.jar is what they run, and
42+
# manifestmergetool.jar is invoked as a subprocess by the sdk's manifestMergeCmd. Missing jars
43+
# would only surface much later as an obscure in-container failure.
44+
for jar in extender.jar manifestmergetool.jar; do
45+
if [ ! -f "${DIR}/../app/${jar}" ]; then
46+
echo "ERROR: server/app/${jar} is missing. Run: ./gradlew server:bootJar manifestmergetool:mainJar"
47+
exit 1
48+
fi
49+
done
50+
4151
COMPOSE_FILES=(-f "${DIR}/../docker/docker-compose.yml")
4252
if [ "$EXTENDER_DEV_CACHE" == "1" ]; then
4353
# The volume is external so that the 'extender-test' and 'extender-test-auth'

server/src/test/java/com/defold/extender/remote/RemoteEngineBuilderTest.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
import static org.mockito.Mockito.verify;
2222

2323
import java.io.File;
24+
import java.io.IOException;
2425
import java.io.InputStream;
2526
import java.nio.charset.StandardCharsets;
2627
import java.nio.file.Files;
2728
import java.nio.file.Path;
29+
import java.util.ArrayList;
2830
import java.util.HashSet;
31+
import java.util.List;
2932
import java.util.Optional;
3033
import java.util.Set;
3134
import java.util.concurrent.ExecutorService;
@@ -40,6 +43,8 @@
4043
import org.junit.jupiter.api.Test;
4144
import org.junit.jupiter.api.Timeout;
4245
import org.junit.jupiter.api.io.TempDir;
46+
import org.junit.jupiter.api.parallel.Execution;
47+
import org.junit.jupiter.api.parallel.ExecutionMode;
4348

4449
import org.apache.http.HttpEntity;
4550

@@ -58,11 +63,21 @@
5863
import io.micrometer.tracing.propagation.Propagator;
5964
import io.micrometer.tracing.test.simple.SimpleTracer;
6065

66+
// The methods below start a WireMock server each and assert on wall-clock deadlines. Running
67+
// them concurrently (junit-platform.properties sets parallel.mode.default=concurrent) puts as
68+
// many Jetty startups as there are cores on top of each other, which on a loaded CI runner
69+
// delays the first response past the socket timeout and fails the test with a spurious
70+
// "Failed to communicate with Extender service".
71+
@Execution(ExecutionMode.SAME_THREAD)
6172
public class RemoteEngineBuilderTest {
6273

6374
private static final long BUILD_SLEEP_TIMEOUT = 100;
6475
private static final long BUILD_RESULT_WAIT_TIMEOUT = 10_000;
65-
private static final int SOCKET_TIMEOUT = 2_000;
76+
// Generous enough that a busy CI runner is never mistaken for a network fault: a
77+
// SocketTimeoutException is not retried by the client and marks the build as failed.
78+
private static final int SOCKET_TIMEOUT = 30_000;
79+
// Only for the test that has to give up on a deliberately stalled response.
80+
private static final int SHORT_SOCKET_TIMEOUT = 2_000;
6681

6782
@TempDir
6883
Path tmpDir;
@@ -71,6 +86,7 @@ public class RemoteEngineBuilderTest {
7186
private ExecutorService executor;
7287
private Path resultLocation;
7388
private SimpleMeterRegistry meterRegistry;
89+
private final List<RemoteEngineBuilder> builders = new ArrayList<>();
7490

7591
@BeforeEach
7692
public void setUp() throws Exception {
@@ -88,6 +104,16 @@ public void setUp() throws Exception {
88104
@AfterEach
89105
public void tearDown() {
90106
executor.shutdownNow();
107+
// Each client owns connection eviction threads, which would otherwise leak for the
108+
// remainder of the suite. Closing is cleanup only, so it never fails a test.
109+
for (RemoteEngineBuilder builder : builders) {
110+
try {
111+
builder.httpClient.close();
112+
} catch (IOException e) {
113+
// ignore
114+
}
115+
}
116+
builders.clear();
91117
builderMock.stop();
92118
}
93119

@@ -101,20 +127,28 @@ private RemoteEngineBuilder createBuilder(int resultDownloadRetries, long buildR
101127

102128
private RemoteEngineBuilder createBuilder(Optional<GCPInstanceService> instanceService,
103129
int resultDownloadRetries, long buildResultWaitTimeout) {
104-
return new RemoteEngineBuilder(
130+
return createBuilder(instanceService, resultDownloadRetries, buildResultWaitTimeout, SOCKET_TIMEOUT);
131+
}
132+
133+
private RemoteEngineBuilder createBuilder(Optional<GCPInstanceService> instanceService,
134+
int resultDownloadRetries, long buildResultWaitTimeout,
135+
int socketTimeout) {
136+
RemoteEngineBuilder builder = new RemoteEngineBuilder(
105137
instanceService,
106138
resultLocation.toString(),
107139
BUILD_SLEEP_TIMEOUT,
108140
buildResultWaitTimeout,
109-
SOCKET_TIMEOUT,
110-
SOCKET_TIMEOUT,
111-
SOCKET_TIMEOUT,
141+
socketTimeout,
142+
socketTimeout,
143+
socketTimeout,
112144
35,
113145
resultDownloadRetries,
114146
100,
115147
meterRegistry,
116148
new SimpleTracer(),
117149
Propagator.NOOP);
150+
builders.add(builder);
151+
return builder;
118152
}
119153

120154
private double reconnectCount(String operation) {
@@ -170,7 +204,10 @@ public void stalledResultDownloadMustNotBlockOtherBuilds() throws Exception {
170204
builderMock.stubFor(get(urlPathEqualTo("/job_result"))
171205
.willReturn(aResponse().withStatus(200).withFixedDelay(30_000).withBody(new byte[] {0x42})));
172206

173-
RemoteEngineBuilder builder = createBuilder(1);
207+
// The short socket timeout is what lets the stuck downloads give up on the 30s delayed
208+
// response inside this test's deadline.
209+
RemoteEngineBuilder builder = createBuilder(Optional.empty(), 1, BUILD_RESULT_WAIT_TIMEOUT,
210+
SHORT_SOCKET_TIMEOUT);
174211

175212
// Occupy the connection pool with builds stuck downloading their result
176213
Path stalledResult1 = submitBuild(builder, "stalled1");

0 commit comments

Comments
 (0)