Bug 720144 - Expose methods in robocop to grab the painted surface and to compare pixels. r=jmaher
authorKartikaya Gupta <kgupta@mozilla.com>
Mon, 30 Jan 2012 22:46:13 -0500
changeset 87023 2934da670f6247a91c99c2b764131b68a2431434
parent 87022 a3e88a6dd4c9e0592a2dcf1e4ce43073b6f1ef0c
child 87024 43550ac61001c4a4857e88f4eb93c194273b6ba5
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjmaher
bugs720144
milestone12.0a1
Bug 720144 - Expose methods in robocop to grab the painted surface and to compare pixels. r=jmaher
build/mobile/robocop/Actions.java.in
build/mobile/robocop/Assert.java.in
build/mobile/robocop/Driver.java.in
build/mobile/robocop/FennecNativeActions.java.in
build/mobile/robocop/FennecNativeAssert.java.in
build/mobile/robocop/FennecNativeDriver.java.in
mobile/android/base/gfx/GeckoSoftwareLayerClient.java
mobile/android/base/gfx/LayerView.java
--- a/build/mobile/robocop/Actions.java.in
+++ b/build/mobile/robocop/Actions.java.in
@@ -55,15 +55,21 @@ public interface Actions {
   /**
    * Listens for a gecko event to be sent from the Gecko instance.
    * The returned object can be used to test if the event has been
    * received. Note that only one event is listened for.
    * 
    * @param geckoEvent The geckoEvent JSONObject's type
    */
   EventExpecter expectGeckoEvent(String geckoEvent);
+
+  /**
+   * Listens for a paint event.
+   */
+  EventExpecter expectPaint();
+
   // Send the string kewsToSend to the application 
   void sendKeys(String keysToSend);
   //Send any of the above keys to the element
   void sendSpecialKey(SpecialKey button);
 
   void drag(int startingX, int endingX, int startingY, int endingY);
 }
--- a/build/mobile/robocop/Assert.java.in
+++ b/build/mobile/robocop/Assert.java.in
@@ -47,9 +47,12 @@ public interface Assert {
   void finalize();
   void ok(boolean condition, String name, String diag);
   void is(Object a, Object b, String name);
   void isnot(Object a, Object b, String name);
   void todo(boolean condition, String name, String diag);
   void todo_is(Object a, Object b, String name);
   void todo_isnot(Object a, Object b, String name);
   void info(String name, String message);
+
+  // robocop-specific asserts
+  void ispixel(int actual, int r, int g, int b, String name);
 }
--- a/build/mobile/robocop/Driver.java.in
+++ b/build/mobile/robocop/Driver.java.in
@@ -63,9 +63,16 @@ public interface Driver {
   int getHeight();
   int getGeckoTop();
   int getGeckoLeft();
   int getGeckoWidth();
   int getGeckoHeight();
 
   void startFrameRecording();
   int stopFrameRecording();
+
+  /**
+   * Get a copy of the painted content region.
+   * @return A 2-D array of pixels (indexed by y, then x). The pixels
+   * are in ARGB-8888 format.
+   */
+  int[][] getPaintedSurface();
 }
--- a/build/mobile/robocop/FennecNativeActions.java.in
+++ b/build/mobile/robocop/FennecNativeActions.java.in
@@ -68,44 +68,53 @@ import java.util.concurrent.SynchronousQ
 import org.json.*;
 
 import com.jayway.android.robotium.solo.Solo;
 
 public class FennecNativeActions implements Actions {
   // Map of IDs to element names.
   private Solo solo;
   private Instrumentation instr;
+  private Activity geckoApp;
 
   // Objects for reflexive access of fennec classes.
   private ClassLoader classLoader;
   private Class gel;
   private Class ge;
   private Class gas;
+  private Class drawListener;
   private Method registerGEL;
   private Method unregisterGEL;
   private Method sendGE;
-
+  private Method getLayerClient;
+  private Method setDrawListener;
 
   public FennecNativeActions(Activity activity, Solo robocop, Instrumentation instrumentation){
     this.solo = robocop;
     this.instr = instrumentation;
+    this.geckoApp = activity;
     // Set up reflexive access of java classes and methods.
     try {
       classLoader = activity.getClassLoader();
       gel = classLoader.loadClass("org.mozilla.gecko.GeckoEventListener");
       ge = classLoader.loadClass("org.mozilla.gecko.GeckoEvent");
       gas = classLoader.loadClass("org.mozilla.gecko.GeckoAppShell");
       Class [] parameters = new Class[2];
       parameters[0] = String.class;
       parameters[1] = gel;
       registerGEL = gas.getMethod("registerGeckoEventListener", parameters);
       unregisterGEL = gas.getMethod("unregisterGeckoEventListener", parameters);
       parameters = new Class[1];
       parameters[0] = ge;
       sendGE = gas.getMethod("sendEventToGecko", parameters);
+
+      getLayerClient = activity.getClass().getMethod("getSoftwareLayerClient");
+      Class gslc = classLoader.loadClass("org.mozilla.gecko.gfx.GeckoSoftwareLayerClient");
+      drawListener = classLoader.loadClass("org.mozilla.gecko.gfx.GeckoSoftwareLayerClient$DrawListener");
+      setDrawListener = gslc.getDeclaredMethod("setDrawListener", drawListener);
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      } catch (SecurityException e) {
        e.printStackTrace();
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      } catch (IllegalArgumentException e) {
        e.printStackTrace();
@@ -200,16 +209,85 @@ public class FennecNativeActions impleme
     } catch (IllegalAccessException e) {
       e.printStackTrace();
     } catch (InvocationTargetException e) {
       e.printStackTrace();
     }
     return null;
   }
 
+  class DrawListenerProxy implements InvocationHandler {
+    private final PaintExpecter mPaintExpecter;
+
+    DrawListenerProxy(PaintExpecter paintExpecter) {
+      mPaintExpecter = paintExpecter;
+    }
+
+    public Object invoke(Object proxy, Method method, Object[] args) {
+      String methodName = method.getName();
+      if ("drawFinished".equals(methodName)) {
+        Log.i("Robocop", "Received drawFinished notification");
+        mPaintExpecter.notifyOfEvent();
+      } else if ("toString".equals(methodName)) {
+        return "DrawListenerProxy";
+      } else if ("equals".equals(methodName)) {
+        return false;
+      } else if ("hashCode".equals(methodName)) {
+        return 0;
+      }
+      return null;
+    }
+  }
+
+  class PaintExpecter implements EventExpecter {
+    private Object mLayerClient;
+    private boolean mPaintDone;
+
+    PaintExpecter() throws IllegalAccessException, InvocationTargetException {
+      mLayerClient = getLayerClient.invoke(geckoApp);
+      setDrawListener.invoke(mLayerClient, Proxy.newProxyInstance(classLoader, new Class[] { drawListener }, new DrawListenerProxy(this)));
+    }
+
+    void notifyOfEvent() {
+      try {
+        setDrawListener.invoke(mLayerClient, (Object)null);
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+      synchronized (this) {
+        mPaintDone = true;
+        this.notifyAll();
+      }
+    }
+
+    public synchronized void blockForEvent() {
+      while (! mPaintDone) {
+        try {
+          this.wait();
+        } catch (InterruptedException ie) {
+          ie.printStackTrace();
+          break;
+        }
+      }
+    }
+
+    public synchronized boolean eventReceived() {
+      return mPaintDone;
+    }
+  }
+
+  public EventExpecter expectPaint() {
+    try {
+      return new PaintExpecter();
+    } catch (Exception e) {
+      e.printStackTrace();
+      return null;
+    }
+  }
+
   public void sendSpecialKey(SpecialKey button) {
     switch( button) {
       case DOWN:
         instr.sendCharacterSync(KeyEvent.KEYCODE_DPAD_DOWN);
         break;
       case UP:
         instr.sendCharacterSync(KeyEvent.KEYCODE_DPAD_UP);
         break;
--- a/build/mobile/robocop/FennecNativeAssert.java.in
+++ b/build/mobile/robocop/FennecNativeAssert.java.in
@@ -238,16 +238,33 @@ public class FennecNativeAssert implemen
     boolean pass = !a.equals(b);
     String diag = "didn't expect " + a.toString() + ", but got it";
     if(pass) {
       diag = a.toString() + " should not equal " + b.toString();
     }
     ok(pass, name, diag);
   }
 
+  public void ispixel(int actual, int r, int g, int b, String name) {
+    // When we read GL pixels the GPU has already processed them and they
+    // are usually off by a little bit. For example a CSS-color pixel of color #64FFF5
+    // was turned into #63FFF7 when it came out of glReadPixels. So in order to compare
+    // against the expected value, we use a little fuzz factor. For the alpha we just
+    // make sure it is always 0xFF.
+    int aAlpha = ((actual >> 24) & 0xFF);
+    int aR = ((actual >> 16) & 0xFF);
+    int aG = ((actual >> 8) & 0xFF);
+    int aB = (actual & 0xFF);
+    boolean pass = (aAlpha == 0xFF) /* alpha */
+                && (Math.abs(aR - r) < 8) /* red */
+                && (Math.abs(aG - g) < 8) /* green */
+                && (Math.abs(aB - b) < 8); /* blue */
+    ok(pass, name, "Color rgba(" + aR + "," + aG + "," + aB + "," + aAlpha + ")" + (pass ? " " : " not") + " close enough to expected rgb(" + r + "," + g + "," + b + ")");
+  }
+
   public void todo(boolean condition, String name, String diag) {
     testInfo test = new testInfo(condition, name, diag, true);
     _logMochitestResult(test, "TEST-UNEXPECTED-PASS", "TEST-KNOWN-FAIL");
     testList.add(test);
   }
 
   public void todo_is(Object a, Object b, String name) {
     boolean pass = a.equals(b);
--- a/build/mobile/robocop/FennecNativeDriver.java.in
+++ b/build/mobile/robocop/FennecNativeDriver.java.in
@@ -40,29 +40,31 @@
 package @ANDROID_PACKAGE_NAME@;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileReader;
 import java.io.FileWriter;
 import java.io.IOException;
+import java.nio.IntBuffer;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.HashMap;
 import java.util.List;
 
 import java.lang.Class;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.lang.reflect.InvocationHandler;
 import java.lang.Long;
 
 import android.app.Activity;
+import android.opengl.GLSurfaceView;
 import android.util.Log;
 import android.view.View;
 
 import org.json.*;
 
 import com.jayway.android.robotium.solo.Solo;
 
 public class FennecNativeDriver implements Driver {
@@ -76,16 +78,17 @@ public class FennecNativeDriver implemen
   private Class gel;
   private Class ge;
   private Class gas;
   private Method registerGEL;
   private Method unregisterGEL;
   private Method sendGE;
   private Method _startFrameRecording;
   private Method _stopFrameRecording;
+  private Method _getPixels;
 
   public FennecNativeDriver(Activity activity, Solo robocop){
     this.activity = activity;
     this.solo = robocop;
 
     // Set up table of fennec_ids.
     locators = convertTextToTable(getFile("/mnt/sdcard/fennec_ids.txt"));
 
@@ -102,16 +105,19 @@ public class FennecNativeDriver implemen
       unregisterGEL = gas.getMethod("unregisterGeckoEventListener", parameters);
       parameters = new Class[1];
       parameters[0] = ge;
       sendGE = gas.getMethod("sendEventToGecko", parameters);
 
       Class gfx = classLoader.loadClass("org.mozilla.gecko.gfx.PanningPerfAPI");
       _startFrameRecording = gfx.getDeclaredMethod("startFrameTimeRecording");
       _stopFrameRecording = gfx.getDeclaredMethod("stopFrameTimeRecording");
+
+      Class layerView = classLoader.loadClass("org.mozilla.gecko.gfx.LayerView");
+      _getPixels = layerView.getDeclaredMethod("getPixels");
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      } catch (SecurityException e) {
        e.printStackTrace();
      } catch (NoSuchMethodException e) {
        e.printStackTrace();
      } catch (IllegalArgumentException e) {
        e.printStackTrace();
@@ -207,16 +213,53 @@ public class FennecNativeDriver implemen
       e.printStackTrace();
     } catch (InvocationTargetException e) {
       e.printStackTrace();
     }
 
     return 0;
   }
 
+  private GLSurfaceView getSurfaceView() {
+    for (View v : solo.getCurrentViews()) {
+      if (v instanceof GLSurfaceView) {
+        return (GLSurfaceView)v;
+      }
+    }
+    return null;
+  }
+
+  public int[][] getPaintedSurface() {
+    GLSurfaceView view = getSurfaceView();
+    if (view == null) {
+      return null;
+    }
+    IntBuffer pixelBuffer;
+    try {
+      pixelBuffer = (IntBuffer)_getPixels.invoke(view);
+    } catch (Exception e) {
+      e.printStackTrace();
+      return null;
+    }
+
+    // now we need to (1) flip the image, because GL likes to do things up-side-down,
+    // and (2) rearrange the bits from AGBR-8888 to ARGB-8888.
+    int w = view.getWidth();
+    int h = view.getHeight();
+    pixelBuffer.position(0);
+    int[][] pixels = new int[h][w];
+    for (int y = h - 1; y >= 0; y--) {
+      for (int x = 0; x < w; x++) {
+        int agbr = pixelBuffer.get();
+        pixels[y][x] = (agbr & 0xFF00FF00) | ((agbr >> 16) & 0x000000FF) | ((agbr << 16) & 0x00FF0000);
+      }
+    }
+    return pixels;
+  }
+
   class scrollHandler implements InvocationHandler {
     public scrollHandler(){};
     public Object invoke(Object proxy, Method method, Object[] args) {
       try{
         //Disect the JSON object into the appropriate variables 
         JSONObject jo = ((JSONObject)args[1]);
         scrollHeight = jo.getInt("y");
         height = jo.getInt("cheight");
--- a/mobile/android/base/gfx/GeckoSoftwareLayerClient.java
+++ b/mobile/android/base/gfx/GeckoSoftwareLayerClient.java
@@ -540,19 +540,19 @@ public class GeckoSoftwareLayerClient ex
         }
 
         int r = Integer.parseInt(matcher.group(1));
         int g = Integer.parseInt(matcher.group(2));
         int b = Integer.parseInt(matcher.group(3));
         return Color.rgb(r, g, b);
     }
 
-    /** Used by robocop for testing purposes. Not for production use! */
+    /** Used by robocop for testing purposes. Not for production use! This is called via reflection by robocop. */
     public void setDrawListener(DrawListener listener) {
         mDrawListener = listener;
     }
 
-    /** Used by robocop for testing purposes. Not for production use! */
+    /** Used by robocop for testing purposes. Not for production use! This is used via reflection by robocop. */
     public interface DrawListener {
         public void drawFinished(int x, int y, int width, int height);
     }
 }
 
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -234,14 +234,14 @@ public class LayerView extends GLSurface
             return System.nanoTime() - mRenderTime;
         }
     }
 
     public int getMaxTextureSize() {
         return mRenderer.getMaxTextureSize();
     }
 
-    /** Used by robocop for testing purposes. Not for production use! */
+    /** Used by robocop for testing purposes. Not for production use! This is called via reflection by robocop. */
     public IntBuffer getPixels() {
         return mRenderer.getPixels();
     }
 }