diff --git a/README.md b/README.md index c958147..87306ee 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ buildscript { } } dependencies { - classpath "gradle.plugin.com.browserstack.gradle:browserstack-gradle-plugin:3.0.5" + classpath "gradle.plugin.com.browserstack.gradle:browserstack-gradle-plugin:3.1.0" } } diff --git a/build.gradle b/build.gradle index 83c9c9b..26a359d 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id 'com.gradle.plugin-publish' version '0.11.0' } -version '3.0.5' +version '3.1.0' pluginBundle { website = 'https://www.browserstack.com' @@ -17,7 +17,7 @@ pluginBundle { description = 'Runs Espresso tests on BrowserStack' tags = ['espresso', 'test', 'browserstack', 'app', 'automate', 'app-automate', 'appautomate', 'app-live', 'applive'] - version = '3.0.5' + version = '3.1.0' } } } diff --git a/src/main/java/com/browserstack/gradle/BrowserStackPlugin.java b/src/main/java/com/browserstack/gradle/BrowserStackPlugin.java index 291817e..8e744e4 100644 --- a/src/main/java/com/browserstack/gradle/BrowserStackPlugin.java +++ b/src/main/java/com/browserstack/gradle/BrowserStackPlugin.java @@ -6,72 +6,108 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; +import java.util.ArrayList; +import java.util.List; + public class BrowserStackPlugin implements Plugin { - private static final String DEFAULT_GROUP = "BrowserStack"; + private static final String DEFAULT_GROUP = "BrowserStack"; - public void apply(Project project) { + public void apply(Project project) { - BrowserStackConfigExtension browserStackConfigExtension = project.getExtensions() - .create("browserStackConfig", BrowserStackConfigExtension.class); + BrowserStackConfigExtension browserStackConfigExtension = project.getExtensions() + .create("browserStackConfig", BrowserStackConfigExtension.class); - // Get android appExtension - AppExtension appExtension = (AppExtension) project.getExtensions().getByName("android"); + // Get android appExtension + AppExtension appExtension = (AppExtension) project.getExtensions().getByName("android"); - // Get all application variants or flavour combinations. - DomainObjectSet appVariants = appExtension.getApplicationVariants(); + // Get all application variants or flavour combinations. + DomainObjectSet appVariants = appExtension.getApplicationVariants(); - // Create tasks for each variant - appVariants.all(applicationVariant -> { - String applicationVariantName = null; - try { - applicationVariantName = Tools.capitalize(applicationVariant.getName()); - } catch (Exception e) { - return; - } + // Create tasks for each variant + final Boolean[] isCLITaskCreated = new Boolean[1]; + isCLITaskCreated[0] = false; + appVariants.all(applicationVariant -> { + String applicationVariantName = null; + try { + applicationVariantName = Tools.capitalize(applicationVariant.getName()); + } catch (Exception e) { + return; + } - // Since we can't use an outer variable in lambda expression which is not final. - final String appVariantName = applicationVariantName; + // Since we can't use an outer variable in lambda expression which is not final. + final String appVariantName = applicationVariantName; + project.getTasks().create("execute" + appVariantName + "TestsOnBrowserstack", EspressoTask.class, (task) -> { + task.setGroup(DEFAULT_GROUP); + task.setDescription("Uploads app / tests to AppAutomate and executes them"); + // Run Espresso tests without building the apk and test apk + if (!project.hasProperty("skipBuildingApks")) { + task.dependsOn("assemble" + appVariantName, "assemble" + appVariantName + "AndroidTest"); + } + task.setAppVariantBaseName(applicationVariant.getBaseName()); + task.setUsername(browserStackConfigExtension.getUsername()); + task.setAccessKey(browserStackConfigExtension.getAccessKey()); + task.setCustomId(browserStackConfigExtension.getCustomId()); + task.setConfigFilePath(browserStackConfigExtension.getConfigFilePath()); + task.setHost(Constants.BROWSERSTACK_API_HOST); + task.setDebug(browserStackConfigExtension.isDebug()); + }); - project.getTasks().create("execute" + appVariantName + "TestsOnBrowserstack", EspressoTask.class, (task) -> { - task.setGroup(DEFAULT_GROUP); - task.setDescription("Uploads app / tests to AppAutomate and executes them"); - // Run Espresso tests without building the apk and test apk - if (!project.hasProperty("skipBuildingApks") ) { - task.dependsOn("assemble" + appVariantName, "assemble" + appVariantName + "AndroidTest"); - } - task.setAppVariantBaseName(applicationVariant.getBaseName()); - task.setUsername(browserStackConfigExtension.getUsername()); - task.setAccessKey(browserStackConfigExtension.getAccessKey()); - task.setCustomId(browserStackConfigExtension.getCustomId()); - task.setConfigFilePath(browserStackConfigExtension.getConfigFilePath()); - task.setHost(Constants.BROWSERSTACK_API_HOST); - task.setDebug(browserStackConfigExtension.isDebug()); - }); + project.getTasks().create("upload" + appVariantName + "ToBrowserstackAppLive", AppLiveUploadTask.class, (task) -> { + task.setGroup(DEFAULT_GROUP); + task.setDescription("Uploads app to AppLive"); + task.dependsOn("assemble" + appVariantName); + task.setAppVariantBaseName(applicationVariant.getBaseName()); + task.setHost(Constants.BROWSERSTACK_API_HOST); + task.setUsername(browserStackConfigExtension.getUsername()); + task.setAccessKey(browserStackConfigExtension.getAccessKey()); + task.setCustomId(browserStackConfigExtension.getCustomId()); + task.setDebug(browserStackConfigExtension.isDebug()); + }); - project.getTasks().create("upload" + appVariantName + "ToBrowserstackAppLive", AppLiveUploadTask.class, (task) -> { - task.setGroup(DEFAULT_GROUP); - task.setDescription("Uploads app to AppLive"); - task.dependsOn("assemble" + appVariantName); - task.setAppVariantBaseName(applicationVariant.getBaseName()); - task.setHost(Constants.BROWSERSTACK_API_HOST); - task.setUsername(browserStackConfigExtension.getUsername()); - task.setAccessKey(browserStackConfigExtension.getAccessKey()); - task.setCustomId(browserStackConfigExtension.getCustomId()); - task.setDebug(browserStackConfigExtension.isDebug()); - }); + project.getTasks().create("upload" + appVariantName + "ToBrowserstackAppAutomate", AppAutomateUploadTask.class, (task) -> { + task.setGroup(DEFAULT_GROUP); + task.setDescription("Uploads app to AppAutomate"); + task.dependsOn("assemble" + appVariantName); + task.setAppVariantBaseName(applicationVariant.getBaseName()); + task.setHost(Constants.BROWSERSTACK_API_HOST); + task.setUsername(browserStackConfigExtension.getUsername()); + task.setAccessKey(browserStackConfigExtension.getAccessKey()); + task.setCustomId(browserStackConfigExtension.getCustomId()); + task.setDebug(browserStackConfigExtension.isDebug()); + }); + if (!isCLITaskCreated[0]) { + project.getTasks().create("browserstackCLIWrapper", CLI.class, (task) -> { + task.setGroup(DEFAULT_GROUP); + task.setDescription("Just a wrapper on the Browserstack CLI. A way to run any Browserstack CLI command directly from gradle. \n" + + "\n" + + "\n" + + "For reference on Browserstack CLI please visit https://www.browserstack.com/app-automate/browserstack-cli\n" + + "\n" + + "\n" + + "Any CLI command passed in the custom option -PcliCommand will be executed and the results will be displayed on the terminal.\n" + + "\n" + + "\n" + + "For example:\n" + + "\n" + + "gradle browserstackCLIWrapper -PcliCommand=”app-automate apps”\n" + + "\n" + + "The browserstack CLI command app-automate apps would run and the result will be displayed on the terminal. "); - project.getTasks().create("upload" + appVariantName + "ToBrowserstackAppAutomate", AppAutomateUploadTask.class, (task) -> { - task.setGroup(DEFAULT_GROUP); - task.setDescription("Uploads app to AppAutomate"); - task.dependsOn("assemble" + appVariantName); - task.setAppVariantBaseName(applicationVariant.getBaseName()); - task.setHost(Constants.BROWSERSTACK_API_HOST); - task.setUsername(browserStackConfigExtension.getUsername()); - task.setAccessKey(browserStackConfigExtension.getAccessKey()); - task.setCustomId(browserStackConfigExtension.getCustomId()); - task.setDebug(browserStackConfigExtension.isDebug()); - }); - }); - } + task.dependsOn("assemble" + appVariantName); + task.setAppVariantBaseName(applicationVariant.getBaseName()); + task.setHost(Constants.BROWSERSTACK_API_HOST); + task.setUsername(browserStackConfigExtension.getUsername()); + task.setAccessKey(browserStackConfigExtension.getAccessKey()); + task.setCustomId(browserStackConfigExtension.getCustomId()); + task.setDebug(browserStackConfigExtension.isDebug()); + if (project.hasProperty("command")) { + System.out.println("Command found: " + project.findProperty("command").toString()); + task.setCommand(project.property("command").toString()); + } + }); + isCLITaskCreated[0] = true; + } + }); + } } diff --git a/src/main/java/com/browserstack/gradle/BrowserStackTask.java b/src/main/java/com/browserstack/gradle/BrowserStackTask.java index 1f99d96..d8f332c 100644 --- a/src/main/java/com/browserstack/gradle/BrowserStackTask.java +++ b/src/main/java/com/browserstack/gradle/BrowserStackTask.java @@ -34,6 +34,8 @@ public class BrowserStackTask extends DefaultTask { private String appVariantBaseName = "debug"; + public String command ; + public void setAppVariantBaseName(String appVariantBaseName) { this.appVariantBaseName = appVariantBaseName; } @@ -70,8 +72,11 @@ public void setHost(String host) { this.host = host; } - protected JSONObject constructDefaultBuildParams() { - JSONObject params = new JSONObject(); + public String getCommand() { return command; } + + public void setCommand(String command) { this.command = command; } + + protected JSONObject constructDefaultBuildParams() { JSONObject params = new JSONObject(); params.put("app", app); // for monitoring, not for external use @@ -149,16 +154,15 @@ public Map locateApks(boolean ignoreTestPath) throws IOException { String dir = System.getProperty("user.dir"); List appApkFiles = new ArrayList<>(); List testApkFiles = new ArrayList<>(); - Files.find(Paths.get(dir), Constants.APP_SEARCH_MAX_DEPTH, (filePath, fileAttr) -> isValidFile(filePath, fileAttr)) .forEach(f -> { + if (f.toString().endsWith("-androidTest.apk")) { testApkFiles.add(f); } else { appApkFiles.add(f); } }); - debugApkPath = findMostRecentPath(appApkFiles); testApkPath = findMostRecentPath(testApkFiles); diff --git a/src/main/java/com/browserstack/gradle/CLI.java b/src/main/java/com/browserstack/gradle/CLI.java new file mode 100644 index 0000000..ba633ca --- /dev/null +++ b/src/main/java/com/browserstack/gradle/CLI.java @@ -0,0 +1,190 @@ +package com.browserstack.gradle; + +import org.gradle.api.tasks.TaskAction; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.*; +import java.net.*; +import java.io.*; +import java.util.Scanner; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +public class CLI extends BrowserStackTask { + + private Boolean isWindows; + private String directory = System.getProperty("user.dir"); + private String fileName = "browserstack"; + private String downloadedFileName = "browserstackcli"; + private String arch; + private String os; + + public void verifyParams() throws Exception { + String username = this.getUsername(); + String accessKey = this.getAccessKey(); + if (username == null || accessKey == null || username == "" || accessKey == "" || username.equals("null")) { + throw new Exception("`username`, `accessKey` and `configFilePath` are compulsory"); + } + } + + private boolean initialize() { + setOS(); + isWindows = isWindows(); + if (isWindows) { + fileName = fileName + ".exe"; + downloadedFileName = downloadedFileName + ".exe"; + String wowArch = System.getenv("PROCESSOR_ARCHITECTURE"); + String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432"); + String realArch = wowArch != null && wowArch.endsWith("64") + || wow64Arch != null && wow64Arch.endsWith("64") + ? "64" : "32"; + if (realArch.equals("64")) { + arch = Constants.ARCH_64_BIT; + } else { + arch = Constants.ARCH_32_BIT; + } + } else { + if (System.getProperty("os.arch").contains("64")) { + arch = Constants.ARCH_64_BIT; + } else { + arch = Constants.ARCH_32_BIT; + } + } + if (!new File(directory, downloadedFileName).exists()) { + install(); + try { + givePermission(); + } catch (InterruptedException e) { + e.printStackTrace(); + return false; + } + } + return true; + } + + private void setOS() { + String osName = System.getProperty("os.name"); + if (osName.toLowerCase().startsWith("windows")) { + os = "windows"; + } else if (osName.toLowerCase().startsWith("mac")) { + os = "darwin"; + } else { + os = "linux"; + } + } + + @TaskAction + void runCLICommands() throws Exception { + verifyParams(); + if (!initialize()) { + System.out.println("Something went wrong!!"); + return; + } + String s = ""; + try { + authenticate(); + StringBuilder commandBuilder = getCommandPrefix(); + commandBuilder.append(" ").append(command); + String finalCommand = commandBuilder.toString(); + Process process = runProcess(finalCommand); + inheritIO(process.getInputStream(), System.out); + inheritIO(process.getErrorStream(), System.err); + process.waitFor(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void inheritIO(final InputStream src, final PrintStream dest) { + new Thread(new Runnable() { + public void run() { + Scanner sc = new Scanner(src); + while (sc.hasNextLine()) { + dest.println(sc.nextLine()); + } + } + }).start(); + } + + private StringBuilder getCommandPrefix() { + StringBuilder commandBuilder = new StringBuilder(""); + if (isWindows) { + commandBuilder.append(downloadedFileName); + } else { + commandBuilder.append("./"); + commandBuilder.append(downloadedFileName); + } + return commandBuilder; + } + + private void authenticate() throws InterruptedException { + StringBuilder commandBuilder = getCommandPrefix(); + commandBuilder.append(" authenticate --username=").append(this.getUsername()).append(" --access-key=").append(this.getAccessKey()); + Process process = runProcess(commandBuilder.toString()); + process.waitFor(); + } + + private Process runProcess(String command) { + Process process = null; + ProcessBuilder builder; + List list = new ArrayList<>(); + Matcher matcher = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(command); + while (matcher.find()) + list.add(matcher.group(1).replace("\"", "")); // Adding .replace("\"", "") to remove surrounding quotes. + + try { + process = Runtime.getRuntime().exec(list.toArray(new String[0]), null, new File(directory)); + } catch (IOException e) { + e.printStackTrace(); + System.exit(-1); + } + return process; + } + + private Boolean isWindows() { + return os.equals("windows"); + } + + private void install() { + try { + String URL = generateDownloadURL(); + URL url = new URL(URL); + InputStream in = url.openStream(); + FileOutputStream fos = new FileOutputStream(new File(directory + "/" + downloadedFileName)); + + int length = -1; + byte[] buffer = new byte[1024];// buffer for portion of data from + // connection + while ((length = in.read(buffer)) > -1) { + fos.write(buffer, 0, length); + } + fos.close(); + in.close(); + } catch (IOException e) { + e.printStackTrace(); + System.exit(-1); + } + } + + private String generateDownloadURL() { + StringBuilder urlBuilder = new StringBuilder(Constants.SYNC_CLI_DOWNLOAD_URL); + urlBuilder.append("arch="); + urlBuilder.append(arch); + urlBuilder.append("&file="); + urlBuilder.append(fileName); + urlBuilder.append("&os="); + urlBuilder.append(os); + urlBuilder.append("&version="); + urlBuilder.append(Constants.SYNC_CLI_VERSION); + return urlBuilder.toString(); + } + + private void givePermission() throws InterruptedException { + if (!isWindows) { + Process process = runProcess("chmod +x " + downloadedFileName); + process.waitFor(); + } + } +} diff --git a/src/main/java/com/browserstack/gradle/Constants.java b/src/main/java/com/browserstack/gradle/Constants.java index 87a1b95..a01b870 100644 --- a/src/main/java/com/browserstack/gradle/Constants.java +++ b/src/main/java/com/browserstack/gradle/Constants.java @@ -10,7 +10,11 @@ public class Constants { APP_AUTOMATE_UPLOAD_PATH = "/app-automate/upload", APP_LIVE_UPLOAD_PATH = "/app-live/upload", TEST_SUITE_UPLOAD_PATH = "/app-automate/espresso/v2/test-suite", - DEFAULT_NETWORK_PROFILE = null; + DEFAULT_NETWORK_PROFILE = null, + SYNC_CLI_VERSION = "3.0.0", + ARCH_64_BIT = "amd64", + ARCH_32_BIT = "386", + SYNC_CLI_DOWNLOAD_URL = "https://browserstack.com/browserstack-cli/download?"; public static final boolean DEFAULT_VIDEO = true, DEFAULT_DEVICE_LOGS = true, diff --git a/test.rb b/test.rb index af634ce..b5a50a7 100644 --- a/test.rb +++ b/test.rb @@ -94,6 +94,14 @@ def validate_env end end + +def run_tests_args + puts "\nRunning CLI tests using ./gradlew with args" + run_basic_espresso_test("./gradlew browserstackCLIWrapper -Pcommand='help'"); + run_basic_espresso_test("./gradlew browserstackCLIWrapper -Pcommand='app-automate apps delete -a bs://3fc4eea395ea6efc69b74e8211ecf2eba8879373'") + print_separator +end + def test validate_env build_plugin @@ -104,6 +112,7 @@ def test run_tests_args setup_repo_with_app_variants run_tests_with_flavors + run_cli_tests remove_repo end