Skip to content

Commit f2feb86

Browse files
committed
Refactor test server management and update configuration files for integration tests
1 parent db871d5 commit f2feb86

6 files changed

Lines changed: 244 additions & 191 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,9 @@ velocity.log*
5050
*Ballerina.lock
5151
Dependencies.toml
5252

53+
# Auto-generated test configuration
54+
ballerina/tests/Config.toml
55+
integration-tests/tests/Config.toml
56+
5357
# Java Logs
5458
java_pid*.hprof

ballerina/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ test.dependsOn ":${packageName}-compiler-plugin:build"
8989
publishToMavenLocal.dependsOn build
9090
publish.dependsOn build
9191

92+
// Use the shared test server from root project
93+
test.dependsOn ":startSharedTestServer"
94+
9295
def stripBallerinaExtensionVersion(String extVersion) {
9396
if (extVersion.matches(project.ext.timestampedVersionRegex)) {
9497
def splitVersion = extVersion.split('-');

ballerina/tests/Config.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Test configuration for unit tests
2-
# Note: Unit tests use lazy gRPC connection and don't require a running server
3-
# For integration tests, use the workflow-integration-tests module
1+
# Auto-generated test configuration
2+
# This file is generated by Gradle before running tests
3+
# DO NOT EDIT - changes will be overwritten
44

55
[ballerina.workflow.workflowConfig]
66
provider = "TEMPORAL"

build.gradle

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,237 @@ task build {
9191
dependsOn(':workflow-compiler-plugin-tests:test')
9292
}
9393

94+
// ============================================================================
95+
// Shared Temporal Test Server Configuration
96+
// Single server instance shared across module and integration tests
97+
// ============================================================================
98+
99+
import java.net.InetSocketAddress
100+
import java.net.Socket
101+
102+
def testServerProcess = null
103+
def testServerWorkDir = file("${project(':workflow-native-test').buildDir}")
104+
def testServerStarted = false
105+
def testServerPort = 7233
106+
def usingExternalServer = false
107+
108+
// Helper function to check if a server is running on a given port
109+
def isServerRunning = { String host, int port ->
110+
try {
111+
def socket = new Socket()
112+
socket.connect(new InetSocketAddress(host, port), 1000)
113+
socket.close()
114+
return true
115+
} catch (Exception e) {
116+
return false
117+
}
118+
}
119+
120+
// Helper function to stop the test server
121+
def stopTestServerIfRunning = {
122+
if (!testServerStarted || usingExternalServer) {
123+
return
124+
}
125+
126+
try {
127+
def pidFile = new File(testServerWorkDir, 'test-server.pid')
128+
if (pidFile.exists()) {
129+
def shadowJarFile = project(':workflow-native-test').tasks.shadowJar.archiveFile.get().asFile
130+
131+
logger.lifecycle("Stopping shared embedded Temporal test server...")
132+
133+
def command = [
134+
'java', '-jar',
135+
shadowJarFile.absolutePath,
136+
'stop',
137+
testServerWorkDir.absolutePath
138+
]
139+
140+
def processBuilder = new ProcessBuilder(command)
141+
processBuilder.inheritIO()
142+
processBuilder.directory(testServerWorkDir)
143+
def stopProcess = processBuilder.start()
144+
stopProcess.waitFor()
145+
146+
logger.lifecycle("Shared embedded Temporal test server stopped")
147+
}
148+
} catch (Exception e) {
149+
logger.warn("Error stopping test server: ${e.message}")
150+
}
151+
testServerStarted = false
152+
}
153+
154+
// Register a build listener to stop the server after build completes
155+
gradle.buildFinished { result ->
156+
if (testServerStarted) {
157+
logger.lifecycle("Build finished - stopping shared test server...")
158+
stopTestServerIfRunning()
159+
}
160+
}
161+
162+
// Start shared test server - used by both module and integration tests
163+
task startSharedTestServer {
164+
dependsOn ":workflow-native-test:shadowJar"
165+
166+
doLast {
167+
// First, check if an external Temporal server is running (for debugging)
168+
if (isServerRunning("localhost", testServerPort)) {
169+
def externalTarget = "localhost:${testServerPort}"
170+
logger.lifecycle("=" * 60)
171+
logger.lifecycle("External Temporal server detected at ${externalTarget}")
172+
logger.lifecycle("Using external server for debugging (skipping embedded server)")
173+
logger.lifecycle("=" * 60)
174+
175+
usingExternalServer = true
176+
testServerStarted = true
177+
178+
// Write Config.toml files for both test suites
179+
writeTestConfig("${project(':workflow-ballerina').projectDir}/tests/Config.toml", externalTarget, true)
180+
writeTestConfig("${project(':workflow-integration-tests').projectDir}/tests/Config.toml", externalTarget, true)
181+
182+
logger.lifecycle("Test Config.toml files updated with external server URL: ${externalTarget}")
183+
return
184+
}
185+
186+
// No external server found - start embedded test server
187+
logger.lifecycle("No external Temporal server found on port ${testServerPort}")
188+
logger.lifecycle("Starting shared embedded Temporal test server on port ${testServerPort}...")
189+
190+
def shadowJarFile = project(':workflow-native-test').tasks.shadowJar.archiveFile.get().asFile
191+
192+
// Ensure work directory exists
193+
testServerWorkDir.mkdirs()
194+
195+
def command = [
196+
'java', '-jar',
197+
shadowJarFile.absolutePath,
198+
'start',
199+
testServerWorkDir.absolutePath,
200+
testServerPort.toString()
201+
]
202+
203+
def processBuilder = new ProcessBuilder(command)
204+
processBuilder.redirectOutput(new File(testServerWorkDir, 'test-server.log'))
205+
processBuilder.redirectErrorStream(true)
206+
processBuilder.directory(testServerWorkDir)
207+
testServerProcess = processBuilder.start()
208+
209+
// Wait for the server to start and write the target file with content
210+
def targetFile = new File(testServerWorkDir, 'test-server.target')
211+
def maxWait = 30 // seconds
212+
def waited = 0
213+
def target = ""
214+
while (waited < maxWait) {
215+
Thread.sleep(1000)
216+
waited++
217+
if (targetFile.exists()) {
218+
target = targetFile.text.trim()
219+
if (target.length() > 0) {
220+
logger.lifecycle("Target file found with content after ${waited}s")
221+
break
222+
}
223+
}
224+
logger.lifecycle("Waiting for test server to start... (${waited}s)")
225+
}
226+
227+
if (target.length() > 0) {
228+
testServerStarted = true
229+
logger.lifecycle("Shared embedded Temporal test server started at: ${target}")
230+
231+
// Write Config.toml files for both test suites
232+
writeTestConfig("${project(':workflow-ballerina').projectDir}/tests/Config.toml", target, false)
233+
writeTestConfig("${project(':workflow-integration-tests').projectDir}/tests/Config.toml", target, false)
234+
235+
logger.lifecycle("Test Config.toml files updated with server URL: ${target}")
236+
} else {
237+
// Check if process is still running
238+
if (!testServerProcess.isAlive()) {
239+
def exitCode = testServerProcess.exitValue()
240+
logger.error("Test server process exited with code: ${exitCode}")
241+
def logFile = new File(testServerWorkDir, 'test-server.log')
242+
if (logFile.exists()) {
243+
logger.error("Test server log:\n${logFile.text}")
244+
}
245+
}
246+
throw new GradleException("Test server failed to start within ${maxWait} seconds")
247+
}
248+
}
249+
}
250+
251+
// Helper method to write test Config.toml
252+
def writeTestConfig(String path, String serverUrl, boolean isExternal) {
253+
def configFile = new File(path)
254+
configFile.parentFile.mkdirs()
255+
def comment = isExternal ?
256+
"# Using EXTERNAL Temporal server for debugging\n# Start your local Temporal server with: temporal server start-dev" :
257+
"# This file is generated by Gradle before running tests\n# DO NOT EDIT - changes will be overwritten"
258+
259+
configFile.text = """# Auto-generated test configuration
260+
${comment}
261+
262+
[ballerina.workflow.workflowConfig]
263+
provider = "TEMPORAL"
264+
url = "${serverUrl}"
265+
namespace = "default"
266+
267+
[ballerina.workflow.workflowConfig.params]
268+
taskQueue = "BALLERINA_WORKFLOW_TASK_QUEUE"
269+
maxConcurrentWorkflows = 100
270+
maxConcurrentActivities = 100
271+
"""
272+
}
273+
274+
// Stop shared test server
275+
task stopSharedTestServer {
276+
doLast {
277+
stopTestServerIfRunning()
278+
}
279+
}
280+
281+
// ============================================================================
282+
// Coverage Merging Task
283+
// Merges coverage reports from module tests and integration tests
284+
// ============================================================================
285+
286+
task mergeCoverageReports {
287+
description = 'Merges Ballerina coverage reports from module and integration tests'
288+
group = 'verification'
289+
290+
dependsOn ':workflow-ballerina:test'
291+
dependsOn ':workflow-integration-tests:test'
292+
293+
doLast {
294+
def moduleReport = file("${project(':workflow-ballerina').projectDir}/target/report/workflow/coverage-report.xml")
295+
def integrationReport = file("${project(':workflow-integration-tests').projectDir}/target/report/workflow_tests/coverage-report.xml")
296+
def mergedReportDir = file("${project.buildDir}/coverage")
297+
def mergedReport = file("${mergedReportDir}/merged-coverage-report.xml")
298+
299+
mergedReportDir.mkdirs()
300+
301+
logger.lifecycle("Merging coverage reports...")
302+
logger.lifecycle(" Module tests: ${moduleReport.exists() ? 'found' : 'not found'}")
303+
logger.lifecycle(" Integration tests: ${integrationReport.exists() ? 'found' : 'not found'}")
304+
305+
if (moduleReport.exists() && integrationReport.exists()) {
306+
// Simple merge: Copy module report as base, then we can use external tools for proper merge
307+
// For now, copy module report as the primary and log integration report location
308+
mergedReport.text = moduleReport.text
309+
310+
// Copy integration report alongside for reference
311+
def integrationCopy = file("${mergedReportDir}/integration-coverage-report.xml")
312+
integrationCopy.text = integrationReport.text
313+
314+
logger.lifecycle("Coverage reports copied to: ${mergedReportDir}")
315+
logger.lifecycle(" - merged-coverage-report.xml (from module tests)")
316+
logger.lifecycle(" - integration-coverage-report.xml (from integration tests)")
317+
logger.lifecycle("")
318+
logger.lifecycle("To view combined coverage in IDEs, configure both report locations.")
319+
} else {
320+
logger.warn("Could not merge coverage reports - one or more reports missing")
321+
}
322+
}
323+
}
324+
94325
release {
95326
buildTasks = ['build']
96327
failOnSnapshotDependencies = true

0 commit comments

Comments
 (0)