001: /*
002: * Copyright 2006-2007 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.opensource.org/licenses/ecl1.php
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.kuali.test.suite;
017:
018: import java.io.InputStream;
019: import java.lang.reflect.Method;
020: import java.net.URL;
021: import java.util.Arrays;
022: import java.util.Collection;
023: import java.util.HashMap;
024: import java.util.HashSet;
025: import java.util.Map;
026: import java.util.Set;
027: import java.util.regex.Pattern;
028:
029: import javax.xml.parsers.DocumentBuilderFactory;
030:
031: import junit.framework.TestCase;
032: import junit.framework.TestSuite;
033:
034: import org.kuali.core.util.AssertionUtils;
035: import org.w3c.dom.NodeList;
036:
037: /**
038: * The abstract superclass of suites of all test classes or methods which {@link RelatesTo} a Kuali JIRA issue that is currently in
039: * a certain state (e.g., in-progress). IDEs or Ant can run the concrete subclasses as JUnit tests.
040: *
041: * @see org.kuali.test.suite.RelatesTo
042: */
043: public abstract class JiraRelatedSuite {
044:
045: public static enum State {
046: IN_PROGRESS("status=3&tempMax=1000"), OPEN_OR_IN_PROGRESS(
047: "status=1&status=3&tempMax=9999"), OPEN_OR_IN_PROGRESS_OR_REOPENED(
048: "status=1&status=3&status=4&tempMax=9999");
049:
050: public final String filterUrl;
051:
052: State(String uniquePart) {
053: this .filterUrl = "https://test.kuali.org/jira/secure/IssueNavigator.jspa?os_username=kuali-rss-feed-user&os_password=kuali-rss-feed-user&view=rss&reset=true&decorator=none&"
054: + uniquePart;
055: }
056: }
057:
058: private static Map<State, Collection<String>> jiraIssuesByState = new HashMap<State, Collection<String>>();
059: private static RuntimeException initializationException = null;
060: private final static Pattern EXPECTED_JIRA_KEY = Pattern
061: .compile("\\p{Upper}+-\\p{Digit}+");
062:
063: /**
064: * Gets the JIRA issues currently in the given state. Caches the results, to avoid queries to the JIRA server, for speed.
065: *
066: * @param state the state to get
067: * @return a Set of the names of all JIRA issues currently in the given state
068: * @throws RuntimeException if the JIRA server cannot be queried for this list, or its response cannot be understood. After this
069: * exception is thrown once, it's always thrown immediately thereafter, to fast-fail KualiTestBase.
070: */
071: private static Collection<String> getNamesOfJiraIssues(State state) {
072: if (initializationException != null) {
073: throw initializationException;
074: }
075: if (!jiraIssuesByState.containsKey(state)) {
076: InputStream jiraIssuesStream = null;
077: try {
078: Collection<String> jiraIssues = new HashSet<String>();
079: NodeList keys;
080: try {
081: jiraIssuesStream = new URL(state.filterUrl)
082: .openStream();
083: keys = DocumentBuilderFactory.newInstance()
084: .newDocumentBuilder().parse(
085: jiraIssuesStream)
086: .getElementsByTagName("key");
087: for (int i = 0; i < keys.getLength(); i++) {
088: String jiraKey = keys.item(i).getTextContent();
089: AssertionUtils.assertThat(EXPECTED_JIRA_KEY
090: .matcher(jiraKey).matches(),
091: "badly formed key: " + jiraKey);
092: jiraIssues.add(jiraKey);
093: }
094: jiraIssuesByState.put(state, jiraIssues);
095: } finally {
096: if (jiraIssuesStream != null) {
097: jiraIssuesStream.close();
098: }
099: }
100: } catch (Throwable e) {
101: initializationException = new RuntimeException(
102: "test framework cannot get list of " + state
103: + " JIRA issues", e);
104: throw initializationException;
105: }
106: }
107: return jiraIssuesByState.get(state);
108: }
109:
110: /**
111: * Filters the JIRA issues which are currently in the given state. The JIRA status is queried once when needed and cached
112: * statically for speed.
113: *
114: * @param from JIRA issues from which to filter
115: * @param state JIRA state to filter on
116: * @return any of the given issues that are currently in the given state in JIRA
117: * @throws RuntimeException if the JIRA server cannot be queried for this list, or its response cannot be understood. After this
118: * exception is thrown once, it's always thrown immediately thereafter, to fast-fail KualiTestBase.
119: */
120: public static Set<RelatesTo.JiraIssue> getMatchingIssues(
121: Collection<RelatesTo.JiraIssue> from, State state) {
122: HashSet<RelatesTo.JiraIssue> result = new HashSet<RelatesTo.JiraIssue>();
123: if (!from.isEmpty()) { // try to avoid the JIRA query
124: for (RelatesTo.JiraIssue issue : from) {
125: if (getNamesOfJiraIssues(state).contains(
126: issue.toString())) {
127: result.add(issue);
128: }
129: }
130: }
131: return result;
132: }
133:
134: private static boolean hasRelatedIssueInState(RelatesTo annotation,
135: State state) {
136: return annotation != null
137: && !getMatchingIssues(
138: Arrays.asList(annotation.value()), state)
139: .isEmpty();
140: }
141:
142: /**
143: * Builds the suite of all test methods (including those within test class sub-suites) which {@link RelatesTo} a JIRA issue in
144: * the given state. This method is for subclasses; it cannot be run by JUnit directly.
145: *
146: * @param state the current state to include
147: * @return the positive suite
148: * @throws java.io.IOException if the directory containing this class file cannot be scanned for other test class files
149: * @throws RuntimeException if the JIRA server cannot be queried for this list, or its response cannot be understood. After this
150: * exception is thrown once, it's always thrown immediately thereafter, to fast-fail KualiTestBase.
151: * @throws Exception is not actually thrown, because the criteria inner classes do not throw it
152: */
153: protected TestSuite getSuite(final State state) throws Exception {
154: TestSuiteBuilder.ClassCriteria classCriteria = new TestSuiteBuilder.ClassCriteria() {
155: public boolean includes(Class<? extends TestCase> testClass) {
156: return hasRelatedIssueInState(testClass
157: .getAnnotation(RelatesTo.class), state);
158: }
159: };
160: TestSuiteBuilder.MethodCriteria methodCriteria = new TestSuiteBuilder.MethodCriteria() {
161: public boolean includes(Method method) {
162: return hasRelatedIssueInState(method
163: .getAnnotation(RelatesTo.class), state);
164: }
165: };
166: TestSuite suite = TestSuiteBuilder.build(classCriteria,
167: methodCriteria);
168: suite.setName(this .getClass().getName());
169: return suite;
170: }
171:
172: /**
173: * Builds the suite of all test methods (including those within test class sub-suites) which do not {@link RelatesTo} a JIRA
174: * issue in the given state. This method is for subclasses; it cannot be run by JUnit directly.
175: *
176: * @param state the current state to exclude
177: * @return the negative suite
178: * @throws java.io.IOException if the directory containing this class file cannot be scanned for other test class files
179: * @throws RuntimeException if the JIRA server cannot be queried for this list, or its response cannot be understood. After this
180: * exception is thrown once, it's always thrown immediately thereafter, to fast-fail KualiTestBase.
181: * @throws Exception is not actually thrown, because the criteria inner class does not throw it
182: */
183: protected TestSuite getNegativeSuite(final State state)
184: throws Exception {
185: TestSuiteBuilder.MethodCriteria negativeMethodCriteria = new TestSuiteBuilder.MethodCriteria() {
186: public boolean includes(Method method) {
187: RelatesTo testClassAnnotation = method
188: .getDeclaringClass().getAnnotation(
189: RelatesTo.class);
190: return !hasRelatedIssueInState(testClassAnnotation,
191: state)
192: && !hasRelatedIssueInState(method
193: .getAnnotation(RelatesTo.class), state);
194: }
195: };
196: TestSuite suite = TestSuiteBuilder.build(
197: TestSuiteBuilder.NULL_CRITERIA, negativeMethodCriteria);
198: suite.setName(this.getClass().getName());
199: return suite;
200: }
201: }
|