Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,29 @@ Add this step directly to your workflow in the [Bitrise Workflow Editor](https:/
<details>
<summary>Description</summary>

Run your XCUI tests on BrowserStack App Automate. This step collects the built IPA from `$BITRISE_IPA_PATH` and the output bundle file from `$BITRISE_TEST_BUNDLE_PATH` environment variables.
Run your XCUI tests on BrowserStack App Automate. This step collects *both the built app and test suite* from the `$BITRISE_BUNDLE_PATH` environment variable, generates an IPA file, uploads and starts a test build.

## Configure the Step

Complete the following steps to configure BrowserStack's XCUI step in Bitrise:

1. Open the Workflow you want to use in the Workflow Editor.
2. Add the [Xcode Archive & Export for iOS](https://www.bitrise.io/integrations/steps/xcode-archive) and [Xcode Build for testing for iOS](https://www.bitrise.io/integrations/steps/xcode-build-for-test) steps to your workflow and configure them.
2. Add the [Xcode Build for testing for iOS](https://www.bitrise.io/integrations/steps/xcode-build-for-test) step to your workflow and configure it.
3. Add the **BrowserStack App Automate - XCUI** step below the **Xcode Archive & Export for iOS** and **Xcode Build for testing for iOS** steps.
3. Add the **BrowserStack App Automate - XCUI** step below the **Xcode Build for testing for iOS** steps.
4. Add your BrowserStack Username and Access Key in the **Authentication** step input.

5. Provide the built application name in the **iOS app under test** input. This is typically the product name in your project.
5. For the **iOS app under test** input, the **BITRISE_IPA_PATH** output variable from the **Xcode Archive & Export for iOS** step exports the IPA file. Add `$BITRISE_IPA_PATH` to the **iOS app under test** input.<br /><br /> For the **XCUI test suite** input, the **BITRISE_TEST_BUNDLE_PATH** output variable from the **Xcode Build for testing for iOS step** exports the test suite. Add `$BITRISE_TEST_BUNDLE_PATH` to the **iOS app under test** input.<br /><br /> If you are not using **Xcode Archive & Export for iOS** and **Xcode Build for testing for iOS** steps, ensure that the **iOS app under test** input points to the path of your app (`.ipa` file). Also, ensure that the **XCUI test suite** input points to the test suite runner file. In the case of the runner app, it should be in the `<any_path>/Debug-iphoneos` directory if you are providing an absolute path.<br />
6. For the **XCUI test suite** input, the **BITRISE_TEST_BUNDLE_PATH** output variable from the **Xcode Build for testing for iOS step** indicates where the app bundle and test suite are located. Add `$BITRISE_TEST_BUNDLE_PATH` to the **iOS app under test** input.<br /><br /> If you are not using the **Xcode Build for testing for iOS** step, ensure that the **XCUI test suite** input points to a directory that contains both the test suite runner file and the app bundle (not .ipa).
6. Add one or more devices in the **Devices** step input.
7. Add one or more devices in the **Devices** step input.
7. Configure additional step inputs like **Debug logs** and **Test Configurations** and start your build.
8. Optionally provide custom IDs for the app and test suite in **Custom IDs** and configure additional step inputs like **Debug logs** and **Test Configurations**.

9. Start your build.

</details>

Expand All @@ -38,9 +42,11 @@ Complete the following steps to configure BrowserStack's XCUI step in Bitrise:

| Key | Description | Flags | Default |
| --- | --- | --- | --- |
| `iOS app` | Set the path of the app (.ipa) file. | Required | N/A |
| `iOS app under test` | Set the name of the .app file (same as `PRODUCT_NAME` under Packaging in Xcode Build Settings). | Required | N/A |
| `XCUI test suite` | Set the path of the output bundle file. | Required | N/A |
| `Devices` | Provide one or more device-OS combination in a new line. For example: <br /> `iPhone 11-13` <br />`iPhone XS-15` | Required | N/A |
| `App Custom ID` | Custom identifier for the app under testing. | Optional | N/A |
| `Test Suite Custom ID` | Custom identifier for the test suite to be run. | Optional | N/A |
| `Instrumentation logs` | Generate instrumentation logs of the test session | Optional | `true` |
| `Network logs` | Generate network logs of your test sessions to capture network traffic, latency, etc. | Optional | `false` |
| `Device Logs` | Generate device logs | Optional | `false` |
Expand Down
2 changes: 1 addition & 1 deletion bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ app:
- A_SECRET_PARAM: $A_SECRET_PARAM
# If you want to share this step into a StepLib
- BITRISE_STEP_ID: xcui-browserstack-official
- BITRISE_STEP_VERSION: "1.0.0"
- BITRISE_STEP_VERSION: "1.1.0"
- BITRISE_STEP_GIT_CLONE_URL: https://github.com/browserstack/browserstack-bitrise-xcui-step.git
- MY_STEPLIB_REPO_FORK_GIT_URL: $MY_STEPLIB_REPO_FORK_GIT_URL

Expand Down
10 changes: 7 additions & 3 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@ const (
UPLOAD_APP_ERROR = "Failed to upload app on BrowserStack, error : %s"
FILE_NOT_AVAILABLE_ERROR = "Failed to upload test suite on BrowserStack, error: file not available"
INVALID_FILE_TYPE_ERROR = "Failed to upload test suite on BrowserStack, error: invalid file type"
APP_CUSTOM_ID_ERROR = "Failed to attach app custom ID, error: %s"
BUILD_FAILED_ERROR = "Failed to execute build on BrowserStack, error: %s"
FETCH_BUILD_STATUS_ERROR = "Failed to fetch test results, error: %s"
HTTP_ERROR = "Something went wrong while processing your request, error: %s"
RUNNER_APP_NOT_FOUND = "xcuitest_testsuite_path: couldn’t find the <AppnameUITests>-Runner.app . Please add the $BITRISE_TEST_BUNDLE_PATH from Xcode Build for testing for iOS step or the absolute path of <AppnameUITests>-Runner.app"
IPA_NOT_FOUND = "app_ipa_path: couldn’t find the iOS app (.ipa file). Please add the $BITRISE_IPA_PATH from Xcode Archive & Export for iOS step or the absolute path of iOS app (.ipa file)"
FILE_ZIP_ERROR = "Something went wrong while processing the test-suite, error: %s"
RUNNER_APP_NOT_FOUND = "xcuitest_testsuite_path: couldn’t find the <AppnameUITests>-Runner.app. Please add the $BITRISE_TEST_BUNDLE_PATH from Xcode Build for testing for iOS step or the absolute path of <AppnameUITests>-Runner.app"
IPA_NOT_FOUND = "Failed to generate an .ipa file. Please verify the value in $BUNDLE_APP_NAME"
FILE_NOT_FOUND = "File not found: %s"
FILE_COPY_ERROR = "Failed to copy file, error: %s"
FILE_DIR_ERROR = "Failed to create directory, error: %s"
FILE_ZIP_ERROR = "Failed to zip file, error: %s"
)
18 changes: 11 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,36 @@ func main() {

username := os.Getenv("browserstack_username")
access_key := os.Getenv("browserstack_accesskey")
ios_app := os.Getenv("app_ipa_path")
app_bundle_name := os.Getenv("app_bundle_name")
app_custom_id := os.Getenv("app_custom_id")
test_suite_custom_id := os.Getenv("test_suite_custom_id")
test_suite_path := os.Getenv("xcui_test_suite")

if username == "" || access_key == "" {
failf(UPLOAD_APP_ERROR, "invalid credentials")
}

if ios_app == "" {
failf(IPA_NOT_FOUND)
}

if test_suite_path == "" {
failf(RUNNER_APP_NOT_FOUND)
}

ipa_file_name := locateAppBundleFileAndIpa(test_suite_path, app_bundle_name)
find_and_zip_file_err := locateTestRunnerFileAndZip(test_suite_path)

if ipa_file_name == "" {
failf(IPA_NOT_FOUND)
}

if find_and_zip_file_err != nil {
failf(find_and_zip_file_err.Error())
}

test_app_app := ipa_file_name
test_runner_app := TEST_RUNNER_ZIP_FILE_NAME

log.Print("Uploading app on BrowserStack App Automate")

upload_app, err := upload(ios_app, APP_UPLOAD_ENDPOINT, username, access_key)
upload_app, err := upload(test_app_app, APP_UPLOAD_ENDPOINT, &app_custom_id, username, access_key)

if err != nil {
failf(err.Error())
Expand All @@ -60,7 +64,7 @@ func main() {

log.Print("Uploading test suite on BrowserStack App Automate")

upload_test_suite, err := upload(test_runner_app, TEST_SUITE_UPLOAD_ENDPOINT, username, access_key)
upload_test_suite, err := upload(test_runner_app, TEST_SUITE_UPLOAD_ENDPOINT, &test_suite_custom_id, username, access_key)

if err != nil {
failf(err.Error())
Expand Down
4 changes: 2 additions & 2 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ func TestBuild(t *testing.T) {
func TestUpload(t *testing.T) {
t.Log("It should throw file not found error with empty path")
{
build, err := upload("", APP_UPLOAD_ENDPOINT, "username", "password")
build, err := upload("", APP_UPLOAD_ENDPOINT, nil, "username", "password")
t.Log(build, err)
require.Equal(t, "", build)
require.Error(t, err)
}

t.Log("It should throw file not found error with invalid path")
{
build, err := upload("invalidpath", APP_UPLOAD_ENDPOINT, "username", "password")
build, err := upload("invalidpath", APP_UPLOAD_ENDPOINT, nil, "username", "password")
t.Log(build, err)
require.Equal(t, "", build)
require.Error(t, err)
Expand Down
10 changes: 9 additions & 1 deletion services.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func build(app_url string, test_suite_url string, username string, access_key st
}

// this function uploads both app and test suite
func upload(app_path string, endpoint string, username string, access_key string) (string, error) {
func upload(app_path string, endpoint string, custom_id *string, username string, access_key string) (string, error) {
if app_path == "" {
return "", errors.New(FILE_NOT_AVAILABLE_ERROR)
}
Expand Down Expand Up @@ -84,6 +84,14 @@ func upload(app_path string, endpoint string, username string, access_key string
return "", errors.New(FILE_NOT_AVAILABLE_ERROR)
}

if custom_id != nil {
fileErr := multipart_writer.WriteField("custom_id", *custom_id)

if fileErr != nil {
return "", errors.New(APP_CUSTOM_ID_ERROR)
}
}

err := multipart_writer.Close()

if err != nil {
Expand Down
28 changes: 23 additions & 5 deletions step.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ title: |-
summary: |
Run your XCUITest tests on BrowserStack App Automate
description: |
Run your XCUITest tests on BrowserStack App Automate. This step collects the built IPA from `$BITRISE_IPA_PATH` and test suite from `$BITRISE_BUNDLE_PATH` Environment Variables
Run your XCUITest tests on BrowserStack App Automate. This step collects the built app and test suite from `$BITRISE_BUNDLE_PATH` environment variable
website: https://github.com/browserstack/browserstack-bitrise-xcui-step
source_code_url: https://github.com/browserstack/browserstack-bitrise-xcui-step
support_url: https://github.com/browserstack/browserstack-bitrise-xcui-step/issues
Expand Down Expand Up @@ -78,14 +78,14 @@ inputs:
is_sensitive: true
description: 'Access Key of the BrowserStack account'

# IPA's
- app_ipa_path: $BITRISE_IPA_PATH
# App and test suite
- app_bundle_name: $BUNDLE_APP_NAME
opts:
title: 'iOS app under test'
summary: 'Path to the app (.ipa) file'
summary: 'Name of the .app file'
is_expand: true
is_required: true
description: 'Path of the app (.ipa) file'
description: 'Name of the .app file generated when building for testing'
- xcui_test_suite: $BITRISE_TEST_BUNDLE_PATH
opts:
title: 'XCUI test suite'
Expand All @@ -111,6 +111,24 @@ inputs:
is_expand: true
is_required: true

# Custom IDs
- app_custom_id:
opts:
title: 'App Custom ID'
summary: 'App Custom ID in BrowserStack'
is_expand: true
is_required: false
description: 'App Custom ID in BrowserStack'
category: 'Customs IDs'
- test_suite_custom_id:
opts:
title: 'Test Suite Custom ID'
summary: 'Test Suite Custom ID in BrowserStack'
is_expand: true
is_required: false
description: 'Test Suite Custom ID in BrowserStack'
category: 'Customs IDs'

# Debug logs inputs
- instrumentation_logs: "true"
opts:
Expand Down
2 changes: 1 addition & 1 deletion structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type TestMapping struct {
type TestSharding struct {
NumberOfShards int `json:"numberOfShards,omitempty"`
Mapping []TestMapping `json:"mapping,omitempty"`
AutoStrategyDevices []string `json:"devices,omitempty"`
AutoStrategyDevices string `json:"deviceSelection,omitempty"`
}

type BrowserStackPayload struct {
Expand Down
60 changes: 45 additions & 15 deletions util_fns.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,31 +270,61 @@ func WalkMatch(root, ext string) []string {
return files_found
}

func locateTestRunnerFileAndZip(test_suite_location string) error {
split_test_suite_path := strings.Split(test_suite_location, "/")
get_file_name := split_test_suite_path[len(split_test_suite_path)-1]
func locateAppFile(location string, file_name string) string {
app_extension := "app"
file_name_and_extension := file_name + "." + app_extension

test_runner_app_path := ""
split_path := strings.Split(location, "/")
get_file_name := split_path[len(split_path)-1]

check_file_extension := strings.Split(get_file_name, ".")
file_path := ""

// Checking 2 conditions here
// 1. test_suite_location - is this runner app
// 2. test_suite_location - if this is a directory, does any runner app exists in this directory.
if len(check_file_extension) > 0 && check_file_extension[len(check_file_extension)-1] == "app" {
test_runner_app_path = test_suite_location
// If location is already .app file, return that. Else if location is directory,
// check if it contains any .app files with the specified name.
check_file_extension := strings.Split(get_file_name, ".")
if len(check_file_extension) > 0 && check_file_extension[len(check_file_extension)-1] == app_extension {
file_path = location
} else if strings.Contains(get_file_name, "test_bundle") {
// if test_suite_location is a directory instead of the file, then check if runner app exits
files := WalkMatch(test_suite_location+"/Debug-iphoneos/", "*-Runner.app")
files := WalkMatch(location+"/Debug-iphoneos/", file_name_and_extension)

if len(files) < 1 {
return errors.New(RUNNER_APP_NOT_FOUND)
failf(FILE_NOT_FOUND, file_name_and_extension)
}
test_runner_app_path = files[len(files)-1]
file_path = files[len(files)-1]
} else {
return errors.New(RUNNER_APP_NOT_FOUND)
failf(FILE_NOT_FOUND, file_name_and_extension)
}

return file_path
}

// Locates .app, moves it into a Payload folder and compresses that folder into .ipa.
func locateAppBundleFileAndIpa(app_bundle_location string, app_bundle_name string) string {
app_bundle_path := locateAppFile(app_bundle_location, app_bundle_name)
app_zip_name := app_bundle_name + ".ipa"

_, mkdir_err := exec.Command("mkdir", "Payload").Output()
if mkdir_err != nil {
failf(FILE_DIR_ERROR, mkdir_err)
}

_, err := exec.Command("cp", "-r", app_bundle_path, "Payload/Application.app").Output()
if err != nil {
failf(FILE_COPY_ERROR, err)
}

_, zipping_err := exec.Command("zip", "-r", "-D", app_zip_name, "Payload").Output()
if zipping_err != nil {
failf(FILE_ZIP_ERROR, zipping_err)
}

return app_zip_name
}

// Locates runner .app and compresses it into .zip.
func locateTestRunnerFileAndZip(test_suite_location string) error {
test_runner_app_path := locateAppFile(test_suite_location, "*-Runner")

file_path := strings.Split(test_runner_app_path, "/")
test_runner_file_name := file_path[len(file_path)-1]

Expand Down