diff --git a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/FastlaneSpec.groovy b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/FastlaneSpec.groovy new file mode 100644 index 00000000..53ffe8d0 --- /dev/null +++ b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/FastlaneSpec.groovy @@ -0,0 +1,31 @@ +package wooga.gradle.build.unity.ios.tasks + +import wooga.gradle.build.IntegrationSpec + +class FastlaneSpec extends IntegrationSpec { + File fastlaneMock + File fastlaneMockPath + + def setupFastlaneMock() { + fastlaneMockPath = File.createTempDir("fastlane", "mock") + + def path = System.getenv("PATH") + environmentVariables.clear("PATH") + String newPath = "${fastlaneMockPath}${File.pathSeparator}${path}" + environmentVariables.set("PATH", newPath) + assert System.getenv("PATH") == newPath + + + fastlaneMock = createFile("fastlane", fastlaneMockPath) + fastlaneMock.executable = true + fastlaneMock << """ + #!/usr/bin/env bash + echo \$@ + env + """.stripIndent() + } + + def setup() { + setupFastlaneMock() + } +} diff --git a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfileSpec.groovy b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfileSpec.groovy index b94b2660..95c6a972 100644 --- a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfileSpec.groovy +++ b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/ImportProvisioningProfileSpec.groovy @@ -30,29 +30,7 @@ import wooga.gradle.build.IntegrationSpec * account with necessary credentials. We only test the invocation of fastlane and its parameters. */ @Requires({ os.macOs }) -class ImportProvisioningProfileSpec extends IntegrationSpec { - - File fastlaneMock - File fastlaneMockPath - - def setupFastlaneMock() { - fastlaneMockPath = File.createTempDir("fastlane", "mock") - - def path = System.getenv("PATH") - environmentVariables.clear("PATH") - String newPath = "${fastlaneMockPath}${File.pathSeparator}${path}" - environmentVariables.set("PATH", newPath) - assert System.getenv("PATH") == newPath - - - fastlaneMock = createFile("fastlane", fastlaneMockPath) - fastlaneMock.executable = true - fastlaneMock << """ - #!/usr/bin/env bash - echo \$@ - env - """.stripIndent() - } +class ImportProvisioningProfileSpec extends FastlaneSpec { def setup() { buildFile << """ @@ -63,8 +41,6 @@ class ImportProvisioningProfileSpec extends IntegrationSpec { destinationDir = file("build") } """.stripIndent() - - setupFastlaneMock() } @Issue("https://github.com/wooga/atlas-build-unity/issues/38") diff --git a/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/PublishTestFlightSpec.groovy b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/PublishTestFlightSpec.groovy new file mode 100644 index 00000000..a9d4b033 --- /dev/null +++ b/src/integrationTest/groovy/wooga/gradle/build/unity/ios/tasks/PublishTestFlightSpec.groovy @@ -0,0 +1,168 @@ +package wooga.gradle.build.unity.ios.tasks + +import spock.lang.Requires +import spock.lang.Unroll + +@Requires({ os.macOs }) +class PublishTestFlightSpec extends FastlaneSpec { + + def setup() { + def ipaFile = File.createTempFile("mockIpa", ".ipa") + + buildFile << """ + task publishTestFlight(type: wooga.gradle.build.unity.ios.tasks.PublishTestFlight) { + appIdentifier = "com.test.testapp" + teamId = "fakeTeamId" + ipa = file("${ipaFile.path}") + } + """.stripIndent() + } + + @Unroll + def "task :#taskToRun executes fastlane #fastlaneCommand #fastlaneSubCommand"() { + given: "ipa path" + def ipaFile = File.createTempFile("mockIpa", ".ipa") + + and: "a configured task" + buildFile << """ + ${taskToRun} { + ipa = file("${ipaFile.path}") + } + """.stripIndent() + + when: + def result = runTasksSuccessfully(taskToRun) + + then: + result.standardOutput.readLines().any { it.matches("${fastlaneCommand} ${fastlaneSubCommand}.*? --ipa ${ipaFile.path}") } + + where: + taskToRun = "publishTestFlight" + fastlaneCommand = "pilot" + fastlaneSubCommand = "upload" + } + + @Unroll + def "task :#taskToRun accepts input #parameter with #method and type #type"() { + given: "task with configured properties" + buildFile << """ + ${taskToRun} { + ${method}(${value}) + } + """.stripIndent() + + and: + if (parameter == "ipa") { + createFile(rawValue.toString(), projectDir) + } + + when: + def result = runTasksSuccessfully(taskToRun) + + then: + result.standardOutput.contains(expectedCommandlineSwitch.replace("#{value_path}", new File(projectDir, rawValue.toString()).path)) + + where: + parameter | rawValue | type | useSetter | expectedCommandlineSwitchRaw + "appIdentifier" | "com.test.app2" | 'String' | true | "--app_identifier #{value}" + "appIdentifier" | "com.test.app3" | 'String' | false | "--app_identifier #{value}" + "appIdentifier" | "com.test.app4" | 'Closure' | true | "--app_identifier #{value}" + "appIdentifier" | "com.test.app5" | 'Closure' | false | "--app_identifier #{value}" + "appIdentifier" | "com.test.app6" | 'Callable' | true | "--app_identifier #{value}" + "appIdentifier" | "com.test.app7" | 'Callable' | false | "--app_identifier #{value}" + "appIdentifier" | "com.test.app8" | 'Object' | true | "--app_identifier #{value}" + "appIdentifier" | "com.test.app9" | 'Object' | false | "--app_identifier #{value}" + + "teamId" | "1234561" | 'String' | true | "--team_id #{value}" + "teamId" | "1234562" | 'String' | false | "--team_id #{value}" + "teamId" | "1234563" | 'Closure' | true | "--team_id #{value}" + "teamId" | "1234564" | 'Closure' | false | "--team_id #{value}" + "teamId" | "1234565" | 'Callable' | true | "--team_id #{value}" + "teamId" | "1234566" | 'Callable' | false | "--team_id #{value}" + "teamId" | "1234567" | 'Object' | true | "--team_id #{value}" + "teamId" | "1234568" | 'Object' | false | "--team_id #{value}" + + "teamName" | "testName" | 'String' | true | "--team_name #{value}" + "teamName" | "testName" | 'String' | false | "--team_name #{value}" + "teamName" | "testName" | 'Closure' | true | "--team_name #{value}" + "teamName" | "testName" | 'Closure' | false | "--team_name #{value}" + "teamName" | "testName" | 'Callable' | true | "--team_name #{value}" + "teamName" | "testName" | 'Callable' | false | "--team_name #{value}" + "teamName" | "testName" | 'Object' | true | "--team_name #{value}" + "teamName" | "testName" | 'Object' | false | "--team_name #{value}" + + "itcProvider" | "testItcProvider" | 'String' | true | "--itc_provider #{value}" + "itcProvider" | "testItcProvider" | 'String' | false | "--itc_provider #{value}" + "itcProvider" | "testItcProvider" | 'Closure' | true | "--itc_provider #{value}" + "itcProvider" | "testItcProvider" | 'Closure' | false | "--itc_provider #{value}" + "itcProvider" | "testItcProvider" | 'Callable' | true | "--itc_provider #{value}" + "itcProvider" | "testItcProvider" | 'Callable' | false | "--itc_provider #{value}" + "itcProvider" | "testItcProvider" | 'Object' | true | "--itc_provider #{value}" + "itcProvider" | "testItcProvider" | 'Object' | false | "--itc_provider #{value}" + + "devPortalTeamId" | "1234561" | 'String' | true | "--dev_portal_team_id #{value}" + "devPortalTeamId" | "1234562" | 'String' | false | "--dev_portal_team_id #{value}" + "devPortalTeamId" | "1234563" | 'Closure' | true | "--dev_portal_team_id #{value}" + "devPortalTeamId" | "1234564" | 'Closure' | false | "--dev_portal_team_id #{value}" + "devPortalTeamId" | "1234565" | 'Callable' | true | "--dev_portal_team_id #{value}" + "devPortalTeamId" | "1234566" | 'Callable' | false | "--dev_portal_team_id #{value}" + "devPortalTeamId" | "1234567" | 'Object' | true | "--dev_portal_team_id #{value}" + "devPortalTeamId" | "1234568" | 'Object' | false | "--dev_portal_team_id #{value}" + + "username" | "tester1" | 'String' | true | "--username #{value}" + "username" | "tester2" | 'String' | false | "--username #{value}" + "username" | "tester3" | 'Closure' | true | "--username #{value}" + "username" | "tester4" | 'Closure' | false | "--username #{value}" + "username" | "tester5" | 'Callable' | true | "--username #{value}" + "username" | "tester6" | 'Callable' | false | "--username #{value}" + "username" | "tester7" | 'Object' | true | "--username #{value}" + "username" | "tester8" | 'Object' | false | "--username #{value}" + + "password" | "pass1" | 'String' | true | "FASTLANE_PASSWORD=#{value}" + "password" | "pass2" | 'String' | false | "FASTLANE_PASSWORD=#{value}" + "password" | "pass3" | 'Closure' | true | "FASTLANE_PASSWORD=#{value}" + "password" | "pass4" | 'Closure' | false | "FASTLANE_PASSWORD=#{value}" + "password" | "pass5" | 'Callable' | true | "FASTLANE_PASSWORD=#{value}" + "password" | "pass6" | 'Callable' | false | "FASTLANE_PASSWORD=#{value}" + "password" | "pass7" | 'Object' | true | "FASTLANE_PASSWORD=#{value}" + "password" | "pass8" | 'Object' | false | "FASTLANE_PASSWORD=#{value}" + + "skipSubmission" | true | 'Boolean' | true | "--skip_submission true" + "skipSubmission" | true | 'Boolean' | false | "--skip_submission true" + "skipSubmission" | true | 'Closure' | true | "--skip_submission true" + "skipSubmission" | true | 'Closure' | false | "--skip_submission true" + "skipSubmission" | true | 'Callable' | true | "--skip_submission true" + "skipSubmission" | true | 'Callable' | false | "--skip_submission true" + "skipSubmission" | false | 'Boolean' | true | "--skip_submission false" + "skipSubmission" | false | 'Boolean' | false | "--skip_submission false" + "skipSubmission" | false | 'Closure' | true | "--skip_submission false" + "skipSubmission" | false | 'Closure' | false | "--skip_submission false" + "skipSubmission" | false | 'Callable' | true | "--skip_submission false" + "skipSubmission" | false | 'Callable' | false | "--skip_submission false" + + "skipWaitingForBuildProcessing" | true | 'Boolean' | true | "--skip_waiting_for_build_processing true" + "skipWaitingForBuildProcessing" | true | 'Boolean' | false | "--skip_waiting_for_build_processing true" + "skipWaitingForBuildProcessing" | true | 'Closure' | true | "--skip_waiting_for_build_processing true" + "skipWaitingForBuildProcessing" | true | 'Closure' | false | "--skip_waiting_for_build_processing true" + "skipWaitingForBuildProcessing" | true | 'Callable' | true | "--skip_waiting_for_build_processing true" + "skipWaitingForBuildProcessing" | true | 'Callable' | false | "--skip_waiting_for_build_processing true" + "skipWaitingForBuildProcessing" | false | 'Boolean' | true | "--skip_waiting_for_build_processing false" + "skipWaitingForBuildProcessing" | false | 'Boolean' | false | "--skip_waiting_for_build_processing false" + "skipWaitingForBuildProcessing" | false | 'Closure' | true | "--skip_waiting_for_build_processing false" + "skipWaitingForBuildProcessing" | false | 'Closure' | false | "--skip_waiting_for_build_processing false" + "skipWaitingForBuildProcessing" | false | 'Callable' | true | "--skip_waiting_for_build_processing false" + "skipWaitingForBuildProcessing" | false | 'Callable' | false | "--skip_waiting_for_build_processing false" + + "ipa" | "build/out1/test.ipa" | 'String' | true | "--ipa #{value_path}" + "ipa" | "build/out2/test.ipa" | 'String' | false | "--ipa #{value_path}" + "ipa" | "build/out3/test.ipa" | 'File' | true | "--ipa #{value_path}" + "ipa" | "build/out4/test.ipa" | 'File' | false | "--ipa #{value_path}" + "ipa" | "build/out5/test.ipa" | 'Closure' | true | "--ipa #{value_path}" + "ipa" | "build/out6/test.ipa" | 'Closure' | false | "--ipa #{value_path}" + + taskToRun = "publishTestFlight" + value = wrapValueBasedOnType(rawValue, type) + method = (useSetter) ? "set${parameter.capitalize()}" : parameter + expectedCommandlineSwitch = expectedCommandlineSwitchRaw.replace("#{value}", rawValue.toString()) + } +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy index eaa1ac1f..9b79ecc5 100644 --- a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPlugin.groovy @@ -23,6 +23,7 @@ import org.gradle.api.Project import org.gradle.api.logging.Logger import org.gradle.api.logging.Logging import org.gradle.api.plugins.BasePlugin +import org.gradle.api.publish.plugins.PublishingPlugin import org.gradle.api.tasks.Sync import org.gradle.util.GUtil import wooga.gradle.build.unity.ios.internal.DefaultIOSBuildPluginExtension @@ -32,6 +33,7 @@ import wooga.gradle.build.unity.ios.tasks.ImportProvisioningProfile import wooga.gradle.build.unity.ios.tasks.KeychainTask import wooga.gradle.build.unity.ios.tasks.ListKeychainTask import wooga.gradle.build.unity.ios.tasks.LockKeychainTask +import wooga.gradle.build.unity.ios.tasks.PublishTestFlight import wooga.gradle.build.unity.ios.tasks.XCodeArchiveTask import wooga.gradle.build.unity.ios.tasks.XCodeExportTask @@ -50,6 +52,7 @@ class IOSBuildPlugin implements Plugin { } project.pluginManager.apply(BasePlugin.class) + project.pluginManager.apply(PublishingPlugin.class) def extension = project.getExtensions().create(IOSBuildPluginExtension, EXTENSION_NAME, DefaultIOSBuildPluginExtension.class) //register some defaults @@ -125,6 +128,18 @@ class IOSBuildPlugin implements Plugin { } }) + project.tasks.withType(PublishTestFlight.class, new Action() { + @Override + void execute(PublishTestFlight task) { + def conventionMapping = task.getConventionMapping() + conventionMapping.map("username", { extension.fastlaneCredentials.username }) + conventionMapping.map("password", { extension.fastlaneCredentials.password }) + conventionMapping.map("devPortalTeamId", { extension.getTeamId() }) + conventionMapping.map("appIdentifier", { extension.getAppIdentifier() }) + conventionMapping.map("ipa", { extension.getAppIdentifier() }) + } + }) + def projects = project.fileTree(project.projectDir) { it.include("*.xcodeproj/project.pbxproj") }.files projects.each { File xcodeProject -> def base = xcodeProject.parentFile @@ -132,7 +147,7 @@ class IOSBuildPlugin implements Plugin { if (projects.size() == 1) { taskNameBase = "" } - generateBuildTasks(taskNameBase, project, base) + generateBuildTasks(taskNameBase, project, base, extension) } } @@ -147,7 +162,7 @@ class IOSBuildPlugin implements Plugin { return "" } - void generateBuildTasks(final String baseName, final Project project, File xcodeProject) { + void generateBuildTasks(final String baseName, final Project project, File xcodeProject, IOSBuildPluginExtension extension) { def tasks = project.tasks def buildKeychain = tasks.create(maybeBaseName(baseName, "buildKeychain"), KeychainTask) { it.baseName = maybeBaseName(baseName, "build") @@ -196,6 +211,22 @@ class IOSBuildPlugin implements Plugin { it.xcarchivePath xcodeArchive } + def publishTestFlight = tasks.create(maybeBaseName(baseName, "publishTestFlight"), PublishTestFlight) { + it.ipa xcodeExport + it.group = PublishingPlugin.PUBLISH_TASK_GROUP + it.description = "Upload binary to TestFlightApp" + } + + project.afterEvaluate(new Action() { + @Override + void execute(Project _) { + if(extension.publishToTestFlight) { + def lifecyclePublishTask = tasks.getByName(PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME) + lifecyclePublishTask.dependsOn(publishTestFlight) + } + } + }) + removeKeychain.mustRunAfter([xcodeArchive, xcodeExport]) lockKeychain.mustRunAfter([xcodeArchive, xcodeExport]) diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy index b857a767..bac57e83 100644 --- a/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginExtension.groovy @@ -62,4 +62,8 @@ interface IOSBuildPluginExtension { void setAdhoc(Boolean value) IOSBuildPluginExtension adhoc(Boolean value) -} \ No newline at end of file + Boolean getPublishToTestFlight() + void setPublishToTestFlight(Boolean value) + IOSBuildPluginExtension publishToTestFlight(Boolean value) + +} diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy index 2b45cba2..5f3c8ee6 100644 --- a/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy +++ b/src/main/groovy/wooga/gradle/build/unity/ios/internal/DefaultIOSBuildPluginExtension.groovy @@ -33,6 +33,7 @@ class DefaultIOSBuildPluginExtension implements IOSBuildPluginExtension { private String configuration private String provisioningName private Boolean adhoc = false + private Boolean publishToTestFlight = false @Override org.gradle.api.credentials.PasswordCredentials getFastlaneCredentials() { @@ -192,6 +193,22 @@ class DefaultIOSBuildPluginExtension implements IOSBuildPluginExtension { return this } + @Override + Boolean getPublishToTestFlight() { + return publishToTestFlight + } + + @Override + void setPublishToTestFlight(Boolean value) { + publishToTestFlight = value + } + + @Override + IOSBuildPluginExtension publishToTestFlight(Boolean value) { + setPublishToTestFlight(value) + return this + } + DefaultIOSBuildPluginExtension() { fastlaneCredentials = new DefaultPasswordCredentials() } diff --git a/src/main/groovy/wooga/gradle/build/unity/ios/tasks/PublishTestFlight.groovy b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/PublishTestFlight.groovy new file mode 100644 index 00000000..73702f41 --- /dev/null +++ b/src/main/groovy/wooga/gradle/build/unity/ios/tasks/PublishTestFlight.groovy @@ -0,0 +1,293 @@ +package wooga.gradle.build.unity.ios.tasks + +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.ConventionTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.SkipWhenEmpty +import org.gradle.api.tasks.TaskAction + +import java.util.concurrent.Callable + +class PublishTestFlight extends ConventionTask { + private Object ipa + + @SkipWhenEmpty + @InputFiles + protected FileCollection getInputFiles() { + project.files(ipa) + } + + @InputFile + /** + * -i, --ipa STRING Path to the ipa file to upload (PILOT_IPA) + */ + File getIpa() { + project.files(ipa).singleFile + } + + void setIpa(Object value) { + ipa = value + } + + PublishTestFlight ipa(Object ipa) { + setIpa(ipa) + this + } + + private Object appIdentifier + + @Optional + @Input + String getAppIdentifier() { + convertToString(appIdentifier) + } + + void setAppIdentifier(Object value) { + appIdentifier = value + } + + PublishTestFlight appIdentifier(Object appIdentifier) { + setAppIdentifier(appIdentifier) + this + } + + private Object username + + @Optional + @Input + String getUsername() { + convertToString(username) + } + + void setUsername(Object value) { + username = value + } + + PublishTestFlight username(Object username) { + setUsername(username) + this + } + + private Object password + + @Optional + @Input + String getPassword() { + convertToString(password) + } + + void setPassword(Object value) { + password = value + } + + PublishTestFlight password(Object password) { + setPassword(password) + this + } + + private Object devPortalTeamId + + @Optional + @Input + String getDevPortalTeamId() { + convertToString(devPortalTeamId) + } + + void setDevPortalTeamId(Object value) { + devPortalTeamId = value + } + + PublishTestFlight devPortalTeamId(Object value) { + setDevPortalTeamId(value) + this + } + + private Object itcProvider + + @Optional + @Input + String getItcProvider() { + convertToString(itcProvider) + } + + void setItcProvider(Object value) { + itcProvider = value + } + + PublishTestFlight itcProvider(Object value) { + setItcProvider(value) + this + } + + private Object teamId + + @Optional + @Input + String getTeamId() { + convertToString(teamId) + } + + void setTeamId(Object value) { + teamId = value + } + + PublishTestFlight teamId(Object value) { + setTeamId(value) + this + } + + private Object teamName + + @Optional + @Input + String getTeamName() { + convertToString(teamName) + } + + void setTeamName(Object value) { + teamName = value + } + + PublishTestFlight teamName(Object value) { + setTeamName(value) + this + } + + private Object skipSubmission + + @Optional + @Input + /** + * Skip the distributing action of pilot and only upload the ipa file (PILOT_SKIP_SUBMISSION) + */ + Boolean getSkipSubmission() { + convertToBoolean(skipSubmission) + } + + void setSkipSubmission(Object value) { + skipSubmission = value + } + + PublishTestFlight skipSubmission(Object value) { + setSkipSubmission(value) + this + } + + private Object skipWaitingForBuildProcessing + + @Optional + @Input + /** + * Don't wait for the build to process. + * + * -z, --skip_waiting_for_build_processing [VALUE] If set to true, the changelog won't be set, `distribute_external` + * option won't work and no build will be distributed to testers. + * (You might want to use this option if you are using this action on CI and have to pay for + * 'minutes used' on your CI plan) (PILOT_SKIP_WAITING_FOR_BUILD_PROCESSING) + */ + Boolean getSkipWaitingForBuildProcessing() { + convertToBoolean(skipWaitingForBuildProcessing) + } + + void setSkipWaitingForBuildProcessing(Object value) { + skipWaitingForBuildProcessing = value + } + + PublishTestFlight skipWaitingForBuildProcessing(Object value) { + setSkipWaitingForBuildProcessing(value) + this + } + + PublishTestFlight() { + super() + outputs.upToDateWhen {false} + } + + /** + * Finds path to executable in PATH. + * + * This function is aimed to make the whole task testable. + * The tests can override the PATH environment variable and + * point to a mock executable. + * + * @param executableName the name of the executable to find in PATH + * @return path to executable or executableName + */ + private static String getExecutable(String executableName) { + def path = System.getenv("PATH").split(File.pathSeparator) + .collect {path -> new File(path, "fastlane")} + .find {path -> path.exists() && path.isFile() && path.canExecute()} + path? path.path : executableName + } + + @TaskAction + protected void publishTestFlight() { + def executablePath = getExecutable("fastlane") + project.exec { + executable executablePath + args "pilot", "upload" + def pw = getPassword() + + if (pw) { + environment('FASTLANE_PASSWORD', pw) + } + + if (getUsername()) { + args "--username", getUsername() + } + + if(getDevPortalTeamId()) { + args "--dev_portal_team_id", getDevPortalTeamId() + } + + if(getTeamId()) { + args "--team_id", getTeamId() + } + + if(getTeamName()) { + args "--team_name", getTeamName() + } + + if(getAppIdentifier()) { + args "--app_identifier", getAppIdentifier() + } + + if(getItcProvider()) { + args "--itc_provider", getItcProvider() + } + + args "--skip_submission", getSkipSubmission() + args "--skip_waiting_for_build_processing", getSkipWaitingForBuildProcessing() + + args "--ipa", getIpa().path + } + } + + + private static Boolean convertToBoolean(Object value) { + if (!value) { + return false + } + + if (value instanceof Callable) { + value = ((Callable) value).call() + } + + value + } + + private static String convertToString(Object value) { + if (!value) { + return null + } + + if (value instanceof Callable) { + value = ((Callable) value).call() + } + + value.toString() + } +} diff --git a/src/test/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginSpec.groovy b/src/test/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginSpec.groovy index 882c9af2..384cd79d 100644 --- a/src/test/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginSpec.groovy +++ b/src/test/groovy/wooga/gradle/build/unity/ios/IOSBuildPluginSpec.groovy @@ -1,11 +1,21 @@ package wooga.gradle.build.unity.ios import nebula.test.ProjectSpec +import org.gradle.api.DefaultTask +import org.gradle.api.Task import spock.lang.Requires +import spock.lang.Shared import spock.lang.Unroll import wooga.gradle.build.unity.ios.internal.DefaultIOSBuildPluginExtension +import wooga.gradle.build.unity.ios.tasks.ImportProvisioningProfile +import wooga.gradle.build.unity.ios.tasks.KeychainTask +import wooga.gradle.build.unity.ios.tasks.ListKeychainTask +import wooga.gradle.build.unity.ios.tasks.LockKeychainTask +import wooga.gradle.build.unity.ios.tasks.PublishTestFlight +import wooga.gradle.build.unity.ios.tasks.XCodeArchiveTask +import wooga.gradle.build.unity.ios.tasks.XCodeExportTask -@Requires({os.macOs}) +@Requires({ os.macOs }) class IOSBuildPluginSpec extends ProjectSpec { public static final String PLUGIN_NAME = 'net.wooga.build-unity-ios' @@ -22,6 +32,149 @@ class IOSBuildPluginSpec extends ProjectSpec { extension instanceof DefaultIOSBuildPluginExtension } + @Unroll("creates the task #taskName") + def 'Creates needed tasks'(String taskName, Class taskType) { + given: + assert !project.plugins.hasPlugin(PLUGIN_NAME) + assert !project.tasks.findByName(taskName) + + when: + project.plugins.apply(PLUGIN_NAME) + def task + project.afterEvaluate { + task = project.tasks.findByName(taskName) + } + + then: + project.evaluate() + taskType.isInstance(task) + + where: + taskName | taskType + "publish" | DefaultTask + "assemble" | DefaultTask + "build" | DefaultTask + "check" | DefaultTask + } + + /* + xcProject = new File(projectDir, "test.xcodeproj") + xcProject.mkdirs() + xcProjectConfig = new File(xcProject, "project.pbxproj") + xcProjectConfig << "" + */ + + @Shared + File xcProject + + @Shared + File xcProjectConfig + + @Unroll() + def 'Creates xcode task :#taskName when project contains single xcode project'(String taskName, Class taskType) { + given: + assert !project.plugins.hasPlugin(PLUGIN_NAME) + assert !project.tasks.findByName(taskName) + + and: "a dummpy xcode project" + xcProject = new File(projectDir, "test.xcodeproj") + xcProject.mkdirs() + xcProjectConfig = new File(xcProject, "project.pbxproj") + xcProjectConfig << "" + + when: + project.plugins.apply(PLUGIN_NAME) + def task + project.afterEvaluate { + task = project.tasks.findByName(taskName) + } + + then: + project.evaluate() + taskType.isInstance(task) + + where: + taskName | taskType + "buildKeychain" | KeychainTask + "unlockKeychain" | LockKeychainTask + "lockKeychain" | LockKeychainTask + "addKeychain" | ListKeychainTask + "removeKeychain" | ListKeychainTask + "importProvisioningProfiles" | ImportProvisioningProfile + "xcodeArchive" | XCodeArchiveTask + "xcodeExport" | XCodeExportTask + "publishTestFlight" | PublishTestFlight + } + + @Unroll() + def 'Creates xcode tasks :#taskNames when project contains multiple xcode projects'() { + given: "a dummpy xcode project" + xcodeProjectNames.each { + xcProject = new File(projectDir, "${it}.xcodeproj") + xcProject.mkdirs() + xcProjectConfig = new File(xcProject, "project.pbxproj") + xcProjectConfig << "" + } + + when: + project.plugins.apply(PLUGIN_NAME) + List tasks + project.afterEvaluate { + tasks = taskNames.collect { project.tasks.findByName(it) } + } + + then: + project.evaluate() + tasks.every { taskType.isInstance(it) } + + where: + taskName | taskType + "buildKeychain" | KeychainTask + "unlockKeychain" | LockKeychainTask + "lockKeychain" | LockKeychainTask + "addKeychain" | ListKeychainTask + "removeKeychain" | ListKeychainTask + "importProvisioningProfiles" | ImportProvisioningProfile + "xcodeArchive" | XCodeArchiveTask + "xcodeExport" | XCodeExportTask + "publishTestFlight" | PublishTestFlight + + xcodeProjectNames = ["first", "second", "third"] + taskNames = ["first", "second", "third"].collect { it + taskName.capitalize() } + } + + @Unroll() + def "task :#taskName #message on task :#dependedTask when publishToTestflight is #publishToTestflight"() { + given: "a dummpy xcode project" + xcProject = new File(projectDir, "test.xcodeproj") + xcProject.mkdirs() + xcProjectConfig = new File(xcProject, "project.pbxproj") + xcProjectConfig << "" + + and: "a project with property set" + project.plugins.apply(PLUGIN_NAME) + IOSBuildPluginExtension extension = project.extensions.findByName(IOSBuildPlugin.EXTENSION_NAME) as IOSBuildPluginExtension + extension.publishToTestFlight(publishToTestflight) + + and: "a dummpy xcode project" + xcProject = new File(projectDir, "test.xcodeproj") + xcProject.mkdirs() + xcProjectConfig = new File(xcProject, "project.pbxproj") + xcProjectConfig << "" + + expect: + project.evaluate() + def task1 = project.tasks.getByName(taskName) + def task2 = project.tasks.getByName(dependedTask) + task1.dependsOn.contains(task2) == dependsOnTask + + where: + taskName | dependedTask | publishToTestflight | dependsOnTask + "publish" | "publishTestFlight" | true | true + "publish" | "publishTestFlight" | false | false + message = (dependsOnTask) ? "depends" : "depends not" + } + @Unroll def 'extension returns #defaultValue value for property #property'() { given: @@ -43,5 +196,6 @@ class IOSBuildPluginSpec extends ProjectSpec { "configuration" | null "provisioningName" | null "adhoc" | false + "publishToTestFlight" | false } }