程序化 SauceLabs 隧道控制

Programatic SauceLabs Tunnel control

有没有人有任何以编程方式控制 SauceLabs SauceConnect 隧道以进行 Selenium WebDriver 测试的经验?具体来自 Java 代码。 SauceLabs 文档中的示例假定隧道是在执行测试之前手动创建的,或者在某处有一个永久隧道供所有测试使用。

每个测试的隧道都需要是唯一的,并且支持在同一个盒子上同时有多个 tunnels/tests 运行。使用该项目的任何人都应该能够使用隧道执行测试,而无需手动步骤来创建隧道或执行隧道软件的特殊安装和配置。

我做了一些研究,想出了一种从代码中控制我的 SauceConnect 隧道的方法。以下是在其他人想要这样做的情况下如何让它工作的简要总结。我正在使用此设置在同一台服务器上 运行 多个隧道,并将每个隧道连接到现场代理服务器。这些示例假设您 运行 在 JVM 上编写代码。

我在博客中整理了一篇更详细的文章 post Controlling Sauce Connect

依赖关系

将以下依赖项添加到您的项目中。 com.saucelabs.ci-sauce

然后您可以使用下面的代码调用库来启动和停止隧道。

开始隧道

Process tunnel = sauceTunnelManager.openConnection( 
        sauceUser,      // username 
        sauceKey,       // apiKey 
        port,           // port 
        null,           // sauceConnectJar 
        tunnelOptions,  // Tunnel options 
        null,           // printStream 
        null,           // verboseLogging 
        null            // sauceConnectPath );

关闭隧道

sauceTunnelManager.closeTunnelsForPlan(
     sauceUser,      // username (same as start tunnel)
     tunnelOptions,  // tunnelOptions (same as start tunnel)
     null);

下面是我编写的一系列 java 类 以编程方式控制 SauceTunnel 作为更广泛的解决方案的一部分,该解决方案使用户能够使用 JUnit 与 BrowserStack 或 SauceLabs 对话:

存储库有一个包含 Sauce-Connect.jar.

的 lib 目录

SauceLabsTunnel

package au.com.somecompany.devops.tunnels;

import au.com.somecompany.devops.tunnels.channel.Channel;
import au.com.somecompany.devops.tunnels.channel.SauceLabsChannel;
import au.com.somecompany.devops.utils.Drop;

public class SauceLabsTunnel extends Thread implements Runnable {
    private Channel Jar;
    private Drop Tunnelled;
    private Drop Tested;

    public SauceLabsTunnel(Drop tunnelled, Drop tested){
        String command = "java -jar lib/Sauce-Connect.jar ";
        String successMessage = "Successful handshake with Sauce Connect server";
        String failureMessage = "Could not reach Sauce Labs REST API";
        String outputTerminal = "Connected! You may start your tests.";

        Jar = new SauceLabsChannel(command, System.getProperty("ACCESSKEY"), successMessage, failureMessage, outputTerminal, System.getProperty("USERNAME"));
        Tunnelled = tunnelled;
        Tested = tested;
    }

    @Override
    public void run() {
        Jar.Launch();

        while(!Jar.IsJARSuccessfullyLaunched()){
            Jar.Launch();
        }
        Jar.PrintOutput();
        Tunnelled.put("TUNNELLED");

        for (String message = Tested.take(); !message.equals("TESTED"); message = Tested.take()) {
            System.out.println("I see Testing is completed. Closing Tunnel...");
        }
        Shutdown();
    }

    public void Shutdown(){
        Jar.Shutdown();
    }
}

频道

package au.com.somecompany.devops.tunnels.channel;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Channel {
    protected Process Process;
    protected String ChannelCommand = null;
    protected String ProcessOutput = null;
    protected String SuccessMessage = null;
    protected String FailureMessage = null;
    protected String OutputTerminal = null;

    public Channel(String command, String successMessage, String failureMessage, String outputTerminal){
        ChannelCommand = command;
        SuccessMessage = successMessage;
        FailureMessage = failureMessage;
        OutputTerminal = outputTerminal;
    }

    public void Launch(){
        Process = ExecuteJAR();
        ProcessOutput = GetOutput(Process.getInputStream());
    }

    public boolean IsJARSuccessfullyLaunched(){
        return ProcessOutput.contains(SuccessMessage) && 
               !ProcessOutput.contains(FailureMessage);
    }

    public void PrintOutput(){
        System.out.print(ProcessOutput);
    }

    public void Shutdown(){
        Process.destroy();
    }

    private String GetOutput(InputStream is){
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        StringBuffer sb = new StringBuffer();
        try {
            String s = br.readLine();
            if(s == null) {
                return null;
            }
            while(!s.contains(OutputTerminal)){
                sb.append(s).append("\n");
                s = br.readLine();
                if(s == null || s.contains(OutputTerminal)){
                    break;
                }
            }
        } catch (IOException e){
            e.printStackTrace();
        }
        return sb.toString();
    }

    private Process ExecuteJAR() {
        Process p;
        try {
            p = Runtime.getRuntime().exec(ChannelCommand);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return p;
    }
}

SauceLabsChannel

package au.com.somecompany.devops.tunnels.channel;

public class SauceLabsChannel extends Channel {
    public SauceLabsChannel(String command, String accessKey, String successMessage, String failureMessage, String outputTerminal, String username){
        super(command, successMessage, failureMessage, outputTerminal);
        ChannelCommand += username + " " + accessKey;
    }
}

掉落

package au.com.somecompany.devops.utils;

public class Drop {
    // Message sent from producer
    // to consumer.
    private String message;
    // True if consumer should wait
    // for producer to send message,
    // false if producer should wait for
    // consumer to retrieve message.
    private boolean empty = true;

    public synchronized String take() {
        // Wait until message is
        // available.
        while (empty) {
            try {
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = true;
        // Notify producer that
        // status has changed.
        notifyAll();
        return message;
    }

    public synchronized void put(String message) {
        // Wait until message has
        // been retrieved.
        while (!empty) {
            try { 
                wait();
            } catch (InterruptedException e) {}
        }
        // Toggle status.
        empty = false;
        // Store message.
        this.message = message;
        // Notify consumer that status
        // has changed.
        notifyAll();
    }
}