From b627dfc054ca845e7641fc2bb86c63b1f77d4843 Mon Sep 17 00:00:00 2001 From: Dustin Johnson Date: Tue, 15 Jan 2013 14:45:12 -0600 Subject: [PATCH] Issue #1487 Use XML to store stats grouping information in the database Amend: Increase length of grouping column to 1024. Add deltaScript to convert the grouping column to xml. Change regex to replace any pluginName fields with dashes, not just satellite-mcidas. Change-Id: I0d5081645c5f11909e5778c8e555d5da3cf77e1a Former-commit-id: 38ecb1f36dce4717d0ba9066c8cd22116770d34b --- .../13.2.1/aggregateRecordGroupingLength.sh | 23 +++ .../convertAggregateRecordGroupToXml.sh | 23 +++ .../convertAggregateRecordGroupToXml.sql | 38 ++++ .../increaseAggregateRecordGroupingLength.sql | 22 +++ .../uf/common/stats/AggregateRecord.java | 12 +- .../uf/common/stats/StatsGrouping.java | 101 +++++++++++ .../uf/common/stats/StatsGroupingColumn.java | 87 +++++++++ .../uf/edex/stats/AggregateManager.java | 73 +++++--- .../edex/stats/data/StatsDataAccumulator.java | 115 ++++++++---- .../uf/edex/stats/util/ConfigLoader.java | 5 +- ...datadelivery.retrieval.util.LevelXmlWriter | 1 - ...delivery.retrieval.util.ParameterXmlWriter | 1 - .../retrieval/util/NullXmlWriter.java | 60 ------- .../common/localization/TestPathManager.java | 34 +++- .../uf/edex/stats/AggregateManagerTest.java | 116 ++++++++++++ .../com/raytheon/uf/edex/stats/MockEvent.java | 169 ++++++++++++++++++ .../stats/data/StatsDataAccumulatorTest.java | 163 +++++++++++------ .../edex_static/base/stats/mockStats.xml | 12 ++ 18 files changed, 863 insertions(+), 192 deletions(-) create mode 100644 deltaScripts/13.2.1/aggregateRecordGroupingLength.sh create mode 100644 deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sh create mode 100644 deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sql create mode 100644 deltaScripts/13.2.1/increaseAggregateRecordGroupingLength.sql create mode 100644 edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGrouping.java create mode 100644 edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGroupingColumn.java delete mode 100644 tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.LevelXmlWriter delete mode 100644 tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.ParameterXmlWriter delete mode 100644 tests/unit/com/raytheon/uf/common/datadelivery/retrieval/util/NullXmlWriter.java create mode 100644 tests/unit/com/raytheon/uf/edex/stats/AggregateManagerTest.java create mode 100644 tests/unit/com/raytheon/uf/edex/stats/MockEvent.java create mode 100644 tests/utility/edex_static/base/stats/mockStats.xml diff --git a/deltaScripts/13.2.1/aggregateRecordGroupingLength.sh b/deltaScripts/13.2.1/aggregateRecordGroupingLength.sh new file mode 100644 index 0000000000..8be0366f68 --- /dev/null +++ b/deltaScripts/13.2.1/aggregateRecordGroupingLength.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +SQL_SCRIPT="increaseAggregateRecordGroupingLength.sql" + +# ensure that the sql script is present +if [ ! -f ${SQL_SCRIPT} ]; then + echo "ERROR: the required sql script - ${SQL_SCRIPT} was not found." + echo "FATAL: the update has failed!" + exit 1 +fi + +echo "INFO: update started - increasing the size of the aggregate.grouping column" + +# run the update +/awips2/psql/bin/psql -U awips -d metadata -f ${SQL_SCRIPT} +if [ $? -ne 0 ]; then + echo "FATAL: the update has failed!" + exit 1 +fi + +echo "INFO: the update has completed successfully!" + +exit 0 diff --git a/deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sh b/deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sh new file mode 100644 index 0000000000..556254619a --- /dev/null +++ b/deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +SQL_SCRIPT="convertAggregateRecordGroupToXml.sql" + +# ensure that the sql script is present +if [ ! -f ${SQL_SCRIPT} ]; then + echo "ERROR: the required sql script - ${SQL_SCRIPT} was not found." + echo "FATAL: the update has failed!" + exit 1 +fi + +echo "INFO: update started - converting the aggregate.grouping column to xml" + +# run the update +/awips2/psql/bin/psql -U awips -d metadata -f ${SQL_SCRIPT} +if [ $? -ne 0 ]; then + echo "FATAL: the update has failed!" + exit 1 +fi + +echo "INFO: the update has completed successfully!" + +exit 0 diff --git a/deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sql b/deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sql new file mode 100644 index 0000000000..424de941cf --- /dev/null +++ b/deltaScripts/13.2.1/convertAggregateRecordGroupToXml.sql @@ -0,0 +1,38 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ +\set ON_ERROR_STOP 1 +\connect metadata; + +-- Start a transaction +BEGIN; + +-- Temporarily replace dashes in pluginName rows with @@@ +update events.aggregate set grouping = regexp_replace(grouping, 'pluginName:(.*?)-(.*)', E'pluginName:\\1@@@\\2', 'g'); + +-- Convert to XML format +update events.aggregate set grouping = regexp_replace(grouping, ':', '" value="', 'g'); +update events.aggregate set grouping = regexp_replace(grouping, '-', '"/>'; + +-- Restore dashes from @@@ +update events.aggregate set grouping = regexp_replace(grouping, ' - * + * * SOFTWARE HISTORY * Date Ticket# Engineer Description * ------------ ---------- ----------- -------------------------- * Aug 21, 2012 jsanchez Initial creation * Nov 12, 2012 dhladky Updates some things for stats - * + * Jan 15, 2013 1487 djohnson Increase length of grouping to 1024. + * * - * + * * @author jsanchez * @version 1.0 */ @@ -56,7 +57,7 @@ import com.raytheon.uf.common.serialization.annotations.DynamicSerializeElement; @XmlRootElement @XmlAccessorType(XmlAccessType.NONE) @DynamicSerialize -public class AggregateRecord extends PersistableDataObject { +public class AggregateRecord extends PersistableDataObject { private static final long serialVersionUID = -4553588456131256014L; @GeneratedValue(strategy = GenerationType.AUTO) @@ -77,6 +78,7 @@ public class AggregateRecord extends PersistableDataObject { private String eventType; @DynamicSerializeElement + @Column(length = 1024) private String grouping; @Column(nullable = false) diff --git a/edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGrouping.java b/edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGrouping.java new file mode 100644 index 0000000000..3995883cbc --- /dev/null +++ b/edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGrouping.java @@ -0,0 +1,101 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ +package com.raytheon.uf.common.stats; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Contains a grouping for statistics. + * + *
+ * 
+ * SOFTWARE HISTORY
+ * 
+ * Date         Ticket#    Engineer    Description
+ * ------------ ---------- ----------- --------------------------
+ * Jan 15, 2013 1487       djohnson     Initial creation
+ * 
+ * 
+ * + * @author djohnson + * @version 1.0 + */ +@XmlRootElement +@XmlAccessorType(XmlAccessType.NONE) +public class StatsGrouping { + + @XmlAttribute(required = true) + private String name; + + @XmlAttribute(required = true) + private String value; + + /** + * Constructor. + */ + public StatsGrouping() { + this(null, null); + } + + /** + * Constructor. + * + * @param name + * @param value + */ + public StatsGrouping(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name + * the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the value + */ + public String getValue() { + return value; + } + + /** + * @param value + * the value to set + */ + public void setValue(String value) { + this.value = value; + } + +} diff --git a/edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGroupingColumn.java b/edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGroupingColumn.java new file mode 100644 index 0000000000..b63902407a --- /dev/null +++ b/edexOsgi/com.raytheon.uf.common.stats/src/com/raytheon/uf/common/stats/StatsGroupingColumn.java @@ -0,0 +1,87 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ +package com.raytheon.uf.common.stats; + +import java.util.List; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import com.google.common.collect.Lists; + +/** + * Contains a list of groupings for statistics. + * + *
+ * 
+ * SOFTWARE HISTORY
+ * 
+ * Date         Ticket#    Engineer    Description
+ * ------------ ---------- ----------- --------------------------
+ * Jan 15, 2013 1487       djohnson     Initial creation
+ * 
+ * 
+ * + * @author djohnson + * @version 1.0 + */ +@XmlRootElement(name = "stat") +@XmlAccessorType(XmlAccessType.NONE) +public class StatsGroupingColumn { + + @XmlElement + private List group = Lists.newArrayList(); + + /** + * @return the group + */ + public List getGroup() { + return group; + } + + /** + * @param group + * the group to set + */ + public void setGroup(List group) { + this.group = group; + } + + /** + * Create a {@link StatsGroupingColumn} to hold the specified + * {@link StatsGrouping} instances. + * + * @param statsGroupings + * the groupings + * @return the column + */ + public static StatsGroupingColumn withGroupings( + StatsGrouping... statsGroupings) { + StatsGroupingColumn column = new StatsGroupingColumn(); + + for (StatsGrouping grouping : statsGroupings) { + column.group.add(grouping); + } + + return column; + } +} diff --git a/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/AggregateManager.java b/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/AggregateManager.java index 44c27f32d7..40af314884 100644 --- a/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/AggregateManager.java +++ b/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/AggregateManager.java @@ -19,7 +19,9 @@ **/ package com.raytheon.uf.edex.stats; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; @@ -28,11 +30,17 @@ import java.util.List; import java.util.Map; import java.util.TimeZone; +import javax.xml.bind.JAXBException; + +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.raytheon.uf.common.event.Event; +import com.raytheon.uf.common.serialization.JAXBManager; import com.raytheon.uf.common.serialization.SerializationUtil; import com.raytheon.uf.common.stats.AggregateRecord; +import com.raytheon.uf.common.stats.StatsGrouping; +import com.raytheon.uf.common.stats.StatsGroupingColumn; import com.raytheon.uf.common.stats.StatsRecord; import com.raytheon.uf.common.stats.xml.StatisticsAggregate; import com.raytheon.uf.common.stats.xml.StatisticsEvent; @@ -61,6 +69,7 @@ import com.raytheon.uf.edex.stats.util.ConfigLoader; * Nov 07, 2012 1317 mpduff Updated Configuration Files. * Nov 28, 2012 1350 rjpeter Simplied aggregation and added aggregation with current db aggregate records. * Jan 07, 2013 1451 djohnson Use newGmtCalendar(). + * Jan 15, 2013 1487 djohnson Use xml for the grouping information on an {@link AggregateRecord}. * * * @author jsanchez @@ -70,6 +79,17 @@ public class AggregateManager { private static final IUFStatusHandler statusHandler = UFStatus .getHandler(AggregateManager.class); + private static final Object[] EMPTY_OBJ_ARR = new Object[0]; + + private static final JAXBManager JAXB_MANAGER; + static { + try { + JAXB_MANAGER = new JAXBManager(StatsGroupingColumn.class); + } catch (JAXBException e) { + throw new ExceptionInInitializerError(e); + } + } + /** In minutes */ private int bucketInterval; @@ -255,8 +275,6 @@ public class AggregateManager { Map> rval = new HashMap>(); TimeRange timeRange = null; Multimap eventsByGroup = null; - final Object[] EMPTY_OBJ_ARR = new Object[0]; - StringBuilder group = new StringBuilder(); for (StatsRecord record : records) { if ((timeRange == null) @@ -275,30 +293,11 @@ public class AggregateManager { Event event = SerializationUtil.transformFromThrift( Event.class, record.getEvent()); - // determine group - boolean addDelim = false; - Iterator gMethodIter = statEvent.getGroupByMethods() - .iterator(); - Iterator gFieldNameIter = statEvent - .getGroupList().iterator(); - group.setLength(0); - - while (gMethodIter.hasNext() && gFieldNameIter.hasNext()) { - Method m = gMethodIter.next(); - String field = gFieldNameIter.next().getName(); - String gVal = String - .valueOf(m.invoke(event, EMPTY_OBJ_ARR)); - - if (addDelim) { - group.append('-'); - } else { - addDelim = true; - } - - group.append(field).append(':').append(gVal); + String groupAsString = determineGroupRepresentationForEvent( + statEvent, event); + if (groupAsString != null) { + eventsByGroup.put(groupAsString, event); } - - eventsByGroup.put(group.toString(), event); } catch (Exception e) { statusHandler .error("Error processing event. Aggregation may be inaccurate. ", @@ -309,6 +308,30 @@ public class AggregateManager { return rval; } + @VisibleForTesting + static String determineGroupRepresentationForEvent( + StatisticsEvent statEvent, Event event) + throws IllegalAccessException, InvocationTargetException, + JAXBException { + Iterator gMethodIter = statEvent.getGroupByMethods().iterator(); + Iterator gFieldNameIter = statEvent.getGroupList() + .iterator(); + List groupings = new ArrayList(); + + while (gMethodIter.hasNext() && gFieldNameIter.hasNext()) { + Method m = gMethodIter.next(); + String field = gFieldNameIter.next().getName(); + String gVal = String.valueOf(m.invoke(event, EMPTY_OBJ_ARR)); + + groupings.add(new StatsGrouping(field, gVal)); + } + + StatsGroupingColumn column = new StatsGroupingColumn(); + column.setGroup(groupings); + + return JAXB_MANAGER.marshalToXml(column); + } + /** * Tests if the bucket interval is a valid value. If value is invalid then * value will be set to default value. diff --git a/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/data/StatsDataAccumulator.java b/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/data/StatsDataAccumulator.java index c21c31ff5e..233a1bd749 100644 --- a/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/data/StatsDataAccumulator.java +++ b/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/data/StatsDataAccumulator.java @@ -27,13 +27,18 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.TreeSet; -import java.util.regex.Pattern; + +import javax.xml.bind.JAXBException; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.raytheon.uf.common.serialization.JAXBManager; import com.raytheon.uf.common.serialization.SerializationException; import com.raytheon.uf.common.serialization.SerializationUtil; import com.raytheon.uf.common.stats.AggregateRecord; +import com.raytheon.uf.common.stats.StatsGrouping; +import com.raytheon.uf.common.stats.StatsGroupingColumn; import com.raytheon.uf.common.stats.data.GraphData; import com.raytheon.uf.common.stats.data.StatsBin; import com.raytheon.uf.common.stats.data.StatsData; @@ -44,34 +49,42 @@ import com.raytheon.uf.common.status.UFStatus; import com.raytheon.uf.common.status.UFStatus.Priority; import com.raytheon.uf.common.time.TimeRange; import com.raytheon.uf.common.time.util.TimeUtil; +import com.raytheon.uf.common.util.CollectionUtil; /** * Accumulates the statistics data. - * + * *
- *
+ * 
  * SOFTWARE HISTORY
- *
+ * 
  * Date         Ticket#    Engineer    Description
  * ------------ ---------- ----------- --------------------------
  * Nov 15, 2012    728     mpduff      Initial creation
- *
+ * Jan 15, 2013 1487       djohnson    Use xml for the grouping information on an {@link AggregateRecord}.
+ * 
  * 
- * + * * @author mpduff * @version 1.0 */ public class StatsDataAccumulator { - private static final Pattern COLON_PATTERN = Pattern.compile(":"); - - private static final Pattern DASH_PATTERN = Pattern.compile("-"); - - private static final String COLON = ":"; private static final IUFStatusHandler statusHandler = UFStatus .getHandler(StatsDataAccumulator.class); + private static final String COLON = ":"; + + private static final JAXBManager JAXB_MANAGER; + static { + try { + JAXB_MANAGER = new JAXBManager(StatsGroupingColumn.class); + } catch (JAXBException e) { + throw new ExceptionInInitializerError(e); + } + } + /** List of records */ private AggregateRecord[] records; @@ -108,7 +121,7 @@ public class StatsDataAccumulator { /** * Set the AggregateRecord[] - * + * * @param records * array of AggregateRecord objects */ @@ -122,28 +135,27 @@ public class StatsDataAccumulator { @VisibleForTesting public void setupGroupings() { for (AggregateRecord aggRec : records) { - String grouping = aggRec.getGrouping(); - String[] groupString = DASH_PATTERN.split(grouping); - String group; - String member; - for (String grp : groupString) { - String[] parts = COLON_PATTERN.split(grp); - group = parts[0]; - member = parts[1]; - if (!groupMemberMap.containsKey(group)) { - groupMemberMap.put(group, new TreeSet()); - } + StatsGroupingColumn columnValue = unmarshalGroupingColumnFromRecord(aggRec); - groupMemberMap.get(group).add(member); + final List groups = columnValue.getGroup(); + if (CollectionUtil.isNullOrEmpty(groups)) { + continue; + } + for (StatsGrouping group : groups) { + final String groupName = group.getName(); + if (!groupMemberMap.containsKey(groupName)) { + groupMemberMap.put(groupName, Sets. newTreeSet()); + } + groupMemberMap.get(groupName).add(group.getValue()); } } - groups = new ArrayList(groupMemberMap.keySet()); + groups = Lists.newArrayList(groupMemberMap.keySet()); } /** * Get the GraphData object - * + * * @param groups * List of groups * @return The GraphData object @@ -184,7 +196,7 @@ public class StatsDataAccumulator { /** * Create the StatsDataMap keys - * + * * @param unitUtils * UnitUtils object * @param groups @@ -202,21 +214,25 @@ public class StatsDataAccumulator { if (record.getEventType().equals(eventType) && record.getField().equals(dataType)) { + StatsGroupingColumn columnValue = unmarshalGroupingColumnFromRecord(record); + + final List columnValueGroups = columnValue + .getGroup(); + if (CollectionUtil.isNullOrEmpty(columnValueGroups)) { + continue; + } + // Have a matching record for (String key : keySequenceMap.keySet()) { keySequenceMap.put(key, ""); } - String[] groupings = DASH_PATTERN.split(record.getGrouping()); - for (String grouping : groupings) { - String[] parts = COLON_PATTERN.split(grouping); - String group = parts[0]; - String groupMember = parts[1]; + for (StatsGrouping group : columnValueGroups) { for (String key : keySequenceMap.keySet()) { - if (group.equals(key)) { + if (group.getName().equals(key)) { keySequenceMap.put(key, keySequenceMap.get(key) - .concat(groupMember + COLON)); + .concat(group.getValue() + COLON)); break; } } @@ -242,9 +258,32 @@ public class StatsDataAccumulator { } } + /** + * Unmarshals the {@link StatsGroupingColumn} from the + * {@link AggregateRecord}. + * + * @param record + * the aggregate record + * @return the unmarshalled column, or an empty column if unable to + * unmarshal + */ + private static StatsGroupingColumn unmarshalGroupingColumnFromRecord( + AggregateRecord record) { + String groupingXmlAsString = record.getGrouping(); + try { + return (StatsGroupingColumn) JAXB_MANAGER + .unmarshalFromXml(groupingXmlAsString); + } catch (JAXBException e) { + statusHandler.handle(Priority.PROBLEM, + "Unable to unmarshal stats grouping column, returning empty record, xml:\n" + + groupingXmlAsString, e); + return new StatsGroupingColumn(); + } + } + /** * Gather the data together in each bin. - * + * * @param unitUtils * UnitUtils object * @param groups @@ -323,7 +362,7 @@ public class StatsDataAccumulator { /** * Set the display units. - * + * * @param displayUnit * the displayUnit to set */ @@ -333,7 +372,7 @@ public class StatsDataAccumulator { /** * TimeStep in minutes - * + * * @param timeStep */ public void setTimeStep(int timeStep) { diff --git a/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/util/ConfigLoader.java b/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/util/ConfigLoader.java index e4e6e01d29..239ebfc79d 100644 --- a/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/util/ConfigLoader.java +++ b/edexOsgi/com.raytheon.uf.edex.stats/src/com/raytheon/uf/edex/stats/util/ConfigLoader.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import com.google.common.annotations.VisibleForTesting; import com.raytheon.uf.common.event.Event; import com.raytheon.uf.common.localization.IPathManager; import com.raytheon.uf.common.localization.LocalizationContext; @@ -57,6 +58,7 @@ import com.raytheon.uf.common.util.ReflectionUtil; * Aug 21, 2012 jsanchez Updated error handling and validated config files. * Nov 07, 2012 1317 mpduff Update config files. * Nov 29, 2012 1350 rjpeter Updated to static, fixed localization, increased validation. + * Jan 15, 2013 1487 djohnson Make validate() static and public, so it can be run independently. * * * @author jsanchez @@ -158,7 +160,8 @@ public class ConfigLoader { * * @param config */ - private void validate(Map eventMap, + @VisibleForTesting + public static void validate(Map eventMap, StatisticsConfig config) { for (Iterator iter = config.getEvents().iterator(); iter .hasNext();) { diff --git a/tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.LevelXmlWriter b/tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.LevelXmlWriter deleted file mode 100644 index eec9b4aec4..0000000000 --- a/tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.LevelXmlWriter +++ /dev/null @@ -1 +0,0 @@ -com.raytheon.uf.common.datadelivery.retrieval.util.NullXmlWriter \ No newline at end of file diff --git a/tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.ParameterXmlWriter b/tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.ParameterXmlWriter deleted file mode 100644 index eec9b4aec4..0000000000 --- a/tests/resources/META-INF/services/com.raytheon.uf.common.datadelivery.retrieval.util.ParameterXmlWriter +++ /dev/null @@ -1 +0,0 @@ -com.raytheon.uf.common.datadelivery.retrieval.util.NullXmlWriter \ No newline at end of file diff --git a/tests/unit/com/raytheon/uf/common/datadelivery/retrieval/util/NullXmlWriter.java b/tests/unit/com/raytheon/uf/common/datadelivery/retrieval/util/NullXmlWriter.java deleted file mode 100644 index 38a044e069..0000000000 --- a/tests/unit/com/raytheon/uf/common/datadelivery/retrieval/util/NullXmlWriter.java +++ /dev/null @@ -1,60 +0,0 @@ -/** - * This software was developed and / or modified by Raytheon Company, - * pursuant to Contract DG133W-05-CQ-1067 with the US Government. - * - * U.S. EXPORT CONTROLLED TECHNICAL DATA - * This software product contains export-restricted data whose - * export/transfer/disclosure is restricted by U.S. law. Dissemination - * to non-U.S. persons whether in the United States or abroad requires - * an export license or other authorization. - * - * Contractor Name: Raytheon Company - * Contractor Address: 6825 Pine Street, Suite 340 - * Mail Stop B8 - * Omaha, NE 68106 - * 402.291.0100 - * - * See the AWIPS II Master Rights File ("Master Rights File.pdf") for - * further licensing information. - **/ -package com.raytheon.uf.common.datadelivery.retrieval.util; - -import com.raytheon.uf.common.datadelivery.retrieval.xml.LevelLookup; -import com.raytheon.uf.common.datadelivery.retrieval.xml.ParameterLookup; - -/** - * Makes parser created XML not write out in test - * - *
- *
- * SOFTWARE HISTORY
- *
- * Date         Ticket#    Engineer    Description
- * ------------ ---------- ----------- --------------------------
- * Jan 10, 2013            djohnson     Initial creation
- *
- * 
- * - * @author djohnson - * @version 1.0 - */ - -public class NullXmlWriter implements LevelXmlWriter, ParameterXmlWriter { - - /** - * {@inheritDoc} - */ - @Override - public void writeParameterXml(ParameterLookup pl, String modelName) - throws Exception { - } - - /** - * {@inheritDoc} - */ - @Override - public void writeLevelXml(LevelLookup ll, String modelName) - throws Exception { - } - -} diff --git a/tests/unit/com/raytheon/uf/common/localization/TestPathManager.java b/tests/unit/com/raytheon/uf/common/localization/TestPathManager.java index 6967828aac..0190fd04d0 100644 --- a/tests/unit/com/raytheon/uf/common/localization/TestPathManager.java +++ b/tests/unit/com/raytheon/uf/common/localization/TestPathManager.java @@ -40,6 +40,7 @@ import com.raytheon.uf.common.util.FileUtil; * ------------ ---------- ----------- -------------------------- * Jul 18, 2012 740 djohnson Initial creation * Oct 23, 2012 1286 djohnson Change to find more localization files. + * Jan 16, 2013 1487 djohnson Avoid adding new localization files to baseline utility directories. * * * @@ -131,21 +132,44 @@ public class TestPathManager extends PathManager { } if (foundFile == null - || !foundFile.exists() || foundFile.getAbsolutePath().startsWith( savedLocalizationFileDir.getAbsolutePath())) { return foundFile; } - // Make a copy in the savedFile folder, this way if any - // modifications are performed we don't mess with the file in - // the baseline + + File savedFile = createTestIsolatedVersionOfLocalizationFile( + context, fileName, foundFile); + return savedFile; + } + + /** + * Creates a test isolated version of the localization file. Allows the + * file to be written to, and changes to be read back, without affecting + * the baselined version of the file. + * + * @param context + * the context + * @param fileName + * the file path + * @param baselinedVersion + * the file reference + * @return + */ + private File createTestIsolatedVersionOfLocalizationFile( + LocalizationContext context, String fileName, File baselinedVersion) { File savedFileBaseDir = new File(savedLocalizationFileDir, context.toPath()); File savedFile = new File(savedFileBaseDir, fileName); savedFile.getParentFile().mkdirs(); try { - FileUtil.copyFile(foundFile, savedFile); + if (baselinedVersion.exists()) { + if (baselinedVersion.isDirectory()) { + savedFile.mkdirs(); + } else { + FileUtil.copyFile(baselinedVersion, savedFile); + } + } } catch (IOException e) { throw new RuntimeException(e); } diff --git a/tests/unit/com/raytheon/uf/edex/stats/AggregateManagerTest.java b/tests/unit/com/raytheon/uf/edex/stats/AggregateManagerTest.java new file mode 100644 index 0000000000..5590fa147e --- /dev/null +++ b/tests/unit/com/raytheon/uf/edex/stats/AggregateManagerTest.java @@ -0,0 +1,116 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ +package com.raytheon.uf.edex.stats; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import javax.xml.bind.JAXBException; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.common.collect.Maps; +import com.raytheon.uf.common.localization.IPathManager; +import com.raytheon.uf.common.localization.LocalizationContext; +import com.raytheon.uf.common.localization.LocalizationContext.LocalizationLevel; +import com.raytheon.uf.common.localization.LocalizationContext.LocalizationType; +import com.raytheon.uf.common.localization.LocalizationFile; +import com.raytheon.uf.common.localization.PathManagerFactory; +import com.raytheon.uf.common.localization.PathManagerFactoryTest; +import com.raytheon.uf.common.serialization.JAXBManager; +import com.raytheon.uf.common.stats.StatsGrouping; +import com.raytheon.uf.common.stats.StatsGroupingColumn; +import com.raytheon.uf.common.stats.xml.StatisticsConfig; +import com.raytheon.uf.common.stats.xml.StatisticsEvent; +import com.raytheon.uf.common.util.FileUtil; +import com.raytheon.uf.edex.stats.util.ConfigLoader; + +/** + * Test {@link AggregateManager}. + * + *
+ * 
+ * SOFTWARE HISTORY
+ * 
+ * Date         Ticket#    Engineer    Description
+ * ------------ ---------- ----------- --------------------------
+ * Jan 15, 2013 1487       djohnson     Initial creation
+ * 
+ * 
+ * + * @author djohnson + * @version 1.0 + */ + +public class AggregateManagerTest { + private static JAXBManager jaxbManager; + + @BeforeClass + public static void classSetUp() throws JAXBException { + jaxbManager = new JAXBManager(StatisticsConfig.class, + StatsGroupingColumn.class); + } + + @Before + public void setUp() { + PathManagerFactoryTest.initLocalization(); + } + + @Test + public void testDeterminingGroupForEvent() throws Exception { + IPathManager pm = PathManagerFactory.getPathManager(); + final LocalizationFile lf = pm.getLocalizationFile( + new LocalizationContext(LocalizationType.EDEX_STATIC, + LocalizationLevel.BASE), FileUtil.join("stats", + "mockStats.xml")); + + final StatisticsConfig statisticsConfig = lf.jaxbUnmarshal( + StatisticsConfig.class, jaxbManager); + + ConfigLoader.validate(Maps. newHashMap(), + statisticsConfig); + + MockEvent mockEvent = new MockEvent(); + mockEvent.setPluginName("somePlugin"); + mockEvent.setFileName("someFileName"); + mockEvent.setProcessingTime(1000L); + mockEvent.setProcessingLatency(500L); + + List groupList = new ArrayList(); + groupList.add(new StatsGrouping("pluginName", "somePlugin")); + groupList.add(new StatsGrouping("fileName", "someFileName")); + StatsGroupingColumn column = new StatsGroupingColumn(); + column.setGroup(groupList); + + final String expectedGroupRepresentation = jaxbManager + .marshalToXml(column); + final String actualGroupRepresentation = AggregateManager.determineGroupRepresentationForEvent( + statisticsConfig.getEvents().iterator().next(), mockEvent); + assertThat(actualGroupRepresentation, + is(equalTo(expectedGroupRepresentation))); + } + +} diff --git a/tests/unit/com/raytheon/uf/edex/stats/MockEvent.java b/tests/unit/com/raytheon/uf/edex/stats/MockEvent.java new file mode 100644 index 0000000000..419ed5be0f --- /dev/null +++ b/tests/unit/com/raytheon/uf/edex/stats/MockEvent.java @@ -0,0 +1,169 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ +package com.raytheon.uf.edex.stats; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.raytheon.uf.common.serialization.annotations.DynamicSerialize; +import com.raytheon.uf.common.serialization.annotations.DynamicSerializeElement; +import com.raytheon.uf.common.stats.ProcessEvent; +import com.raytheon.uf.common.stats.StatisticsEvent; + +/** + * Mock event based from {@link ProcessEvent}. + * + *
+ * 
+ * SOFTWARE HISTORY
+ * 
+ * Date         Ticket#    Engineer    Description
+ * ------------ ---------- ----------- --------------------------
+ * Jan 15, 2013 1487       djohnson     Initial creation
+ * 
+ * 
+ * + * @author djohnson + * @version 1.0 + */ +@DynamicSerialize +public class MockEvent extends StatisticsEvent { + + private static final long serialVersionUID = 1L; + + private static final Map FIELD_UNIT_MAP; + static { + Map m = new HashMap(); + m.put("processingLatency", "ms"); + m.put("processingTime", "ms"); + FIELD_UNIT_MAP = Collections.unmodifiableMap(m); + } + + @DynamicSerializeElement + private String message; + + @DynamicSerializeElement + private String pluginName; + + @DynamicSerializeElement + private String fileName; + + /* + * Processing time in milliseconds + */ + @DynamicSerializeElement + private long processingTime; + + /* + * Processing latency in milliseconds + */ + @DynamicSerializeElement + private long processingLatency; + + public MockEvent() { + } + + @Override + protected Map getFieldUnitMap() { + return FIELD_UNIT_MAP; + } + + /** + * @return the fileName + */ + public String getFileName() { + return fileName; + } + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * @return the pluginName + */ + public String getPluginName() { + return pluginName; + } + + /** + * @return the processingLatency in milliseconds + */ + public long getProcessingLatency() { + return processingLatency; + } + + /** + * @return the processingTime in milliseconds + */ + public long getProcessingTime() { + return processingTime; + } + + /** + * @param fileName + * the fileName to set + */ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * @param message + * the message to set + */ + public void setMessage(String message) { + this.message = message; + } + + /** + * @param pluginName + * the pluginName to set + */ + public void setPluginName(String pluginName) { + this.pluginName = pluginName; + } + + /** + * @param processingLatency + * the processingLatency in milliseconds to set + */ + public void setProcessingLatency(long processingLatency) { + this.processingLatency = processingLatency; + } + + /** + * @param processingTime + * the processingTime in milliseconds to set + */ + public void setProcessingTime(long processingTime) { + this.processingTime = processingTime; + } + + @Override + public String toString() { + return super.toString() + " : " + getMessage(); + } + +} \ No newline at end of file diff --git a/tests/unit/com/raytheon/uf/edex/stats/data/StatsDataAccumulatorTest.java b/tests/unit/com/raytheon/uf/edex/stats/data/StatsDataAccumulatorTest.java index 4e6288206e..7b9e9664f0 100644 --- a/tests/unit/com/raytheon/uf/edex/stats/data/StatsDataAccumulatorTest.java +++ b/tests/unit/com/raytheon/uf/edex/stats/data/StatsDataAccumulatorTest.java @@ -1,47 +1,92 @@ +/** + * This software was developed and / or modified by Raytheon Company, + * pursuant to Contract DG133W-05-CQ-1067 with the US Government. + * + * U.S. EXPORT CONTROLLED TECHNICAL DATA + * This software product contains export-restricted data whose + * export/transfer/disclosure is restricted by U.S. law. Dissemination + * to non-U.S. persons whether in the United States or abroad requires + * an export license or other authorization. + * + * Contractor Name: Raytheon Company + * Contractor Address: 6825 Pine Street, Suite 340 + * Mail Stop B8 + * Omaha, NE 68106 + * 402.291.0100 + * + * See the AWIPS II Master Rights File ("Master Rights File.pdf") for + * further licensing information. + **/ package com.raytheon.uf.edex.stats.data; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.ArrayList; -import java.util.Calendar; +import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Set; -import java.util.TimeZone; + +import javax.xml.bind.JAXBException; import org.junit.Test; +import com.google.common.collect.Maps; +import com.raytheon.uf.common.datadelivery.event.retrieval.DataRetrievalEvent; +import com.raytheon.uf.common.datadelivery.event.retrieval.SubscriptionRetrievalEvent; +import com.raytheon.uf.common.serialization.JAXBManager; import com.raytheon.uf.common.stats.AggregateRecord; +import com.raytheon.uf.common.stats.StatsGrouping; +import com.raytheon.uf.common.stats.StatsGroupingColumn; +import com.raytheon.uf.common.stats.data.StatsData; import com.raytheon.uf.common.stats.util.UnitUtils; import com.raytheon.uf.common.time.TimeRange; +import com.raytheon.uf.common.time.util.TimeUtil; +/** + * + * Test {@link StatsDataAccumulator}. + * + *
+ * 
+ * SOFTWARE HISTORY
+ * 
+ * Date         Ticket#    Engineer    Description
+ * ------------ ---------- ----------- --------------------------
+ * Jan 15, 2013 1487       djohnson     Use XML for grouping column.
+ * 
+ * 
+ * + * @author djohnson + * @version 1.0 + */ public class StatsDataAccumulatorTest { + private static final JAXBManager JAXB_MANAGER; + static { + try { + JAXB_MANAGER = new JAXBManager(StatsGroupingColumn.class); + } catch (JAXBException e) { + throw new ExceptionInInitializerError(e); + } + } + @Test public void testCalculateBinsCalculatesCorrectly() { - Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); - c.set(Calendar.MILLISECOND, 0); - c.set(Calendar.SECOND, 0); - c.set(Calendar.MINUTE, 0); - c.set(Calendar.HOUR_OF_DAY, 0); - c.set(Calendar.DAY_OF_MONTH, 1); - c.set(Calendar.MONTH, 0); - long start = c.getTimeInMillis(); - - c.add(Calendar.DAY_OF_MONTH, 1); - long end = c.getTimeInMillis(); - - TimeRange tr = new TimeRange(start, end); + TimeRange tr = new TimeRange(0L, TimeUtil.MILLIS_PER_DAY); StatsDataAccumulator acc = new StatsDataAccumulator(); acc.setTimeRange(tr); acc.setTimeStep(5); acc.calculateBins(); - int expectedBinCount = 288; // 5 minute bins 12 per hour, * 24 + int expectedBinCount = 288; // 5 minute bins 12 per hour, * 24 int actualBinCount = acc.bins.keySet().size(); - assertEquals("Bin Counts do not match", expectedBinCount, actualBinCount); + assertEquals("Bin Counts do not match", expectedBinCount, + actualBinCount); int count = 0; for (long bin : acc.bins.keySet()) { @@ -51,7 +96,7 @@ public class StatsDataAccumulatorTest { } @Test - public void testSetupGroupings() { + public void testSetupGroupings() throws JAXBException { List recordList = getTestRecords(); StatsDataAccumulator acc = new StatsDataAccumulator(); acc.setRecords(recordList.toArray(new AggregateRecord[recordList.size()])); @@ -62,33 +107,39 @@ public class StatsDataAccumulatorTest { expectedGroups.add("provider"); expectedGroups.add("plugin"); - List expectedGroupMembers = new ArrayList(); - expectedGroupMembers.add("nomads"); - expectedGroupMembers.add("madis"); - expectedGroupMembers.add("owner0"); - expectedGroupMembers.add("owner1"); - expectedGroupMembers.add("owner2"); - expectedGroupMembers.add("owner3"); - expectedGroupMembers.add("owner4"); - expectedGroupMembers.add("grid"); + List expectedPlugins = Arrays.asList("grid"); + List expectedProviders = Arrays.asList("nomads", "madis"); + List expectedOwners = Arrays.asList("owner0", "owner1", + "owner2", "owner3", "owner4"); + Map> expectedGroupsToValues = Maps.newHashMap(); + expectedGroupsToValues.put("provider", expectedProviders); + expectedGroupsToValues.put("plugin", expectedPlugins); + expectedGroupsToValues.put("owner", expectedOwners); // Check the groups - for (String group : acc.groups) { - assertTrue(expectedGroups.contains(group)); + for (String expected : expectedGroups) { + assertTrue("Did not find group [" + expected + + "] in the group collection!", + acc.groups.contains(expected)); } // Check the group members - for (String key: acc.groupMemberMap.keySet()) { - for (String member: acc.groupMemberMap.get(key)) { - assertTrue(expectedGroupMembers.contains(member)); + final Map> groupMemberMap = acc.groupMemberMap; + for (Entry> entry : expectedGroupsToValues + .entrySet()) { + final String groupName = entry.getKey(); + final Set setToCheck = groupMemberMap.get(groupName); + for (String member : entry.getValue()) { + assertTrue("Did not find entry [" + member + "] for group [" + + groupName + "]!", setToCheck.contains(member)); } } } @Test - public void testCreateStatsDataMapCreation() { - String eventType = "com.raytheon.uf.common.datadelivery.event.retrieval.DataRetrievalEvent"; + public void testCreateStatsDataMapCreation() throws JAXBException { + String eventType = DataRetrievalEvent.class.getName(); String dataType = "bytes"; String displayUnit = "MB"; @@ -116,54 +167,54 @@ public class StatsDataAccumulatorTest { expectedSet.add("owner3:madis"); expectedSet.add("owner4:madis"); - for (String key : acc.statsDataMap.keySet()) { - assertTrue(expectedSet.contains(key)); + final Map statsDataMap = acc.statsDataMap; + for (String expected : expectedSet) { + assertTrue("Did not find expected value (" + expected + + "] as key in the statsDataMap!", + statsDataMap.containsKey(expected)); } } // Build the Aggregate records - private List getTestRecords() { + private List getTestRecords() throws JAXBException { String plugin = "plugin"; String provider = "provider"; String nomads = "nomads"; String madis = "madis"; String owner = "owner"; String grid = "grid"; - String dash = "-"; - String colon = ":"; - List groupings = new ArrayList(); - for (int i = 0; i < 5; i++) { - groupings.add(plugin + colon + grid + dash + owner + colon + owner + i); + List groupingColumns = new ArrayList(); + for (int i = 0; i < 15; i++) { + groupingColumns.add(StatsGroupingColumn.withGroupings( + new StatsGrouping(plugin, grid), new StatsGrouping(owner, + owner + i))); } for (int i = 0; i < 5; i++) { - groupings.add(plugin + colon + grid + dash + owner + colon + owner + i); + groupingColumns.add(StatsGroupingColumn.withGroupings( + new StatsGrouping(provider, nomads), new StatsGrouping( + owner, owner + i))); } for (int i = 0; i < 5; i++) { - groupings.add(plugin + colon + grid + dash + owner + colon + owner + i); - } - - for (int i = 0; i < 5; i++) { - groupings.add(provider + colon + nomads + dash + owner + colon + owner + i); - } - for (int i = 0; i < 5; i++) { - groupings.add(provider + colon + madis + dash + owner + colon + owner + i); + groupingColumns.add(StatsGroupingColumn.withGroupings( + new StatsGrouping(provider, madis), new StatsGrouping( + owner, owner + i))); } List records = new ArrayList(); - for (String group : groupings) { + for (StatsGroupingColumn group : groupingColumns) { AggregateRecord r = new AggregateRecord(); - if (group.contains("provider")) { - r.setEventType("com.raytheon.uf.common.datadelivery.event.retrieval.DataRetrievalEvent"); + if ("provider".equals(group.getGroup().iterator().next().getName())) { + r.setEventType(DataRetrievalEvent.class.getName()); r.setField("bytes"); } else { - r.setEventType("com.raytheon.uf.common.datadelivery.event.retrieval.SubscriptionRetrievalEvent"); + r.setEventType(SubscriptionRetrievalEvent.class.getName()); r.setField("numRecords"); } - r.setGrouping(group); + r.setGrouping(JAXB_MANAGER.marshalToXml(group)); records.add(r); } diff --git a/tests/utility/edex_static/base/stats/mockStats.xml b/tests/utility/edex_static/base/stats/mockStats.xml new file mode 100644 index 0000000000..3c9c5aef7a --- /dev/null +++ b/tests/utility/edex_static/base/stats/mockStats.xml @@ -0,0 +1,12 @@ + + + + + + + + +