TestNG源码分析05-初始化01

initializeEverything

从上面文章我们知道TestNG先执行了初始化,即执行了initializeEverything方法,上代码

/** Invoked by the remote runner. */
  public void initializeEverything() {
    // The Eclipse plug-in (RemoteTestNG) might have invoked this method already
    // so don't initialize suites twice.
    if (m_isInitialized) {
      return;
    }

    initializeSuitesAndJarFile();
    initializeConfiguration();
    initializeDefaultListeners();
    initializeCommandLineSuites();
    initializeCommandLineSuitesParams();
    initializeCommandLineSuitesGroups();

    m_isInitialized = true;
  }

代码结构简单明了,连续执行了6个方法,目的就是构建XmlSuite对象

initializeSuitesAndJarFile

先看第一个initializeSuitesAndJarFile() 方法,在IDEA插件那节我们提到过,这个方法会被IDEARemoteTestNG中重写的run方法先调用一次,这里需要大家关注下

这个方法的目的就是通过命令行或者jarPath是的配置文件,解析成XmlSuite并放入到m_suites中,
m_suites是一个XmlSuite 列表

protected List m_suites = Lists.newArrayList();

public void initializeSuitesAndJarFile() {
    // The IntelliJ plug-in might have invoked this method already so don't initialize suites twice.
    //这里很清楚的描述了这个方法可能已经被idea插件调用,通过标识符 isSuiteInitialized 来控制是否已经执行过
    if (isSuiteInitialized) {
      return;
    }
    //第一次执行时设置isSuiteInitialized 为true,避免多次执行
    isSuiteInitialized = true;
	
	/**
	  * 判断m_suites是否为空,这里的 m_suites 是一个 List<XmlSuite>,不为空时执行解析逻辑
	  * 通过这里我们可以看到,m_suites 不为空时,解析完直接返回了
	  * 如果我们通过Java程序手动构建一个 List<XmlSuite> 并赋值给 m_suites,
	  * 这样就可以抛弃XML的配置,通过自定义的方式生成要运行类和方法,毕竟xml最终也是要解析成 XmlSuite 对象 
	  */
    if (!m_suites.isEmpty()) {
      //解析 这里的逻辑和解析Xml文件差不多,主要是解析m_suites每个suite中的suite-file节点
      parseSuiteFiles(); // to parse the suite files (<suite-file>), if any
      return;
    }

    //
    // Parse the suites that were passed on the command line
	//
    /**
	  * 解析命令行的suite文件,关于命令行参数 m_stringSuites如何解析的,参考Idea插件一节中内容
	  * 这个属性是个IDEARemoteTestNG调用configure方法时设置的
	  * 循环解析每一个命令行xml文件
	  */
    for (String suitePath : m_stringSuites) {
      parseSuite(suitePath);
    }

    //
    // jar path
    //
    // If suites were passed on the command line, they take precedence over the suite file
    // inside that jar path
    //通过命令行 -testjar指定
    // 如果m_stringSuites不为空 就不会使用jar中的suite文件
    if (m_jarPath != null && !m_stringSuites.isEmpty()) {
      StringBuilder suites = new StringBuilder();
      for (String s : m_stringSuites) {
        suites.append(s);
      }
      Utils.log(
          "TestNG",
          2,
          "Ignoring the XML file inside " + m_jarPath + " and using " + suites + " instead");
      return;
    }
    if (isStringEmpty(m_jarPath)) {
      return;
    }

    // We have a jar file and no XML file was specified: try to find an XML file inside the jar
    File jarFile = new File(m_jarPath);
	//-xmlpathinjar指定testng文件
    JarFileUtils utils =
        new JarFileUtils(getProcessor(), m_xmlPathInJar, m_testNames, m_parallelMode);
    
    //utils.extractSuitesFrom(jarFile) 大概原理就是遍历jar中文件,如果找到testng的xml文件
    //则创建临时文件并把内容拷贝出来,否则就把所有的class执行
    m_suites.addAll(utils.extractSuitesFrom(jarFile));
  }

parseSuite 从命令行解析

private void parseSuite(String suitePath) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("suiteXmlPath: \"" + suitePath + "\"");
    }
    try {
      //内部调用了Parser.parse方法去解析XmlSuite对象
      Collection<XmlSuite> allSuites = Parser.parse(suitePath, getProcessor());
      
      for (XmlSuite s : allSuites) {
        //设置当前Suite的并发级别 默认为NULL
        if (this.m_parallelMode != null) {
          s.setParallel(this.m_parallelMode);
        }
        //设置当前Suite的并发线程数量 默认为-1
        if (this.m_threadCount > 0) {
          s.setThreadCount(this.m_threadCount);
        }
        //m_testNames为空时 直接加入到 m_suites 中
        if (m_testNames == null) {
          m_suites.add(s);
          continue;
        }
        // If test names were specified, only run these test names
        //命令行可以 以-testnames 指定test的name,如果指定只运行指定的test节点就可以
        //参考下方具体代码
        TestNamesMatcher testNamesMatcher = new TestNamesMatcher(s, m_testNames);
        List<String> missMatchedTestname = testNamesMatcher.getMissMatchedTestNames();
        if (!missMatchedTestname.isEmpty()) {
          throw new TestNGException("The test(s) <" + missMatchedTestname + "> cannot be found.");
        }
        m_suites.addAll(testNamesMatcher.getSuitesMatchingTestNames());
      }
    } catch (IOException e) {
      e.printStackTrace(System.out);
    } catch (Exception ex) {
      // Probably a Yaml exception, unnest it
      Throwable t = ex;
      while (t.getCause() != null) {
        t = t.getCause();
      }
      if (t instanceof TestNGException) {
        throw (TestNGException) t;
      }
      throw new TestNGException(t);
    }
  }
private OverrideProcessor getProcessor() {
	/**
	  * -groups 命令行参数赋值给 m_includedGroups
	  * excludegroups 命令行参数赋值给 m_excludedGroups
	  */
    return new OverrideProcessor(m_includedGroups, m_excludedGroups);
}

在这个方法内部只是调用了 Parser.parse(suitePath, getProcessor()); 方法去解析xml文件,并且传入了一个 IPostProcessor 接口实例,即 OverrideProcessor, 目的是通过命令行的参数指定哪些分组可以运行,哪些分组被排除不可以运行,与指定test名称目的是一致的

Parser.parse 解析

public static Collection<XmlSuite> parse(String suite, IPostProcessor processor)
      throws IOException {
    return newParser(suite, processor).parse();
}
//调用静态方法生成 Parser 对象
private static Parser newParser(String path, IPostProcessor processor) {
    Parser result = new Parser(path);
    result.setPostProcessor(processor);
    return result;
}

public Parser(String fileName) {
    init(fileName, null);
}

/** The default file name for the TestNG test suite if none is specified (testng.xml). */
public static final String DEFAULT_FILENAME = "testng.xml";
  
private void init(String fileName, InputStream is) {
	/** 
	  * 把 fileName 赋值给 m_fileName
	  * 如果为空 默认解析 testng.xml
	  */
    m_fileName = fileName != null ? fileName : DEFAULT_FILENAME;
    m_inputStream = is;
}

public Collection<XmlSuite> parse() throws IOException {
    // Each suite found is put in this list, using their canonical
    // path to make sure we don't add a same file twice
    // (e.g. "testng.xml" and "./testng.xml")
    //已经遍历的文件列表
    List<String> processedSuites = Lists.newArrayList();
    XmlSuite resultSuite = null;
	//待解析文件列表
    List<String> toBeParsed = Lists.newArrayList();
    //添加的列表 这个用来保存suite-files节点中的xml文件
    List<String> toBeAdded = Lists.newArrayList();
    //删除的列表
    List<String> toBeRemoved = Lists.newArrayList();
	
	//获取xml文件的路径
    if (m_fileName != null) {
      URI uri = constructURI(m_fileName);
      if (uri == null || uri.getScheme() == null) {
        uri = new File(m_fileName).toURI();
      }
      if ("file".equalsIgnoreCase(uri.getScheme())) {
        File mainFile = new File(uri);
        //getCanonicalPath() 返回资源惟一的规范形式
        //把资源文件加入待解析列表
        toBeParsed.add(mainFile.getCanonicalPath());
      } else {
        toBeParsed.add(uri.toString());
      }
    }

    /*
     * Keeps a track of parent XmlSuite for each child suite
     * 这里key保存的子suite的路径,value为一个双端队列,队列中放入的是父suite
     */
    Map<String, Queue<XmlSuite>> childToParentMap = Maps.newHashMap();
    
    while (!toBeParsed.isEmpty()) {
      for (String currentFile : toBeParsed) {
        File parentFile = null;
        InputStream inputStream = null;
        //把文件转换成 InputStream 流
        if (hasFileScheme(currentFile)) {
          File currFile = new File(currentFile);
          parentFile = currFile.getParentFile();
          inputStream = m_inputStream != null ? m_inputStream : new FileInputStream(currFile);
        }
		//获取 IFileParser 实例 默认是 SuiteXmlParser
        IFileParser<XmlSuite> fileParser = getParser(currentFile);
        //解析文件生成 XmlSuite 
        XmlSuite currentXmlSuite = fileParser.parse(currentFile, inputStream, m_loadClasses);
		//设置已经解析标识
        currentXmlSuite.setParsed(true);
        //把解析过的文件加入到已解析文件列表
        processedSuites.add(currentFile);
        //加入到准备删除列表
        toBeRemoved.add(currentFile);
        
	    /*
		 * 绑定XmlSuite的父子关系,一般这里也是针对suite-files节点用的,
		 * 第一次循环解析父节点时不会进入到此逻辑,第二次循环子节点会进入此逻辑
		 */
        if (childToParentMap.containsKey(currentFile)) {
          XmlSuite parentSuite = childToParentMap.get(currentFile).remove();
          // Set parent
          currentXmlSuite.setParentSuite(parentSuite);
          // append children
          parentSuite.getChildSuites().add(currentXmlSuite);
        }
		// 把解析的currentXmlSuite赋值给 resultSuite 属性
        if (null == resultSuite) {
          resultSuite = currentXmlSuite;
        }
		
		/**
		  * 如果testng.xml中有 suite-files节点 类似于下面这样 下面这段代码就会执行
		  * <suite-files> 
          *      <suite-file path="xx.xml"/>  
  		  *		 <suite-file path="xx.xml"/> 
		  * </suite-files>
		  */
        List<String> suiteFiles = currentXmlSuite.getSuiteFiles();
        if (!suiteFiles.isEmpty()) {
          for (String path : suiteFiles) {
            String canonicalPath = path;
            if (hasFileScheme(path)) {
              //优先从当前xml的父目录中找对应的xml文件,没有则认为是全路径,走else逻辑
              if (parentFile != null && new File(parentFile, path).exists()) {
                canonicalPath = new File(parentFile, path).getCanonicalPath();
              } else {
                canonicalPath = new File(path).getCanonicalPath();
              }
            }
            //如果当前xml已经被解析过则不再次解析,已经解析过的文件会放到processedSuites中
            if (!processedSuites.contains(canonicalPath)) {
              // 加入到要添加的列表中
              toBeAdded.add(canonicalPath);
              if (childToParentMap.containsKey(canonicalPath)) {
                childToParentMap.get(canonicalPath).add(currentXmlSuite);
              } else {
              	//双端队列
                Queue<XmlSuite> parentQueue = new ArrayDeque<>();
                parentQueue.add(currentXmlSuite);
                //子节点加入childToParentMap
                childToParentMap.put(canonicalPath, parentQueue);
              }
            }
          }
        }
      }

      //
      // Add and remove files from toBeParsed before we loop
      //
      /*
	   * 1.把待解析列表中待删除的列表(已经解析)的删除,
	   * 2.重置要删除的列表
	   * 3.待解析列表加入要添加的列表(子suite xml文件)
	   * 4.重置要添加文件列表 为下一次循环做准备
	   */
      toBeParsed.removeAll(toBeRemoved);
      toBeRemoved = Lists.newArrayList();

      toBeParsed.addAll(toBeAdded);
      toBeAdded = Lists.newArrayList();
    }

    // returning a list of single suite to keep changes minimum
    /**
      * 把解析的结果放入到列表中并返回
      * 这里有个疑问就是解析的结果是个XmlSuite对象,由于XmlSuite也有父子关系
      * 不知道为什么作者要返回一个列表对象?
      */
    List<XmlSuite> resultList = Lists.newArrayList();
    resultList.add(resultSuite);
	
	// 对解析结果进行后置处理
    if (m_postProcessor != null) {
      return m_postProcessor.process(resultList);
    } else {
      return resultList;
    }
}
process 分组后置处理
public class OverrideProcessor implements IPostProcessor {

  private String[] m_groups;
  private String[] m_excludedGroups;

  public OverrideProcessor(String[] groups, String[] excludedGroups) {
    m_groups = groups;
    m_excludedGroups = excludedGroups;
  }
  /**
    * 循环遍历Suite下的每个XmlTest,设置要运行的组和排除的组
    * 这里可以看到 这个只针对当前Suite,对子Suite无效
    */
  @Override
  public Collection<XmlSuite> process(Collection<XmlSuite> suites) {
    for (XmlSuite s : suites) {
    
      if (m_groups != null && m_groups.length > 0) {
        for (XmlTest t : s.getTests()) {
          t.setIncludedGroups(Arrays.asList(m_groups));
        }
      }
      
      if (m_excludedGroups != null && m_excludedGroups.length > 0) {
        for (XmlTest t : s.getTests()) {
          t.setExcludedGroups(Arrays.asList(m_excludedGroups));
        }
      }
    }
    return suites;
  }
}
TestNamesMatcher Test匹配
/** The class to work with "-testnames" */
//本类省略部分代码
public final class TestNamesMatcher {

  private final List<XmlSuite> cloneSuites = Lists.newArrayList();
  private final List<String> matchedTestNames = Lists.newArrayList();
  private final List<XmlTest> matchedTests = Lists.newArrayList();
  private final List<String> testNames;

  public TestNamesMatcher(XmlSuite xmlSuite, List<String> testNames) {
    this.testNames = testNames;
    //调用此方法
    cloneIfContainsTestsWithNamesMatchingAny(xmlSuite, this.testNames);
  }

  //这个方法是递归调用 核心逻辑是在cloneIfSuiteContainTestsWithNamesMatchingAny中
  private void cloneIfContainsTestsWithNamesMatchingAny(XmlSuite xmlSuite, List<String> testNames) {
    if (testNames == null || testNames.isEmpty()) {
      throw new TestNGException("Please provide a valid list of names to check.");
    }

    // Start searching in the current suite.
    //addIfNotNull把匹配到的结果放到cloneSuites 列表中
    addIfNotNull(cloneIfSuiteContainTestsWithNamesMatchingAny(xmlSuite));

    // Search through all the child suites.
    for (XmlSuite suite : xmlSuite.getChildSuites()) {
      cloneIfContainsTestsWithNamesMatchingAny(suite, testNames);
    }
  }

  private XmlSuite cloneIfSuiteContainTestsWithNamesMatchingAny(XmlSuite suite) {
    List<XmlTest> tests = Lists.newLinkedList();
    //循环遍历Suite下的所有XmlTest节点,只有test的名字包含在指定命令行中,就把要执行的放入到
    //matchedTests matchedTestNames
    for (XmlTest xt : suite.getTests()) {
      if (xt.nameMatchesAny(testNames)) {
        tests.add(xt);
        matchedTestNames.add(xt.getName());
        matchedTests.add(xt);
      }
    }
    if (tests.isEmpty()) {
      return null;
    }
    return cleanClone(suite, tests);
  }
  
  //nameMatchesAny是XmlTest中的方法 我们只粘出部分 只是调用了contains方法
  public boolean nameMatchesAny(List<String> names) {
    return names.contains(getName());
  }

  //cleanClone 把XmlSuite克隆,清空Test节点,只保留匹配到的节点并返回克隆对象
  private static XmlSuite cleanClone(XmlSuite xmlSuite, List<XmlTest> tests) {
    XmlSuite result = (XmlSuite) xmlSuite.clone();
    result.getTests().clear();
    result.getTests().addAll(tests);
    return result;
  }
  
  //匹配后调用此方法 判断是否所有指定的test都找到了,为空证明全部找到,否则会有未找到的
  public List<String> getMissMatchedTestNames() {
    List<String> tmpTestNames = Lists.newArrayList();
    //把所有要找的test放到tmpTestNames 中并剔除已经匹配到的 matchedTestNames中包证明已找到
    tmpTestNames.addAll(testNames);
    tmpTestNames.removeIf(matchedTestNames::contains);
    return tmpTestNames;
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值