diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 986f52e9..765ae590 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -105,7 +105,6 @@ jobs: ENABLE_WASM_SDK_BUILD: "false" ENABLE_EMBEDDED_WASM_SDK_BUILD: "false" ENABLE_ANDROID_SDK_BUILD: "false" - ENABLE_ANDROID_EMULATOR_TESTS: "false" ENABLE_CXX_INTEROP: "false" LINUX_SWIFT_VERSIONS: ${{ inputs.linux_swift_versions }} MACOS_SWIFT_VERSIONS: ${{ inputs.macos_swift_versions }} diff --git a/.github/workflows/package_test.yml b/.github/workflows/package_test.yml index c20db540..3c24c682 100644 --- a/.github/workflows/package_test.yml +++ b/.github/workflows/package_test.yml @@ -39,10 +39,6 @@ on: type: boolean description: "Build with the Swift SDK for Android. Set false to skip it." default: true - enable_android_emulator_tests: - type: boolean - description: "Run the Android emulator tests after the SDK build. Set false to skip it." - default: true enable_cxx_interop: type: boolean description: "Run the Cxx interoperability check. Set false to skip it." @@ -113,10 +109,14 @@ on: type: string description: "Android NDK version list (JSON array)." default: '["r30"]' - android_sdk_triples: + android_sdk_build_triples: + type: string + description: "Android SDK triples (JSON array) that are built only. They share one job. Empty builds none." + default: '["aarch64-unknown-linux-android28"]' + android_emulator_test_triples: type: string - description: "Android SDK triples (JSON array)." - default: '["aarch64-unknown-linux-android28", "x86_64-unknown-linux-android28"]' + description: "Android SDK triples (JSON array) that are built and then run on an emulator, each in its own job. An emulator's architecture must be one the runner can accelerate, which is the runner's own. A triple is in this list or in android_sdk_build_triples, not both. Empty runs no emulator." + default: '["x86_64-unknown-linux-android28"]' # ----- Commands & flags ----- linux_command: @@ -439,7 +439,6 @@ jobs: ENABLE_WASM_SDK_BUILD: ${{ inputs.enable_wasm_sdk_build }} ENABLE_EMBEDDED_WASM_SDK_BUILD: ${{ inputs.enable_embedded_wasm_sdk_build }} ENABLE_ANDROID_SDK_BUILD: ${{ inputs.enable_android_sdk_build }} - ENABLE_ANDROID_EMULATOR_TESTS: ${{ inputs.enable_android_emulator_tests }} ENABLE_CXX_INTEROP: ${{ inputs.enable_cxx_interop }} CXX_INTEROP_SWIFT_VERSIONS: ${{ inputs.cxx_interop_swift_versions }} LINUX_SWIFT_VERSIONS: ${{ inputs.linux_swift_versions }} @@ -460,7 +459,8 @@ jobs: EMBEDDED_WASM_SDK_VERSIONS: ${{ inputs.embedded_wasm_sdk_versions }} ANDROID_SDK_VERSIONS: ${{ inputs.android_sdk_versions }} ANDROID_NDK_VERSIONS: ${{ inputs.android_ndk_versions }} - ANDROID_SDK_TRIPLES: ${{ inputs.android_sdk_triples }} + ANDROID_SDK_BUILD_TRIPLES: ${{ inputs.android_sdk_build_triples }} + ANDROID_EMULATOR_TEST_TRIPLES: ${{ inputs.android_emulator_test_triples }} LINUX_COMMAND: ${{ inputs.linux_command }} LINUX_SETUP_COMMAND: ${{ inputs.linux_setup_command }} MACOS_COMMAND: ${{ inputs.macos_command }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ca5ef8c8..e1d9fccf 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -164,7 +164,6 @@ jobs: enable_wasm_sdk_build: false enable_embedded_wasm_sdk_build: false enable_android_sdk_build: false - enable_android_emulator_tests: false # Two labeled commands, so a run exercises the label suffix. linux_command: | Debug-Build: swift build @@ -206,7 +205,6 @@ jobs: enable_wasm_sdk_build: false enable_embedded_wasm_sdk_build: false enable_android_sdk_build: false - enable_android_emulator_tests: false enable_cxx_interop: false unified_package_test_commands: @@ -223,7 +221,6 @@ jobs: enable_wasm_sdk_build: false enable_embedded_wasm_sdk_build: false enable_android_sdk_build: false - enable_android_emulator_tests: false enable_cxx_interop: false package_path: tests/TestPackage linux_swift_versions: '["6.2", "6.3"]' diff --git a/.github/workflows/scripts/matrix/generate-matrix.swift b/.github/workflows/scripts/matrix/generate-matrix.swift index 640e1aca..a4130c4d 100644 --- a/.github/workflows/scripts/matrix/generate-matrix.swift +++ b/.github/workflows/scripts/matrix/generate-matrix.swift @@ -39,7 +39,6 @@ struct Configuration { @Input("ENABLE_MACOS_SWIFTLY") var swiftlyEnabled = true @Input("ENABLE_WINDOWS") var windowsEnabled = true @Input("ENABLE_FREEBSD") var freeBSDEnabled = true - @Input("ENABLE_ANDROID_EMULATOR_TESTS") var androidEmulatorEnabled = true @Input("ENABLE_CXX_INTEROP") var cxxInteropEnabled = true /// The release `current` names, such as `6.4`. The workflow supplies it, so that a pinned @@ -94,9 +93,11 @@ struct Configuration { @Input("WINDOWS_JOB_TIMEOUT") var windowsTimeoutMinutes = 0 @Input("ANDROID_NDK_VERSIONS") var androidNDKVersions = ["r30"] - @Input("ANDROID_SDK_TRIPLES") var androidTriples = [ - "aarch64-unknown-linux-android28", "x86_64-unknown-linux-android28", - ] + /// The triples that are built (not executed). + @Input("ANDROID_SDK_BUILD_TRIPLES") var androidBuildTriples = ["aarch64-unknown-linux-android28"] + /// The triples that are built and then run on an emulator. The emulator's architecture must be + /// one the runner can accelerate, which is the runner's own. + @Input("ANDROID_EMULATOR_TEST_TRIPLES") var androidEmulatorTestTriples = ["x86_64-unknown-linux-android28"] @Input("CXX_INTEROP_SWIFT_VERSIONS") var cxxInteropVersions: [String] = [] @@ -124,10 +125,10 @@ struct Configuration { [self.linuxOverrides, self.macOSOverrides, self.windowsOverrides] + self.sdkBuilds.map(\.overrides) } - /// The Android SDK build, whose output the emulator tests run. + /// The Android SDK build. var androidSDKBuild: SDKBuild { guard let build = self.sdkBuilds.first(where: { $0.kind == .android }) else { - fatal("no Android SDK build is configured, so nothing produces what the emulator runs.") + fatal("no Android SDK build is configured.") } return build } @@ -139,15 +140,37 @@ extension Configuration { /// Fails on a pair of inputs that cannot both be honored. Each would otherwise produce a /// matrix without the jobs the caller asked for. func validatePairings(in mode: Matrix.Mode) { - // Toolchains mode emits neither the emulator nor the build it runs, so the pairing only - // has to hold where both could appear. - if mode == .jobs && self.androidEmulatorEnabled && !self.androidSDKBuild.enabled { - fatal( - """ - enable_android_emulator_tests needs enable_android_sdk_build; the emulator runs what that build \ - produces. - """ - ) + // Toolchains mode emits no Android entries, so the triples only matter where they could appear. + if mode == .jobs && self.androidSDKBuild.enabled { + if self.androidBuildTriples.isEmpty && self.androidEmulatorTestTriples.isEmpty { + fatal( + """ + enable_android_sdk_build is set, but android_sdk_build_triples and android_emulator_test_triples are \ + both empty, so there is nothing to build. + """ + ) + } + + let triples = self.androidBuildTriples + self.androidEmulatorTestTriples + if triples.contains(where: { $0.allSatisfy(\.isWhitespace) }) { + fatal( + """ + android_sdk_build_triples and android_emulator_test_triples must name each triple, but one is blank; \ + the SDK build is given it as an empty --android-sdk-triple. + """ + ) + } + + let repeated = Set(triples.filter { triple in triples.filter { $0 == triple }.count > 1 }) + if !repeated.isEmpty { + fatal( + """ + \(repeated.sorted().joined(separator: ", ")) appears more than once across \ + android_sdk_build_triples and android_emulator_test_triples; a triple is built once, and either \ + only built or built and tested. + """ + ) + } } // An Apple-platform target rides on a macOS entry, so asking for one without enabling // macOS produces no jobs at all. @@ -483,13 +506,10 @@ extension Generator { guard build.kind == .android else { return jobs } jobs.android = SwiftBuildJobs.AndroidBuild( - triples: self.configuration.androidTriples, - ndkVersions: self.configuration.androidNDKVersions, - emulatorEnabled: self.configuration.androidEmulatorEnabled + buildTriples: self.configuration.androidBuildTriples, + emulatorTestTriples: self.configuration.androidEmulatorTestTriples, + ndkVersions: self.configuration.androidNDKVersions ) - // The emulator runs what the build produced, so the build is told to make test binaries - // and takes none of the flags inputs. - jobs.flags = .always(self.configuration.androidEmulatorEnabled ? "--build-tests" : "") return jobs } @@ -1018,12 +1038,12 @@ struct SwiftBuildJobs: JobGroup { /// The parts of an Android SDK build that the other SDK builds have no counterpart for. struct AndroidBuild { - /// The triples the SDK is installed for. - var triples: [String] + /// The triples that are built and nothing more. + var buildTriples: [String] + /// The triples that are built and run on an emulator, each in an entry of its own. + var emulatorTestTriples: [String] /// The NDK releases the entries fan out over. var ndkVersions: [String] - /// Whether the executor runs what the build produced on an emulator. - var emulatorEnabled: Bool } /// One entry's value from every axis. @@ -1059,34 +1079,75 @@ struct SwiftBuildJobs: JobGroup { } } - var entries: [MatrixEntry] { - self.combinations.map { combination in - let toolchain = Toolchain(label: combination.versionLabel, releases: self.releases) - var sdk = self.sdk - sdk?.ndkVersion = combination.ndkVersion - sdk?.triples = self.android?.triples - return MatrixEntry( - platform: self.platform, - name: self.settings.name( - "\(self.settings.namePrefix) \(combination.versionLabel)", - combination.ndkVersion.map { "NDK \($0)" }, - self.images.count > 1 ? combination.image?.distribution : nil, - self.runners.count > 1 ? combination.runner.name : nil, - for: combination.variant - ), - runner: combination.runner.labels, - swiftBuild: MatrixEntry.SwiftBuild( - toolchain, - sdk: sdk, - container: combination.image?.container(toolchain) - ), - setupCommand: self.setupCommand, - command: self.command(for: combination), - commandArguments: self.flags.arguments(for: combination.versionLabel), - env: self.environment, - androidEmulator: self.android?.emulatorEnabled - ) + /// The triples one entry builds, and whether it runs the emulator on them. + private struct TripleGroup { + var triples: [String] + var runsEmulator: Bool + + /// Names what the entry builds, such as `aarch64-android28+armv7-androideabi28`. The API level + /// is part of the name because two triples of one architecture can build in separate entries. + var nameSuffix: String { + self.triples.map { $0.replacing("-unknown-linux", with: "") }.joined(separator: "+") + } + + /// The emulator runs what the build produced, so an entry that runs it builds the tests. An + /// Android build takes none of the flags inputs. + var buildArguments: [String] { + self.runsEmulator ? ["--build-tests"] : [] + } + } + + /// How the Android triples divide across entries, or none for a build with no Android triples. + /// Each emulator triple gets an entry of its own, and the build-only triples share one that has + /// no emulator to run, so no emulator waits on builds it never runs. + private var tripleGroups: [TripleGroup]? { + guard let android = self.android else { + return nil + } + let emulatorGroups = android.emulatorTestTriples.map { TripleGroup(triples: [$0], runsEmulator: true) } + if android.buildTriples.isEmpty { + return emulatorGroups } + return emulatorGroups + [TripleGroup(triples: android.buildTriples, runsEmulator: false)] + } + + var entries: [MatrixEntry] { + self.combinations.flatMap { combination -> [MatrixEntry] in + guard let groups = self.tripleGroups else { + return [self.entry(for: combination, group: nil, named: false)] + } + // Entries are told apart by what they build only when the triples are split across them. + return groups.map { self.entry(for: combination, group: $0, named: groups.count > 1) } + } + } + + private func entry(for combination: Combination, group: TripleGroup?, named: Bool) -> MatrixEntry { + let toolchain = Toolchain(label: combination.versionLabel, releases: self.releases) + var sdk = self.sdk + sdk?.ndkVersion = combination.ndkVersion + sdk?.triples = group?.triples + return MatrixEntry( + platform: self.platform, + name: self.settings.name( + "\(self.settings.namePrefix) \(combination.versionLabel)", + combination.ndkVersion.map { "NDK \($0)" }, + named ? group?.nameSuffix : nil, + self.images.count > 1 ? combination.image?.distribution : nil, + self.runners.count > 1 ? combination.runner.name : nil, + for: combination.variant + ), + runner: combination.runner.labels, + swiftBuild: MatrixEntry.SwiftBuild( + toolchain, + sdk: sdk, + container: combination.image?.container(toolchain) + ), + setupCommand: self.setupCommand, + command: self.command(for: combination), + commandArguments: group?.buildArguments ?? self.flags.arguments(for: combination.versionLabel), + env: self.environment, + androidEmulator: group?.runsEmulator + ) } /// The command one entry runs. A group whose command is the check itself takes no diff --git a/.github/workflows/toolchain_matrix.yml b/.github/workflows/toolchain_matrix.yml index 242a8c44..788df7cc 100644 --- a/.github/workflows/toolchain_matrix.yml +++ b/.github/workflows/toolchain_matrix.yml @@ -161,7 +161,6 @@ jobs: ENABLE_WASM_SDK_BUILD: "false" ENABLE_EMBEDDED_WASM_SDK_BUILD: "false" ENABLE_ANDROID_SDK_BUILD: "false" - ENABLE_ANDROID_EMULATOR_TESTS: "false" ENABLE_CXX_INTEROP: "false" LINUX_SWIFT_VERSIONS: ${{ inputs.linux_swift_versions }} LINUX_HOST_ARCHS: ${{ inputs.linux_host_archs }} diff --git a/tests/MatrixGeneratorValidator/Sources/MatrixTestSupport/EntryPoint.swift b/tests/MatrixGeneratorValidator/Sources/MatrixTestSupport/EntryPoint.swift index 019b5230..905b5180 100644 --- a/tests/MatrixGeneratorValidator/Sources/MatrixTestSupport/EntryPoint.swift +++ b/tests/MatrixGeneratorValidator/Sources/MatrixTestSupport/EntryPoint.swift @@ -59,7 +59,6 @@ public struct EntryPoint: Sendable, CustomStringConvertible { "ENABLE_WASM_SDK_BUILD", "ENABLE_EMBEDDED_WASM_SDK_BUILD", "ENABLE_ANDROID_SDK_BUILD", - "ENABLE_ANDROID_EMULATOR_TESTS", "ENABLE_CXX_INTEROP", ] diff --git a/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/CommandAxisTests.swift b/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/CommandAxisTests.swift index ac120ab7..b3950f43 100644 --- a/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/CommandAxisTests.swift +++ b/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/CommandAxisTests.swift @@ -31,6 +31,8 @@ private let everyGroup = [ "LINUX_STATIC_SDK_VERSIONS": #"["6.3"]"#, "ANDROID_SDK_VERSIONS": #"["6.3"]"#, "ANDROID_NDK_VERSIONS": #"["r27d"]"#, + // Empty, so the Android build stays a single entry; with an emulator triple it splits in two. + "ANDROID_EMULATOR_TEST_TRIPLES": "[]", ] /// The names those groups produce when each runs one command. @@ -454,6 +456,7 @@ struct VersionOverrideCommandTests { // command instead ran what the caller replaced, and still passed. var environment = [ "ANDROID_NDK_VERSIONS": #"["r27d"]"#, + "ANDROID_EMULATOR_TEST_TRIPLES": "[]", "LINUX_SWIFT_VERSIONS": #"["6.3"]"#, ] environment[enableKey] = "true" diff --git a/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/EntryPointTests.swift b/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/EntryPointTests.swift index f01a61e7..5cbae21e 100644 --- a/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/EntryPointTests.swift +++ b/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/EntryPointTests.swift @@ -54,9 +54,12 @@ struct EntryPointDefaultTests { "Embedded Wasm SDK Swift current", "Embedded Wasm SDK Swift nightly-release", "Embedded Wasm SDK Swift nightly-main", - "Android SDK Swift current NDK r30", - "Android SDK Swift nightly-release NDK r30", - "Android SDK Swift nightly-main NDK r30", + "Android SDK Swift current NDK r30 x86_64-android28", + "Android SDK Swift current NDK r30 aarch64-android28", + "Android SDK Swift nightly-release NDK r30 x86_64-android28", + "Android SDK Swift nightly-release NDK r30 aarch64-android28", + "Android SDK Swift nightly-main NDK r30 x86_64-android28", + "Android SDK Swift nightly-main NDK r30 aarch64-android28", "Cxx interop Swift current-2", "Cxx interop Swift current-1", "Cxx interop Swift current", @@ -119,9 +122,12 @@ struct EntryPointDefaultTests { viaWorkflow["ENABLE_ANDROID_SDK_BUILD"] = "true" let expected = [ - "Android SDK Swift current NDK r30", - "Android SDK Swift nightly-release NDK r30", - "Android SDK Swift nightly-main NDK r30", + "Android SDK Swift current NDK r30 x86_64-android28", + "Android SDK Swift current NDK r30 aarch64-android28", + "Android SDK Swift nightly-release NDK r30 x86_64-android28", + "Android SDK Swift nightly-release NDK r30 aarch64-android28", + "Android SDK Swift nightly-main NDK r30 x86_64-android28", + "Android SDK Swift nightly-main NDK r30 aarch64-android28", ] let workflow = try Generator.run(viaWorkflow, manifests: manifest) diff --git a/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/JobGroupTests.swift b/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/JobGroupTests.swift index b25f478c..49df5d17 100644 --- a/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/JobGroupTests.swift +++ b/tests/MatrixGeneratorValidator/Tests/MatrixGeneratorTests/JobGroupTests.swift @@ -54,39 +54,167 @@ struct SDKTests { #expect(generated.entries.first?.setupCommand == "cd sub") } - @Test("Android entries carry an NDK version each and the triples") - func androidNDKAndTriples() throws { - let generated = try Generator.run([ + /// The environment for an Android SDK build of one Swift version and one NDK release, which + /// each test adds its triples to. + private func androidEnvironment(_ triples: [String: String] = [:]) -> [String: String] { + [ "ENABLE_ANDROID_SDK_BUILD": "true", "ANDROID_SDK_VERSIONS": #"["6.3"]"#, - "ANDROID_NDK_VERSIONS": #"["r27d","r28c"]"#, - "ANDROID_SDK_TRIPLES": #"["aarch64-unknown-linux-android28"]"#, - ]) + "ANDROID_NDK_VERSIONS": #"["r27d"]"#, + ].merging(triples) { _, new in new } + } + + /// Leaves only the build-only list, so the build stays a single entry. + private let noEmulatorTriples = ["ANDROID_EMULATOR_TEST_TRIPLES": "[]"] + + @Test("Android entries carry an NDK version each and the triples") + func androidNDKAndTriples() throws { + let generated = try Generator.run( + androidEnvironment([ + "ANDROID_NDK_VERSIONS": #"["r27d","r28c"]"#, + "ANDROID_SDK_BUILD_TRIPLES": #"["aarch64-unknown-linux-android28"]"#, + ]).merging(noEmulatorTriples) { _, new in new } + ) #expect(generated.count == 2) #expect(generated.entries.compactMap { $0.swiftBuild?.sdk?.ndkVersion } == ["r27d", "r28c"]) #expect(generated.entries.first?.swiftBuild?.sdk?.triples == ["aarch64-unknown-linux-android28"]) } - @Test("Emulator checks ask the build for test binaries") - func emulatorRequestsTestBinaries() throws { - let withoutEmulator = try Generator.run([ - "ENABLE_ANDROID_SDK_BUILD": "true", - "ANDROID_SDK_VERSIONS": #"["6.3"]"#, - "ANDROID_NDK_VERSIONS": #"["r27d"]"#, - ]) - #expect(withoutEmulator.entries.first?.androidEmulator == false) - #expect(withoutEmulator.entries.first?.commandArguments == []) + @Test("By default the emulator triple builds and runs the emulator, and the build-only triple builds beside it") + func defaultTriples() throws { + let generated = try Generator.run(androidEnvironment()) + #expect( + generated.names == [ + "Android SDK Swift 6.3 NDK r27d x86_64-android28", + "Android SDK Swift 6.3 NDK r27d aarch64-android28", + ] + ) + #expect( + generated.entries.map { $0.swiftBuild?.sdk?.triples } == [ + ["x86_64-unknown-linux-android28"], + ["aarch64-unknown-linux-android28"], + ] + ) + #expect(generated.entries.map(\.androidEmulator) == [true, false]) + // The emulator script stages the test binaries the build produced, so only that entry builds them. + #expect(generated.entries.map(\.commandArguments) == [["--build-tests"], []]) + } - // The emulator script stages what the build produced, so without this there is - // nothing to run. - let withEmulator = try Generator.run([ - "ENABLE_ANDROID_SDK_BUILD": "true", - "ENABLE_ANDROID_EMULATOR_TESTS": "true", - "ANDROID_SDK_VERSIONS": #"["6.3"]"#, - "ANDROID_NDK_VERSIONS": #"["r27d"]"#, + @Test("Each emulator triple gets its own entry and the build-only triples share one") + func triplesDivideAcrossEntries() throws { + let generated = try Generator.run( + androidEnvironment([ + "ANDROID_SDK_BUILD_TRIPLES": #"["aarch64-unknown-linux-android28","armv7-unknown-linux-androideabi28"]"#, + "ANDROID_EMULATOR_TEST_TRIPLES": #"["x86_64-unknown-linux-android28","i686-unknown-linux-android28"]"#, + ]) + ) + #expect( + generated.names == [ + "Android SDK Swift 6.3 NDK r27d x86_64-android28", + "Android SDK Swift 6.3 NDK r27d i686-android28", + "Android SDK Swift 6.3 NDK r27d aarch64-android28+armv7-androideabi28", + ] + ) + #expect( + generated.entries.map { $0.swiftBuild?.sdk?.triples } == [ + ["x86_64-unknown-linux-android28"], + ["i686-unknown-linux-android28"], + ["aarch64-unknown-linux-android28", "armv7-unknown-linux-androideabi28"], + ] + ) + #expect(generated.entries.map(\.androidEmulator) == [true, true, false]) + #expect(generated.entries.map(\.commandArguments) == [["--build-tests"], ["--build-tests"], []]) + } + + @Test("Emulator triples of one architecture at different API levels get distinct names") + func sameArchitectureDifferentAPILevels() throws { + let generated = try Generator.run( + androidEnvironment([ + "ANDROID_SDK_BUILD_TRIPLES": "[]", + "ANDROID_EMULATOR_TEST_TRIPLES": #"["x86_64-unknown-linux-android28","x86_64-unknown-linux-android35"]"#, + ]) + ) + #expect( + generated.names == [ + "Android SDK Swift 6.3 NDK r27d x86_64-android28", + "Android SDK Swift 6.3 NDK r27d x86_64-android35", + ] + ) + } + + @Test("When only the emulator list is non-empty, the build stays a single unnamed entry") + func emulatorListAloneStaysOneEntry() throws { + let generated = try Generator.run(androidEnvironment(["ANDROID_SDK_BUILD_TRIPLES": "[]"])) + #expect(generated.names == ["Android SDK Swift 6.3 NDK r27d"]) + #expect(generated.entries.map(\.androidEmulator) == [true]) + #expect(generated.entries.map(\.commandArguments) == [["--build-tests"]]) + } + + @Test("When only the build list is non-empty, the build stays a single unnamed entry with no test binaries") + func buildListAloneStaysOneEntry() throws { + let generated = try Generator.run(androidEnvironment(noEmulatorTriples)) + #expect(generated.names == ["Android SDK Swift 6.3 NDK r27d"]) + #expect(generated.entries.map(\.androidEmulator) == [false]) + #expect(generated.entries.map(\.commandArguments) == [[]]) + #expect(generated.entries.map { $0.swiftBuild?.sdk?.triples } == [["aarch64-unknown-linux-android28"]]) + } + + @Test("A triple in both lists fails rather than being built twice") + func tripleInBothListsFails() throws { + let result = try Generator.runRaw( + androidEnvironment([ + "ANDROID_SDK_BUILD_TRIPLES": #"["x86_64-unknown-linux-android28"]"#, + "ANDROID_EMULATOR_TEST_TRIPLES": #"["x86_64-unknown-linux-android28"]"#, + ]) + ) + #expect(result.exitCode != 0) + #expect(result.standardError.contains("x86_64-unknown-linux-android28")) + #expect(result.standardError.contains("android_sdk_build_triples")) + #expect(result.standardError.contains("android_emulator_test_triples")) + } + + @Test("A triple repeated within a list fails rather than producing two entries with one name") + func repeatedTripleFails() throws { + let result = try Generator.runRaw( + androidEnvironment([ + "ANDROID_SDK_BUILD_TRIPLES": "[]", + "ANDROID_EMULATOR_TEST_TRIPLES": #"["x86_64-unknown-linux-android28","x86_64-unknown-linux-android28"]"#, + ]) + ) + #expect(result.exitCode != 0) + #expect(result.standardError.contains("appears more than once")) + } + + @Test("A blank triple fails rather than reaching the SDK build as an empty argument") + func blankTripleFails() throws { + let result = try Generator.runRaw(androidEnvironment(["ANDROID_SDK_BUILD_TRIPLES": #"[""]"#])) + #expect(result.exitCode != 0) + #expect(result.standardError.contains("blank")) + } + + @Test("Both lists empty fails rather than producing no Android jobs") + func bothListsEmptyFails() throws { + let result = try Generator.runRaw( + androidEnvironment([ + "ANDROID_SDK_BUILD_TRIPLES": "[]", + "ANDROID_EMULATOR_TEST_TRIPLES": "[]", + ]) + ) + #expect(result.exitCode != 0) + #expect(result.standardError.contains("nothing to build")) + } + + @Test("The triples are not checked when there is no Android build") + func triplesIgnoredWithoutAndroidBuild() throws { + // Another group is enabled so that a run which emitted nothing at all would not pass. + let generated = try Generator.run([ + "ENABLE_LINUX": "true", + "LINUX_SWIFT_VERSIONS": #"["6.3"]"#, + "ANDROID_SDK_BUILD_TRIPLES": "[]", + "ANDROID_EMULATOR_TEST_TRIPLES": "[]", ]) - #expect(withEmulator.entries.first?.androidEmulator == true) - #expect(withEmulator.entries.first?.commandArguments == ["--build-tests"]) + #expect(generated.exitCode == 0) + #expect(generated.names == ["Linux Swift 6.3"]) } } @@ -496,25 +624,6 @@ struct InvariantTests { } } - @Test("Asking for the Android emulator without the SDK build fails") - func emulatorWithoutSDKBuildFails() throws { - // The emulator runs what the SDK build produced, so on its own it yields no - // jobs at all - a green run that tested nothing. - let generated = try Generator.run(["ENABLE_ANDROID_EMULATOR_TESTS": "true"]) - #expect(generated.exitCode != 0) - #expect(generated.standardError.contains("enable_android_sdk_build")) - - // Toolchain mode suppresses both, so it must not fail there. - let toolchains = try Generator.run([ - "MATRIX_MODE": "toolchains", - "ENABLE_ANDROID_EMULATOR_TESTS": "true", - "ENABLE_LINUX": "true", - "LINUX_SWIFT_VERSIONS": #"["6.3"]"#, - ]) - #expect(toolchains.exitCode == 0) - #expect(toolchains.names == ["Linux Swift 6.3"]) - } - @Test("An override key naming no version fails rather than losing its arguments") func unmatchedOverrideKeyFails() throws { // These carry warnings-as-errors for NIO-family repositories. A key left