changeset 742:061618d8bcba

Eclipse Host memory chart This commit adds a new view in Eclipse for monitoring a host's memory, similar to the host's Memory tab in the Swing GUI. Reviewed-by: jerboaa Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2012-September/003350.html
author Elliott Baron <ebaron@redhat.com>
date Thu, 25 Oct 2012 13:35:41 -0400
parents e9531651f79d
children 19cb0438ff2e
files eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/HostMemoryViewPart.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/SWTHostMemoryView.java eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/SWTHostMemoryViewProvider.java eclipse/com.redhat.thermostat.eclipse.test.ui/src/com/redhat/thermostat/eclipse/test/ui/SWTHostMemoryViewTest.java eclipse/com.redhat.thermostat.eclipse.test/src/com/redhat/thermostat/eclipse/test/views/HostMemoryViewPartTest.java
diffstat 5 files changed, 1159 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/HostMemoryViewPart.java	Thu Oct 25 13:35:41 2012 -0400
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import com.redhat.thermostat.client.core.views.HostMemoryViewProvider;
+import com.redhat.thermostat.client.ui.HostMemoryController;
+import com.redhat.thermostat.common.dao.HostInfoDAO;
+import com.redhat.thermostat.common.dao.HostRef;
+import com.redhat.thermostat.common.dao.MemoryStatDAO;
+import com.redhat.thermostat.common.utils.OSGIUtils;
+import com.redhat.thermostat.eclipse.views.SWTComponent;
+
+public class HostMemoryViewPart extends HostRefViewPart {
+
+    private HostMemoryController controller;
+
+    @Override
+    protected void createControllerView(HostRef ref) {
+        HostInfoDAO hostInfoDao = OSGIUtils.getInstance().getService(
+                HostInfoDAO.class);
+        MemoryStatDAO memoryStatDao = OSGIUtils.getInstance().getService(
+                MemoryStatDAO.class);
+        HostMemoryViewProvider viewProvider = OSGIUtils.getInstance()
+                .getService(HostMemoryViewProvider.class);
+        controller = createController(hostInfoDao, memoryStatDao, ref,
+                viewProvider);
+        SWTComponent view = (SWTComponent) controller.getView();
+        view.createControl(top);
+    }
+
+    public HostMemoryController createController(HostInfoDAO hostInfoDao,
+            MemoryStatDAO memoryStatDao, HostRef ref,
+            HostMemoryViewProvider viewProvider) {
+        return new HostMemoryController(hostInfoDao, memoryStatDao, ref,
+                viewProvider);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/SWTHostMemoryView.java	Thu Oct 25 13:35:41 2012 -0400
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import java.awt.Color;
+import java.awt.EventQueue;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.PlatformUI;
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.renderer.xy.XYItemRenderer;
+import org.jfree.data.time.FixedMillisecond;
+import org.jfree.data.time.RegularTimePeriod;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+
+import com.redhat.thermostat.client.core.views.HostMemoryView;
+import com.redhat.thermostat.client.locale.LocaleResources;
+import com.redhat.thermostat.client.ui.ChartColors;
+import com.redhat.thermostat.common.locale.Translate;
+import com.redhat.thermostat.common.model.DiscreteTimeData;
+import com.redhat.thermostat.common.utils.DisplayableValues;
+import com.redhat.thermostat.common.utils.DisplayableValues.Scale;
+import com.redhat.thermostat.eclipse.ThermostatConstants;
+import com.redhat.thermostat.eclipse.views.SWTComponent;
+
+public class SWTHostMemoryView extends HostMemoryView implements SWTComponent {
+    public static final String TEST_ID_TOTAL_MEM = "SWTHostMemoryView.totalMemory";
+    public static final String TEST_ID_LEGEND_ITEM_LABEL = "SWTHostMemoryView.legendItemLabel";
+    public static final String TEST_ID_LEGEND_ITEM_CHECKBOX = "SWTHostMemoryView.legendItemCheckbox";
+    
+    private static final String LEGEND_COLOUR_BLOCK = "\u2588";
+    private static final int H_INDENT = 20;
+    private static final int SPACER_WIDTH = 10;
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+    
+    private final TimeSeriesCollection memoryCollection;
+    private final Map<String, TimeSeries> dataset;
+    private final Map<String, Color> colors;
+    private final CopyOnWriteArrayList<GraphVisibilityChangeListener> listeners;
+    private final Map<String, Composite> checkboxes;
+    private final CountDownLatch latch;
+    
+    private Composite parent;
+    private Label totalMemory;
+    private ViewVisibilityWatcher watcher;
+    private JFreeChart chart;
+    private Composite legendTop;
+    
+    public SWTHostMemoryView() {
+        this.memoryCollection = new TimeSeriesCollection();
+        this.dataset = Collections.synchronizedMap(new HashMap<String, TimeSeries>());
+        this.colors = new HashMap<String, Color>();
+        this.listeners = new CopyOnWriteArrayList<GraphVisibilityChangeListener>();
+        this.checkboxes = new HashMap<String, Composite>();
+        this.watcher = new ViewVisibilityWatcher(notifier);
+        this.latch = new CountDownLatch(1);
+        this.chart = createMemoryChart();
+    }
+    
+    public void createControl(Composite parent) {
+        this.parent = parent;
+        
+        Label summaryLabel = new Label(parent, SWT.LEAD);
+        Font stdFont = summaryLabel.getFont();
+        Font boldFont = new Font(stdFont.getDevice(),
+                stdFont.getFontData()[0].getName(),
+                stdFont.getFontData()[0].getHeight(), SWT.BOLD);
+        
+        summaryLabel.setText(translator.localize(LocaleResources.HOST_MEMORY_SECTION_OVERVIEW));
+        summaryLabel.setFont(boldFont);
+        
+        Composite detailsTop = new Composite(parent, SWT.NONE);
+        detailsTop.setLayout(new GridLayout(3, false));
+        
+        Label cpuModelLabel = new Label(detailsTop, SWT.TRAIL);
+        cpuModelLabel.setText(translator.localize(LocaleResources.HOST_INFO_MEMORY_TOTAL));
+        GridData hIndentLayoutData = new GridData();
+        hIndentLayoutData.horizontalIndent = H_INDENT;
+        cpuModelLabel.setLayoutData(hIndentLayoutData);
+        
+        Label cpuModelSpacer = new Label(detailsTop, SWT.NONE);
+        cpuModelSpacer.setLayoutData(new GridData(SPACER_WIDTH, SWT.DEFAULT));
+        
+        totalMemory = new Label(detailsTop, SWT.LEAD);
+        totalMemory.setData(ThermostatConstants.TEST_TAG, TEST_ID_TOTAL_MEM);
+        totalMemory.setText("Unknown");
+        
+        Composite chartTop = new RecentTimeSeriesChartComposite(parent, SWT.NONE, chart);
+        chartTop.setLayout(new GridLayout());
+        chartTop.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+        
+        legendTop = new Composite(parent, SWT.NONE);
+        RowLayout legendLayout = new RowLayout(SWT.HORIZONTAL);
+        legendLayout.center = true;
+        legendLayout.wrap = false;
+        legendLayout.marginHeight = 0;
+        legendTop.setLayout(legendLayout);
+        
+        // Notify threads that controls are created
+        latch.countDown();
+        
+        // Don't start giving updates until controls are created
+        watcher.watch(parent, ThermostatConstants.VIEW_ID_HOST_MEMORY);
+    }
+
+    @Override
+    public void setTotalMemory(final String newValue) {
+        PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if (!totalMemory.isDisposed()) {
+                    totalMemory.setText(newValue);
+                }
+            }
+        });
+    }
+    
+    private JFreeChart createMemoryChart() {
+        // FIXME associate a fixed color with each type
+
+        JFreeChart chart = ChartFactory.createTimeSeriesChart(
+                null, // Title
+                translator.localize(LocaleResources.HOST_MEMORY_CHART_TIME_LABEL), // x-axis Label
+                translator.localize(LocaleResources.HOST_MEMORY_CHART_SIZE_LABEL, Scale.MiB.name()), // y-axis Label
+                memoryCollection, // Dataset
+                false, // Show Legend
+                false, // Use tooltips
+                false // Configure chart to generate URLs?
+                );
+
+        chart.getPlot().setBackgroundPaint( new Color(255,255,255,0) );
+        chart.getPlot().setBackgroundImageAlpha(0.0f);
+        chart.getPlot().setOutlinePaint(new Color(0,0,0,0));
+
+        NumberAxis rangeAxis = (NumberAxis) chart.getXYPlot().getRangeAxis();
+        rangeAxis.setAutoRangeMinimumSize(100);
+
+        return chart;
+    }
+    
+    private void fireShowHideHandlers(boolean show, String tag) {
+        for (GraphVisibilityChangeListener listener: listeners) {
+            if (show) {
+                listener.show(tag);
+            } else {
+                listener.hide(tag);
+            }
+        }
+    }
+    
+    @Override
+    public void addMemoryChart(final String tag, final String humanReadableName) {
+        EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                int colorIndex = colors.size();
+                colors.put(tag, ChartColors.getColor(colorIndex));
+                TimeSeries series = new TimeSeries(tag);
+                dataset.put(tag, series);
+
+                addLegendItem(tag, humanReadableName);
+                
+                updateColors();
+            }
+        });
+    }
+
+    @Override
+    public void removeMemoryChart(final String tag) {
+        EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                TimeSeries series = dataset.remove(tag);
+                memoryCollection.removeSeries(series);
+                
+                removeLegendItem(tag);
+
+                updateColors();
+            }
+        });
+    }
+
+    @Override
+    public void showMemoryChart(final String tag) {
+        EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                TimeSeries series = dataset.get(tag);
+                memoryCollection.addSeries(series);
+
+                updateColors();
+            }
+        });
+    }
+
+    @Override
+    public void hideMemoryChart(final String tag) {
+        EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                TimeSeries series = dataset.get(tag);
+                memoryCollection.removeSeries(series);
+
+                updateColors();
+            }
+        });
+    }
+
+    @Override
+    public void addMemoryData(final String tag, List<DiscreteTimeData<? extends Number>> data) {
+        final List<DiscreteTimeData<? extends Number>> copy = new ArrayList<>(data);
+        EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                final TimeSeries series = dataset.get(tag);
+                for (DiscreteTimeData<? extends Number> timeData: copy) {
+                    RegularTimePeriod period = new FixedMillisecond(timeData.getTimeInMillis());
+                    if (series.getDataItem(period) == null) {
+                        Long sizeInBytes = (Long) timeData.getData();
+                        Double sizeInMegaBytes = DisplayableValues.Scale.convertTo(Scale.MiB, sizeInBytes);
+                        series.add(new FixedMillisecond(timeData.getTimeInMillis()), sizeInMegaBytes, false);
+                    }
+                }
+                series.fireSeriesChanged();
+            }
+        });
+    }
+
+    @Override
+    public void clearMemoryData(final String tag) {
+        EventQueue.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                TimeSeries series = dataset.get(tag);
+                series.clear();
+            }
+        });
+    }
+    
+    @Override
+    public void addGraphVisibilityListener(GraphVisibilityChangeListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeGraphVisibilityListener(GraphVisibilityChangeListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Adding or removing series to the series collection may change the order
+     * of existing items. Plus the paint for the index is now out-of-date. So
+     * let's walk through all the series and set the right paint for those.
+     */
+    private void updateColors() {
+        XYItemRenderer itemRenderer = chart.getXYPlot().getRenderer();
+        for (int i = 0; i < memoryCollection.getSeriesCount(); i++) {
+            String tag = (String) memoryCollection.getSeriesKey(i);
+            Color color = colors.get(tag);
+            itemRenderer.setSeriesPaint(i, color);
+        }
+    }
+    
+    private Composite createLabelWithLegend(Composite parent, String text, Color color, final String tag) {
+        Composite top = new Composite(parent, SWT.NONE);
+        RowLayout topLayout = new RowLayout(SWT.HORIZONTAL);
+        topLayout.marginHeight = 0;
+        topLayout.center = true;
+        top.setLayout(topLayout);
+        
+        final Button checkBox = new Button(top, SWT.CHECK);
+        checkBox.setData(ThermostatConstants.TEST_TAG, TEST_ID_LEGEND_ITEM_CHECKBOX);
+        checkBox.addSelectionListener(new SelectionListener() {
+            
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                fireShowHideHandlers(checkBox.getSelection(), tag);
+            }
+            
+            @Override
+            public void widgetDefaultSelected(SelectionEvent e) {
+                widgetSelected(e);
+            }
+        });
+        checkBox.setSelection(true);
+        checkBox.setAlignment(SWT.RIGHT);
+        
+        Label colourBlock = new Label(top, SWT.NONE);
+        colourBlock.setText(LEGEND_COLOUR_BLOCK);
+        
+        // Convert to SWT colour
+        final org.eclipse.swt.graphics.Color swtColour = new org.eclipse.swt.graphics.Color(
+                PlatformUI.getWorkbench().getDisplay(), color.getRed(),
+                color.getGreen(), color.getBlue());
+        colourBlock.addDisposeListener(new DisposeListener() {
+
+            @Override
+            public void widgetDisposed(DisposeEvent e) {
+                swtColour.dispose();
+            }
+        });
+        colourBlock.setForeground(swtColour);
+        
+        Label colourText = new Label(top, SWT.NONE);
+        colourText.setData(ThermostatConstants.TEST_TAG, TEST_ID_LEGEND_ITEM_LABEL);
+        colourText.setText(text);
+        return top;
+    }
+
+    private void addLegendItem(final String tag, final String humanReadableName) {
+        // We need to wait for the controls to be fully constructed
+        // before modifying the legend
+        ChartUtils.runAfterCreated(latch, new Runnable() {
+            @Override
+            public void run() {
+                Composite checkbox = createLabelWithLegend(legendTop,
+                        humanReadableName, colors.get(tag), tag);
+                checkboxes.put(tag, checkbox);
+                parent.layout();
+            }
+        });
+    }
+
+    private void removeLegendItem(final String tag) {
+        // We need to wait for the controls to be fully constructed
+        // before modifying the legend
+        ChartUtils.runAfterCreated(latch, new Runnable() {
+            @Override
+            public void run() {
+                Composite checkbox = checkboxes.remove(tag);
+                checkbox.dispose();
+                parent.layout();
+            }
+        });
+    }
+    
+    public JFreeChart getChart() {
+        return chart;
+    }
+    
+    public TimeSeries getSeries(String tag) {
+        return dataset.get(tag);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.chart.common/src/com/redhat/thermostat/eclipse/chart/common/SWTHostMemoryViewProvider.java	Thu Oct 25 13:35:41 2012 -0400
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012 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.eclipse.chart.common;
+
+import com.redhat.thermostat.client.core.views.HostMemoryView;
+import com.redhat.thermostat.client.core.views.HostMemoryViewProvider;
+
+public class SWTHostMemoryViewProvider implements HostMemoryViewProvider {
+
+    @Override
+    public HostMemoryView createView() {
+        return new SWTHostMemoryView();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.test.ui/src/com/redhat/thermostat/eclipse/test/ui/SWTHostMemoryViewTest.java	Thu Oct 25 13:35:41 2012 -0400
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2012 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.eclipse.test.ui;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Widget;
+import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
+import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException;
+import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
+import org.eclipse.swtbot.swt.finder.matchers.WithId;
+import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences;
+import org.eclipse.swtbot.swt.finder.waits.DefaultCondition;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotCheckBox;
+import org.eclipse.swtbot.swt.finder.widgets.SWTBotLabel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.redhat.thermostat.client.core.views.HostMemoryView.GraphVisibilityChangeListener;
+import com.redhat.thermostat.common.model.DiscreteTimeData;
+import com.redhat.thermostat.eclipse.ThermostatConstants;
+import com.redhat.thermostat.eclipse.chart.common.SWTHostMemoryView;
+
+@RunWith(SWTBotJunit4ClassRunner.class)
+public class SWTHostMemoryViewTest implements GraphVisibilityChangeListener {
+    private SWTWorkbenchBot bot;
+    private SWTHostMemoryView view;
+    private Shell shell;
+
+    @Before
+    public void beforeTest() throws Exception {
+        bot = new SWTWorkbenchBot();
+        
+        Display.getDefault().syncExec(new Runnable() {
+
+            @Override
+            public void run() {
+                shell = new Shell(Display.getCurrent());
+                Composite parent = new Composite(shell, SWT.NONE);
+                parent.setLayout(new GridLayout());
+                parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true,
+                        true));
+                view = new SWTHostMemoryView();
+                view.createControl(parent);
+                view.addGraphVisibilityListener(SWTHostMemoryViewTest.this);
+                shell.open();
+            }
+        });
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        Display.getDefault().syncExec(new Runnable() {
+
+            @Override
+            public void run() {
+                if (shell != null) {
+                    shell.close();
+                    view = null;
+                }
+            }
+        });
+    }
+
+    @Test
+    public void testSetTotalMemory() throws Exception {
+        String totalMem = "8 GB";
+
+        view.setTotalMemory(totalMem);
+
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                SWTBotLabel label = bot.labelWithId(ThermostatConstants.TEST_TAG,
+                        SWTHostMemoryView.TEST_ID_TOTAL_MEM);
+                return !label.getText().equals("Unknown"); // TODO Externalize
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Total memory label unchanged after set";
+            }
+
+        });
+    }
+
+    @Test
+    public void testAddMemoryChart() throws Exception {
+        final String tag = "TEST";
+        String humanReadableName = "Test";
+
+        addSeries(tag, humanReadableName);
+
+        // Verify legend added
+        SWTBotLabel label = bot.labelWithId(ThermostatConstants.TEST_TAG,
+                SWTHostMemoryView.TEST_ID_LEGEND_ITEM_LABEL);
+        assertEquals(humanReadableName, label.getText());
+    }
+
+    @Test
+    public void testAddMemoryChartMultiple() throws Exception {
+        final String tag1 = "TEST1";
+        final String tag2 = "TEST2";
+        String humanReadableName1 = "Test 1";
+        String humanReadableName2 = "Test 2";
+
+        addSeries(tag1, humanReadableName1);
+        addSeries(tag2, humanReadableName2);
+
+        // Verify legend added
+        SWTBotLabel label = bot.labelWithId(ThermostatConstants.TEST_TAG,
+                SWTHostMemoryView.TEST_ID_LEGEND_ITEM_LABEL, 0);
+        assertEquals(humanReadableName1, label.getText());
+        label = bot.labelWithId(ThermostatConstants.TEST_TAG,
+                SWTHostMemoryView.TEST_ID_LEGEND_ITEM_LABEL, 1);
+        assertEquals(humanReadableName2, label.getText());
+    }
+
+    @Test
+    public void testRemoveMemoryChart() throws Exception {
+        final String tag = "TEST";
+        String humanReadableName = "Test";
+
+        addSeries(tag, humanReadableName);
+        
+        view.showMemoryChart(tag);
+        waitUntilSeriesShown(1);
+
+        view.removeMemoryChart(tag);
+
+        checkSeriesRemoved(tag, 0);
+
+        // Verify legend removed
+        checkLegendRemoved();
+    }
+
+    @Test
+    public void testRemoveMemoryChartMultiple() throws Exception {
+        final String tag1 = "TEST1";
+        final String tag2 = "TEST2";
+        String humanReadableName1 = "Test 1";
+        String humanReadableName2 = "Test 2";
+
+        addSeries(tag1, humanReadableName1);
+        addSeries(tag2, humanReadableName2);
+        
+        // Show both series
+        view.showMemoryChart(tag1);
+        waitUntilSeriesShown(1);
+
+        view.showMemoryChart(tag2);
+        waitUntilSeriesShown(2);
+
+        view.removeMemoryChart(tag2);
+
+        checkSeriesRemoved(tag2, 1);
+
+        // Verify legend removed
+        checkLegendItemRemoved(1);
+    }
+
+    private void checkSeriesRemoved(final String tag, final int numSeries) {
+        // Wait until series removed from chart and dataset
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                JFreeChart chart = view.getChart();
+                XYPlot plot = chart.getXYPlot();
+                int count = plot.getSeriesCount();
+                return view.getSeries(tag) == null && count == numSeries;
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Data series never removed";
+            }
+        });
+    }
+
+    private void checkLegendRemoved() {
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                boolean result = false;
+
+                // Don't make this wait
+                long saveTimeout = SWTBotPreferences.TIMEOUT;
+                SWTBotPreferences.TIMEOUT = 0;
+                try {
+                    bot.widget(WithId.withId(ThermostatConstants.TEST_TAG,
+                            SWTHostMemoryView.TEST_ID_LEGEND_ITEM_LABEL));
+                } catch (WidgetNotFoundException e) {
+                    result = true;
+                }
+                SWTBotPreferences.TIMEOUT = saveTimeout;
+
+                return result;
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Legend not removed";
+            }
+        });
+    }
+    
+    private void checkLegendItemRemoved(final int size) {
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                List<? extends Widget> widgets = bot.widgets(WithId.withId(ThermostatConstants.TEST_TAG,
+                        SWTHostMemoryView.TEST_ID_LEGEND_ITEM_LABEL));
+                return widgets.size() == size;
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Legend item not removed";
+            }
+        });
+    }
+
+    @Test
+    public void testHideMemoryChart() throws Exception {
+        String tag = "TEST";
+        String humanReadableName = "Test";
+
+        addSeries(tag, humanReadableName);
+        view.showMemoryChart(tag);
+
+        waitUntilSeriesShown(1);
+
+        // Click checkbox to trigger hideMemoryChart
+        SWTBotCheckBox checkbox = bot.checkBoxWithId(
+                ThermostatConstants.TEST_TAG,
+                SWTHostMemoryView.TEST_ID_LEGEND_ITEM_CHECKBOX);
+        checkbox.click();
+
+        // Wait until series hidden
+        waitUntilSeriesShown(0);
+    }
+
+    @Test
+    public void testShowMemoryChart() throws Exception {
+        String tag = "TEST";
+        String humanReadableName = "Test";
+
+        addSeries(tag, humanReadableName);
+        view.showMemoryChart(tag);
+
+        waitUntilSeriesShown(1);
+    }
+
+    @Test
+    public void testShowHiddenMemoryChart() throws Exception {
+        String tag = "TEST";
+        String humanReadableName = "Test";
+
+        addSeries(tag, humanReadableName);
+        view.showMemoryChart(tag);
+
+        waitUntilSeriesShown(1);
+
+        // Click checkbox to trigger hideMemoryChart
+        SWTBotCheckBox checkbox = bot.checkBoxWithId(
+                ThermostatConstants.TEST_TAG,
+                SWTHostMemoryView.TEST_ID_LEGEND_ITEM_CHECKBOX);
+        checkbox.click();
+
+        // Wait until series hidden
+        waitUntilSeriesShown(0);
+
+        // Click checkbox to trigger showMemoryChart
+        checkbox.click();
+
+        waitUntilSeriesShown(1);
+    }
+    
+    @Test
+    public void testAddMemoryData() throws Exception {
+        String tag = "TEST";
+        String humanReadableName = "Test";
+
+        addSeries(tag, humanReadableName);
+        view.showMemoryChart(tag);
+
+        waitUntilSeriesShown(1);
+        
+        // Add some test data
+        List<DiscreteTimeData<? extends Number>> data = new ArrayList<DiscreteTimeData<? extends Number>>();
+        data.add(new DiscreteTimeData<Long>(1000L, 134217728L)); // 128MiB
+        data.add(new DiscreteTimeData<Long>(2000L, 268435456L)); // 256MiB
+        data.add(new DiscreteTimeData<Long>(3000L, 536870912L)); // 512MiB
+        
+        final JFreeChart chart = view.getChart();
+        final TimeSeries series = view.getSeries(tag);
+        
+        addData(tag, data, series);
+        
+        TimeSeriesCollection dataset = (TimeSeriesCollection) chart.getXYPlot().getDataset();
+        int seriesIndex = chart.getXYPlot().getDataset().indexOf(series.getKey());
+        
+        assertEquals(1000L, dataset.getX(seriesIndex, 0));
+        assertEquals(2000L, dataset.getX(seriesIndex, 1));
+        assertEquals(3000L, dataset.getX(seriesIndex, 2));
+        
+        assertEquals(128D, dataset.getY(seriesIndex, 0));
+        assertEquals(256D, dataset.getY(seriesIndex, 1));
+        assertEquals(512D, dataset.getY(seriesIndex, 2));
+    }
+
+    private void addData(String tag,
+            final List<DiscreteTimeData<? extends Number>> data,
+            final TimeSeries series) {
+        view.addMemoryData(tag, data);
+
+        // Wait until data added to chart
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                return series.getItemCount() == data.size();
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Data never added";
+            }
+        });
+    }
+    
+    @Test
+    public void testAddMemoryDataMultiple() throws Exception {
+        String tag1 = "TEST1";
+        String tag2 = "TEST2";
+        String humanReadableName1 = "Test 1";
+        String humanReadableName2 = "Test 2";
+
+        addSeries(tag1, humanReadableName1);
+        view.showMemoryChart(tag1);
+        
+        addSeries(tag2, humanReadableName2);
+        view.showMemoryChart(tag2);
+
+        waitUntilSeriesShown(2);
+        
+        // Add some test data
+        List<DiscreteTimeData<? extends Number>> data1 = new ArrayList<DiscreteTimeData<? extends Number>>();
+        data1.add(new DiscreteTimeData<Long>(1000L, 134217728L)); // 128MiB
+        data1.add(new DiscreteTimeData<Long>(2000L, 268435456L)); // 256MiB
+        data1.add(new DiscreteTimeData<Long>(3000L, 536870912L)); // 512MiB
+        
+        List<DiscreteTimeData<? extends Number>> data2 = new ArrayList<DiscreteTimeData<? extends Number>>();
+        data2.add(new DiscreteTimeData<Long>(1500L, 536870912L)); // 512MiB
+        data2.add(new DiscreteTimeData<Long>(2500L, 134217728L)); // 128MiB
+        data2.add(new DiscreteTimeData<Long>(3500L, 268435456L)); // 256MiB
+        
+        final JFreeChart chart = view.getChart();
+        final TimeSeries series1 = view.getSeries(tag1);
+        final TimeSeries series2 = view.getSeries(tag2);
+        
+        addData(tag1, data1, series1);
+        addData(tag2, data2, series2);
+        
+        TimeSeriesCollection dataset = (TimeSeriesCollection) chart.getXYPlot().getDataset();
+        int series1Index = chart.getXYPlot().getDataset().indexOf(series1.getKey());
+        int series2Index = chart.getXYPlot().getDataset().indexOf(series2.getKey());
+        
+        assertEquals(1000L, dataset.getX(series1Index, 0));
+        assertEquals(2000L, dataset.getX(series1Index, 1));
+        assertEquals(3000L, dataset.getX(series1Index, 2));
+        
+        assertEquals(128D, dataset.getY(series1Index, 0));
+        assertEquals(256D, dataset.getY(series1Index, 1));
+        assertEquals(512D, dataset.getY(series1Index, 2));
+        
+        assertEquals(1500L, dataset.getX(series2Index, 0));
+        assertEquals(2500L, dataset.getX(series2Index, 1));
+        assertEquals(3500L, dataset.getX(series2Index, 2));
+        
+        assertEquals(512D, dataset.getY(series2Index, 0));
+        assertEquals(128D, dataset.getY(series2Index, 1));
+        assertEquals(256D, dataset.getY(series2Index, 2));
+    }
+    
+    @Test
+    public void testClearMemoryData() throws Exception {
+        String tag1 = "TEST1";
+        String tag2 = "TEST2";
+        String humanReadableName1 = "Test 1";
+        String humanReadableName2 = "Test 2";
+
+        addSeries(tag1, humanReadableName1);
+        view.showMemoryChart(tag1);
+        
+        addSeries(tag2, humanReadableName2);
+        view.showMemoryChart(tag2);
+
+        waitUntilSeriesShown(2);
+        
+        // Add some test data
+        List<DiscreteTimeData<? extends Number>> data1 = new ArrayList<DiscreteTimeData<? extends Number>>();
+        data1.add(new DiscreteTimeData<Long>(1000L, 134217728L)); // 128MiB
+        data1.add(new DiscreteTimeData<Long>(2000L, 268435456L)); // 256MiB
+        data1.add(new DiscreteTimeData<Long>(3000L, 536870912L)); // 512MiB
+        
+        List<DiscreteTimeData<? extends Number>> data2 = new ArrayList<DiscreteTimeData<? extends Number>>();
+        data2.add(new DiscreteTimeData<Long>(1500L, 536870912L)); // 512MiB
+        data2.add(new DiscreteTimeData<Long>(2500L, 134217728L)); // 128MiB
+        data2.add(new DiscreteTimeData<Long>(3500L, 268435456L)); // 256MiB
+        
+        final TimeSeries series1 = view.getSeries(tag1);
+        final TimeSeries series2 = view.getSeries(tag2);
+        
+        addData(tag1, data1, series1);
+        addData(tag2, data2, series2);
+        
+        // Remove data from series
+        view.clearMemoryData(tag1);
+        
+        // Wait until data removed to chart
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                return series1.getItemCount() == 0;
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Data never added";
+            }
+        });
+        
+        // Check other series' size
+        assertEquals(data2.size(), series2.getItemCount());
+    }
+
+    private void waitUntilSeriesShown(final int numSeries) {
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                JFreeChart chart = view.getChart();
+                XYPlot plot = chart.getXYPlot();
+                int count = plot.getSeriesCount();
+                return count == numSeries;
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Data series never shown/hidden";
+            }
+        });
+    }
+
+    private void addSeries(final String tag, String humanReadableName) {
+        view.addMemoryChart(tag, humanReadableName);
+
+        // Wait until series added
+        bot.waitUntil(new DefaultCondition() {
+
+            @Override
+            public boolean test() throws Exception {
+                return view.getSeries(tag) != null;
+            }
+
+            @Override
+            public String getFailureMessage() {
+                return "Data series never added";
+            }
+        });
+
+        // Wait for legend
+        bot.labelWithId(ThermostatConstants.TEST_TAG,
+                SWTHostMemoryView.TEST_ID_LEGEND_ITEM_LABEL);
+    }
+
+    @Override
+    public void show(String tag) {
+        view.showMemoryChart(tag);
+    }
+
+    @Override
+    public void hide(String tag) {
+        view.hideMemoryChart(tag);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eclipse/com.redhat.thermostat.eclipse.test/src/com/redhat/thermostat/eclipse/test/views/HostMemoryViewPartTest.java	Thu Oct 25 13:35:41 2012 -0400
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2012 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.eclipse.test.views;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.widgets.Composite;
+import org.junit.Test;
+
+import com.redhat.thermostat.client.core.views.BasicView;
+import com.redhat.thermostat.client.core.views.HostMemoryViewProvider;
+import com.redhat.thermostat.client.ui.HostMemoryController;
+import com.redhat.thermostat.common.dao.HostInfoDAO;
+import com.redhat.thermostat.common.dao.HostRef;
+import com.redhat.thermostat.common.dao.MemoryStatDAO;
+import com.redhat.thermostat.eclipse.chart.common.HostMemoryViewPart;
+import com.redhat.thermostat.eclipse.chart.common.RefViewPart;
+import com.redhat.thermostat.eclipse.chart.common.SWTHostMemoryView;
+
+public class HostMemoryViewPartTest extends AbstractRefViewPartTest<HostRef> {
+
+    @Test
+    public void testSelectionAfter() throws Exception {
+        view.createPartControl(parent);
+
+        HostRef hostRef = new HostRef("TEST", "Test");
+        IStructuredSelection selection = mockSelection(hostRef);
+        view.selectionChanged(hostVMView, selection);
+
+        verify(thermoView).createControl(any(Composite.class));
+    }
+
+    @Override
+    protected void mockController() {
+        HostMemoryController controller = mock(HostMemoryController.class);
+        thermoView = mock(SWTHostMemoryView.class);
+
+        HostInfoDAO hostInfoDao = mock(HostInfoDAO.class);
+        MemoryStatDAO memStatDao = mock(MemoryStatDAO.class);
+        HostMemoryViewProvider viewProvider = mock(HostMemoryViewProvider.class);
+        when(osgi.getService(HostInfoDAO.class)).thenReturn(hostInfoDao);
+        when(osgi.getService(MemoryStatDAO.class)).thenReturn(memStatDao);
+        when(osgi.getService(HostMemoryViewProvider.class)).thenReturn(
+                viewProvider);
+
+        doReturn(controller).when(((HostMemoryViewPart) view))
+                .createController(same(hostInfoDao), same(memStatDao),
+                        any(HostRef.class), same(viewProvider));
+        when(controller.getView()).thenReturn((BasicView) thermoView);
+    }
+
+    @Override
+    protected RefViewPart<HostRef> createViewPart() {
+        return new HostMemoryViewPart();
+    }
+
+}