hbase-8018: Add 'Flaky Testcase Detector' tool into dev-tools

git-svn-id: https://svn.apache.org/repos/asf/hbase/trunk@1512876 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
jeffreyz 2013-08-11 06:22:36 +00:00
parent 6de1ab3f2f
commit 4dc52261a1
8 changed files with 695 additions and 0 deletions

View File

@ -0,0 +1,67 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
jenkins-tools
=============
A tool which pulls test case results from Jenkins server. It displays a union of failed test cases
from the last 15(by default and actual number of jobs can be less depending on availablity) runs
recorded in Jenkins sever and track how each of them are performed for all the last 15 runs(passed,
not run or failed)
Pre-requirement(run under folder jenkins-tools)
Please download jenkins-client from https://github.com/cosmin/jenkins-client
1) git clone git://github.com/cosmin/jenkins-client.git
2) make sure the dependency jenkins-client version in ./buildstats/pom.xml matches the
downloaded jenkins-client(current value is 0.1.6-SNAPSHOT)
Build command(run under folder jenkins-tools):
mvn clean package
Usage are:
java -jar ./buildstats/target/buildstats.jar <Jenkins HTTP URL> <Job Name> [number of last most recent jobs to check]
Sample commands are:
java -jar ./buildstats/target/buildstats.jar https://builds.apache.org HBase-TRUNK
Sample output(where 1 means "PASSED", 0 means "NOT RUN AT ALL", -1 means "FAILED"):
Failed Test Cases 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3632 3633 3634 3635
org.apache.hadoop.hbase.catalog.testmetareadereditor.testretrying 1 1 -1 0 1 1 1 1 -1 0 1 1 1 1
org.apache.hadoop.hbase.client.testadmin.testdeleteeditunknowncolumnfamilyandortable 0 1 1 1 -1 0 1 1 0 1 1 1 1 1
org.apache.hadoop.hbase.client.testfromclientsidewithcoprocessor.testclientpoolthreadlocal 1 1 1 1 1 1 1 1 0 1 1 -1 0 1
org.apache.hadoop.hbase.client.testhcm.testregioncaching 1 1 -1 0 1 1 -1 0 -1 0 -1 0 1 1
org.apache.hadoop.hbase.client.testmultiparallel.testflushcommitswithabort 1 1 1 1 1 1 1 1 -1 0 1 1 1 1
org.apache.hadoop.hbase.client.testscannertimeout.test3686a 1 1 1 1 1 1 1 1 -1 0 1 1 1 1
org.apache.hadoop.hbase.coprocessor.example.testrowcountendpoint.org.apache.hadoop.hbase.coprocessor.example.testrowcountendpoint 0 -1 0 -1 0 0 0 -1 0 0 0 0 0 0
org.apache.hadoop.hbase.coprocessor.example.testzookeeperscanpolicyobserver.org.apache.hadoop.hbase.coprocessor.example.testzookeeperscanpolicyobserver 0 -1 0 -1 0 0 0 -1 0 0 0 0 0 0
org.apache.hadoop.hbase.master.testrollingrestart.testbasicrollingrestart 1 1 1 1 -1 0 1 1 1 1 1 1 -1 0
org.apache.hadoop.hbase.regionserver.testcompactionstate.testmajorcompaction 1 1 -1 0 1 1 1 1 1 1 1 1 1 1
org.apache.hadoop.hbase.regionserver.testcompactionstate.testminorcompaction 1 1 -1 0 1 1 1 1 1 1 1 1 1 1
org.apache.hadoop.hbase.replication.testreplication.loadtesting 1 1 1 1 1 1 1 1 1 -1 0 1 1 1
org.apache.hadoop.hbase.rest.client.testremoteadmin.org.apache.hadoop.hbase.rest.client.testremoteadmin 0 0 0 0 0 0 0 0 -1 0 0 0 0 0
org.apache.hadoop.hbase.rest.client.testremotetable.org.apache.hadoop.hbase.rest.client.testremotetable 0 0 0 0 0 0 0 0 -1 0 0 0 0 0
org.apache.hadoop.hbase.security.access.testtablepermissions.testbasicwrite 0 1 1 1 1 1 1 1 1 1 1 1 1 -1
org.apache.hadoop.hbase.testdrainingserver.testdrainingserverwithabort 1 1 1 1 1 -1 0 1 1 1 1 1 -1 0
org.apache.hadoop.hbase.util.testhbasefsck.testregionshouldnotbedeployed 1 1 1 1 1 1 -1 0 -1 0 -1 -1 0 -1

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.hbase</groupId>
<artifactId>buildstats</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>buildstats</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.offbytwo.jenkins</groupId>
<artifactId>jenkins-client</artifactId>
<version>0.1.6-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<inherited>true</inherited>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>org.apache.hadoop.hbase.devtools.buildstats.TestResultHistory</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>buildstats</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-my-jar-with-dependencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.devtools.buildstats;
import com.offbytwo.jenkins.model.BaseModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BuildResultWithTestCaseDetails extends BaseModel {
List<TestSuite> suites;
/* default constructor needed for Jackson */
public BuildResultWithTestCaseDetails() {
this(new ArrayList<TestSuite>());
}
public BuildResultWithTestCaseDetails(List<TestSuite> s) {
this.suites = s;
}
public BuildResultWithTestCaseDetails(TestSuite... s) {
this(Arrays.asList(s));
}
public List<TestSuite> getSuites() {
return suites;
}
public void setSuites(List<TestSuite> s) {
suites = s;
}
}

View File

@ -0,0 +1,88 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.devtools.buildstats;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Set;
public class HistoryReport {
private List<Integer> buildsWithTestResults;
private Map<String, int[]> historyResults;
private Map<Integer, Set<String>> skippedTests;
public HistoryReport() {
buildsWithTestResults = new ArrayList<Integer>();
this.historyResults = new HashMap<String, int[]>();
}
public Map<String, int[]> getHistoryResults() {
return this.historyResults;
}
public Map<Integer, Set<String>> getSkippedTests() {
return this.skippedTests;
}
public List<Integer> getBuildsWithTestResults() {
return this.buildsWithTestResults;
}
public void setBuildsWithTestResults(List<Integer> src) {
this.buildsWithTestResults = src;
}
public void setHistoryResults(Map<String, int[]> src, Map<Integer, Set<String>> skippedTests) {
this.skippedTests = skippedTests;
this.historyResults = src;
}
public void printReport() {
System.out.printf("%-30s", "Failed Test Cases Stats");
for (Integer i : getBuildsWithTestResults()) {
System.out.printf("%5d", i);
}
System.out.println("\n========================================================");
SortedSet<String> keys = new TreeSet<String>(getHistoryResults().keySet());
for (String failedTestCase : keys) {
System.out.println();
int[] resultHistory = getHistoryResults().get(failedTestCase);
System.out.print(failedTestCase);
for (int i = 0; i < resultHistory.length; i++) {
System.out.printf("%5d", resultHistory[i]);
}
}
System.out.println();
if (skippedTests == null) return;
System.out.printf("\n%-30s\n", "Skipped Test Cases Stats");
for (Integer i : getBuildsWithTestResults()) {
Set<String> tmpSkippedTests = skippedTests.get(i);
if (tmpSkippedTests == null || tmpSkippedTests.isEmpty()) continue;
System.out.printf("======= %d skipped(Or don't have) following test suites =======\n", i);
for (String skippedTestcase : tmpSkippedTests) {
System.out.println(skippedTestcase);
}
}
}
}

View File

@ -0,0 +1,61 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.devtools.buildstats;
public class TestCaseResult {
private String className;
private int failedSince;
private String name;
private String status;
public String getName() {
return name;
}
public String getClassName() {
return className;
}
public int failedSince() {
return failedSince;
}
public String getStatus() {
return status;
}
public void setName(String s) {
name = s;
}
public void setClassName(String s) {
className = s;
}
public void setFailedSince(int s) {
failedSince = s;
}
public void setStatus(String s) {
status = s;
}
public String getFullName() {
return (this.className + "." + this.name).toLowerCase();
}
}

View File

@ -0,0 +1,260 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.devtools.buildstats;
import com.offbytwo.jenkins.JenkinsServer;
import com.offbytwo.jenkins.client.JenkinsHttpClient;
import com.offbytwo.jenkins.model.*;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
public class TestResultHistory {
public final static String STATUS_REGRESSION = "REGRESSION";
public final static String STATUS_FAILED = "FAILED";
public final static String STATUS_PASSED = "PASSED";
public final static String STATUS_FIXED = "FIXED";
public static int BUILD_HISTORY_NUM = 15;
private JenkinsHttpClient client;
private String jobName;
public TestResultHistory(String apacheHTTPURL, String jobName, String userName, String passWord)
throws URISyntaxException {
this.client = new JenkinsHttpClient(new URI(apacheHTTPURL), userName, passWord);
this.jobName = jobName;
}
public static void main(String[] args) {
if (args.length < 2) {
printUsage();
return;
}
String apacheHTTPUrl = args[0];
String jobName = args[1];
if (args.length > 2) {
int tmpHistoryJobNum = -1;
try {
tmpHistoryJobNum = Integer.parseInt(args[2]);
} catch (NumberFormatException ex) {
// ignore
}
if (tmpHistoryJobNum > 0) {
BUILD_HISTORY_NUM = tmpHistoryJobNum;
}
}
try {
TestResultHistory buildHistory = new TestResultHistory(apacheHTTPUrl, jobName, "", "");
HistoryReport report = buildHistory.getReport();
// display result in console
report.printReport();
} catch (Exception ex) {
System.out.println("Got unexpected exception: " + ex.getMessage());
}
}
protected static void printUsage() {
System.out.println("<Jenkins HTTP URL> <Job Name> [Number of Historical Jobs to Check]");
System.out.println("Sample Input: \"https://builds.apache.org\" "
+ "\"HBase-TRUNK-on-Hadoop-2.0.0\" ");
}
public HistoryReport getReport() {
HistoryReport report = new HistoryReport();
List<Integer> buildWithTestResults = new ArrayList<Integer>();
Map<String, int[]> failureStats = new HashMap<String, int[]>();
try {
JenkinsServer jenkins = new JenkinsServer(this.client);
Map<String, Job> jobs = jenkins.getJobs();
JobWithDetails job = jobs.get(jobName.toLowerCase()).details();
// build test case failures stats for the past 10 builds
Build lastBuild = job.getLastBuild();
int startingBuildNumber =
(lastBuild.getNumber() - BUILD_HISTORY_NUM > 0) ? lastBuild.getNumber()
- BUILD_HISTORY_NUM + 1 : 1;
Map<Integer, HashMap<String, String>> executedTestCases =
new HashMap<Integer, HashMap<String, String>>();
Map<Integer, Set<String>> skippedTestCases = new TreeMap<Integer, Set<String>>();
Set<String> allExecutedTestCases = new HashSet<String>();
Map<Integer, Set<String>> normalizedTestSet = new HashMap<Integer, Set<String>>();
String buildUrl = lastBuild.getUrl();
for (int i = startingBuildNumber; i <= lastBuild.getNumber(); i++) {
HashMap<String, String> buildExecutedTestCases = new HashMap<String, String>(2048);
String curBuildUrl = buildUrl.replaceFirst("/" + lastBuild.getNumber(), "/" + i);
List<String> failedCases = null;
try {
failedCases = getBuildFailedTestCases(curBuildUrl, buildExecutedTestCases);
buildWithTestResults.add(i);
} catch (Exception ex) {
// can't get result so skip it
continue;
}
executedTestCases.put(i, buildExecutedTestCases);
HashSet<String> tmpSet = new HashSet<String>();
for (String tmpTestCase : buildExecutedTestCases.keySet()) {
allExecutedTestCases.add(tmpTestCase.substring(0, tmpTestCase.lastIndexOf(".")));
tmpSet.add(tmpTestCase.substring(0, tmpTestCase.lastIndexOf(".")));
}
normalizedTestSet.put(i, tmpSet);
// set test result failed cases of current build
for (String curFailedTestCase : failedCases) {
if (failureStats.containsKey(curFailedTestCase)) {
int[] testCaseResultArray = failureStats.get(curFailedTestCase);
testCaseResultArray[i - startingBuildNumber] = -1;
} else {
int[] testResult = new int[BUILD_HISTORY_NUM];
testResult[i - startingBuildNumber] = -1;
// refill previous build test results for newly failed test case
for (int k = startingBuildNumber; k < i; k++) {
HashMap<String, String> tmpBuildExecutedTestCases = executedTestCases.get(k);
if (tmpBuildExecutedTestCases != null
&& tmpBuildExecutedTestCases.containsKey(curFailedTestCase)) {
String statusStr = tmpBuildExecutedTestCases.get(curFailedTestCase);
testResult[k - startingBuildNumber] = convertStatusStringToInt(statusStr);
}
}
failureStats.put(curFailedTestCase, testResult);
}
}
// set test result for previous failed test cases
for (String curTestCase : failureStats.keySet()) {
if (!failedCases.contains(curTestCase) && buildExecutedTestCases.containsKey(curTestCase)) {
String statusVal = buildExecutedTestCases.get(curTestCase);
int[] testCaseResultArray = failureStats.get(curTestCase);
testCaseResultArray[i - startingBuildNumber] = convertStatusStringToInt(statusVal);
}
}
}
// check which test suits skipped
for (int i = startingBuildNumber; i <= lastBuild.getNumber(); i++) {
Set<String> skippedTests = new HashSet<String>();
HashMap<String, String> tmpBuildExecutedTestCases = executedTestCases.get(i);
if (tmpBuildExecutedTestCases == null || tmpBuildExecutedTestCases.isEmpty()) continue;
// normalize test case names
Set<String> tmpNormalizedTestCaseSet = normalizedTestSet.get(i);
for (String testCase : allExecutedTestCases) {
if (!tmpNormalizedTestCaseSet.contains(testCase)) {
skippedTests.add(testCase);
}
}
skippedTestCases.put(i, skippedTests);
}
report.setBuildsWithTestResults(buildWithTestResults);
for (String failedTestCase : failureStats.keySet()) {
int[] resultHistory = failureStats.get(failedTestCase);
int[] compactHistory = new int[buildWithTestResults.size()];
int index = 0;
for (Integer i : buildWithTestResults) {
compactHistory[index] = resultHistory[i - startingBuildNumber];
index++;
}
failureStats.put(failedTestCase, compactHistory);
}
report.setHistoryResults(failureStats, skippedTestCases);
} catch (Exception ex) {
System.out.println(ex);
ex.printStackTrace();
}
return report;
}
/**
* @param statusVal
* @return 1 means PASSED, -1 means FAILED, 0 means SKIPPED
*/
static int convertStatusStringToInt(String statusVal) {
if (statusVal.equalsIgnoreCase(STATUS_REGRESSION) || statusVal.equalsIgnoreCase(STATUS_FAILED)) {
return -1;
} else if (statusVal.equalsIgnoreCase(STATUS_PASSED)) {
return 1;
}
return 0;
}
/**
* Get failed test cases of a build
* @param buildURL Jenkins build job URL
* @param executedTestCases Set of test cases which was executed for the build
* @return list of failed test case names
*/
List<String> getBuildFailedTestCases(String buildURL, HashMap<String, String> executedTestCases)
throws IOException {
List<String> result = new ArrayList<String>();
String apiPath =
urlJoin(buildURL,
"testReport?depth=10&tree=suites[cases[className,name,status,failedSince]]");
List<TestSuite> suites = client.get(apiPath, BuildResultWithTestCaseDetails.class).getSuites();
result = getTestSuiteFailedTestcase(suites, executedTestCases);
return result;
}
private List<String> getTestSuiteFailedTestcase(List<TestSuite> suites,
HashMap<String, String> executedTestCases) {
List<String> result = new ArrayList<String>();
if (suites == null) {
return result;
}
for (TestSuite curTestSuite : suites) {
for (TestCaseResult curTestCaseResult : curTestSuite.getCases()) {
if (curTestCaseResult.getStatus().equalsIgnoreCase(STATUS_FAILED)
|| curTestCaseResult.getStatus().equalsIgnoreCase(STATUS_REGRESSION)) {
// failed test case
result.add(curTestCaseResult.getFullName());
}
executedTestCases.put(curTestCaseResult.getFullName(), curTestCaseResult.getStatus());
}
}
return result;
}
String urlJoin(String path1, String path2) {
if (!path1.endsWith("/")) {
path1 += "/";
}
if (path2.startsWith("/")) {
path2 = path2.substring(1);
}
return path1 + path2;
}
}

View File

@ -0,0 +1,47 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.hbase.devtools.buildstats;
import com.offbytwo.jenkins.model.BaseModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TestSuite extends BaseModel {
List<TestCaseResult> cases;
public TestSuite() {
this(new ArrayList<TestCaseResult>());
}
public TestSuite(List<TestCaseResult> s) {
this.cases = s;
}
public TestSuite(TestCaseResult... s) {
this(Arrays.asList(s));
}
public List<TestCaseResult> getCases() {
return cases;
}
public void setCases(List<TestCaseResult> s) {
cases = s;
}
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.hbase</groupId>
<artifactId>jenkins-tools</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>jenkins-client</module>
<module>buildstats</module>
</modules>
</project>