changeset 2480:e2e5b4797c2a

[Byteman] Add tests for GraphDataset. Reviewed-by: aazores Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2016-October/021296.html
author Severin Gehwolf <sgehwolf@redhat.com>
date Tue, 20 Sep 2016 19:07:51 +0200
parents 81cdf24da562
children bcf729e138c8
files vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDataset.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDatasetTest.java
diffstat 5 files changed, 695 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDataset.java	Tue Oct 18 18:34:05 2016 +0200
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDataset.java	Tue Sep 20 19:07:51 2016 +0200
@@ -51,10 +51,13 @@
 import org.jfree.data.xy.XYSeriesCollection;
 
 import com.redhat.thermostat.common.Pair;
+import com.redhat.thermostat.shared.locale.Translate;
 import com.redhat.thermostat.vm.byteman.common.BytemanMetric;
 
 class GraphDataset {
     
+    private static final Translate<LocaleResources> t = LocaleResources.createLocalizer();
+    
     enum CoordinateType {
         INTEGRAL,
         REAL,
@@ -192,11 +195,14 @@
                 switch (ytype) {
                 case REAL:
                     y = y.doubleValue() + y1.doubleValue();
+                    break;
                 default:
                     y = y.longValue() + y1.longValue();
                 }
+                xyseries.updateByIndex(idx, y);
+            } else {
+                xyseries.add(x, y);
             }
-            xyseries.add(x, y);
         }
         XYSeriesCollection xycollection = new  XYSeriesCollection();
         xycollection.addSeries(xyseries);
@@ -233,7 +239,7 @@
                 String first = p.getFirst().toString();
                 String second = "";
                 double increment = ((Number) p.getSecond()).doubleValue();
-                if(dataset.getRowKeys().contains(first)) {
+                if (dataset.getRowKeys().contains(first)) {
                     dataset.incrementValue(increment, first, second);
                 } else {
                     dataset.addValue(increment, first, second);
@@ -281,7 +287,7 @@
         // set with a range axis which displays the numeric
         // values symbolically.
 
-        XYSeries xyseries = new XYSeries(ykey + " against  " + xkey);
+        XYSeries xyseries = new XYSeries(t.localize(LocaleResources.X_AGAINST_Y, xkey, ykey).getContents());
         int count = 0;
         HashMap<String, Number> tickmap = new HashMap<String, Number>();
 
@@ -379,6 +385,10 @@
                 updateCType = true;
             }
         }
+        if (value instanceof Double || value.getClass() == double.class) {
+            ctype = CoordinateType.REAL;
+            updateCType = true;
+        }
         if (updateCType) {
             if (isX) {
                 xtype = ctype;
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java	Tue Oct 18 18:34:05 2016 +0200
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/LocaleResources.java	Tue Sep 20 19:07:51 2016 +0200
@@ -59,7 +59,8 @@
     FILTER_VALUE_LABEL,
     NO_FILTER_NAME,
     X_COORD,
-    Y_COORD
+    Y_COORD,
+    X_AGAINST_Y,
     ;
     
     static final String RESOURCE_BUNDLE = LocaleResources.class.getPackage().getName() + ".strings";
--- a/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java	Tue Oct 18 18:34:05 2016 +0200
+++ b/vm-byteman/client-swing/src/main/java/com/redhat/thermostat/vm/byteman/client/swing/internal/SwingVmBytemanView.java	Tue Sep 20 19:07:51 2016 +0200
@@ -928,7 +928,7 @@
         JFreeChart graph = null;
         switch (xtype) {
             case CATEGORY:
-                if(ytype == CoordinateType.CATEGORY) {
+                if (ytype == CoordinateType.CATEGORY) {
                     // use a bar chart with multiple bars per category 1 value
                     // where each bar counts the frequency for the second category
                     CategoryDataset categoryDataset = dataset.getCategoryDataset();
@@ -951,7 +951,7 @@
                 }
                 break;
             case TIME:
-                if(ytype == CoordinateType.CATEGORY) {
+                if (ytype == CoordinateType.CATEGORY) {
                     // we need to draw a graph of category (state) value against time
                     // with step transitions between states
                     //
@@ -974,7 +974,7 @@
                 break;
             case INTEGRAL:
             case REAL:
-                if(ytype == CoordinateType.CATEGORY) {
+                if (ytype == CoordinateType.CATEGORY) {
                     // we could treat the numeric values as category values (or ranges?)
                     // and draw this as a bar chart
                     CategoryDataset categoryDataset = dataset.getCategoryDataset();
@@ -982,7 +982,7 @@
                                                         categoryDataset, PlotOrientation.VERTICAL,
                                                         true, true, false);
                     // for now draw an empty graph
-                } else if(ytype == CoordinateType.TIME) {
+                } else if (ytype == CoordinateType.TIME) {
                     // we could group the time values as time ranges
                     // and draw this as a bar chart
                     //
--- a/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties	Tue Oct 18 18:34:05 2016 +0200
+++ b/vm-byteman/client-swing/src/main/resources/com/redhat/thermostat/vm/byteman/client/swing/internal/strings.properties	Tue Sep 20 19:07:51 2016 +0200
@@ -18,3 +18,4 @@
 NO_FILTER_NAME = <No Filter>
 X_COORD = x:
 Y_COORD = y:
+X_AGAINST_Y = {0} against {1}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-byteman/client-swing/src/test/java/com/redhat/thermostat/vm/byteman/client/swing/internal/GraphDatasetTest.java	Tue Sep 20 19:07:51 2016 +0200
@@ -0,0 +1,675 @@
+/*
+ * Copyright 2012-2016 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.byteman.client.swing.internal;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.jfree.data.category.CategoryDataset;
+import org.jfree.data.xy.XYDataset;
+import org.junit.Test;
+
+import com.redhat.thermostat.common.Pair;
+import com.redhat.thermostat.vm.byteman.client.swing.internal.GraphDataset.CoordinateType;
+import com.redhat.thermostat.vm.byteman.common.BytemanMetric;
+
+public class GraphDatasetTest {
+
+    private static final double DELTA = 0.001;
+    private static final String TEST_WRITER_ID = "test-writerId";
+    private static final String COUNT_FIELD = "count";
+    private static final String TEST_VM_ID = "test-vmId";
+    
+    private static List<BytemanMetric> buildMetrics(MetricConfig config) {
+        List<BytemanMetric> mList = new ArrayList<>();
+        for (int i = 0; i < config.getNumMetrics(); i++) {
+            BytemanMetric m = new BytemanMetric(TEST_WRITER_ID);
+            m.setVmId(TEST_VM_ID);
+            m.setData(config.produceDataJson(i));
+            m.setMarker(config.produceMarker(i));
+            m.setTimeStamp(config.produceTimeStamp(i));
+            mList.add(m);
+        }
+        return mList;
+    }
+    
+    @Test
+    public void testEmptyXYDataSet() {
+        final List<BytemanMetric> empty = Collections.emptyList();
+        // x => marker
+        GraphDataset dataset = new GraphDataset(empty, GraphDataset.MARKER_KEY, GraphDataset.TIMESTAMP_KEY, null, null);
+        XYDataset actualDataset = dataset.getXYDataset();
+        assertEquals(0, actualDataset.getSeriesCount());
+        
+        // y => marker
+        dataset = new GraphDataset(empty, GraphDataset.TIMESTAMP_KEY, GraphDataset.MARKER_KEY, null, null);
+        actualDataset = dataset.getXYDataset();
+        assertEquals(0, actualDataset.getSeriesCount());
+    }
+
+    @Test
+    public void testXYDataSetBasic() {
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+           @Override
+           public String produceDataJson(int i) {
+               return new DataJsonBuilder()
+                       .addKeyValue(COUNT_FIELD, new Long(i))
+                       .addKeyValue("foobar", "baz" + i) // extra fields shouldn't matter
+                       .build();
+           }
+        });
+        // plots (x, y) where (x_i, y_i) == (i, i)
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        XYDataset xyDataSet = dataSet.getXYDataset();
+        assertEquals(1, xyDataSet.getSeriesCount());
+        assertEquals(DefaultMetricConfig.DEFAULT_NUM, xyDataSet.getItemCount(0));
+        
+        // sanity check some values
+        assertEquals(0L, xyDataSet.getX(0, 0));
+        assertEquals(0.0, xyDataSet.getY(0, 0));
+        assertEquals(1L, xyDataSet.getX(0, 1));
+        assertEquals(1.0, xyDataSet.getY(0, 1));
+        assertEquals(99L, xyDataSet.getX(0, 99));
+        assertEquals(99.0, xyDataSet.getY(0, 99));
+    }
+    
+    @Test
+    public void testXYDataSetFiltered() {
+        final String filterKey = "foo";
+        final String filterValue = "bar";
+        // Produce metrics so we filter every other metric
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+           @Override
+           public String produceDataJson(int i) {
+               if (i % 2 == 0) {
+                   return new DataJsonBuilder()
+                       .addKeyValue(COUNT_FIELD, new Long(i))
+                       .build();
+               } else {
+                   return new DataJsonBuilder()
+                           .addKeyValue(COUNT_FIELD, new Long(i))
+                           .addKeyValue(filterKey, filterValue)
+                           .build();
+               }
+           }
+        });
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, filterKey, filterValue);
+        XYDataset xyDataSet = dataSet.getXYDataset();
+        assertEquals(1, xyDataSet.getSeriesCount());
+        assertEquals(DefaultMetricConfig.DEFAULT_NUM/2, xyDataSet.getItemCount(0));
+        
+        // sanity check some values
+        assertEquals(1L, xyDataSet.getX(0, 0));
+        assertEquals(1.0, xyDataSet.getY(0, 0));
+        assertEquals(3L, xyDataSet.getX(0, 1));
+        assertEquals(3.0, xyDataSet.getY(0, 1));
+        assertEquals(99L, xyDataSet.getX(0, 49));
+        assertEquals(99.0, xyDataSet.getY(0, 49));
+    }
+    
+    /**
+     * When there is a repeated x value in our data the corresponding y values
+     * contain the running sum. That is for a series x_i = a, y_i = 0.i for all
+     * n values, then the x_n == sum(y_0,y_n). So for n == 4
+     * we have: x_4 = (0.0 + 0.1 + 0.2 + 0.3).  
+     */
+    @Test
+    public void testXYDataSetXRepeatDoubleSingleKey() {
+        final long timeStamp = 322L; // constant numeric x coordinate
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+           @Override
+           public String produceDataJson(int i) {
+               String dStringVal = String.format("0.%d", i);
+               Double dVal = Double.parseDouble(dStringVal);
+                   return new DataJsonBuilder()
+                       .addKeyValue(COUNT_FIELD, dVal)
+                       .build();
+           }
+           
+           @Override
+           public int getNumMetrics() {
+               return 4;
+           }
+           
+           @Override
+           public long produceTimeStamp(int i) {
+               return timeStamp; // same timestamp for all metrics
+           }
+        });
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        XYDataset xyDataSet = dataSet.getXYDataset();
+        assertEquals(1, xyDataSet.getSeriesCount());
+        assertEquals(1, xyDataSet.getItemCount(0));
+        assertEquals(0.6, xyDataSet.getYValue(0, 0), DELTA);
+        assertEquals(timeStamp, (long)xyDataSet.getXValue(0, 0));
+    }
+    
+    @Test
+    public void testXYDataSetXRepeatLongMultipleRepeatedKeys() {
+        final int repeatAmount = 3;
+        final long timestamps[] = new long[] {
+            322L, 300L, -1L
+        };
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+           @Override
+           public String produceDataJson(int i) {
+               Long lVal = Long.valueOf(i);
+                   return new DataJsonBuilder()
+                       .addKeyValue(COUNT_FIELD, lVal)
+                       .build();
+           }
+           
+           @Override
+           public int getNumMetrics() {
+               // x key repeats repeatAmount times
+               return timestamps.length * repeatAmount;
+           }
+           
+           @Override
+           public long produceTimeStamp(int i) {
+               int idx = (i % repeatAmount);
+               return timestamps[idx]; // same timestamp every repeatAmount times
+           }
+        });
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        XYDataset xyDataSet = dataSet.getXYDataset();
+        assertEquals(1, xyDataSet.getSeriesCount());
+        assertEquals(repeatAmount, xyDataSet.getItemCount(0));
+        // added coordinates are sorted by x (ascending)
+        assertEquals(timestamps[2], (long)xyDataSet.getXValue(0, 0));
+        assertEquals(timestamps[1], (long)xyDataSet.getXValue(0, 1));
+        assertEquals(timestamps[0], (long)xyDataSet.getXValue(0, 2));
+        assertEquals("0 + 3 + 6 = 9", 9.0, xyDataSet.getYValue(0, 2), DELTA);
+        assertEquals("1 + 4 + 7 = 12", 12.0, xyDataSet.getYValue(0, 1), DELTA);
+        assertEquals("2 + 5 + 8 = 15", 15.0, xyDataSet.getYValue(0, 0), DELTA);
+    }
+    
+    /**
+     * Special case of testXYDataSetXRepeatLongMultipleRepeatedKeys where
+     * y_i = 1 for all i.
+     */
+    @Test
+    public void testXYDataSetXRepeatLongMultipleRepeatedKeysFrequency() {
+        final int repeatAmount = 3;
+        final long timestamps[] = new long[] {
+            322L, 300L, -1L
+        };
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+           @Override
+           public String produceDataJson(int i) {
+               Long lVal = Long.valueOf(1);
+                   return new DataJsonBuilder()
+                       .addKeyValue(COUNT_FIELD, lVal)
+                       .build();
+           }
+           
+           @Override
+           public int getNumMetrics() {
+               // x key repeats repeatAmount times
+               return timestamps.length * repeatAmount;
+           }
+           
+           @Override
+           public long produceTimeStamp(int i) {
+               int idx = (i % repeatAmount);
+               return timestamps[idx]; // same timestamp every repeatAmount times
+           }
+        });
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        XYDataset xyDataSet = dataSet.getXYDataset();
+        assertEquals(1, xyDataSet.getSeriesCount());
+        assertEquals(repeatAmount, xyDataSet.getItemCount(0));
+        // added coordinates are sorted by x (ascending)
+        assertEquals(timestamps[2], (long)xyDataSet.getXValue(0, 0));
+        assertEquals(timestamps[1], (long)xyDataSet.getXValue(0, 1));
+        assertEquals(timestamps[0], (long)xyDataSet.getXValue(0, 2));
+        assertEquals(timestamps[2] + " occurred 3 times in the data set", 3.0, xyDataSet.getYValue(0, 2), DELTA);
+        assertEquals(timestamps[1] + " occurred 3 times in the data set", 3.0, xyDataSet.getYValue(0, 1), DELTA);
+        assertEquals(timestamps[2] + " occurred 3 times in the data set", 3.0, xyDataSet.getYValue(0, 0), DELTA);
+    }
+    
+    @Test
+    public void testCategoryDatasetEmpty() {
+        final List<BytemanMetric> empty = Collections.emptyList();
+        GraphDataset dataSet = new GraphDataset(empty /* no matter */, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        CategoryDataset actualSet = dataSet.getCategoryDataset();
+        assertEquals(0, actualSet.getColumnCount());
+        assertEquals(0, actualSet.getRowCount());
+    }
+    
+    /**
+     * Category data plot for x = category, y = numeric. In that case
+     * expect the y value (per category) to be the sum of all y values with
+     * category x.
+     */
+    @Test
+    public void testCategoryDatasetYNumeric() {
+        final String evenMarker = "even_marker";
+        final String oddMarker = "odd_marker";
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+            
+            @Override
+            public String produceMarker(int i) {
+                if (i % 2 == 0) {
+                    // even
+                    return evenMarker;
+                } else {
+                    // odd
+                    return oddMarker;
+                }
+            }
+            
+            @Override
+            public String produceDataJson(int i) {
+                Long iLong = Long.valueOf(i);
+                return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .build();
+            }
+            
+            @Override
+            public int getNumMetrics() {
+                return 12;
+            }
+        });
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.MARKER_KEY, COUNT_FIELD, null, null);
+        CategoryDataset actualSet = dataSet.getCategoryDataset();
+        assertEquals("two different markers", 2, actualSet.getRowCount());
+        assertEquals("one value per marker", 1, actualSet.getColumnCount());
+        String actualEvenMarker = (String)actualSet.getRowKey(0);
+        String actualOddMarker = (String)actualSet.getRowKey(1);
+        assertEquals(evenMarker, actualEvenMarker);
+        assertEquals(oddMarker, actualOddMarker);
+        double oddSum = 1.0 + 3.0 + 5 + 7 + 9 + 11; // odd numbers in range [0,12)
+        double evenSum = 2.0 + 4 + 6 + 8 + 10; // even numbers in range [0, 12)
+        Comparable<?> columnKey = actualSet.getColumnKey(0);
+        assertEquals(oddSum, (double)actualSet.getValue(oddMarker, columnKey), DELTA);
+        assertEquals(evenSum, (double)actualSet.getValue(evenMarker, columnKey), DELTA);
+    }
+    
+    /**
+     * Category data plot for x = category, y = category. In that case
+     * expect the y value to be the frequency of category x.
+     */
+    @Test
+    public void testCategoryDatasetYCategory() {
+        final String evenMarker = "even_marker";
+        final String oddMarker = "odd_marker";
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+            
+            @Override
+            public String produceMarker(int i) {
+                if (i % 2 == 0) {
+                    // even
+                    return evenMarker;
+                } else {
+                    // odd
+                    return oddMarker;
+                }
+            }
+            
+            @Override
+            public int getNumMetrics() {
+                // pick an odd number so as to have uneven frequency spread between odd/even numbers
+                return 100 - 1;
+            }
+        });
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.MARKER_KEY, GraphDataset.MARKER_KEY, null, null);
+        CategoryDataset actualSet = dataSet.getCategoryDataset();
+        assertEquals("two different markers", 2, actualSet.getRowCount());
+        assertEquals(2, actualSet.getColumnCount());
+        String actualEvenMarker = (String)actualSet.getRowKey(0);
+        String actualOddMarker = (String)actualSet.getRowKey(1);
+        assertEquals(evenMarker, actualEvenMarker);
+        assertEquals(oddMarker, actualOddMarker);
+        String evenKey = (String)actualSet.getColumnKey(0);
+        String oddKey = (String)actualSet.getColumnKey(1);
+        assertEquals("100/2 = 50. i.e. frequency of even/odd numbers.",
+                49.0, (double)actualSet.getValue(oddMarker, oddKey), DELTA);
+        assertEquals("100/2 = 49. i.e. frequency of even/odd numbers.",
+                50.0, (double)actualSet.getValue(evenMarker, evenKey), DELTA);
+    }
+    
+    @Test
+    public void testEmptyCategoryTimePlot() {
+        final List<BytemanMetric> empty = Collections.emptyList();
+        // x => marker
+        GraphDataset dataset = new GraphDataset(empty, GraphDataset.MARKER_KEY, GraphDataset.TIMESTAMP_KEY, null, null);
+        XYDataset actualDataset = dataset.getCategoryTimePlot(new String[][] { {""} });
+        assertEquals(0, actualDataset.getSeriesCount());
+        
+        // y => timestamp
+        dataset = new GraphDataset(empty, GraphDataset.TIMESTAMP_KEY, GraphDataset.TIMESTAMP_KEY, null, null);
+        actualDataset = dataset.getCategoryTimePlot(new String[][] { {""} });
+        assertEquals(0, actualDataset.getSeriesCount());
+    }
+    
+    @Test
+    public void testCategoryTimePlotBasic() {
+        final String[][] retvals = new String[][] { {""} };
+        final String evenMarker = "even_marker";
+        final String oddMarker = "odd_marker";
+        final int numMetrics = 5;
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig() {
+            
+            @Override
+            public String produceMarker(int i) {
+                if (i % 2 == 0) {
+                    // even
+                    return evenMarker;
+                } else {
+                    // odd
+                    return oddMarker;
+                }
+            }
+            
+            @Override
+            public int getNumMetrics() {
+                return numMetrics;
+            }
+            
+            @Override
+            public long produceTimeStamp(int i) {
+                return i * 2;
+            }
+            
+        });
+        GraphDataset dataset = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.MARKER_KEY, null, null);
+        XYDataset actualDataset = dataset.getCategoryTimePlot(retvals);
+        assertEquals("Expected one series, exactly", 1, actualDataset.getSeriesCount());
+        assertEquals(5, actualDataset.getItemCount(0));
+        for (int i = 0; i < numMetrics; i++) {
+            double retval = actualDataset.getXValue(0, i);
+            double expectedVal = Double.valueOf(i * 2);
+            assertEquals(expectedVal, retval, DELTA);
+        }
+        // Y-values are unique numbers per marker. I.e. 0 and 1 in this case
+        for (int i = 0; i < numMetrics; i++) {
+            double yVal = actualDataset.getYValue(0, i);
+            double expectedVal;
+            if (i %2 == 0) {
+                expectedVal = 0.0;
+            } else {
+                expectedVal = 1.0;
+            }
+            assertEquals(expectedVal, yVal, DELTA);
+        }
+        
+        // verify correct return values have been set
+        List<String> retvalList = Arrays.asList(retvals[0]);
+        Collections.sort(retvalList);
+        assertEquals(evenMarker, retvalList.get(0));
+        assertEquals(oddMarker, retvalList.get(1));
+    }
+    
+    @Test
+    public void testXType() {
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig());
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.MARKER_KEY, GraphDataset.TIMESTAMP_KEY, null, null);
+        assertEquals(CoordinateType.CATEGORY, dataSet.getXType());
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(CoordinateType.TIME, dataSet.getXType());
+        dataSet = new GraphDataset(mList, GraphDataset.FREQUENCY_KEY, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(CoordinateType.INTEGRAL, dataSet.getXType());
+        mList = buildMetrics(new DefaultMetricConfig() {
+            @Override
+            public String produceDataJson(int i) {
+                Long iLong = Long.valueOf(i);
+                return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .build();
+            }
+        });
+        dataSet = new GraphDataset(mList, COUNT_FIELD, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(CoordinateType.REAL, dataSet.getXType());
+    }
+    
+    @Test
+    public void testXLabel() {
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig());
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.MARKER_KEY, GraphDataset.TIMESTAMP_KEY, null, null);
+        assertEquals(GraphDataset.MARKER_KEY, dataSet.getXLabel());
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(GraphDataset.TIMESTAMP_KEY, dataSet.getXLabel());
+        dataSet = new GraphDataset(mList, GraphDataset.FREQUENCY_KEY, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(GraphDataset.FREQUENCY_KEY, dataSet.getXLabel());
+        
+        mList = buildMetrics(new DefaultMetricConfig() {
+            @Override
+            public String produceDataJson(int i) {
+                Long iLong = Long.valueOf(i);
+                return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .build();
+            }
+        });
+        dataSet = new GraphDataset(mList, COUNT_FIELD, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(COUNT_FIELD, dataSet.getXLabel());
+    }
+    
+    @Test
+    public void testYType() {
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig());
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(CoordinateType.CATEGORY, dataSet.getYType());
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.TIMESTAMP_KEY, null, null);
+        assertEquals(CoordinateType.TIME, dataSet.getYType());
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.FREQUENCY_KEY, null, null);
+        assertEquals(CoordinateType.INTEGRAL, dataSet.getYType());
+        mList = buildMetrics(new DefaultMetricConfig() {
+            @Override
+            public String produceDataJson(int i) {
+                Long iLong = Long.valueOf(i);
+                return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .build();
+            }
+        });
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        assertEquals(CoordinateType.REAL, dataSet.getYType());
+    }
+    
+    @Test
+    public void testYLabel() {
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig());
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(GraphDataset.MARKER_KEY, dataSet.getYLabel());
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.TIMESTAMP_KEY, null, null);
+        assertEquals(GraphDataset.TIMESTAMP_KEY, dataSet.getYLabel());
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.FREQUENCY_KEY, null, null);
+        assertEquals(GraphDataset.FREQUENCY_KEY, dataSet.getYLabel());
+        mList = buildMetrics(new DefaultMetricConfig() {
+            @Override
+            public String produceDataJson(int i) {
+                Long iLong = Long.valueOf(i);
+                return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .build();
+            }
+        });
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        assertEquals(COUNT_FIELD, dataSet.getYLabel());
+    }
+    
+    @Test
+    public void testSize() {
+        List<BytemanMetric> mList = buildMetrics(new DefaultMetricConfig());
+        GraphDataset dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, GraphDataset.MARKER_KEY, null, null);
+        assertEquals(DefaultMetricConfig.DEFAULT_NUM, dataSet.size());
+        final int numMetrics = 10;
+        mList = buildMetrics(new DefaultMetricConfig() {
+            @Override
+            public int getNumMetrics() {
+                return numMetrics;
+            }
+            
+            @Override
+            public String produceDataJson(int i) {
+                Long iLong = Long.valueOf(i);
+                return new DataJsonBuilder()
+                    .addKeyValue(COUNT_FIELD, iLong)
+                    .build();
+            }
+        });
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        assertEquals(numMetrics, dataSet.size());
+        
+        // some metrics are filtered (implicitly) so should not add to the data
+        // set
+        mList = buildMetrics(new DefaultMetricConfig() {
+            @Override
+            public String produceDataJson(int i) {
+                if (i % 2 == 0) {
+                    Long iLong = Long.valueOf(i);
+                    return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .build();
+                } else {
+                    return new DataJsonBuilder().build();
+                }
+            }
+        });
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, null, null);
+        assertEquals(DefaultMetricConfig.DEFAULT_NUM/2, dataSet.size());
+        
+        // explicit filtering
+        final double fooValue = 3000.0;
+        final String fooKey = "foo-key";
+        mList = buildMetrics(new DefaultMetricConfig() {
+            @Override
+            public String produceDataJson(int i) {
+                if (i % 30 == 0) {
+                    Long iLong = Long.valueOf(i);
+                    return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .addKeyValue(fooKey, Double.valueOf(fooValue))
+                        .build();
+                } else {
+                    Long iLong = Long.valueOf(i);
+                    return new DataJsonBuilder()
+                        .addKeyValue(COUNT_FIELD, iLong)
+                        .build();
+                }
+            }
+        });
+        dataSet = new GraphDataset(mList, GraphDataset.TIMESTAMP_KEY, COUNT_FIELD, fooKey, String.valueOf(fooValue));
+        assertEquals("0, 30, 60, 90 => 4", 4, dataSet.size());
+    }
+
+    static interface MetricConfig {
+        
+        String produceDataJson(int i);
+        
+        String produceMarker(int i);
+        
+        long produceTimeStamp(int i);
+        
+        int getNumMetrics();
+    }
+    
+    static class DefaultMetricConfig implements MetricConfig {
+        
+        private static final int DEFAULT_NUM = 100;
+
+        @Override
+        public String produceDataJson(int i) {
+            return "{\"key\": \"value\"}";
+        }
+
+        @Override
+        public String produceMarker(int i) {
+            return "marker-value";
+        }
+
+        @Override
+        public long produceTimeStamp(int i) {
+            return i;
+        }
+
+        @Override
+        public int getNumMetrics() {
+            return DEFAULT_NUM;
+        }
+        
+    }
+    
+    static class DataJsonBuilder {
+        
+        private final List<Pair<String, Object>> keyValues = new ArrayList<>();
+        
+        DataJsonBuilder addKeyValue(String key, Object value) {
+            keyValues.add(new Pair<>(key, value));
+            return this;
+        }
+        
+        String build() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("{");
+            int i = 0;
+            for (Pair<String, Object> pair: keyValues) {
+                builder.append("\"" + pair.getFirst() + "\": ");
+                Object val = pair.getSecond();
+                if (val instanceof Long) {
+                    Long lVal = (Long)val;
+                    builder.append(lVal);
+                } else if (val instanceof Double) {
+                    Double dVal = (Double)val;
+                    builder.append(dVal);
+                } else if (val instanceof Boolean) {
+                    Boolean bVal = (Boolean)val;
+                    builder.append(bVal);
+                } else {
+                    if (!(val instanceof String)) {
+                        throw new AssertionError("Unexpected value type: " + val.getClass());
+                    }
+                    String sVal = (String)val;
+                    builder.append("\"" + sVal + "\"");
+                }
+                if (i != keyValues.size() - 1) {
+                    builder.append(",\n");
+                }
+                i++;
+            }
+            builder.append("}");
+            return builder.toString();
+        }
+    }
+}