TestNG初始化
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;
}
}