DataNode RPC to NameNode timeout

在启动Standby NameNode的后经常发现DataNode端报socket异常,具体日志是

java.net.SocketTimeoutException: Call From dn to ns2 failed on socket timeout exception: java.net.SocketTimeoutException: 60000 millis timeout while waiting for channel to be ready for read. ch : java.nio.channels.SocketChannel[connected local=/dn:58211 remote=ns2]; For more details see:  http://wiki.apache.org/hadoop/SocketTimeout
Caused by: java.net.SocketTimeoutException: 60000 millis timeout while waiting for channel to be ready for read. ch : java.nio.channels.SocketChannel[connected local=/dn:58211 remote=ns2]

并且,timeout只发生在DN向Standby发送Heartbeat的时候,而跟Active的通讯都是正常的。
初步怀疑是Standby 拉取Edit导致的full gc问题,但是相应时间standby的gc日志是完全正常的。随后jstack看到所有的时间都花费在updateCountForQuota方法上。updateCountForQuota方法就是从root节点开始递归的更新所有quota。
这步操作在Standby每次拉取editlog之后都会执行,我在测试集群生成1亿个INodeDir以后就可以稳定出现这一情况。
针对这个问题,我发了一个jira,地址是HDFS-9143,随后社区有个duplicate的jira,地址是HDFS-6763
我的解决方案就是每次editlog tail结束的时候不去调用updateCountForQuota方法,transition to active的时候调用一次就好了。类似于6763 V1方案,而V2,V3方案则对ActiveNameNode启动时候updateCountForQuota操作进行了优化。
这个改进将会消除Standby与DN之间经常出现的SocketTimeout同时也会加快Standby启动时间。

YARN MRAppMaster与Scheduler流程说明(三)

接上一篇说明,继续app提交在ResouceManager中的状态机流转。
9.FairScheduler.allocate
allocate方法基本上是申请mrappmaster和mrappmaster启动后向ResourceManager申请container资源使用的。它首先检查app是否在已经注册到scheduler中,然后检查申请资源状态,释放掉相应资源,并加入到AppSchedulingInfo结构中,这些内容会在MRAppmaster的流程中更具体的介绍。随后是获取能够运行appmaster的container资源的方法,代码为:

  public synchronized ContainersAndNMTokensAllocation
      pullNewlyAllocatedContainersAndNMTokens() {
    List<Container> returnContainerList =
        new ArrayList<Container>(newlyAllocatedContainers.size());
    List<NMToken> nmTokens = new ArrayList<NMToken>();
    for (Iterator<RMContainer> i = newlyAllocatedContainers.iterator(); i
      .hasNext();) {
      RMContainer rmContainer = i.next();
      Container container = rmContainer.getContainer();
      try {
        // create container token and NMToken altogether.
        container.setContainerToken(rmContext.getContainerTokenSecretManager()
          .createContainerToken(container.getId(), container.getNodeId(),
            getUser(), container.getResource()));
        NMToken nmToken =
            rmContext.getNMTokenSecretManager().createAndGetNMToken(getUser(),
              getApplicationAttemptId(), container);
        if (nmToken != null) {
          nmTokens.add(nmToken);
        }
      } catch (IllegalArgumentException e) {
        // DNS might be down, skip returning this container.
        LOG.error("Error trying to assign container token and NM token to" +
            " an allocated container " + container.getId(), e);
        continue;
      }
      returnContainerList.add(container);
      i.remove();
      rmContainer.handle(new RMContainerEvent(rmContainer.getContainerId(),
        RMContainerEventType.ACQUIRED));
    }
    return new ContainersAndNMTokensAllocation(returnContainerList, nmTokens);
  }

这里需要说明的是由于YARN大部分操作都是通过Distpatcher进行异步操作,NodeManager向ResouceManager汇报心跳的时候会最后触发FairScheduler的NODE_UPDATE事件,NodeManager会汇报最新执行的,执行结束的Container,Scheduler会放到相应的结构中。然后会从父节点开始分配,直到叶子节点,分配的规则会在调度器部门专门写。最后选出合适的队列,并且从队列里面再根据fair规则分配,检查注册的App是否有合适的需要分配,App中的分配是根据优先级进行的,appmaster>reduce>map,然后再根据本地性找合适的container进行分配。分配成功后会将生成一个RMContainer对象,代表一个运行的container,并且加入到newlyAllocatedContainers结构中。

    // Create RMContainer
    RMContainer rmContainer = new RMContainerImpl(container, 
        getApplicationAttemptId(), node.getNodeID(),
        appSchedulingInfo.getUser(), rmContext);
 
    // Add it to allContainers list.
    newlyAllocatedContainers.add(rmContainer);
    liveContainers.put(container.getId(), rmContainer);  
        // Update consumption and track allocations
    appSchedulingInfo.allocate(type, node, priority, request, container);
    Resources.addTo(currentConsumption, container.getResource());
 
    // Inform the container
    rmContainer.handle(
        new RMContainerEvent(container.getId(), RMContainerEventType.START));

并发送RMContainerEventType.START到dispatcher进行处理。
回到allocate方法,将刚才说到的newlyAllcatedContainers取出,并封装ContainersAndNMTokensAllocation返回,相当于调度成功。

10.RMContainerEventType.START
状态机及处理代码为:

    // Transitions from NEW state
    .addTransition(RMContainerState.NEW, RMContainerState.ALLOCATED,
        RMContainerEventType.START, new ContainerStartedTransition())
 
 
 
   private static final class ContainerStartedTransition extends
      BaseTransition {
 
    @Override
    public void transition(RMContainerImpl container, RMContainerEvent event) {
      container.eventHandler.handle(new RMAppAttemptContainerAllocatedEvent(
          container.appAttemptId));
    }
  }

就是出发RMAppAttemptEventType.CONTAINER_ALLOCATED给dispatcher调用RMAppimpl进行处理。

11.RMAppAttemptEventType.CONTAINER_ALLOCATED
触发RMAppAttemptEventType.CONTAINER_ALLOCATED事件的方法很多,但是目前为止RMAppimpl状态为RMAppAttemptState.SCHEDULED,通过之前的状态就很容易找到状态机处理函数的入口为:

       // Transitions from SCHEDULED State
      .addTransition(RMAppAttemptState.SCHEDULED,
          EnumSet.of(RMAppAttemptState.ALLOCATED_SAVING,
            RMAppAttemptState.SCHEDULED),
          RMAppAttemptEventType.CONTAINER_ALLOCATED,
          new AMContainerAllocatedTransition())

处理函数其实很简单,就是从get第9步获取的container,并返回。如果获取失败重新调度。否则store这个attempt,触发RMStateStoreEventType.STORE_APP_ATTEMPT事件,代码如下:

  public synchronized void storeNewApplicationAttempt(RMAppAttempt appAttempt) {
    Credentials credentials = getCredentialsFromAppAttempt(appAttempt);
 
    ApplicationAttemptState attemptState =
        new ApplicationAttemptState(appAttempt.getAppAttemptId(),
          appAttempt.getMasterContainer(), credentials,
          appAttempt.getStartTime());
 
    dispatcher.getEventHandler().handle(
      new RMStateStoreAppAttemptEvent(attemptState));
  }

12.RMStateStoreEventType.STORE_APP_ATTEMPT
这个事件很简单,就是根据配置是否保存和保存在哪种介质中,最后触发了RMAppAttemptEventType.ATTEMPT_NEW_SAVED事件,并发送到Dispatcher中。

13.RMAppAttemptEventType.ATTEMPT_NEW_SAVED
这个事件回到RMAppattemptImpl中处理,最后触发AMLauncherEventType.LAUNCH事件。

14.AMLauncherEventType.LAUNCH
这个事件由ApplicationMasterLauncher来处理,ApplicationMasterLauncher是ResouceManger中的一个服务,负责向NodeManager发送请求执行AppMaster。
AMLauncherEventType.LAUNCH的处理函数为

  private void launch(RMAppAttempt application) {
    Runnable launcher = createRunnableLauncher(application, 
        AMLauncherEventType.LAUNCH);
    masterEvents.add(launcher);
  }

masterEvents是一个BlockingQueue,ApplicationMasterLauncher有个线程LauncherThread负责调用生成的Runnable。至于与NodeManager交互放到下一篇中介绍。

YARN MRAppMaster与Scheduler流程说明(二)

上一篇主要介绍了一下YARN的基本概念以及Client的提交流程,这篇文章将会从ResourceManager入手,详解一下提交的流程。

ResouceManger接收任务

ResourceManager通过ClientRMService,首先要解析ApplicationSubmissionContext对象,获得提交的id,同时得到用户和提交的组等相应信息。随后生成一个RMApp,并在ResourceManager中注册,RMApp是Job的抽象,每个Job有一个RMApp与其对应。如果一切检验没问题后,正式提交给ResouceManager。
此后进入了状态机转换的部分,将分阶段说明:
1.RMAppEventType.START

      this.rmContext.getDispatcher().getEventHandler()
        .handle(new RMAppEvent(applicationId, RMAppEventType.START));

可以看到事件类型是RMAppEvent,再到ResourceManager中找相应代码,看看异步dispatcher注册的handle类是哪个,代码如下:

      // Register event handler for RmAppEvents
      rmDispatcher.register(RMAppEventType.class,
          new ApplicationEventDispatcher(rmContext));

进入ApplicationEventDipatcher看一下代码为

  @Private
  public static final class ApplicationEventDispatcher implements
      EventHandler<RMAppEvent> {
 
    private final RMContext rmContext;
 
    public ApplicationEventDispatcher(RMContext rmContext) {
      this.rmContext = rmContext;
    }
 
    @Override
    public void handle(RMAppEvent event) {
      ApplicationId appID = event.getApplicationId();
      RMApp rmApp = this.rmContext.getRMApps().get(appID);
      if (rmApp != null) {
        try {
          rmApp.handle(event);
        } catch (Throwable t) {
          LOG.error("Error in handling event type " + event.getType()
              + " for application " + appID, t);
        }
      }
    }
  }

这样我们就知道去找RMApp的实现类,RMAppImpl去找状态机改变及hook函数。其实也可以用比较简单的方法,就是在hadoop yarn代码中用grep去定位到包含RMAppEventType.START的类,基本上就八九不离十了。当然知道原来是首先需要掌握的。
随后看一下RMAppImpl的代码,状态机部分:

    .addTransition(RMAppState.NEW, RMAppState.NEW_SAVING,
        RMAppEventType.START, new RMAppNewlySavingTransition())

这表示状态RMAppEventType.START会触发RMAppNewlySavingTransition的hook函数,同时将RMAppState由NEW转换为NEW_SAVING。
在RMAppNewlySavingTransition的transition函数中调用了storeNewApplication

  public synchronized void storeNewApplication(RMApp app) {
    ApplicationSubmissionContext context = app
                                            .getApplicationSubmissionContext();
    assert context instanceof ApplicationSubmissionContextPBImpl;
    ApplicationState appState =
        new ApplicationState(app.getSubmitTime(), app.getStartTime(), context,
          app.getUser());
    dispatcher.getEventHandler().handle(new RMStateStoreAppEvent(appState));
  }

至此,将新的Event重新发到dispatcher中进行处理。

2.RMStateStoreEventType.STORE_APP
RMStateStoreEventType由RMStateStore类来处理,相应的代码如下:

      if(isRecoveryEnabled) {
        recoveryEnabled = true;
        rmStore =  RMStateStoreFactory.getStore(conf);
      } else {
        recoveryEnabled = false;
        rmStore = new NullRMStateStore();
      }
 
      try {
        rmStore.init(conf);
        rmStore.setRMDispatcher(rmDispatcher);
        rmStore.setResourceManager(rm);
      } catch (Exception e) {
        // the Exception from stateStore.init() needs to be handled for
        // HA and we need to give up master status if we got fenced
        LOG.error("Failed to init state store", e);
        throw e;
      }

默认的我们都是不带recovery,重启后需要重跑,因为recovery读大量目录,时间较慢,可以看到RMStateStore注册dispatcher中。
RMStateStore的handle函数,将event推送给handleStoreEvent方法进行处理,代码如下:

      LOG.info("Storing info for app: " + appId);
      try {
        if (event.getType().equals(RMStateStoreEventType.STORE_APP)) {
          storeApplicationStateInternal(appId, appStateData);
          notifyDoneStoringApplication(appId, storedException);
        } else {
          assert event.getType().equals(RMStateStoreEventType.UPDATE_APP);
          updateApplicationStateInternal(appId, appStateData);
          notifyDoneUpdatingApplication(appId, storedException);
        }
      } catch (Exception e) {
        LOG.error("Error storing app: " + appId, e);
        notifyStoreOperationFailed(e);
      }

处理完后会执行notifyDoneStoringApplication方法

  private void notifyDoneStoringApplication(ApplicationId appId,
                                                  Exception storedException) {
    rmDispatcher.getEventHandler().handle(
        new RMAppNewSavedEvent(appId, storedException));
  }

又向dispatcher中推送了一个RMAppNewSavedEvent的事件。

3.RMAppEventType.APP_NEW_SAVED
同1的处理,状态机最后将event推送到RMAppImpl处理,代码如下:

   .addTransition(RMAppState.NEW_SAVING, RMAppState.SUBMITTED,
        RMAppEventType.APP_NEW_SAVED, new AddApplicationToSchedulerTransition())

处理方式调用AddApplicationToSchedulerTransition

      app.handler.handle(new AppAddedSchedulerEvent(app.applicationId,
        app.submissionContext.getQueue(), app.user));

在ResouceManager中注册了AppAddSchedulerEvent的父类SchdulerEventType

      schedulerDispatcher = createSchedulerEventDispatcher();
      addIfService(schedulerDispatcher);
      rmDispatcher.register(SchedulerEventType.class, schedulerDispatcher);

Scheduler有自己独立的线程去处理,而不去堵塞ResourceManager的处理线程。由于有多重Scheduler,我这里只介绍我们使用的FairScheduler的流程,其它代码查看基本类似。

4.SchedulerEventType.APP_ADDED
FairScheduler的handle方法对多个事件进行处理,APP_ADDED的处理如下

    case APP_ADDED:
      if (!(event instanceof AppAddedSchedulerEvent)) {
        throw new RuntimeException("Unexpected event type: " + event);
      }
      AppAddedSchedulerEvent appAddedEvent = (AppAddedSchedulerEvent) event;
      addApplication(appAddedEvent.getApplicationId(),
        appAddedEvent.getQueue(), appAddedEvent.getUser());
      break;

在addAplication方法中,要获得提交时候生成的RMApp对象,提交的用户以及queue的名称,把任务放到queue中用于后面的调度,调用的代码是assignToQueue,这里会对有效性就行验证。随后在FairScheduler中生成SchedulerApplication对象,代表该任务被调度器接收等待调度,并放到Map<ApplicationId, SchedulerApplication> applications结构中。最后还是通过dispatcher将event转发出去。

    rmContext.getDispatcher().getEventHandler()
        .handle(new RMAppEvent(applicationId, RMAppEventType.APP_ACCEPTED));

5.RMAppEventType.APP_ACCEPTED
还是通过RMAppImpl来处理APP_ACCEPTED事件。处理方法如下:

  private static final class StartAppAttemptTransition extends RMAppTransition {
    @Override
    public void transition(RMAppImpl app, RMAppEvent event) {
      app.createAndStartNewAttempt(false);
    };
  }

这一步就是生成一个attempt,每个app每次执行都生成一个新的attempt,并保存在RMApp的内存结构中,attempt可能由于各种原因导致失败,app就会重新启动一个attempt,默认是超过4次,app就fail掉了。最后通过handler将事件发出处理。

6.RMAppAttemptEventType.START
RMAppAttemptEventType.START由刚刚生成的RMAppAttemptImpl来处理,如下代码:

      .addTransition(RMAppAttemptState.NEW, RMAppAttemptState.SUBMITTED,
          RMAppAttemptEventType.START, new AttemptStartedTransition())

RMAppAttemptEventType.START触发状态由NEW转化为SUBMITTED,并执行AttemptStartedTransition方法。AttemptStartedTransition方法内需要注意的调用为:

      // Register with the ApplicationMasterService
      appAttempt.masterService
          .registerAppAttempt(appAttempt.applicationAttemptId);
 
      // Add the applicationAttempt to the scheduler and inform the scheduler
      // whether to transfer the state from previous attempt.
      appAttempt.eventHandler.handle(new AppAttemptAddedSchedulerEvent(
        appAttempt.applicationAttemptId, transferStateFromPreviousAttempt));
    }

主要就是向applicationMaster注册,并且发起调度的事件,期望能够被Scheduler调度。

7.SchedulerEventType.APP_ATTEMPT_ADDED
FairScheduler处理APP_ATTEMPT_ADDED事件。

    SchedulerApplication application =
        applications.get(applicationAttemptId.getApplicationId());
    String user = application.getUser();
    FSLeafQueue queue = (FSLeafQueue) application.getQueue();
 
    FSSchedulerApp attempt =
        new FSSchedulerApp(applicationAttemptId, user,
            queue, new ActiveUsersManager(getRootQueueMetrics()),
            rmContext);
    if (transferStateFromPreviousAttempt) {
      attempt.transferStateFromPreviousAttempt(application
        .getCurrentAppAttempt());
    }
    application.setCurrentAppAttempt(attempt);
 
    boolean runnable = maxRunningEnforcer.canAppBeRunnable(queue, user);
 
    RMApp rmApp = rmContext.getRMApps().get(attempt.getApplicationId());
    queue.addApp(attempt, runnable,rmApp.getApplicationSubmissionContext().getPriority());
    if (runnable) {
      maxRunningEnforcer.trackRunnableApp(attempt);
    } else {
      maxRunningEnforcer.trackNonRunnableApp(attempt);
    }
 
    queue.getMetrics().submitAppAttempt(user);
    ClusterMetrics.getMetrics().addPendingApp(attempt.getApplicationId());
 
    LOG.info("Added Application Attempt " + applicationAttemptId
        + " to scheduler from user: " + user + " is isAttemptRecovering : " + isAttemptRecovering);
 
    if (isAttemptRecovering) {
      if (LOG.isDebugEnabled()) {
        LOG.debug(applicationAttemptId
            + " is recovering. Skipping notifying ATTEMPT_ADDED");
      }
    } else {
      rmContext.getDispatcher().getEventHandler().handle(
        new RMAppAttemptEvent(applicationAttemptId,
            RMAppAttemptEventType.ATTEMPT_ADDED));
    }

生成FSSchedulerApp,对应了RMApp中生成的RMAppAttempt。然后根据fair得原则,检查该任务是否可以执行,如果超出queue同时运行的任务数把状态置为目前不可运行,否则就是可运行状态。至此,app处于pending状态,根据队列的情况稍后可以调度还是等待转化为可以调度状态。最后通过dispatcher发送event,RMAppAttemptEventType.ATTEMPT_ADDED通过RMAppAttempt来处理。

8.RMAppAttemptEventType.ATTEMPT_ADDED
RMAppAttemptImpl处理ATTEMPT_ADDED事件,调用了SchedulerTransition代码,如下

    public RMAppAttemptState transition(RMAppAttemptImpl appAttempt,
        RMAppAttemptEvent event) {
      //Build blackList for AM
      Set<NodeId> failNodes = appAttempt.getFailMasterNodes();
      List<String> blackList = new ArrayList<String>(failNodes.size());
      for(NodeId node : failNodes) {
        blackList.add(node.toString());
      }
      if (!appAttempt.submissionContext.getUnmanagedAM()) {
        // Request a container for the AM.
        ResourceRequest request =
            BuilderUtils.newResourceRequest(
                AM_CONTAINER_PRIORITY, ResourceRequest.ANY, appAttempt
                    .getSubmissionContext().getResource(), 1);
 
        // SchedulerUtils.validateResourceRequests is not necessary because
        // AM resource has been checked when submission
        Allocation amContainerAllocation = appAttempt.scheduler.allocate(
            appAttempt.applicationAttemptId,
            Collections.singletonList(request), EMPTY_CONTAINER_RELEASE_LIST, blackList, null);
        if (amContainerAllocation != null
            && amContainerAllocation.getContainers() != null) {
          assert (amContainerAllocation.getContainers().size() == 0);
        }
        return RMAppAttemptState.SCHEDULED;
      } else {
        // save state and then go to LAUNCHED state
        appAttempt.storeAttempt();
        return RMAppAttemptState.LAUNCHED_UNMANAGED_SAVING;
      }
    }

这里向Scheduler申请一个container,用于执行appmaster,这个container的优先级是0,可以放到任意的机器上运行,同时将申请时候的内存使用量也写上,container个数为1。
提前说明一下,appmaster的优先级是0,reduce是10,map是20,数字越小越优先调度。随后调用FairScheduler的allocate方法,这是最重要的一个方法,包含了mrappmaster申请,mrappmaster向scheduler申请资源等,放到下一篇继续说明。

Appium的源码编译安装

Appium是现在比较活跃的开源自动化测试平台,因为更新速度很快,建议编译安装,了解其更多有意思的功能。
Appium支持ios android selendroid的自动化测试。在mac下配置ios环境还是相对简单的,但是android真机的配置就不是那么简单了,在此详细记录基于源码的编译安装。

准备工作 node

  • git clone https://github.com/appium/appium.git
  • 安装好node环境(brew安装最好)
  • 安装 mocha 和grunt-cli
npm install -g mocha
npm install -g grunt-cli

android真机配置

因为android虚拟器跑起来非常慢,如果不是专业的android的开发,安装跑andorid studio环境也没有必要
有对应的apk和sdk使用真机就能跑我们的测试脚本了。

准备工作:

  • 安装java jdk 配置JAVA_HOME
  • 安装android jdk,可以在线安装(国内速度超慢),所以快捷的方式是下载adt-bundle,解压后直接可用,下载地址
  • 配置ANDROID_HOME
  • 环境变量的配置代码见下方:
  • 执行环境检测 bin/appium-doctor.js –android 出现如下结果证明android环境配置成功
# ~/.bash_profile的配置内容
# 修改完之后source ~/.bash_profile生效

export ANDROID_HOME=/Users/zhangmeng/Documents/adt-bundle-mac-x86_64-20131030/sdk
export PATH=/Users/zhangmeng/Documents/adt-bundle-mac-x86_64-20131030/sdk/platform-tools:$PATH
export PATH=/Users/zhangmeng/Documents/adt-bundle-mac-x86_64-20131030/sdk/tools:$PATH
export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home
export PATH=$JAVA_HOME/bin:$PATH

配置手机

  • 开启开发者选项,设置-{}开发者选项,如果没有找到,参考
  • 打开USB调试(如下图)
  • 部分手机需要在 连接USB的时候选用 MTP媒体模式才会生效
  • 在命令行执行如下指令,能够列出后(如果不行, 重新插拔一下usb,还可以尝试方法

adb kill-server
adb devices

其中list出来的就是手机的udid,用于后面的测试使用,如下图

执行初始化脚本

按照上面的步骤执行完成之后,运行命令./reset.sh –andorid –verbose即可。
在没有读这个reset.sh脚本的时候真的是被各种的环境搞的头晕脑胀,各种报错,包括:
基本都是有命令运行不通造成的,所以在这里大概介绍一下在appium reset android中的到底做了些什么,帮助大家理解这个启动脚本,以便配合自己的应用解决编译的问题,这个也是源码编译的好处之一,可以及时的解决更新服务。

  • android API 不匹配
  • Device chrome not configured yet
  • uninstall io.appium.android.ime卡住不再运行

reset.sh分析


reset_android() { echo "RESETTING ANDROID" require_java echo "* Configuring Android bootstrap" run_cmd rm -rf build/android_bootstrap run_cmd "$grunt" configAndroidBootstrap echo "* Building Android bootstrap" run_cmd "$grunt" buildAndroidBootstrap reset_unlock_apk reset_unicode_ime reset_settings_apk if $include_dev ; then reset_apidemos reset_toggle_test if $npmlink ; then link_appium_adb fi fi echo "* Setting Android config to Appium's version" run_cmd "$grunt" setConfigVer:android reset_chromedriver }
  • 配置Android bootstrap
    • 删除下build/android_bootstrap目录
    • 执行grunt configAndroidBootstrap:配置UiAutomation需要的编译文件 appium/lib/devices/android/bootstrap/build.xml project.properties local.properties
      • 生成AppiumBootstrap的编译文件:用于运行 android create uitest-project -n AppiumBootstrap -t android-19 -p xx/appium/lib/devices/android/bootstrap/
  • 编译 Android bootstrap
    • grunt buildAndroidBootstrap:使用ant编译AppiumBootstrap.jar,放置到appium/build/android_bootstrap/下
  • 编译apk文件(build目录下)
    • 编译 unlock apk: 唤醒和解锁andorid手机或是虚拟器详情
    • 编译 unicode ime apk: android对ASCII码的支持不好,所以会安装这个utf7的输入法,将sendKeys中的输入转为unicode识别的编码,详情
    • 编译 appium-settings apk:用于控制android系统 详情
  • 如果开启了测试模式 –dev参数
    • 编译sample-code下的app:ToggleTest apiDemos
  • 更新 appium-adb模块:运行./bin/npmlink.sh -l appium-adb
  • 更新appium的版本号
  • reset_chromedriver 详情参考

运行测试用例

  • node . -U 4df752b06833bfd3 (显示下面的提示证明Appium Server能够正常启动)
  • 详细的运行参数参考
  • 运行测试用例 : mocha wd-android-helloworld.js (wd.js
  • 其中支持原生的browser、chrome、还有apk的测试

var wd = require("wd");
var driver = wd.promiseChainRemote({
    host: 'localhost',
    port: 4723
});

driver
    .init({
        browserName: 'Chrome',//Chrome or Browser(原生,默认主页是google建议最好翻墙不然卡住)
        platformName: 'Android',
        platformVersion: '4.4.4',
        deviceName: 'Android Emulator'
      //,app: '/Users/zhangmeng/Downloads/com.taobao.taobao-5.3.1-121.apk' //如果选择测试app的内容 browserName设置为'';
      //执行app后会把对应的apk安装到真机中
    })
    .get('http://www.baidu.com')
    .sleep(5000)
    .title().then(function (title){
        console.log('this is the website title', title)
    })
    .quit()
    .done();

ios 虚拟器配置

配置和启动服务

$ git clone https://github.com/appium/appium.git
$ cd appium
$ ./reset.sh --ios --verbose
$ sudo ./bin/authorize-ios.js # for ios only 修改权限
$ node .

测试脚本

参见 safari-wd-search-test.js

参考

  • https://github.com/appium/appium/blob/master/docs/en/contributing-to-appium/appium-from-source.md
  • https://github.com/appium/appium/blob/master/docs/en/contributing-to-appium/grunt.md
  • http://university.utest.com/android-ui-testing-uiautomatorviewer-and-uiautomator/
  • http://developer.android.com/tools/help/shell.html

YARN MRAppMaster与Scheduler流程说明(一)

基本说明

Hadoop基本上可以分为计算层与存储层, 这篇文章详细说明一下计算层YARN的一些基本流程,概念。其中包括任务提交的过程,调度过程,container运行过程等方面。
下图是Apache社区中YARN的一个基本流程图:
YARN流程
基本过程可以概述为以下部分:
1.Client提交任务
2.ResourceManager接受任务,生成相应结构
3.RM状态机转换,通过Scheduler找到能够运行MRAppmaster的NodeManager
4.ApplicationMasterLauncher向NM发送RPC运行MRAppmaster
5.Node Manager update向RM汇报运行情况,Scheduler调度需要在该Node上运行的container,MRAppmaster通过heartbeat向RM汇报以及fairscheduler根据请求资源情况,返回给MRAppmaster可以运行的container信息
6.MRAppmaster向NM发送RPC运行container
7.NM获得运行时,经过本地化,运行该container

状态机与异步

要想详细说明YARN中的流程,就必须要对其中的一些设计思想进行说明,那就必须要了解YARN中状态机的转变以及异步执行。
了解JobTracker代码的人都应该知道,至于JobTracker扩展的重要因素是JobTracker中记录了全量信息,包括所有Job,Job中的每个MapTask以及ReduceTask的状态,并且所有的执行都是串行执行,大大降低了调度效率。为此YARN首先把Job的职责分为两部分,MRAppmaster负责了task管理,ResourceManager负责调度与节点管理。同时,YARN中大量使用了事件驱动模型,都是通过底层的异步dispatcher来将各个module进行解耦,最大降低了由于串行带来的瓶颈。
此外,YARN与MRV1最大的不同之处就在于状态机的引用,状态机+事件驱动使得YARN的内部调度效率大大提高,但是也带来了代码阅读的极大不方便。下面结合一些代码说明一下如何阅读相应的部分:

  public StateMachineFactory
             &lt;OPERAND, STATE, EVENTTYPE, EVENT&gt;
          addTransition(STATE preState, STATE postState,
                        EVENTTYPE eventType,
                        SingleArcTransition&lt;OPERAND, EVENT&gt; hook){
    return new StateMachineFactory&lt;OPERAND, STATE, EVENTTYPE, EVENT&gt;
        (this, new ApplicableSingleOrMultipleTransition&lt;OPERAND, STATE, EVENTTYPE, EVENT&gt;
           (preState, eventType, new SingleInternalArc(postState, hook)));
  }

基本上YARN中的状态机都是调用这一接口,他的意思是preState是原始状态,postState是变更后状态,eventType是触发状态转变的事件,hook是状态转变后需要执行的钩子。基本上我们只要关注eventType,就能找到需要执行的hook,然后记录下postState即可。而每一种eventType都注册到一个handler进行处理。比如下面看一下RMAppImpl代码:

    .addTransition(RMAppState.NEW, RMAppState.NEW_SAVING,
        RMAppEventType.START, new RMAppNewlySavingTransition())

上面这一过程就是由RMAppEventType.START事件触发,状态由RMAppState.NEW转变为RMAppState.NEW_SAVING,同时执行RMAppNewlySavingTransition函数。

  private static final class RMAppNewlySavingTransition extends RMAppTransition {
    @Override
    public void transition(RMAppImpl app, RMAppEvent event) {
 
      // If recovery is enabled then store the application information in a
      // non-blocking call so make sure that RM has stored the information
      // needed to restart the AM after RM restart without further client
      // communication
      LOG.info("Storing application with id " + app.applicationId);
      app.rmContext.getStateStore().storeNewApplication(app);
    }
  }

调用的是storeNewApplication(app)方法:

  public synchronized void storeNewApplication(RMApp app) {
    ApplicationSubmissionContext context = app
                                            .getApplicationSubmissionContext();
    assert context instanceof ApplicationSubmissionContextPBImpl;
    ApplicationState appState =
        new ApplicationState(app.getSubmitTime(), app.getStartTime(), context,
          app.getUser());
    dispatcher.getEventHandler().handle(new RMStateStoreAppEvent(appState));
  }

可以看出在这一步向disptacher发送了一个event,dispatcher将相应event发送到handler处理,至此流程流动了起来。
下面从作业执行的流程一步步详解。

Client提交任务

因为YARN支持多种任务提交,不仅局限与Map Reduce框架,同时支持Spark,甚至自己写的框架(例如我们开发并在线上一直运行的OLS实时系统),每个框架都需要自己写自己的Client端。这里从简单出发,分析MR framework,有时间会分享一下我们所写的OLS框架。
MR Framework使用JobClient来提交任务。流程很简单,首先是将所有jar包,依赖包,split信息,conf等拷贝到hdfs上,随后通过RPC发送submit。submit之前Client会调用YARNRunner对job进行一个封装,方法是createApplicationSubmissionContext,主要的用途就是把任务所有信息,包括任务名称,提交队列,以及MRAppmaster的启动参数进行设置,为下面MRAppmaster在NodeManager的启动进行准备。
封装好ApplicationSubmissionContext后,Client就向Resoucemanager的服务ClientRMService发送RPC请求,提交任务。
至此,Client的工作完成了。总的来说,Client负责的是任务切分,conf各种设置,MRAppmaster启动脚本生成等任务。

近况 2015-10

最近孩子快要降生,一切很好。
工作方面,最近接手了Hadoop YARN层,整理了一些文档与知识点,最近在博客上发布。如果看到的哪位,如果觉得还可以,我不反对新的工作邀约,联系方式见站内。
博客方面,博客地址近期从美国回游日本,同时近期会整理插件,提高访问速度。

前端自动化测试基础-mocha篇

安装

    npm install -g mocha

命令行用法

常用的命令行为:

   mocha -u bdd -R spec -t 5000 --recursive
  • -u:测试方式 bdd|tdd|exports
  • -R:选择报表的展现方式,报表展现方式,默认为spec,附加的 例如mocha-lcov-reporter(需要自己安装)
  • -t:超时时间设置,当测试中有异步的时候如果超过设定时间会退出测试,默认2s
  • –recursive:默认会把test文件夹和子文件夹中的所有的测试文件执行一遍

详解参考官网Usage

describe it hook

初次接触mocha的人,常常会觉得这几个概念很抽象,用简单的语言概括来说:

  • describe:用于将测试分类,可以嵌套,范围从大到小
  • it:真正包裹测试断言的作用域

  • hook:before beforeEach after afterEach 为测试做辅助的作用域,例如 before中可以执行数据库的初始化,或者检测活动;after中用于清除使用的变量等。

mocha和BDD测试

mocha支持bdd和tdd的测试,支持should/expect的断言方式,常和chai结合在一起使用


//npm install chai之后 var chai = require('chai'); var expect = chai.expect; var Person = function (name) { this.name = name; }; var zhangmeng = new Person('zhangmeng'); describe('zhangmeng attribute', function () { it ('zhangmeng should be a person ', function () { expect(zhangmeng).to.be.an.instanceof(Person); }) });

异步的处理

在javascript的世界 测试异步程序是特别常见的,例如文件的读写、数据库的访问等等,mocha对异步的支持也特别好,你只需要在最里面的函数中增加对应的回调即可,此外mocha是支持promise的

/**
 * @fileOverView mocha-async-demo
 * @author zhangmeng on 15/10/12
 */

//使用异步callback的方式

var fs = require('fs');
var fileName = '/opt/local/share/nginx/html/my-git/f2e-testing/basic/files/name.json';

var chai = require('chai');
var expect = chai.expect;
var Q = require('q');


//使用回调的方式测试

describe('file content validation through callback', function () {
    //读取文件内容
    var fileObj = {};
    before(function (done) {
        //async
        fs.readFile(fileName, 'utf-8', function (err, data) {
            if (err) {
                throw err;
            }
            fileObj = JSON.parse(data);
            done();
        });
    });

    it ('expect name to be zhangmeng', function () {
        var name = fileObj.name;
        expect(name).to.equal("zhangmeng");
    });

    it ('expect name to be zhangmeng', function () {
        var age = fileObj.age;
        expect(age).to.equal('29');
    });
});



//使用promise的方式例子
describe('file content validation through promise', function () {
    var fileObj = {};
    var readFilePromise = function(path, encoding) {
        var encoding = encoding || 'utf-8';
        var deferred = Q.defer();
        fs.readFile(path, encoding, function(err, text) {
            if(err) {
                deferred.reject(new Error(err));
            } else {
                deferred.resolve(text);
            }
        });
        return deferred.promise;
    };

    before('read name.json', function () {
        //return 支持promise的异步
        return readFilePromise(fileName).then(function(data) {
            try {
                fileObj = JSON.parse(data);
            } catch(err) {
                console.log(err);
            }
        })
    });

    it ('name should be zhangmeng', function () {
       var name = fileObj.name;
       expect(name).to.equal('zhangmeng');
    });

    it ('age should be 29', function () {
        var age = fileObj.age;
        expect(age).to.equal('29');
    });
});

执行顺序

关于it和hook之间的顺序,有时非常容易混淆,先上结论:

  • beforeEach会对当前describe下的所有子case生效。
  • before和after的代码没有特殊顺序要求。
  • 同一个describe下可以有多个before,执行顺序与代码顺序相同。
  • 同一个describe下的执行顺序为before, beforeEach, afterEach, after(*),见下例。
  • 当一个it有多个before的时候,执行顺序从最外围的describe的before开始,其余同理。
  • 当没有it的时候,before还有beforeEach的内容都不会执行(*)
  • it的内容是按照顺序执行的 即使前面的it的内容完成的时间偏后,也会按照顺序执行(*)

describe('earth', function(){ beforeEach(function(){ console.log('see.. this function is run EACH time, before each describe()') }) describe('singapre', function(){ before(function () { console.log('it will happen before beforeEach and only once') }) it('birds should fly', function(){ /** ... */ }) it('horse should gallop', function(){ /** ... */ }) }) describe('malaysia', function(){ it('birds should soar', function(){ /** ... */ }) }) }) //执行结果 //earth //singapre //it will happen before beforeEach and only once //see.. this function is run EACH time, before each describe() //✓ birds should fly //see.. this function is run EACH time, before each describe() //✓ horse should gallop //malaysia //see.. this function is run EACH time, before each describe() //✓ birds should soar

源码

linux no login user heap dump

有时候我们使用no login用户启动java进程,当需要进行heap dump等操作的时候需要使用以下命令进行。

sudo su - user -c "jmap -dump:format=b,file=dump.bin pid" -s /bin/bash

前端UI自动化测试

测试手段

UI测试目前主要有方式:

  • record-and-replay: 主要是指利用录制工具去记录用户的行为,并且把这种“行为“存储到脚本中,以便将来用于检测程序或应用是否能够产出预期的效果。常用的record-and-replay工具有:微软的RPF以及google早期出品的abite
  • e2e测试(end-to-end testing):这种测试方式不光可以测试UI层,还可以将整个系统的功能进行测试。通常这种测试会使用第三方的测试工具作为测试doubles层以提升测试效率。

测试内容

没人可以否认UI测试是耗时且昂贵的,所以在写测试的时候一定要慎重的选择使用UI测试的case,下图就是一种比较“聪明”的UI测试架构。我们可以将UI层进行拆分:视图层还有UI逻辑层。如果大家知道 MVX 这种架构,就会知道,UI逻辑层更像是 MVX 中的Controller层和Model层,视图层是比较难以测试和描述的,因此不建议将对视图层的内容作为UI测试的重点,当然我们也可以使用简单的spec来描述视图层的内容,或是对于视图的样式等使用 galenframework类似的框架进行测试 (后面的blog会专门介绍这个框架,它脱离了phantomCss的检测方式,使用特殊的spec方式来描述case,对于前端来说,非常值得学习)。

因此我们更多的测试会围绕UI逻辑层进行。UI逻辑层主要的用途如下,因此我们的case就围绕着对这两部分功能的测试进行编写。

  • 用户和浏览器的交互(操作和更新html)
  • 监听html的事件并且将信息通过request传递给后台

测试框架

UI测试框架主要由两部分构成:客户端的Test环境和测试服务,测试框架的基本原理很简单,本着经济有效的原则,设计了这款使用开源技术的UI测试框架,跨平台、支持多语言、且支持PC端和mobile端的测试方案,本人是前端,所以下例都是基于Nodejs/javascript书写。

UI测试服务端的构建

对于UI测试的服务端平台来说,非常欣赏BrowserStack这个测试平台。实时的、Web-based、多语言,多浏览器、多机型支持,API和接口全面丰富的基于云端的测试平台,除了价格比较贵($39/month),绝对是最完的测试利器。
对于UI测试来说,浏览器宿主环境是非常重要的,而服务端的Hub架构就是通过代理服务器的方式帮你操纵各种类型的浏览器进行自动化测试。在此我们选择了selenium-standalone来实现pc端的server(内置Jetty服务器);appium这个node服务器作为mobile端的server hub。

Selenium-standalone

selenium-standalone支持node安装方式,通过下列脚本可以安装执行,同时可以配置对应的hub信息。

    npm install selenium-standalone@latest -g
    selenium-standalone start -- -role node -hub http://localhost:4444/grid/register -port 5556
  • selenium默认支持的浏览器为Firefox和phantom,如果要使用它操纵其他的浏览器参考如下方式安装对应驱动:
  • chrome:selenium-standalone install –drivers.chrome.version=2.15 –drivers.chrome.baseURL=http://chromedriver.storage.googleapis.com
  • safari:下载,并在safari中安装SafariDriver.safariextz插件
  • ie:selenium-standalone install –drivers.chrome.version=2.15 –drivers.chrome.baseURL=http://chromedriver.storage.googleapis.com

Appium

####简介
mobile端的开发越来越火热,为了保证开发质量,也有很多针对移动端的测试工具应运而生。Appium就是其中很活跃的开源框架。本质上它包括两部分内容:

  • 基于express的server用于发送/接收client端的协议命令
  • 作为bootstrap客户端用于将命令传递给对应的UIAutomator/UIAutomation/Google’s Instrumentation

Appium最大的特色就是支持ios/android/firefoxos多种平台的测试,native、h5、hybrid都支持,以及所有支持jsonWireProtocal协议的脚本语言:python,java,nodejs ruby都可以用来书写用例

####安装

因为Appium的社区发展的很快,建议使用源码编译使用,而不是使用AppiumGUI(它本身是由第三方社区维护,并不属于appium的核心产品 所以很多bug更新的并不及时,例如测试h5页面的时候页面会出现),此外还可以根据自己的要求修改源码和调试,下面就简要介绍一下源码安装的方法, 安装详细方法 请见 Running Appium from Source

  • 配置IOS环境
    • xcode安装好
  • 配置Andorid环境
    • java jdk 配置好并设置好JAVA_HOME
    • android sdk安装并配置好ANDROID_HOME
    • 建议在真机下进行测试(模拟器启动速度慢),参见executing_test_on_real_devices
  • 运行下方代码
  • 以IOS为例:编译安装并启动的结果如下:
     git clone https://github.com/appium/appium.git
     cd appium
     ./reset.sh --verbose #感谢g*f*w 安装过程痛苦而漫长,使用--verbose显示日志吧,至少知道在哪里卡住
     sudo ./bin/authorize-ios.js # for ios only modify /etc/authorization
     node .

如果需要详细的server启动配置,请参考Appium server arguments,例如 只想实现针对safari进行h5页面的自动化测试,配置参数为:

    node . --safari

UI测试客户端框架

前面提到了jsonWireProtcal协议,主要用于客户端的Testcase中定义对浏览器的操作,实现了这个协议的框架和语言有很多,这个大家自行选择。协议形如

GET /session/:sessionId/screenshot
Take a screenshot of the current page.

个人比较欣赏wd.js这个框架,它是一个webdriver/selenium 2的node端实现,各种异步promise支持,自定义方法非常方便,同时支持mocha和chai的无缝嵌入。

简单用法

var wd = require("wd");
var driver = wd.promiseChainRemote({
    protocol: 'http:',
    hostname: '127.0.0.1',
    port: '4444',
    path: '/wd/hub'
});

driver
    .init({browserName: 'safari'})
    .get('http://www.baidu.com')
    .sleep(5000)
    .title().then(function (title){
        console.log('this is the website title', title)
    })
    .quit();

chain和promise的写法

将异步转化为Q chain的链式调用方式,内置Q
支持自定义的promise,代码如下所示,详细代码见github

/**
 * @fileOverView wd-promise wd 链式调用实例
 * @author zhangmeng on 15/10/4
 */

var wd = require("wd");
//内置Q chain
var Q = wd.Q;
var browser = wd.promiseChainRemote({
    protocol: 'http:',
    hostname: '127.0.0.1',
    port: '4444',
    path: '/wd/hub'
});

/**
 * 自定义链式调用用于实现drag 和 drop的操作
 * @param fromElm cssSelector
 * @param toElm cssSelector
 * @returns {Function} browser
 */
var dragNdrop = function (fromElm, toElm) {
    return function () {
        return Q.all([
            browser.elementByCssSelector(fromElm),
            browser.elementByCssSelector(toElm)
        ]).then(function (els) {
            console.log(els);
            return browser
                    .moveTo(els[0])
                    .buttonDown()
                    .moveTo(els[1])
                    .buttonUp();
        });
    }
};

browser
    .init({browserName:'chrome'})
    .get('http://localhost:63342/my-git/f2e-testing/ui-wd-tests/test-html/test-dragNdrop.html')
    //chain link
    .then(dragNdrop('.dragable','.dropable'))
    .sleep(1000)
    .fin(function() { return browser.quit(); })
    .done();

Asserter用法和自定义Asseter

wd.js内置了基本的Asserter,同时支持自定义的断言。多数结合waitfor“句式“使用。这个在实际中经常应用,例如当页面中某个元素出现特定状态的时候去做某事,或者是判断某异步的加载完成的时候执行某操作等。

内置的判断包括
– nonEmptyText
– isDisplayed
– isNotDisplayed
– textInclude
– jsCondition
– isVisible
– isHidden
– jsCondition(常用)

waitfor包括:

  • waitFor
  • waitForElementByCss(elem, asserter, timeout, pollFreq, callback)(常用,判定当某元素存在,且满足某asserter的时候调用回调)
  • waitForConditionInBrowser(jsExpression) 需要设置异步超时时间,setAsyncScriptTimeout

如果上述都不满足还可以自定义Asserter,下面是对应的例子,使用多种方法判断ajax加载完成后进行测试内容,详情见wd-asserter.js

//自定义方法
var tableHasBeenLoaded = new Asserter(
    function(browser, cb) {
        var jsConditionExpr = '($("#tbody tr").length > 0) ? true: false';
        var _eval = browser.eval;
        _eval.apply( browser , [jsConditionExpr, function(err, res) {
            if(err) {return cb(err);}
            cb(null, res, res);
        }]);
    }
);
browser
    .init({browserName: 'chrome'})
    .setAsyncScriptTimeout(30000)
    .get('http://localhost:63342/my-git/f2e-testing/ui-wd-tests/test-html/test-assert.html')
    //------------- case2 jsCondition  waitForConditionInBrowser new Asserter waitForAjaxLoaded -----
    .elementByCss('#getBtn')
    .click() //click to trigger ajaxloading
    //.waitFor(tableHasBeenLoaded, 4000)
    .execute('alert("ajax finished")')
    .sleep(2000)
    .fin(function () {
        return browser.quit();
    })
    .done();

自定义操作方法

使用wd.PromiseChainWebdriver.prototype可以将自定义的方法chain到链式调用中去,同时还可以使用promise来实现,例如上面dragNdrop的例子

//method1 of self-defined method
wd.PromiseChainWebdriver.prototype.waitForAjaxLoaded = function (timeout) {
    //this为browser内容
    return this.waitFor(tableHasBeenLoaded, timeout)
}
//method2

function selfDefinedFunction() {
    return browser.xxxxx
}

browser.init().get().selfDefinedFunction().xx

插入js代码

在测试的实际应用中,经常需要引入需要的类库或者辅助代码来实现测试的目的,那么应该怎么操作呢,wd.js按照jsonWireProtocal是支持执行js代码的,一般通过下面两个方法。最常见的是要测的代码中是没有对应的类库的 如果要使用,例如jquery kissy,那么需要预先inject对应的代码,类似js bookmark书签,或者chrome的插件中的content_script代码。具体代码参见wd-jsinject.js
– execute():执行同步代码
– executeAsync():执行的内容中含有异步的内容

//load.js 用于load javascript类库
var loadScript = function (scriptUrl, callback) {
    var script = document.createElement('script');
    var head = document.getElementsByTagName('head')[0];
    var done = false;
    script.onload = script.onreadystatechange = (function() {
        if (!done && (!this.readyState || this.readyState == 'loaded'
            || this.readyState == 'complete')) {
            done = true;
            script.onload = script.onreadystatechange = null;
            head.removeChild(script);
            callback();
        }
    });
    script.src = scriptUrl;
    head.appendChild(script);
};
loadScript = loadScript(arguments[0], arguments[arguments.length - 1]);
//loadScript('//cdn.bootcss.com/jquery/2.1.4/jquery.js');

//dom.js 判断类库是否正确引入,设置
Fn = {};
var appendChild = setTimeout(function() {
    $("#i_am_an_id").append('<div class="child">I am the child</div>')
}, arguments[0]);

var removeChildren = function () {
    $("#i_am_an_id").empty();
};

Fn = {
    appendChild: appendChild,
    removeChildren: removeChildren
};

//定义object方便链式操作中调用
window.Fn = Fn;

//wd-jsInject.js

var jsFileToString = function (filePath) {
    var file = fs.readFileSync(filePath, "utf8");
    return file;
};
//读取本地的代码
var codeUrl = '/opt/local/share/nginx/html/my-git/f2e-testing/ui-wd-tests/scripts/dom.js';
//加载jquery等类库
var loadUrl = '/opt/local/share/nginx/html/my-git/f2e-testing/ui-wd-tests/scripts/load.js';

//读取js代码(自动转化为jsExpression)
var executeStr = jsFileToString(codeUrl);
var loadScriptStr = jsFileToString(loadUrl);

browser
      .init({browserName:'chrome'})
      .get('http://localhost:63342/my-git/f2e-testing/ui-wd-tests/test-html/test-injectjs.html')
      //inject jquery
      .setAsyncScriptTimeout(30000)
      .executeAsync(loadScriptStr, ["//cdn.bootcss.com/jquery/2.1.4/jquery.js"])
      .execute(executeStr)
      //测试jquery是否正常引入
      .execute('Fn.appendChild', [1000])
      .execute('Fn.removeChildren()')
      .sleep(2000)
      .fin(function() { return browser.quit(); })
      .done();

结合mocha和chai

mocha是用于测试的框架,chai用于辅助断言,wd.js支持两者的无缝接入,可以使ui测试变得像单元测试一样简单。参考下面的demo,就是把三者结合在一起,通过wd对appium访问ios虚拟机,对手机淘宝搜索结果页进行UI测试的例子,代码详见Github F2E-testing UI test

require('../helpers/setup');
var wd = require("wd");
var serverConfig = require('../helpers/server').appium;
var desired = require('../helpers/caps').ios90s;
var begin_page_url = 'http://s.m.taobao.com/h5?search-btn=&event_submit_do_new_search_auction=1&_input_charset=utf-8&topSearch=1&atype=b&searchfrom=1&action=home%3Aredirect_app_action&from=1';

describe('test page of taobao search', function () {
    this.timeout(300000);
    var driver;
    before(function () {
        driver = wd.promiseChainRemote(serverConfig);
        require("../helpers/logger").configure(driver);//显示日志
        return driver.init(desired);
    });

    after(function () {
        return driver.quit();
    });

    //1打开淘宝搜索页面
    //2点击搜索框
    //3进入到搜索结果页面
    it("should open iphone+6s search page", function () {
        var inputValue = 'iphone 6s';
        return driver
              .get(begin_page_url)
              .sleep(1000)
              .waitForElementByName('q', 2000)
              .sendKeys(inputValue)
              .waitForElementByName('search')
              .tap()
              .sleep(5000)
              .eval('window.location.href')
              .should.eventually.include('q=iphone+6s')
    });


});