@@ -52,59 +52,121 @@ kotlin.sourceSets.main {
5252 kotlin.srcDirs(" otherSourceDir" )
5353}
5454
55+ fun bundledBinDirectories (): List <File > = listOf (
56+ projectDir.resolve(" ../../../../bin" ),
57+ projectDir.resolve(" bin" ),
58+ rootProject.layout.projectDirectory.asFile.resolve(" bin" ),
59+ )
5560
61+ fun resolveBundledBinaries (): List <File > =
62+ bundledBinDirectories().flatMap { directory -> (directory.listFiles() ? : emptyArray()).toList() }
63+
64+ fun provideEditorExecutable (): File =
65+ resolveBundledBinaries()
66+ .also { println (" [${it.joinToString()} ]" ) }
67+ .firstOrNull { it.name.startsWith(" godot." ) && it.name.contains(" editor" ) && ! it.name.contains(" console" ) }
68+ .also { println (" Godot executable selected: $it " ) }
69+ ? : error(" Could not find editor executable" )
70+
71+ fun currentExportTarget (): String = when {
72+ HostManager .hostIsLinux -> " tests_linux"
73+ HostManager .hostIsMac -> " tests_macos"
74+ HostManager .hostIsMingw -> " tests_windows"
75+ else -> throw IllegalStateException (" Unsupported OS for exporting" )
76+ }
5677
57- tasks {
58- fun currentExportTarget (): String = when {
59- HostManager .hostIsLinux -> " tests_linux"
60- HostManager .hostIsMac -> " tests_macos"
61- HostManager .hostIsMingw -> " tests_windows"
62- else -> throw IllegalStateException (" Unsupported OS for exporting" )
63- }
64-
65- fun resolveBundledBinaries (): List <File > =
66- listOf (
67- projectDir.resolve(" ../../../../bin" ),
68- projectDir.resolve(" bin" ),
69- rootProject.layout.projectDirectory.asFile.resolve(" bin" ),
70- )
71- .flatMap { directory -> (directory.listFiles() ? : emptyArray()).toList() }
78+ fun File.ensureEmptyDirectory () {
79+ deleteRecursively()
80+ mkdirs()
81+ }
7282
73- fun findWindowsBundledBinary (target : String , consoleWrapper : Boolean ): File ? {
74- val expectedPrefix = " godot.windows.template_${target} .x86_64.jvm."
83+ fun findWindowsBundledBinary (target : String , consoleWrapper : Boolean ): File ? {
84+ val expectedPrefix = " godot.windows.template_${target} .x86_64.jvm."
7585
76- return resolveBundledBinaries().firstOrNull { file ->
77- file.name.startsWith(expectedPrefix) && when {
78- consoleWrapper -> file.name.endsWith(" .console.exe" )
79- else -> file.name.endsWith(" .exe" ) && ! file.name.endsWith(" .console.exe" )
80- }
86+ return resolveBundledBinaries().firstOrNull { file ->
87+ file.name.startsWith(expectedPrefix) && when {
88+ consoleWrapper -> file.name.endsWith(" .console.exe" )
89+ else -> file.name.endsWith(" .exe" ) && ! file.name.endsWith(" .console.exe" )
8190 }
8291 }
92+ }
8393
84- fun ensureWindowsExportTemplate (target : String , consoleWrapper : Boolean = false) {
85- if (! HostManager .hostIsMingw) {
86- return
87- }
94+ fun ensureWindowsExportTemplate (target : String , consoleWrapper : Boolean = false) {
95+ if (! HostManager .hostIsMingw) {
96+ return
97+ }
8898
89- val destinationName = buildString {
90- append(" godot.windows.template_${target} .x86_64" )
91- if (consoleWrapper) {
92- append(" .console" )
93- }
94- append(" .exe" )
95- }
96- val destination = projectDir.resolve(destinationName)
97- if (destination.exists()) {
98- return
99+ val destinationName = buildString {
100+ append(" godot.windows.template_${target} .x86_64" )
101+ if (consoleWrapper) {
102+ append(" .console" )
99103 }
104+ append(" .exe" )
105+ }
106+ val destination = projectDir.resolve(destinationName)
107+ if (destination.exists()) {
108+ return
109+ }
100110
101- val source = findWindowsBundledBinary(target, consoleWrapper) ? : throw IllegalStateException (
102- " Could not find Windows ${target} export ${if (consoleWrapper) " console wrapper" else " template" } in the local bin directories."
111+ // Each CI export job only provides the template for the target it builds
112+ // (debug for "dev tests", release for "release tests"). If the other target's
113+ // template isn't present, skip it instead of failing the whole prepare step -
114+ // the export only uses the template matching its target.
115+ val source = findWindowsBundledBinary(target, consoleWrapper)
116+ if (source == null ) {
117+ logger.lifecycle(
118+ " Skipping Windows ${target} export ${if (consoleWrapper) " console wrapper" else " template" } : " +
119+ " not present in the local bin directories." ,
103120 )
121+ return
122+ }
104123
105- source.copyTo(destination, overwrite = true )
124+ source.copyTo(destination, overwrite = true )
125+ }
126+
127+ fun findExportedExecutable (): File ? {
128+ val exportedFiles = projectDir.resolve(" export" ).listFiles()?.toList().orEmpty()
129+ println (" Test executables: [${exportedFiles.joinToString()} ]" )
130+ exportedFiles.forEach { it.setExecutable(true ) }
131+
132+ val exportedExecutable = when {
133+ HostManager .hostIsMingw -> exportedFiles.firstOrNull { it.name.endsWith(" .console.exe" ) }
134+ HostManager .hostIsMac -> exportedFiles.firstOrNull { it.name.endsWith(" .app" ) }
135+ else -> exportedFiles.firstOrNull { it.name.contains(" x86_64" ) }
136+ } ? : exportedFiles.firstOrNull { it.name.endsWith(" .exe" ) }
137+
138+ return if (exportedExecutable?.name?.endsWith(" .app" ) == true ) {
139+ exportedExecutable.resolve(" Contents/MacOS" ).listFiles()?.firstOrNull()
140+ } else {
141+ exportedExecutable
106142 }
143+ }
144+
145+ fun requireExportedExecutable (): File =
146+ findExportedExecutable()
147+ ? : error(" No exported test executable found in ${projectDir.resolve(" export" )} . Run exportDebug or exportRelease first." )
148+
149+ fun registerExportTask (name : String , exportFlag : String , description : String ) = tasks.register<Exec >(name) {
150+ group = " verification"
151+ this .description = description
152+ dependsOn(" importResources" , " prepareHostExportTemplates" )
153+
154+ environment(" JAVA_HOME" , System .getProperty(" java.home" ))
155+ workingDir = projectDir
156+
157+ doFirst {
158+ projectDir.resolve(" export" ).ensureEmptyDirectory()
159+ }
160+
161+ commandLine(
162+ provideEditorExecutable().absolutePath,
163+ " --headless" ,
164+ exportFlag,
165+ currentExportTarget(),
166+ )
167+ }
107168
169+ tasks {
108170 val prepareHostExportTemplates = register(" prepareHostExportTemplates" ) {
109171 group = " verification"
110172 description = " Ensures export presets can find the host export templates in a project-local path."
@@ -143,42 +205,9 @@ tasks {
143205 )
144206 }
145207 }
146- val exportDebug by registering(Exec ::class ) {
147- group = " verification"
148- description = " Exports the tests for the current host OS in debug mode"
149- dependsOn(importResources, prepareHostExportTemplates)
150-
151- environment(" JAVA_HOME" , System .getProperty(" java.home" ))
152- workingDir = projectDir
153-
154- projectDir.resolve(" export" ).deleteRecursively()
155- projectDir.resolve(" export" ).mkdirs()
156-
157- commandLine(
158- provideEditorExecutable().absolutePath,
159- " --headless" ,
160- " --export-debug" ,
161- currentExportTarget(),
162- )
163- }
164- val exportRelease by registering(Exec ::class ) {
165- group = " verification"
166- description = " Exports the tests for the current host OS in release mode"
167- dependsOn(importResources, prepareHostExportTemplates)
168-
169- environment(" JAVA_HOME" , System .getProperty(" java.home" ))
170- workingDir = projectDir
208+ val exportDebug = registerExportTask(" exportDebug" , " --export-debug" , " Exports the tests for the current host OS in debug mode" )
209+ val exportRelease = registerExportTask(" exportRelease" , " --export-release" , " Exports the tests for the current host OS in release mode" )
171210
172- projectDir.resolve(" export" ).deleteRecursively()
173- projectDir.resolve(" export" ).mkdirs()
174-
175- commandLine(
176- provideEditorExecutable().absolutePath,
177- " --headless" ,
178- " --export-release" ,
179- currentExportTarget(),
180- )
181- }
182211 register<Exec >(" runGDTests" ) {
183212 group = " verification"
184213 description = " Runs GDUnit tests from the source Godot project."
@@ -193,8 +222,6 @@ tasks {
193222 scriptArgs = listOf (
194223 " -s" ,
195224 " res://addons/gdUnit4/bin/GdUnitCmdTool.gd" ,
196- " -rd" ,
197- " //reports" ,
198225 " -a" ,
199226 " test" ,
200227 " -c" ,
@@ -207,47 +234,11 @@ tasks {
207234 group = " verification"
208235 description = " Runs GDUnit tests from the exported package."
209236
210- val hasExistingExport = projectDir.resolve(" export" )
211- .listFiles()
212- ?.any { exportedFile ->
213- exportedFile.name.endsWith(" .exe" ) ||
214- exportedFile.name.endsWith(" .app" ) ||
215- exportedFile.name.contains(" x86_64" )
216- } == true
217- if (! hasExistingExport) {
218- dependsOn(exportDebug)
219- }
220-
221237 setupTestExecution {
222- val executable = projectDir
223- .resolve(" export" )
224- .listFiles()
225- ?.also {
226- println (" Test executables: [${it.joinToString()} ]" )
227- it.forEach { file -> file.setExecutable(true ) }
228- }
229- ?.let { exportedFiles ->
230- when {
231- HostManager .hostIsMingw -> exportedFiles.firstOrNull { it.name.endsWith(" .console.exe" ) }
232- HostManager .hostIsMac -> exportedFiles.firstOrNull { it.name.endsWith(" .app" ) }
233- else -> exportedFiles.firstOrNull { it.name.contains(" x86_64" ) }
234- } ? : exportedFiles.firstOrNull { it.name.endsWith(" .exe" ) }
235- }
236- ?.let { exportedExecutable ->
237- if (exportedExecutable.name.endsWith(" .app" )) {
238- exportedExecutable.resolve(" Contents/MacOS" ).listFiles()?.firstOrNull()
239- } else {
240- exportedExecutable
241- }
242- }
243- ?.absolutePath
244-
245238 TestExecutionCommand (
246- executable = executable ? : " no_test_executable_found " ,
239+ executable = requireExportedExecutable().absolutePath ,
247240 useProjectPathOverride = false ,
248- // Exported builds cannot use GdUnitCmdTool (it pulls in editor-only code).
249- // Use our minimal runner instead. See test_runner/README.md.
250- scriptArgs = listOf (" -s" , " res://test_runner/ExportTestMain.gd" ),
241+ scriptArgs = emptyList(),
251242 )
252243 }
253244 }
@@ -292,6 +283,13 @@ fun Exec.setupTestExecution(commandProvider: () -> TestExecutionCommand) {
292283 append(runtimeArgs.joinToString(" " , transform = ::windowsQuote))
293284 },
294285 )
286+ } else if (HostManager .hostIsLinux) {
287+ // Force line-buffered stdout/stderr so CI shows progress in real time.
288+ // Godot block-buffers when piped, so on a timeout-kill the buffered tail
289+ // (where an exported run actually stalls) is otherwise lost.
290+ this @setupTestExecution.commandLine(
291+ " stdbuf" , " -oL" , " -eL" , command.executable, * runtimeArgs.toTypedArray(),
292+ )
295293 } else {
296294 this @setupTestExecution.commandLine(command.executable, * runtimeArgs.toTypedArray())
297295 }
@@ -304,17 +302,3 @@ fun windowsQuote(argument: String): String {
304302 }
305303 return " \" $argument \" "
306304}
307-
308- fun provideEditorExecutable (): File = (
309- listOf (
310- projectDir.resolve(" ../../../../bin" ),
311- projectDir.resolve(" bin" ),
312- rootProject.layout.projectDirectory.asFile.resolve(" bin" ),
313- )
314- .flatMap { (it.listFiles() ? : arrayOf()).toList() }
315- .also {
316- println (" [${it.joinToString()} ]" )
317- }
318- .firstOrNull { it.name.startsWith(" godot." ) && it.name.contains(" editor" ) && ! it.name.contains(" console" ) }
319- .also { println (" Godot executable selected: $it " )}
320- ? : throw Exception (" Could not find editor executable" ))
0 commit comments