/*******************************************************************************
* Copyright (c) 2013 Rene Schneider, GEBIT Solutions GmbH and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package de.gebit.integrity.remoting.entities.setlist;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* The Setlist is a data structure to contain a test execution plan as well as test results. Its name refers to the
* setlists used by bands to write down the songs played during a concert (okay, it's a stupid reason to name it like
* that, but hey, I didn't have any better idea ;-) ).<br>
* <br>
* Setlists are stored in a flat way, with the entries actually resembling a tree structure by referencing each other
* via their IDs. This allows for easy incremental update of single entries regardless of the size of the subtrees below
* them, as long as the IDs of each entry stay the same.
*
* @author Rene Schneider - initial API and implementation
*
*/
@SuppressWarnings("unchecked")
public class SetList implements Serializable {
/**
* Serialization.
*/
private static final long serialVersionUID = -5710551695226775511L;
/**
* A flat list of all stored entries. The position in the list is the entries' ID and allows for quick retrieval.
*/
private List<SetListEntry> entries = new ArrayList<SetListEntry>();
/**
* The reference to the entry that is currently being executed.
*/
private Integer entryInExecutionReference;
/**
* The path of entries in execution, starting at the root.
*/
private transient List<SetListEntry> pathOfEntriesInExecution = new ArrayList<SetListEntry>();
/**
* A map of entries that have a "result" in terms of having failed or succeeded. This maps sets to their results.
*/
private transient HashMap<SetListEntry, SetListEntryResultStates> resultBearingEntryResultMap;
/**
* A map of entries that are executable. The value is the number of the entry in the total list of executable
* entries.
*/
private transient HashMap<SetListEntry, Integer> executableEntryResultIndex;
/**
* The results of executable entries. Indexed by the numeric value in {@link #executableEntryResultIndex}.
*/
private transient ArrayList<SetListEntryResultStates> executableEntryResultStates;
/**
* The current position in the entry list. Used when creating new entries.
*/
private transient int entryListPosition;
/**
* The last ID value given to an entry of each type.
*/
private transient Map<SetListEntryTypes, Integer> lastCreatedEntryIdMap = new HashMap<SetListEntryTypes, Integer>();
/**
* A counter counting the results.
*/
private transient Map<SetListEntryResultStates, Integer> executableEntryResultStateCounts = new HashMap<SetListEntryResultStates, Integer>();
/**
* The fully qualified names of entries. This is a name which is calculated by using the name of an entry plus its
* parents names (recursively). It is intended to provide a "best-effort" way to map entries from one set list with
* entries from another set list from a different run of the same (or roughly the same) test scripts. Having said
* this, there are NO GUARANTEES WHATSOEVER that this mapping is actually successful in a particular scenario.
*/
private transient Map<SetListEntry, String> fullyQualifiedNameMap = new HashMap<SetListEntry, String>();
/**
* This is the reverse of {@link #fullyQualifiedNameMap}.
*/
private transient Map<String, SetListEntry> fullyQualifiedNameReverseMap = new HashMap<String, SetListEntry>();
/**
* Recreates transient data from the list of entries. Used after deserialization of the whole structure in order to
* prepare it for being actually used. Transient entries are redundant and not transmitted for size reasons.
*/
public void recreateTransientData() {
resultBearingEntryResultMap = new HashMap<SetListEntry, SetListEntryResultStates>();
executableEntryResultIndex = new HashMap<SetListEntry, Integer>();
executableEntryResultStates = new ArrayList<SetListEntryResultStates>();
executableEntryResultStateCounts = new HashMap<SetListEntryResultStates, Integer>();
lastCreatedEntryIdMap = new HashMap<SetListEntryTypes, Integer>();
fullyQualifiedNameMap = new HashMap<SetListEntry, String>();
fullyQualifiedNameReverseMap = new HashMap<String, SetListEntry>();
int tempPosition = 0;
for (SetListEntry tempEntry : entries) {
SetListEntryResultStates tempResultState = determineEntryResultState(tempEntry);
if (tempResultState != null) {
resultBearingEntryResultMap.put(tempEntry, tempResultState);
if (SetListEntryTypes.SUITE != tempEntry.getType() && SetListEntryTypes.RESULT != tempEntry.getType()) {
// Skip suites here - we don't consider those "executable". Only the sub-elements are considered
// executable. And skip results themselves - these aren't executable at all.
executableEntryResultIndex.put(tempEntry, tempPosition);
executableEntryResultStates.add(tempResultState);
executableEntryResultStateCounts.put(tempResultState,
getNumberOfEntriesInResultState(tempResultState) + 1);
tempPosition++;
}
}
String tempFullyQualifiedBaseName = calculateFullyQualifiedBaseName(tempEntry);
String tempFullyQualifiedName = null;
int tempCounter = 0;
do {
tempFullyQualifiedName = tempFullyQualifiedBaseName + (tempCounter > 0 ? "#" + tempCounter : "");
tempCounter++;
} while (fullyQualifiedNameReverseMap.containsKey(tempFullyQualifiedName));
fullyQualifiedNameMap.put(tempEntry, tempFullyQualifiedName);
fullyQualifiedNameReverseMap.put(tempFullyQualifiedName, tempEntry);
}
pathOfEntriesInExecution = new ArrayList<SetListEntry>();
setEntryInExecutionReference(entryInExecutionReference);
}
/**
* Determines the result state for a specific entry.
*
* @param anEntry
* the entry
* @return the result state or null in case the entry doesn't have a result
*/
protected SetListEntryResultStates determineEntryResultState(SetListEntry anEntry) {
boolean tempEntryIsResultOfTableTestRow = (anEntry.getType() == SetListEntryTypes.RESULT
&& getParent(anEntry).getType() == SetListEntryTypes.TABLETEST);
List<SetListEntry> tempResultEntries = tempEntryIsResultOfTableTestRow ? null
: resolveReferences(anEntry, SetListEntryAttributeKeys.RESULT);
if (tempEntryIsResultOfTableTestRow || tempResultEntries.size() > 0) {
SetListEntry tempResultEntry = tempEntryIsResultOfTableTestRow ? anEntry : tempResultEntries.get(0);
switch (anEntry.getType()) {
case SUITE:
if (tempResultEntry.getAttribute(SetListEntryAttributeKeys.SUCCESS_COUNT) != null) {
int tempFailureCount = (Integer) tempResultEntry
.getAttribute(SetListEntryAttributeKeys.FAILURE_COUNT);
int tempTestExceptionCount = (Integer) tempResultEntry
.getAttribute(SetListEntryAttributeKeys.TEST_EXCEPTION_COUNT);
int tempCallExceptionCount = (Integer) tempResultEntry
.getAttribute(SetListEntryAttributeKeys.CALL_EXCEPTION_COUNT);
int tempExceptionCount = tempTestExceptionCount + tempCallExceptionCount;
if (tempExceptionCount > 0) {
return SetListEntryResultStates.EXCEPTION;
} else if (tempFailureCount > 0) {
return SetListEntryResultStates.FAILED;
} else {
return SetListEntryResultStates.SUCCESSFUL;
}
}
return SetListEntryResultStates.UNKNOWN;
case CALL:
if (tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG) != null) {
if (Boolean.TRUE
.equals(tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG))) {
return SetListEntryResultStates.SUCCESSFUL;
} else {
return SetListEntryResultStates.EXCEPTION;
}
}
return SetListEntryResultStates.UNKNOWN;
case TEST:
case RESULT:
if (tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG) != null) {
if (Boolean.TRUE
.equals(tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG))) {
return SetListEntryResultStates.SUCCESSFUL;
} else if (Boolean.FALSE
.equals(tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG))) {
if (tempResultEntry.getAttribute(SetListEntryAttributeKeys.EXCEPTION) != null) {
return SetListEntryResultStates.EXCEPTION;
} else {
return SetListEntryResultStates.FAILED;
}
}
}
return SetListEntryResultStates.UNKNOWN;
case TABLETEST:
boolean tempHasException = false;
boolean tempHasFailure = false;
boolean tempHasAnyResult = false;
for (int i = 0; i < tempResultEntries.size(); i++) {
tempResultEntry = tempResultEntries.get(i);
if (tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG) != null) {
tempHasAnyResult = true;
if (Boolean.FALSE
.equals(tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG))) {
if (tempResultEntry.getAttribute(SetListEntryAttributeKeys.EXCEPTION) != null) {
tempHasException = true;
break;
} else {
tempHasFailure = true;
}
}
}
}
if (tempHasException) {
return SetListEntryResultStates.EXCEPTION;
} else if (tempHasFailure) {
return SetListEntryResultStates.FAILED;
} else if (tempHasAnyResult) {
return SetListEntryResultStates.SUCCESSFUL;
} else {
return SetListEntryResultStates.UNKNOWN;
}
case VARIABLE_ASSIGNMENT:
if (Boolean.TRUE.equals(tempResultEntry.getAttribute(SetListEntryAttributeKeys.RESULT_SUCCESS_FLAG))) {
return SetListEntryResultStates.SUCCESSFUL;
}
return SetListEntryResultStates.UNKNOWN;
default:
return null;
}
}
return null;
}
public int getNumberOfExecutableEntries() {
return executableEntryResultStates.size();
}
/**
* Returns the number of entries in the given result state.
*
* @param aState
* the state to query
* @return the number
*/
public int getNumberOfEntriesInResultState(SetListEntryResultStates aState) {
Integer tempResult = executableEntryResultStateCounts.get(aState);
return tempResult != null ? tempResult : 0;
}
/**
* Returns the result state of an entry from the map.
*
* @param anEntry
* the entry
* @return the result state, or null if the entry doesn't have one
*/
public SetListEntryResultStates getResultStateForEntry(SetListEntry anEntry) {
return resultBearingEntryResultMap.get(anEntry);
}
/**
* Returns the result state of an executable entry at the specified position in the list of executable entries.
*
* @param aPosition
* the position
* @return the result state, or null if the position doesn't have one
*/
public SetListEntryResultStates getResultStateForExecutableEntry(int aPosition) {
if (aPosition < 0 || aPosition >= executableEntryResultStates.size()) {
return null;
} else {
return executableEntryResultStates.get(aPosition);
}
}
/**
* Creates a new entry. Uses up an entry ID in the process.<br>
* <br>
* Note that this doesn't necessarily create a new entry, it may also reuse an already existing one! It's part of
* the concept of the {@link SetList} to actually create entries on the first (dry) run through the tests, and reuse
* old entries during the second (actual) test run, updating their result status etc. in the process.
*
* @param aType
* the type of entry
* @return the new entry
*/
public SetListEntry createEntry(SetListEntryTypes aType) {
if (entries.size() > entryListPosition) {
lastCreatedEntryIdMap.put(aType, entryListPosition);
entryListPosition++;
SetListEntry tempNextEntry = entries.get(entryListPosition - 1);
if (tempNextEntry.getType() != aType) {
throw new IllegalStateException(
"Severe internal data inconsistency detected! Cannot continue test execution.");
}
return tempNextEntry;
} else {
SetListEntry tempNewEntry = new SetListEntry(entryListPosition, aType);
entries.add(tempNewEntry);
lastCreatedEntryIdMap.put(aType, tempNewEntry.getId());
entryListPosition++;
return tempNewEntry;
}
}
/**
* Returns the ID that was last given to an entry of the specified type.
*
* @param aType
* the type
* @return the ID if available, or null if none has been given to an entry of that type
*/
public Integer getLastCreatedEntryId(SetListEntryTypes aType) {
if (aType != null) {
return lastCreatedEntryIdMap.get(aType);
} else {
if (entries.size() == 0) {
return null;
} else {
return entries.get(entries.size() - 1).getId();
}
}
}
/**
* Returns the lowest ID given to any of the specified entry types.
*
* @param someTypes
* the types
* @return the ID if available, or null if none has been given to any entry of the types
*/
public Integer getLastCreatedEntryId(SetListEntryTypes... someTypes) {
List<Integer> tempList = new ArrayList<Integer>();
for (SetListEntryTypes tempType : someTypes) {
Integer tempEntryRef = getLastCreatedEntryId(tempType);
if (tempEntryRef != null) {
tempList.add(tempEntryRef);
}
}
if (tempList.size() == 0) {
return null;
} else {
Collections.sort(tempList);
return tempList.get(tempList.size() - 1);
}
}
/**
* Adds a reference to another entry to a specified parent entry under a specified attribute key.
*
* @param aReferringEntry
* the parent entry
* @param anAttributeKey
* the attribute key under which the reference shall be created
* @param aReferredEntry
* the entry to refer
*/
public void addReference(SetListEntry aReferringEntry, SetListEntryAttributeKeys anAttributeKey,
SetListEntry aReferredEntry) {
LinkedList<Integer> tempList = (LinkedList<Integer>) aReferringEntry.getAttribute(LinkedList.class,
anAttributeKey, new LinkedList<Integer>());
if (!tempList.contains(aReferredEntry.getId())) {
tempList.add(aReferredEntry.getId());
}
aReferredEntry.setAttribute(SetListEntryAttributeKeys.PARENT, aReferringEntry.getId());
}
/**
* Clears the entry list position.
*/
public void rewind() {
entryListPosition = 0;
lastCreatedEntryIdMap.clear();
}
/**
* Integrates a list of updated entries into this {@link SetList}.
*
* @param someUpdatedEntries
* the updated entries
*/
public void integrateUpdates(SetListEntry[] someUpdatedEntries) {
for (SetListEntry tempEntry : someUpdatedEntries) {
entries.set(tempEntry.getId(), tempEntry);
}
for (SetListEntry tempEntry : someUpdatedEntries) {
SetListEntry tempEntryToUse = null;
if (tempEntry.getType() == SetListEntryTypes.RESULT) {
SetListEntry tempParent = getParent(tempEntry);
if (tempParent.getType() == SetListEntryTypes.TEST || tempParent.getType() == SetListEntryTypes.CALL
|| tempParent.getType() == SetListEntryTypes.TABLETEST) {
tempEntryToUse = tempParent;
}
} else {
tempEntryToUse = tempEntry;
}
if (tempEntryToUse != null) {
if (resultBearingEntryResultMap != null && resultBearingEntryResultMap.containsKey(tempEntryToUse)) {
SetListEntryResultStates tempResultState = determineEntryResultState(tempEntryToUse);
resultBearingEntryResultMap.put(tempEntryToUse, tempResultState);
Integer tempResultIndex = executableEntryResultIndex.get(tempEntryToUse);
if (tempResultIndex != null) {
SetListEntryResultStates tempPreviousState = executableEntryResultStates.set(tempResultIndex,
tempResultState);
// Decrement previous state counter, increment new state counter
executableEntryResultStateCounts.put(tempPreviousState,
getNumberOfEntriesInResultState(tempPreviousState) - 1);
executableEntryResultStateCounts.put(tempResultState,
getNumberOfEntriesInResultState(tempResultState) + 1);
}
}
// In case of tabletest results, also store their result in the result map, since we display those
// results directly in the tree. See also issue #78, which was fixed by this.
if (resultBearingEntryResultMap != null && tempEntryToUse.getType() == SetListEntryTypes.TABLETEST
&& tempEntry.getType() == SetListEntryTypes.RESULT) {
SetListEntryResultStates tempResultState = determineEntryResultState(tempEntry);
if (tempResultState != null) {
resultBearingEntryResultMap.put(tempEntry, tempResultState);
}
}
}
}
}
@Override
public String toString() {
StringBuffer tempBuffer = new StringBuffer();
for (SetListEntry tempEntry : entries) {
tempBuffer.append(tempEntry.toString() + "\n");
}
return tempBuffer.toString();
}
/**
* Returns a list of entries that are referred by the given entry under the given attribute key.
*
* @param anEntry
* the entry
* @param anAttributeKey
* the attribute key
* @return the list of resolved entries
*/
public List<SetListEntry> resolveReferences(SetListEntry anEntry, SetListEntryAttributeKeys anAttributeKey) {
return resolveReferences((List<Integer>) anEntry.getAttribute(anAttributeKey));
}
/**
* Resolves a list of entry IDs to the actual entries.
*
* @param someItemIds
* the IDs to resolve
* @return the list of entries
*/
public List<SetListEntry> resolveReferences(List<Integer> someItemIds) {
List<SetListEntry> tempList = new LinkedList<SetListEntry>();
if (someItemIds != null) {
for (Integer tempItemId : someItemIds) {
tempList.add(entries.get(tempItemId));
}
}
return tempList;
}
/**
* Resolves a single entry reference (ID).
*
* @param aReference
* the ID of the entry
* @return the entry, or null if there is none with that ID
*/
public SetListEntry resolveReference(Integer aReference) {
if (aReference == null || aReference < 0 || aReference >= entries.size()) {
return null;
} else {
return entries.get(aReference);
}
}
public SetListEntry getRootEntry() {
return entries.get(0);
}
/**
* Returns the parent entry for a given entry.
*
* @param anEntry
* the child entry
* @return the parent, or null if none was found
*/
public SetListEntry getParent(SetListEntry anEntry) {
if (anEntry == null) {
return null;
}
Integer tempParent = (Integer) anEntry.getAttribute(SetListEntryAttributeKeys.PARENT);
if (tempParent != null) {
return resolveReference(tempParent);
} else {
return null;
}
}
/**
* Gets the fully qualified name for an entry. For more info regarding this name, see
* {@link #fullyQualifiedNameMap}.
*
* @param anEntry
* the entry to look for
* @return the fully qualified name
*/
public String getFullyQualifiedName(SetListEntry anEntry) {
return fullyQualifiedNameMap.get(anEntry);
}
/**
* Gets the fully qualified name for an entry. For more info regarding this name, see
* {@link #fullyQualifiedNameMap}.
*
* @param anEntryReference
* the entry to look for
* @return the fully qualified name
*/
public String getFullyQualifiedName(Integer anEntryReference) {
return getFullyQualifiedName(resolveReference(anEntryReference));
}
/**
* Finds an entry based on a given fully qualified name. For more info regarding this name, see
* {@link #fullyQualifiedNameMap}.
*
* @param aName
* the name to search for
* @return the entry or null if none was found
*/
public SetListEntry findEntryByFullyQualifiedName(String aName) {
return fullyQualifiedNameReverseMap.get(aName);
}
/**
* Determines the fully qualified base name for an entry (this entry is possibly extended with a counter to resolve
* duplicates, hence it is a base name). For more info regarding this name, see {@link #fullyQualifiedNameMap}.
*
* @param anEntry
* the entry to calculate the name for
* @return the name
*/
protected String calculateFullyQualifiedBaseName(SetListEntry anEntry) {
String tempName = (String) anEntry.getAttribute(SetListEntryAttributeKeys.NAME);
if (tempName == null) {
tempName = Integer.toString(anEntry.getId());
}
SetListEntry tempParent = getParent(anEntry);
String tempParentName = (tempParent != null ? calculateFullyQualifiedBaseName(tempParent) : "");
return tempParentName + "|" + tempName;
}
/**
* Updates the reference to the entry that's currently in execution, recalculating the execution path.
*
* @param anEntryReference
* the new entry in execution
*/
public void setEntryInExecutionReference(Integer anEntryReference) {
entryInExecutionReference = anEntryReference;
pathOfEntriesInExecution.clear();
if (anEntryReference != null) {
SetListEntry tempCurrent = resolveReference(anEntryReference);
while (tempCurrent != null) {
pathOfEntriesInExecution.add(tempCurrent);
tempCurrent = getParent(tempCurrent);
}
}
}
public SetListEntry getEntryInExecution() {
return resolveReference(entryInExecutionReference);
}
public int getEntryListPosition() {
return entryListPosition;
}
public List<SetListEntry> getEntriesInExecution() {
return pathOfEntriesInExecution;
}
/**
* Checks whether a given entry is currently being executed.
*
* @param anEntry
* the entry
* @return true if the entry is being executed, false otherwise
*/
public boolean isEntryInExecution(SetListEntry anEntry) {
if (entryInExecutionReference == null) {
return false;
} else {
switch (anEntry.getType()) {
case TEST:
case CALL:
case TABLETEST:
case VARIABLE_ASSIGNMENT:
return anEntry.getId() == entryInExecutionReference;
case SETUP:
case SUITE:
case TEARDOWN:
return anEntry.getId() == entryInExecutionReference || pathOfEntriesInExecution.contains(anEntry);
case EXECUTION:
return true;
default:
return false;
}
}
}
/**
* Returns the name of the fork that executes the given entry.
*
* @param anEntry
* the entry
* @return the forks' name (index 0) and description (index 1, if available, otherwise null) or null if the entry is
* executed by the master
*/
public String[] getForkExecutingEntry(SetListEntry anEntry) {
String[] tempForkName = new String[2];
tempForkName[0] = (String) anEntry.getAttribute(SetListEntryAttributeKeys.FORK_NAME);
if (tempForkName[0] != null) {
tempForkName[1] = (String) anEntry.getAttribute(SetListEntryAttributeKeys.FORK_DESCRIPTION);
return tempForkName;
} else {
SetListEntry tempParent = getParent(anEntry);
if (tempParent != null) {
return getForkExecutingEntry(tempParent);
} else {
return null;
}
}
}
}