@@ -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+
94325release {
95326 buildTasks = [' build' ]
96327 failOnSnapshotDependencies = true
0 commit comments