TestNG源码分析03-IDEA插件

插件运行

一般我们使用IDEA执行测试都是通过如下图的方式
在这里插入图片描述

  1. 直接点击第5行绿色按钮运行整个测试类,如果有多个测试 方法则都会运行
  2. 直接点击第8行绿色按钮运行某个具体方法

这一点我们也可以通过IDEA的Run/Debug Configurations可以看到区别,如果我们通过第一种方式运行,Configuration下的TestKind为Class,如果通过第二种方式运行,Configuration下的TestKind为Method
在这里插入图片描述
在这里插入图片描述

插件调用

上节内容我们知道,TestNG框架的核心类就是org.testng.TestNG这个类,无论如何TestNG最终会调用到run方法执行测试,那插件启动也会调用到么,我们可以通过Debug方式去看下调用栈信息

如前面代码我们在第9行打个断点,以Debug方式运行,截图如下
在这里插入图片描述
通过截图我们可以很清楚的看到,最终调用TestNG类的run方法,不过在执行run方法之前,有两个类的方法要运行,如下

  • com.intellij.rt.testng.RemoteTestNGStarter#main
  • com.intellij.rt.testng.IDEARemoteTestNG#run

通过两个类的全限类名我们应该可以猜到这两个类是IDEA插件中的类,我们通过IDEA是无法查看这两个类的。还记得第一节中提到的idea插件源码吗?在 git hub testng_rt 中可以找到这两个类,下面我们就简单分析下这两个类吧,看下有些什么内容。

这里要说明的问题就是,由于IDEA使用插件代码版本和下载下来代码并不完全不一致,不过大致内容不一样,如果大家看到Debug代码行数与源码有出入,请忽略

RemoteTestNGStarter

在介绍RemoteTestNGStarter类之前,我们还是先把Debug下,这个类对应的一些变量粘出来,如下图
在这里插入图片描述

RemoteTestNGStarter 整个类只有一个main方法,直接进入主题 看代码中注释,大概步骤如下

  • 解析main方法args参数,把参数赋值给resultArgs
  • 解析临时文件中内容,找到 testng.xml位置
  • 通过JCommander解析参数并赋值给IDEARemoteTestNG对象
  • 执行IDEARemoteTestNG#run方法
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package com.intellij.rt.testng;

import com.beust.jcommander.JCommander;
import com.intellij.rt.execution.testFrameworks.ForkedDebuggerHelper;
import org.testng.CommandLineArgs;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class RemoteTestNGStarter {
    private static final String SOCKET = "-socket";

    public static void main(String[] args) throws Exception {
        int i = 0;
        //参数 @name开头
        String param = null;
        //命令行文件 @@@开头
        String commandFileName = null;
        //工作目录 @w@开头
        String workingDirs = null;
        //最终收集的命令行参数,保证顺序使用了ArrayList
        List<String> resultArgs = new ArrayList<String>();
        //循环遍历传入的参数
        for (; i < args.length; i++) {
            String arg = args[i];
            if (arg.startsWith("@name")) {
                param = arg.substring(5);
                continue;
            } else if (arg.startsWith("@w@")) {
                //获取工作目录,通过debug参数我们这个是有值的
                workingDirs = arg.substring(3);
                continue;
            } else if (arg.startsWith("@@@")) {
                commandFileName = arg.substring(3);
                continue;
            } else if (arg.startsWith(ForkedDebuggerHelper.DEBUG_SOCKET)) {
                //String DEBUG_SOCKET = "-debugSocket";
                continue;
            } else if (arg.startsWith(SOCKET)) {
                //获取端口号
                final int port = Integer.parseInt(arg.substring(SOCKET.length()));
                try {
                    final Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), port);  //start collecting tests
                    final DataInputStream os = new DataInputStream(socket.getInputStream());
                    try {
                        //阻塞式等待,等待准备完毕的标识,这里应该有另一个线程通过此端口写数据
                        os.readBoolean();//wait for ready flag
                    } finally {
                        os.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                continue; //do not add socket to actual params
            } else if (arg.equals("-temp")) {
                //碰到此标识结束 for循环结束时,我们代码运行到此处,args只有前两个参数写到了resultArgs中 即 -usedefaultlisteners false
                break;
            }
            resultArgs.add(arg);
        }

        //通过args最后一个参数获取文件位置
        final File temp = new File(args[++i]);

        final BufferedReader reader = new BufferedReader(new FileReader(temp));

        final List<String> newArgs = new ArrayList<String>();
        try {
            final String cantRunMessage = "CantRunException";
            while (true) {
                //读取文件 我们可以通过上面的路径 直接在浏览器中查看,参考下方图片
                String line = reader.readLine();
                while (line == null) {
                    line = reader.readLine();
                }
                //line=C:\Users\AI\.IntelliJIdea2019.3\system\temp-testng-customsuite.xml
                //逻辑不会走入这里
                if (line.startsWith(cantRunMessage) && !new File(line).exists()) {
                    System.err.println(line.substring(cantRunMessage.length()));
                    while (true) {
                        line = reader.readLine();
                        if (line == null || line.equals("end")) break;
                        System.err.println(line);
                    }
                    System.exit(1);
                    return;
                }
                if (line.equals("end")) break;
                newArgs.add(line);
            }
        } finally {
            reader.close();
        }

        //此时把newArgs所有元素加入到resultArgs中,resultArgs中应该有3个元素 -usedefaultlisteners false C:\Users\AI\.IntelliJIdea2019.3\system\temp-testng-customsuite.xml
        resultArgs.addAll(newArgs);

        //逻辑不会进入
        if (commandFileName != null) {
            if (workingDirs != null && new File(workingDirs).length() > 0) {
                System.exit(new TestNGForkedSplitter(workingDirs, newArgs)
                        .startSplitting(args, param, commandFileName, null));
                return;
            }
        }

        //调用TestNG 不过这里需要注意的是IDEARemoteTestNG 并不是TestNG框架中的类
        /**
         * 仔细看下面这些代码,和我们之前分析TestNG#main方法中逻辑很类似
         * 这里也是先解析参数,赋值,调用run方法运行
         */
        final IDEARemoteTestNG testNG = new IDEARemoteTestNG(param);
        //这个对象熟悉吧,接收命令行参数的
        CommandLineArgs cla = new CommandLineArgs();
        /**
         * 这里我们需要注意就是resultArgs只有三个参数,根据JCommander框架原则 useDefaultListeners属性默认为true,会被修改为false
         * C:\Users\AI\.IntelliJIdea2019.3\system\temp-testng-customsuite.xml这个参数没有对应的命令行参数收集,最终会被收集到suiteFiles属性中
         */
        new JCommander(Collections.singletonList(cla), resultArgs.toArray(new String[0]));
        //赋值
        testNG.configure(cla);
        //运行
        testNG.run();
    }
}

在这里插入图片描述

IDEARemoteTestNG

从IDEARemoteTestNG源码中我们了解到, IDEARemoteTestNG继承TestNG类 并重写run方法
源码中configure方法也被重写,但是内部是直接调用,所以并不是真正的重写

因为configure首先被调用操作赋值,这里我们先从configure方法说起,这里我们只关注 useDefaultListeners属性与suiteFiles属性相关的操作

protected void configure(CommandLineArgs cla) {
    ...省略部分代码 
    // Note: can't use a Boolean field here because we are allowing a boolean
    // parameter with an arity of 1 ("-usedefaultlisteners false")
    //这里根据传入的值选择是否要使用默认监听器,这里的值为false
    if (cla.useDefaultListeners != null) {
      setUseDefaultListeners("true".equalsIgnoreCase(cla.useDefaultListeners));
    }
    ...省略部分代码 
    
	//suiteFiles中是框架给我们生成的testng.xml路径
    if (cla.suiteFiles != null) {
      setTestSuites(cla.suiteFiles);
    }
    
    ...省略部分代码 
  }

  //把suites赋值给 m_stringSuites 属性
  public void setTestSuites(List<String> suites) {
    m_stringSuites = suites;
  }

再看 run方法 大概逻辑如下

  • 通过xml文件解析为XmlSuite对象
  • 改变运行时的一些参数
  • 加入监听器
  • 运行
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.rt.testng;


import org.testng.CommandLineArgs;
import org.testng.TestNG;
import org.testng.collections.Lists;
import org.testng.xml.XmlClass;
import org.testng.xml.XmlInclude;
import org.testng.xml.XmlSuite;
import org.testng.xml.XmlTest;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class IDEARemoteTestNG extends TestNG {

    private final String myParam;

    //有参构造函数
    public IDEARemoteTestNG(String param) {
        //param从上面传过来为null 此参数会被赋值给org.testng.xml.XmlInclude#m_invocationNumbers属性
        myParam = param;
    }

    //递归把suites内容全部放入到 outSuites 对象中
    private static void calculateAllSuites(List<XmlSuite> suites, List<XmlSuite> outSuites) {
        for (XmlSuite s : suites) {
            //先加入 父XmlSuite
            outSuites.add(s);
            //再去加入 子XmlSuite
            calculateAllSuites(s.getChildSuites(), outSuites);
        }
    }

    @Override
    public void configure(CommandLineArgs cla) {
        super.configure(cla);
    }

    @Override
    public void run() {
        try {
          /**
           * 调用 initializeSuitesAndJarFile方法 这个方法后面我们会详细讲解 这里只说出大概逻辑
           * 方法内部就是根据 m_stringSuites属性去解析一个xml文件 生成 XmlSuite 对象
           * 最终把 生成对象放入到 List<XmlSuite> m_suites的属性中
           */
            initializeSuitesAndJarFile();

            List<XmlSuite> suites = Lists.newArrayList();
            calculateAllSuites(m_suites, suites);
            if (suites.size() > 0) {
                for (XmlSuite suite : suites) {
                    final List<XmlTest> tests = suite.getTests();
                    for (XmlTest test : tests) {
                        try {
                            //myParam为空 进不了此逻辑 可以暂时不关心内部逻辑
                            if (myParam != null) {
                                for (XmlClass aClass : test.getXmlClasses()) {
                                    List<XmlInclude> includes = new ArrayList<XmlInclude>();
                                    for (XmlInclude include : aClass.getIncludedMethods()) {
                                        //myParam对应m_invocationNumbers 即失败调用次数
                                        includes.add(new XmlInclude(include.getName(), Collections.singletonList(Integer.parseInt(myParam)), 0));
                                    }
                                    aClass.setIncludedMethods(includes);
                                }
                            }
                        } catch (NumberFormatException e) {
                            System.err.println("Invocation number: expected integer but found: " + myParam);
                        }
                    }
                }
                //插件监听器
                attachListeners(new IDEATestNGRemoteListener());
                //调用run方法
                super.run();
            } else {
                System.out.println("##teamcity[enteredTheMatrix]");
                System.err.println("Nothing found to run");
            }
            System.exit(0);
        } catch (Throwable cause) {
            cause.printStackTrace(System.err);
            System.exit(-1);
        }
    }
	
	/**
      * 监听器我们只讲到这里,后续会有专门的文章介绍监听器,后续完成后我们会具体分析IDEA监听器的作用
      * 需要注意的就是,这里的监听器都有一个 IDEATestNGRemoteListener 的类
      * 实际上IDEA只是在实现接口时,每个实现类内部都持有一个 IDEATestNGRemoteListener 的引用
      * 方法具体实现都是通过IDEATestNGRemoteListener来完成
      */
    private void attachListeners(IDEATestNGRemoteListener listener) {
        //ISuiteListener
        addListener((Object) new IDEATestNGSuiteListener(listener));
        //TestListener
        addListener((Object) new IDEATestNGTestListener(listener));
        try {
            Class<?> configClass = Class.forName("com.intellij.rt.testng.IDEATestNGConfigurationListener");
            Object configurationListener = configClass.getConstructor(new Class[]{IDEATestNGRemoteListener.class}).newInstance(listener);
            //IConfigurationListener
            addListener(configurationListener);

            Class<?> invokeClass = Class.forName("com.intellij.rt.testng.IDEATestNGInvokedMethodListener");
            Object invokedMethodListener = invokeClass.getConstructor(new Class[]{IDEATestNGRemoteListener.class}).newInstance(listener);
            //IInvokedMethodListener
            addListener(invokedMethodListener);

            //start with configuration started if invoke method listener was not added, otherwise with
            //反射调用IDEATestNGConfigurationListener#setIgnoreStarted方法
            configClass.getMethod("setIgnoreStarted").invoke(configurationListener);
        } catch (Throwable ignored) {
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值