Bug 1055393 - gtests for GMPStorage and PB mode storage. r=edwin
authorChris Pearce <cpearce@mozilla.com>
Tue, 21 Oct 2014 19:16:19 +1300
changeset 227590 83667d52bfa1a9648e267bf459e62625d4bedd88
parent 227589 530811c1e3ea1edbe33e8a157b705793336ea172
child 227591 6e287db609ed1b93a49115ccbaf38d1d5e008526
push id7326
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:58:42 +0000
treeherdermozilla-aurora@d3a3b2a0f2f8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedwin
bugs1055393
milestone36.0a1
Bug 1055393 - gtests for GMPStorage and PB mode storage. r=edwin
content/media/gmp/GMPDecryptorProxy.h
content/media/gtest/TestGMPCrossOrigin.cpp
content/media/gtest/moz.build
dom/media/gmp-plugin/fake.info
dom/media/gmp-plugin/gmp-fake.cpp
dom/media/gmp-plugin/gmp-test-decryptor.cpp
dom/media/gmp-plugin/gmp-test-decryptor.h
dom/media/gmp-plugin/gmp-test-storage.cpp
dom/media/gmp-plugin/gmp-test-storage.h
dom/media/gmp-plugin/moz.build
--- a/content/media/gmp/GMPDecryptorProxy.h
+++ b/content/media/gmp/GMPDecryptorProxy.h
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef GMPDecryptorProxy_h_
 #define GMPDecryptorProxy_h_
 
 #include "GMPCallbackBase.h"
 #include "gmp-decryption.h"
+#include "nsString.h"
 
 namespace mp4_demuxer {
 class CryptoSample;
 }
 
 class GMPDecryptorProxyCallback : public GMPCallbackBase {
 public:
   ~GMPDecryptorProxyCallback() {}
--- a/content/media/gtest/TestGMPCrossOrigin.cpp
+++ b/content/media/gtest/TestGMPCrossOrigin.cpp
@@ -1,24 +1,29 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "gtest/gtest.h"
-
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
 #include "mozilla/StaticPtr.h"
 #include "GMPVideoDecoderProxy.h"
 #include "GMPVideoEncoderProxy.h"
+#include "GMPDecryptorProxy.h"
 #include "GMPService.h"
-
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsIFile.h"
 #include "nsISimpleEnumerator.h"
+#include "mozilla/Atomics.h"
+#include "nsNSSComponent.h"
+
+using namespace std;
 
 using namespace mozilla;
 using namespace mozilla::gmp;
 
 struct GMPTestRunner
 {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPTestRunner)
 
@@ -96,16 +101,443 @@ GMPTestRunner::RunTestGMPCrossOrigin()
               encoder1->ParentID() == encoder2->ParentID());
 
   if (decoder1) decoder1->Close();
   if (decoder2) decoder2->Close();
   if (encoder1) encoder1->Close();
   if (encoder2) encoder2->Close();
 }
 
+class NotifyObserversTask : public nsRunnable {
+public:
+  NotifyObserversTask(const char* aTopic)
+    : mTopic(aTopic)
+  {}
+  NS_IMETHOD Run() {
+    MOZ_ASSERT(NS_IsMainThread());
+    nsCOMPtr<nsIObserverService> observerService =
+        mozilla::services::GetObserverService();
+    if (observerService) {
+      observerService->NotifyObservers(nullptr, mTopic, nullptr);
+    }
+    return NS_OK;
+  }
+  const char* mTopic;
+};
+
+class ClearGMPStorageTask : public nsIRunnable
+                          , public nsIObserver {
+public:
+  ClearGMPStorageTask(nsIRunnable* Continuation,
+                      nsIThread* aTarget)
+    : mContinuation(Continuation)
+    , mTarget(aTarget)
+  {}
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+  NS_IMETHOD Run() MOZ_OVERRIDE {
+    MOZ_ASSERT(NS_IsMainThread());
+    nsCOMPtr<nsIObserverService> observerService =
+        mozilla::services::GetObserverService();
+    EXPECT_TRUE(observerService);
+    observerService->AddObserver(this, "gmp-clear-storage-complete", false);
+    if (observerService) {
+      observerService->NotifyObservers(nullptr, "gmp-clear-storage", nullptr);
+    }
+    return NS_OK;
+  }
+
+  NS_IMETHOD Observe(nsISupports* aSubject,
+                     const char* aTopic,
+                     const char16_t* aSomeData) MOZ_OVERRIDE
+  {
+    if (!strcmp(aTopic, "gmp-clear-storage-complete")) {
+      nsCOMPtr<nsIObserverService> observerService =
+          mozilla::services::GetObserverService();
+      EXPECT_TRUE(observerService);
+      observerService->RemoveObserver(this, "gmp-clear-storage-complete");
+      mTarget->Dispatch(mContinuation, NS_DISPATCH_NORMAL);
+    }
+    return NS_OK;
+  }
+
+private:
+  virtual ~ClearGMPStorageTask() {}
+  nsRefPtr<nsIRunnable> mContinuation;
+  nsCOMPtr<nsIThread> mTarget;
+};
+
+NS_IMPL_ISUPPORTS(ClearGMPStorageTask, nsIRunnable, nsIObserver)
+
+static void
+ClearGMPStorage(nsIRunnable* aContinuation,
+                nsIThread* aTarget)
+{
+  nsRefPtr<ClearGMPStorageTask> task(new ClearGMPStorageTask(aContinuation, aTarget));
+  NS_DispatchToMainThread(task, NS_DISPATCH_NORMAL);
+}
+
+static void
+SimulatePBModeExit()
+{
+  NS_DispatchToMainThread(new NotifyObserversTask("last-pb-context-exited"), NS_DISPATCH_SYNC);
+}
+
+static nsCString
+GetNodeId(const nsAString& aOrigin,
+          const nsAString& aTopLevelOrigin,
+          bool aInPBMode)
+{
+  nsRefPtr<GeckoMediaPluginService> service =
+    GeckoMediaPluginService::GetGeckoMediaPluginService();
+  EXPECT_TRUE(service);
+  nsCString nodeId;
+  nsresult rv = service->GetNodeId(aOrigin,
+                                   aTopLevelOrigin,
+                                   aInPBMode,
+                                   nodeId);
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+  return nodeId;
+}
+
+static bool
+IsGMPStorageIsEmpty()
+{
+  nsRefPtr<GeckoMediaPluginService> service =
+    GeckoMediaPluginService::GetGeckoMediaPluginService();
+  MOZ_ASSERT(service);
+  nsCOMPtr<nsIFile> storage;
+  nsresult rv = service->GetStorageDir(getter_AddRefs(storage));
+  EXPECT_TRUE(NS_SUCCEEDED(rv));
+  bool exists = false;
+  if (storage) {
+    storage->Exists(&exists);
+  }
+  return !exists;
+}
+
+static void
+AssertIsOnGMPThread()
+{
+  nsRefPtr<GeckoMediaPluginService> service =
+    GeckoMediaPluginService::GetGeckoMediaPluginService();
+  MOZ_ASSERT(service);
+  nsCOMPtr<nsIThread> thread;
+  service->GetThread(getter_AddRefs(thread));
+  MOZ_ASSERT(thread);
+  nsCOMPtr<nsIThread> currentThread;
+  DebugOnly<nsresult> rv = NS_GetCurrentThread(getter_AddRefs(currentThread));
+  MOZ_ASSERT(NS_SUCCEEDED(rv));
+  MOZ_ASSERT(currentThread == thread);
+}
+
+static already_AddRefed<nsIThread>
+GetGMPThread()
+{
+  nsRefPtr<GeckoMediaPluginService> service =
+    GeckoMediaPluginService::GetGeckoMediaPluginService();
+  nsCOMPtr<nsIThread> thread;
+  EXPECT_TRUE(NS_SUCCEEDED(service->GetThread(getter_AddRefs(thread))));
+  return thread.forget();
+}
+class GMPStorageTest : public GMPDecryptorProxyCallback
+{
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPStorageTest)
+
+  void DoTest(void (GMPStorageTest::*aTestMethod)()) {
+    EnsureNSSInitializedChromeOrContent();
+    nsCOMPtr<nsIThread> thread(GetGMPThread());
+    ClearGMPStorage(NS_NewRunnableMethod(this, aTestMethod), thread);
+    AwaitFinished();
+  }
+
+  GMPStorageTest()
+    : mDecryptor(nullptr)
+    , mMonitor("GMPStorageTest")
+    , mFinished(false)
+  {
+  }
+
+  void
+  Update(const nsCString& aMessage)
+  {
+    nsTArray<uint8_t> msg;
+    msg.AppendElements(aMessage.get(), aMessage.Length());
+    mDecryptor->UpdateSession(1, NS_LITERAL_CSTRING("fake-session-id"), msg);
+  }
+
+  void TestGetNodeId()
+  {
+    AssertIsOnGMPThread();
+
+    EXPECT_TRUE(IsGMPStorageIsEmpty());
+
+    const nsString origin1 = NS_LITERAL_STRING("example1.com");
+    const nsString origin2 = NS_LITERAL_STRING("example2.org");
+
+    nsCString PBnodeId1 = GetNodeId(origin1, origin2, true);
+    nsCString PBnodeId2 = GetNodeId(origin1, origin2, true);
+
+    // Node ids for the same origins should be the same in PB mode.
+    EXPECT_TRUE(PBnodeId1.Equals(PBnodeId2));
+
+    nsCString PBnodeId3 = GetNodeId(origin2, origin1, true);
+
+    // Node ids with origin and top level origin swapped should be different.
+    EXPECT_TRUE(!PBnodeId3.Equals(PBnodeId1));
+
+    // Getting node ids in PB mode should not result in the node id being stored.
+    EXPECT_TRUE(IsGMPStorageIsEmpty());
+
+    nsCString nodeId1 = GetNodeId(origin1, origin2, false);
+    nsCString nodeId2 = GetNodeId(origin1, origin2, false);
+
+    // NodeIds for the same origin pair in non-pb mode should be the same.
+    EXPECT_TRUE(nodeId1.Equals(nodeId2));
+
+    // Node ids for a given origin pair should be different for the PB origins should be the same in PB mode.
+    EXPECT_TRUE(!PBnodeId1.Equals(nodeId1));
+    EXPECT_TRUE(!PBnodeId2.Equals(nodeId2));
+
+    nsCOMPtr<nsIThread> thread(GetGMPThread());
+    ClearGMPStorage(NS_NewRunnableMethodWithArg<nsCString>(
+      this, &GMPStorageTest::TestGetNodeId_Continuation, nodeId1), thread);
+  }
+
+  void TestGetNodeId_Continuation(nsCString aNodeId1) {
+    EXPECT_TRUE(IsGMPStorageIsEmpty());
+
+    // Once we clear storage, the node ids generated for the same origin-pair
+    // should be different.
+    const nsString origin1 = NS_LITERAL_STRING("example1.com");
+    const nsString origin2 = NS_LITERAL_STRING("example2.org");
+    nsCString nodeId3 = GetNodeId(origin1, origin2, false);
+    EXPECT_TRUE(!aNodeId1.Equals(nodeId3));
+
+    SetFinished();
+  }
+
+  void CreateDecryptor(const nsAString& aOrigin,
+                       const nsAString& aTopLevelOrigin,
+                       bool aInPBMode) {
+    nsRefPtr<GeckoMediaPluginService> service =
+      GeckoMediaPluginService::GetGeckoMediaPluginService();
+    EXPECT_TRUE(service);
+
+    const nsCString nodeId = GetNodeId(aOrigin,
+                                       aTopLevelOrigin,
+                                       aInPBMode);
+    EXPECT_TRUE(!nodeId.IsEmpty());
+
+    nsTArray<nsCString> tags;
+    tags.AppendElement(NS_LITERAL_CSTRING("fake"));
+
+    nsresult rv = service->GetGMPDecryptor(&tags, nodeId, &mDecryptor);
+    EXPECT_TRUE(NS_SUCCEEDED(rv));
+    EXPECT_TRUE(!!mDecryptor);
+
+    if (mDecryptor) {
+      mDecryptor->Init(this);
+    }
+  }
+
+  void TestBasicStorage() {
+    AssertIsOnGMPThread();
+    EXPECT_TRUE(IsGMPStorageIsEmpty());
+
+    nsRefPtr<GeckoMediaPluginService> service =
+      GeckoMediaPluginService::GetGeckoMediaPluginService();
+
+    CreateDecryptor(NS_LITERAL_STRING("example1.com"),
+                    NS_LITERAL_STRING("example2.com"),
+                    false);
+
+    // Send a message to the fake GMP for it to run its own tests internally.
+    // It sends us a "test-storage complete" message when its passed, or
+    // some other message if its tests fail.
+    Expect(NS_LITERAL_CSTRING("test-storage complete"),
+           NS_NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+    Update(NS_LITERAL_CSTRING("test-storage"));
+  }
+
+  void TestCrossOriginStorage() {
+    EXPECT_TRUE(!mDecryptor);
+
+    // Open decryptor on one, origin, write a record, and test that that
+    // record can't be read on another origin.
+    CreateDecryptor(NS_LITERAL_STRING("example3.com"),
+                    NS_LITERAL_STRING("example4.com"),
+                    false);
+
+    // Send the decryptor the message "store recordid $time"
+    // Wait for the decrytor to send us "stored recordid $time"
+    auto t = time(0);
+    nsCString response("stored crossOriginTestRecordId ");
+    response.AppendInt((int64_t)t);
+    Expect(response, NS_NewRunnableMethod(this,
+      &GMPStorageTest::TestCrossOriginStorage_RecordStoredContinuation));
+
+    nsCString update("store crossOriginTestRecordId ");
+    update.AppendInt((int64_t)t);
+    Update(update);
+  }
+
+  void TestCrossOriginStorage_RecordStoredContinuation() {
+    // Close the old decryptor, and create a new one on a different origin,
+    // and try to read the record.
+    Shutdown();
+
+    CreateDecryptor(NS_LITERAL_STRING("example5.com"),
+                    NS_LITERAL_STRING("example6.com"),
+                    false);
+
+    Expect(NS_LITERAL_CSTRING("retrieve crossOriginTestRecordId succeeded (length 0 bytes)"),
+           NS_NewRunnableMethod(this, &GMPStorageTest::SetFinished));
+    Update(NS_LITERAL_CSTRING("retrieve crossOriginTestRecordId"));
+  }
+
+  void TestPBStorage() {
+
+    // Open decryptor on one, origin, write a record, close decryptor,
+    // open another, and test that record can be read, close decryptor,
+    // then send pb-last-context-closed notification, then open decryptor
+    // and check that it can't read that data; it should have been purged.
+    CreateDecryptor(NS_LITERAL_STRING("pb1.com"),
+                    NS_LITERAL_STRING("pb2.com"),
+                    true);
+
+    // Send the decryptor the message "store recordid $time"
+    // Wait for the decrytor to send us "stored recordid $time"
+    nsCString response("stored pbdata test-pb-data");
+    Expect(response, NS_NewRunnableMethod(this,
+      &GMPStorageTest::TestPBStorage_RecordStoredContinuation));
+
+    nsCString update("store pbdata test-pb-data");
+    Update(update);
+  }
+
+  void TestPBStorage_RecordStoredContinuation() {
+    Shutdown();
+
+    CreateDecryptor(NS_LITERAL_STRING("pb1.com"),
+                    NS_LITERAL_STRING("pb2.com"),
+                    true);
+
+    Expect(NS_LITERAL_CSTRING("retrieve pbdata succeeded (length 12 bytes)"),
+           NS_NewRunnableMethod(this,
+              &GMPStorageTest::TestPBStorage_RecordRetrievedContinuation));
+    Update(NS_LITERAL_CSTRING("retrieve pbdata"));
+  }
+
+  void TestPBStorage_RecordRetrievedContinuation() {
+    Shutdown();
+    SimulatePBModeExit();
+
+    CreateDecryptor(NS_LITERAL_STRING("pb1.com"),
+                    NS_LITERAL_STRING("pb2.com"),
+                    true);
+
+    Expect(NS_LITERAL_CSTRING("retrieve pbdata succeeded (length 0 bytes)"),
+           NS_NewRunnableMethod(this,
+              &GMPStorageTest::SetFinished));
+    Update(NS_LITERAL_CSTRING("retrieve pbdata"));
+
+  }
+
+  void Expect(const nsCString& aMessage, nsIRunnable* aContinuation) {
+    mExpected.AppendElement(ExpectedMessage(aMessage, aContinuation));
+  }
+
+  void AwaitFinished() {
+    while (!mFinished) {
+      NS_ProcessNextEvent(nullptr, true);
+    }
+    mFinished = false;
+  }
+
+  void Shutdown() {
+    if (mDecryptor) {
+      mDecryptor->Close();
+      mDecryptor = nullptr;
+    }
+  }
+
+  void Dummy() {
+  }
+
+  void SetFinished() {
+    mFinished = true;
+    Shutdown();
+    NS_DispatchToMainThread(NS_NewRunnableMethod(this, &GMPStorageTest::Dummy));
+  }
+
+  virtual void SessionMessage(const nsCString& aSessionId,
+                              const nsTArray<uint8_t>& aMessage,
+                              const nsCString& aDestinationURL) MOZ_OVERRIDE
+  {
+    MonitorAutoLock mon(mMonitor);
+
+    nsCString msg((const char*)aMessage.Elements(), aMessage.Length());
+    EXPECT_TRUE(mExpected.Length() > 0);
+    bool matches = mExpected[0].mMessage.Equals(msg);
+    EXPECT_TRUE(matches);
+    if (mExpected.Length() > 0 && matches) {
+      nsRefPtr<nsIRunnable> continuation = mExpected[0].mContinuation;
+      mExpected.RemoveElementAt(0);
+      if (continuation) {
+        NS_DispatchToCurrentThread(continuation);
+      }
+    }
+  }
+
+  virtual void ResolveNewSessionPromise(uint32_t aPromiseId,
+                                        const nsCString& aSessionId) MOZ_OVERRIDE { }
+  virtual void ResolveLoadSessionPromise(uint32_t aPromiseId,
+                                         bool aSuccess) MOZ_OVERRIDE {}
+  virtual void ResolvePromise(uint32_t aPromiseId) MOZ_OVERRIDE {}
+  virtual void RejectPromise(uint32_t aPromiseId,
+                             nsresult aException,
+                             const nsCString& aSessionId) MOZ_OVERRIDE { }
+  virtual void ExpirationChange(const nsCString& aSessionId,
+                                GMPTimestamp aExpiryTime) MOZ_OVERRIDE {}
+  virtual void SessionClosed(const nsCString& aSessionId) MOZ_OVERRIDE {}
+  virtual void SessionError(const nsCString& aSessionId,
+                            nsresult aException,
+                            uint32_t aSystemCode,
+                            const nsCString& aMessage) MOZ_OVERRIDE {}
+  virtual void KeyIdUsable(const nsCString& aSessionId,
+                           const nsTArray<uint8_t>& aKeyId) MOZ_OVERRIDE { }
+  virtual void KeyIdNotUsable(const nsCString& aSessionId,
+                              const nsTArray<uint8_t>& aKeyId) MOZ_OVERRIDE {}
+  virtual void SetCaps(uint64_t aCaps) MOZ_OVERRIDE {}
+  virtual void Decrypted(uint32_t aId,
+                         GMPErr aResult,
+                         const nsTArray<uint8_t>& aDecryptedData) MOZ_OVERRIDE { }
+  virtual void Terminated() MOZ_OVERRIDE { }
+
+private:
+  ~GMPStorageTest() { }
+
+  struct ExpectedMessage {
+    ExpectedMessage(const nsCString& aMessage, nsIRunnable* aContinuation)
+      : mMessage(aMessage)
+      , mContinuation(aContinuation)
+    {}
+    nsCString mMessage;
+    nsRefPtr<nsIRunnable> mContinuation;
+  };
+
+  nsTArray<ExpectedMessage> mExpected;
+
+  GMPDecryptorProxy* mDecryptor;
+  Monitor mMonitor;
+  Atomic<bool> mFinished;
+};
+
 void
 GMPTestRunner::DoTest(void (GMPTestRunner::*aTestMethod)())
 {
   nsRefPtr<GeckoMediaPluginService> service =
     GeckoMediaPluginService::GetGeckoMediaPluginService();
   nsCOMPtr<nsIThread> thread;
 
   service->GetThread(getter_AddRefs(thread));
@@ -116,8 +548,28 @@ TEST(GeckoMediaPlugins, GMPTestCodec) {
   nsRefPtr<GMPTestRunner> runner = new GMPTestRunner();
   runner->DoTest(&GMPTestRunner::RunTestGMPTestCodec);
 }
 
 TEST(GeckoMediaPlugins, GMPCrossOrigin) {
   nsRefPtr<GMPTestRunner> runner = new GMPTestRunner();
   runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin);
 }
+
+TEST(GeckoMediaPlugins, GMPStorageGetNodeId) {
+  nsRefPtr<GMPStorageTest> runner = new GMPStorageTest();
+  runner->DoTest(&GMPStorageTest::TestGetNodeId);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageBasic) {
+  nsRefPtr<GMPStorageTest> runner = new GMPStorageTest();
+  runner->DoTest(&GMPStorageTest::TestBasicStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPStorageCrossOrigin) {
+  nsRefPtr<GMPStorageTest> runner = new GMPStorageTest();
+  runner->DoTest(&GMPStorageTest::TestCrossOriginStorage);
+}
+
+TEST(GeckoMediaPlugins, GMPStoragePrivateBrowsing) {
+  nsRefPtr<GMPStorageTest> runner = new GMPStorageTest();
+  runner->DoTest(&GMPStorageTest::TestPBStorage);
+}
--- a/content/media/gtest/moz.build
+++ b/content/media/gtest/moz.build
@@ -22,13 +22,15 @@ if CONFIG['MOZ_WEBM_ENCODER']:
                         'TestWebMWriter.cpp',
 ]
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
 LOCAL_INCLUDES += [
     '/content/media/encoder',
     '/content/media/gmp',
+    '/security/certverifier',
+    '/security/pkix/include',
 ]
 
 FINAL_LIBRARY = 'xul-gtest'
 
 FAIL_ON_WARNINGS = True
--- a/dom/media/gmp-plugin/fake.info
+++ b/dom/media/gmp-plugin/fake.info
@@ -1,4 +1,4 @@
 Name: fake
 Description: Fake GMP Plugin
 Version: 1.0
-APIs: encode-video[h264], decode-video[h264]
+APIs: encode-video[h264], decode-video[h264], eme-decrypt[fake]
--- a/dom/media/gmp-plugin/gmp-fake.cpp
+++ b/dom/media/gmp-plugin/gmp-fake.cpp
@@ -44,16 +44,20 @@
 #include <limits.h>
 
 #include "gmp-platform.h"
 #include "gmp-video-host.h"
 #include "gmp-video-encode.h"
 #include "gmp-video-decode.h"
 #include "gmp-video-frame-i420.h"
 #include "gmp-video-frame-encoded.h"
+#include "gmp-decryption.h"
+
+#include "gmp-test-decryptor.h"
+#include "gmp-test-storage.h"
 
 #if defined(_MSC_VER)
 #define PUBLIC_FUNC __declspec(dllexport)
 #else
 #define PUBLIC_FUNC
 #endif
 
 static int g_log_level = 0;
@@ -76,17 +80,17 @@ static int g_log_level = 0;
 const char* kLogStrings[] = {
   "Critical",
   "Error",
   "Info",
   "Debug"
 };
 
 
-static GMPPlatformAPI* g_platform_api = NULL;
+GMPPlatformAPI* g_platform_api = NULL;
 
 class FakeVideoEncoder;
 class FakeVideoDecoder;
 
 struct EncodedFrame {
   uint32_t length_;
   uint8_t h264_compat_;
   uint32_t magic_;
@@ -103,17 +107,17 @@ struct EncodedFrame {
 class FakeEncoderTask : public GMPTask {
  public:
   FakeEncoderTask(FakeVideoEncoder* encoder,
                   GMPVideoi420Frame* frame,
                   GMPVideoFrameType type)
       : encoder_(encoder), frame_(frame), type_(type) {}
 
   virtual void Run();
-  virtual void Destroy() {}
+  virtual void Destroy() { delete this; }
 
   FakeVideoEncoder* encoder_;
   GMPVideoi420Frame* frame_;
   GMPVideoFrameType type_;
 };
 
 class FakeVideoEncoder : public GMPVideoEncoder {
  public:
@@ -257,17 +261,17 @@ void FakeEncoderTask::Run() {
 class FakeDecoderTask : public GMPTask {
  public:
   FakeDecoderTask(FakeVideoDecoder* decoder,
                   GMPVideoEncodedFrame* frame,
                   int64_t time)
       : decoder_(decoder), frame_(frame), time_(time) {}
 
   virtual void Run();
-  virtual void Destroy() {}
+  virtual void Destroy() { delete this; }
 
   FakeVideoDecoder* decoder_;
   GMPVideoEncodedFrame* frame_;
   int64_t time_;
 };
 
 class FakeVideoDecoder : public GMPVideoDecoder {
  public:
@@ -392,16 +396,19 @@ extern "C" {
   PUBLIC_FUNC GMPErr
   GMPGetAPI (const char* aApiName, void* aHostAPI, void** aPluginApi) {
     if (!strcmp (aApiName, "decode-video")) {
       *aPluginApi = new FakeVideoDecoder (static_cast<GMPVideoHost*> (aHostAPI));
       return GMPNoErr;
     } else if (!strcmp (aApiName, "encode-video")) {
       *aPluginApi = new FakeVideoEncoder (static_cast<GMPVideoHost*> (aHostAPI));
       return GMPNoErr;
+    } else if (!strcmp (aApiName, "eme-decrypt")) {
+      *aPluginApi = new FakeDecryptor(static_cast<GMPDecryptorHost*> (aHostAPI));
+      return GMPNoErr;
     }
     return GMPGenericErr;
   }
 
   PUBLIC_FUNC void
   GMPShutdown (void) {
     g_platform_api = NULL;
   }
new file mode 100644
--- /dev/null
+++ b/dom/media/gmp-plugin/gmp-test-decryptor.cpp
@@ -0,0 +1,294 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gmp-test-decryptor.h"
+#include "gmp-test-storage.h"
+
+#include <string>
+#include <vector>
+#include <iostream>
+#include <istream>
+#include <iterator>
+#include <sstream>
+#include <assert.h>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/NullPtr.h"
+
+using namespace std;
+
+FakeDecryptor* FakeDecryptor::sInstance = nullptr;
+
+static bool sFinishedTruncateTest = false;
+static bool sFinishedReplaceTest = false;
+static bool sMultiClientTest = false;
+
+void
+MaybeFinish()
+{
+  if (sFinishedTruncateTest && sFinishedReplaceTest && sMultiClientTest) {
+    FakeDecryptor::Message("test-storage complete");
+  }
+}
+
+FakeDecryptor::FakeDecryptor(GMPDecryptorHost* aHost)
+  : mHost(aHost)
+  , mCallback(nullptr)
+{
+  assert(!sInstance);
+  sInstance = this;
+}
+
+void FakeDecryptor::DecryptingComplete()
+{
+  sInstance = nullptr;
+  delete this;
+}
+
+void
+FakeDecryptor::Message(const std::string& aMessage)
+{
+  assert(sInstance);
+  const static std::string sid("fake-session-id");
+  sInstance->mCallback->SessionMessage(sid.c_str(), sid.size(),
+                                       (const uint8_t*)aMessage.c_str(), aMessage.size(),
+                                       nullptr, 0);
+}
+
+std::vector<std::string>
+Tokenize(const std::string& aString)
+{
+  std::stringstream strstr(aString);
+  std::istream_iterator<std::string> it(strstr), end;
+  return std::vector<std::string>(it, end);
+}
+
+static const string TruncateRecordId = "truncate-record-id";
+static const string TruncateRecordData = "I will soon be truncated";
+
+class ReadThenTask : public GMPTask {
+public:
+  ReadThenTask(string aId, ReadContinuation* aThen)
+    : mId(aId)
+    , mThen(aThen)
+  {}
+  void Run() MOZ_OVERRIDE {
+    ReadRecord(mId, mThen);
+  }
+  void Destroy() MOZ_OVERRIDE {
+    delete this;
+  }
+  ReadContinuation* mThen;
+  string mId;
+};
+
+class TestEmptyContinuation : public ReadContinuation {
+public:
+  void ReadComplete(GMPErr aErr, const std::string& aData) MOZ_OVERRIDE {
+    if (aData != "") {
+      FakeDecryptor::Message("FAIL TestEmptyContinuation record was not truncated");
+    }
+    sFinishedTruncateTest = true;
+    MaybeFinish();
+    delete this;
+  }
+};
+
+class TruncateContinuation : public ReadContinuation {
+public:
+  void ReadComplete(GMPErr aErr, const std::string& aData) MOZ_OVERRIDE {
+    if (aData != TruncateRecordData) {
+      FakeDecryptor::Message("FAIL TruncateContinuation read data doesn't match written data");
+    }
+    WriteRecord(TruncateRecordId, nullptr, 0,
+                new ReadThenTask(TruncateRecordId, new TestEmptyContinuation()));
+    delete this;
+  }
+};
+
+class VerifyAndFinishContinuation : public ReadContinuation {
+public:
+  VerifyAndFinishContinuation(string aValue)
+    : mValue(aValue)
+  {}
+  void ReadComplete(GMPErr aErr, const std::string& aData) MOZ_OVERRIDE {
+    if (aData != mValue) {
+      FakeDecryptor::Message("FAIL VerifyAndFinishContinuation read data doesn't match expected data");
+    }
+    sFinishedReplaceTest = true;
+    MaybeFinish();
+    delete this;
+  }
+  string mValue;
+};
+
+class VerifyAndOverwriteContinuation : public ReadContinuation {
+public:
+  VerifyAndOverwriteContinuation(string aId, string aValue, string aOverwrite)
+    : mId(aId)
+    , mValue(aValue)
+    , mOverwrite(aOverwrite)
+  {}
+  void ReadComplete(GMPErr aErr, const std::string& aData) MOZ_OVERRIDE {
+    if (aData != mValue) {
+      FakeDecryptor::Message("FAIL VerifyAndOverwriteContinuation read data doesn't match expected data");
+    }
+    WriteRecord(mId, mOverwrite, new ReadThenTask(mId, new VerifyAndFinishContinuation(mOverwrite)));
+    delete this;
+  }
+  string mId;
+  string mValue;
+  string mOverwrite;
+};
+
+static const string OpenAgainRecordId = "open-again-record-id";
+
+class OpenedSecondTimeContinuation : public OpenContinuation {
+public:
+  OpenedSecondTimeContinuation(GMPRecord* aRecord)
+    : mRecord(aRecord)
+  {
+  }
+
+  virtual void OpenComplete(GMPErr aStatus, GMPRecord* aRecord) MOZ_OVERRIDE {
+    if (GMP_SUCCEEDED(aStatus)) {
+      FakeDecryptor::Message("FAIL OpenSecondTimeContinuation should not be able to re-open record.");
+    }
+
+    // Succeeded, open should have failed.
+    sMultiClientTest = true;
+    MaybeFinish();
+
+    mRecord->Close();
+
+    delete this;
+  }
+  GMPRecord* mRecord;
+};
+
+class OpenedFirstTimeContinuation : public OpenContinuation {
+public:
+  virtual void OpenComplete(GMPErr aStatus, GMPRecord* aRecord) MOZ_OVERRIDE {
+    if (GMP_FAILED(aStatus)) {
+      FakeDecryptor::Message("FAIL OpenAgainContinuation to open record initially.");
+      sMultiClientTest = true;
+      MaybeFinish();
+      return;
+    }
+
+    auto err = GMPOpenRecord(OpenAgainRecordId, new OpenedSecondTimeContinuation(aRecord));
+
+    delete this;
+  }
+};
+
+void
+FakeDecryptor::TestStorage()
+{
+  // Basic I/O tests. We run three cases concurrently. The tests, like
+  // GMPStorage run asynchronously. When they've all passed, we send
+  // a message back to the parent process, or a failure message if not.
+
+  // Test 1: Basic I/O test, and test that writing 0 bytes in a record
+  // deletes record.
+  //
+  // Write data to truncate record, then
+  // read data, verify that we read what we wrote, then
+  // write 0 bytes to truncate record, then
+  // read data, verify that 0 bytes was read
+  // set sFinishedTruncateTest=true and MaybeFinish().
+  WriteRecord(TruncateRecordId,
+              TruncateRecordData,
+              new ReadThenTask(TruncateRecordId, new TruncateContinuation()));
+
+  // Test 2: Test that overwriting a record with a shorter record truncates
+  // the record to the shorter record.
+  //
+  // Write record, then
+  // read and verify record, then
+  // write a shorter record to same record.
+  // read and verify
+  // set sFinishedReplaceTest=true and MaybeFinish().
+  string id = "record1";
+  string record1 = "This is the first write to a record.";
+  string overwrite = "A shorter record";
+  WriteRecord(id,
+              record1,
+              new ReadThenTask(id, new VerifyAndOverwriteContinuation(id, record1, overwrite)));
+
+  // Test 3: Test that opening a record while it's already open fails.
+  //
+  // Open record1, then
+  // open record1, should fail.
+  // close record1,
+  // set sMultiClientTest=true and MaybeFinish().
+
+  GMPOpenRecord(OpenAgainRecordId, new OpenedFirstTimeContinuation());
+
+  // Note: Once all tests finish, dispatch "test-pass" message,
+  // which ends the test for the parent.
+}
+
+class ReportWritten : public GMPTask {
+public:
+  ReportWritten(const string& aRecordId, const string& aValue)
+    : mRecordId(aRecordId)
+    , mValue(aValue)
+  {}
+  void Run() MOZ_OVERRIDE {
+    FakeDecryptor::Message("stored " + mRecordId + " " + mValue);
+  }
+  void Destroy() MOZ_OVERRIDE {
+    delete this;
+  }
+  const string mRecordId;
+  const string mValue;
+};
+
+class ReportReadStatusContinuation : public ReadContinuation {
+public:
+  ReportReadStatusContinuation(const string& aRecordId)
+    : mRecordId(aRecordId)
+  {}
+  void ReadComplete(GMPErr aErr, const std::string& aData) MOZ_OVERRIDE {
+    if (GMP_FAILED(aErr)) {
+      FakeDecryptor::Message("retrieve " + mRecordId + " failed");
+    } else {
+      stringstream ss;
+      ss << aData.size();
+      string len;
+      ss >> len;
+      FakeDecryptor::Message("retrieve " + mRecordId + " succeeded (length " +
+                             len + " bytes)");
+    }
+    delete this;
+  }
+  string mRecordId;
+};
+
+void
+FakeDecryptor::UpdateSession(uint32_t aPromiseId,
+                             const char* aSessionId,
+                             uint32_t aSessionIdLength,
+                             const uint8_t* aResponse,
+                             uint32_t aResponseSize)
+{
+  std::string response((const char*)aResponse, (const char*)(aResponse)+aResponseSize);
+  std::vector<std::string> tokens = Tokenize(response);
+  const string& task = tokens[0];
+  if (task == "test-storage") {
+    TestStorage();
+  } else if (task == "store") {
+      // send "stored record" message on complete.
+    const string& id = tokens[1];
+    const string& value = tokens[2];
+    WriteRecord(id,
+                value,
+                new ReportWritten(id, value));
+  } else if (task == "retrieve") {
+    const string& id = tokens[1];
+    ReadRecord(id, new ReportReadStatusContinuation(id));
+  }
+}
new file mode 100644
--- /dev/null
+++ b/dom/media/gmp-plugin/gmp-test-decryptor.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FAKE_DECRYPTOR_H__
+#define FAKE_DECRYPTOR_H__
+
+#include "gmp-decryption.h"
+#include "gmp-async-shutdown.h"
+#include <string>
+#include "mozilla/Attributes.h"
+
+class FakeDecryptor : public GMPDecryptor {
+public:
+
+  FakeDecryptor(GMPDecryptorHost* aHost);
+
+  virtual void Init(GMPDecryptorCallback* aCallback) MOZ_OVERRIDE {
+    mCallback = aCallback;
+  }
+
+  virtual void CreateSession(uint32_t aPromiseId,
+                             const char* aInitDataType,
+                             uint32_t aInitDataTypeSize,
+                             const uint8_t* aInitData,
+                             uint32_t aInitDataSize,
+                             GMPSessionType aSessionType) MOZ_OVERRIDE
+  {
+  }
+
+  virtual void LoadSession(uint32_t aPromiseId,
+                           const char* aSessionId,
+                           uint32_t aSessionIdLength) MOZ_OVERRIDE
+  {
+  }
+
+  virtual void UpdateSession(uint32_t aPromiseId,
+                             const char* aSessionId,
+                             uint32_t aSessionIdLength,
+                             const uint8_t* aResponse,
+                             uint32_t aResponseSize) MOZ_OVERRIDE;
+
+  virtual void CloseSession(uint32_t aPromiseId,
+                            const char* aSessionId,
+                            uint32_t aSessionIdLength) MOZ_OVERRIDE
+  {
+  }
+
+  virtual void RemoveSession(uint32_t aPromiseId,
+                             const char* aSessionId,
+                             uint32_t aSessionIdLength) MOZ_OVERRIDE
+  {
+  }
+
+  virtual void SetServerCertificate(uint32_t aPromiseId,
+                                    const uint8_t* aServerCert,
+                                    uint32_t aServerCertSize) MOZ_OVERRIDE
+  {
+  }
+
+  virtual void Decrypt(GMPBuffer* aBuffer,
+                       GMPEncryptedBufferMetadata* aMetadata) MOZ_OVERRIDE
+  {
+  }
+
+  virtual void DecryptingComplete() MOZ_OVERRIDE;
+
+  static void Message(const std::string& aMessage);
+
+private:
+
+  static FakeDecryptor* sInstance;
+
+  void TestStorage();
+
+  GMPDecryptorCallback* mCallback;
+  GMPDecryptorHost* mHost;
+};
+
+#endif
new file mode 100644
--- /dev/null
+++ b/dom/media/gmp-plugin/gmp-test-storage.cpp
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gmp-test-storage.h"
+#include <vector>
+#include <assert.h>
+#include "mozilla/Attributes.h"
+
+class WriteRecordClient : public GMPRecordClient {
+public:
+  GMPErr Init(GMPRecord* aRecord,
+              GMPTask* aContinuation,
+              const uint8_t* aData,
+              uint32_t aDataSize) {
+    mRecord = aRecord;
+    mContinuation = aContinuation;
+    mData.insert(mData.end(), aData, aData + aDataSize);
+    return mRecord->Open();
+  }
+
+  virtual void OpenComplete(GMPErr aStatus) MOZ_OVERRIDE {
+    mRecord->Write(&mData.front(), mData.size());
+  }
+
+  virtual void ReadComplete(GMPErr aStatus,
+                            const uint8_t* aData,
+                            uint32_t aDataSize) MOZ_OVERRIDE {}
+
+  virtual void WriteComplete(GMPErr aStatus) MOZ_OVERRIDE {
+    // Note: Call Close() before running continuation, in case the
+    // continuation tries to open the same record; if we call Close()
+    // after running the continuation, the Close() call will arrive
+    // just after the Open() call succeeds, immediately closing the
+    // record we just opened.
+    mRecord->Close();
+    if (mContinuation) {
+      GMPRunOnMainThread(mContinuation);
+    }
+    delete this;
+  }
+
+private:
+  GMPRecord* mRecord;
+  GMPTask* mContinuation;
+  std::vector<uint8_t> mData;
+};
+
+GMPErr
+WriteRecord(const std::string& aRecordName,
+            const uint8_t* aData,
+            uint32_t aNumBytes,
+            GMPTask* aContinuation)
+{
+  GMPRecord* record;
+  WriteRecordClient* client = new WriteRecordClient();
+  auto err = GMPOpenRecord(aRecordName.c_str(),
+                           aRecordName.size(),
+                           &record,
+                           client);
+  if (GMP_FAILED(err)) {
+    return err;
+  }
+  return client->Init(record, aContinuation, aData, aNumBytes);
+}
+
+GMPErr
+WriteRecord(const std::string& aRecordName,
+            const std::string& aData,
+            GMPTask* aContinuation)
+{
+  return WriteRecord(aRecordName,
+                     (const uint8_t*)aData.c_str(),
+                     aData.size(),
+                     aContinuation);
+}
+
+class ReadRecordClient : public GMPRecordClient {
+public:
+  GMPErr Init(GMPRecord* aRecord,
+              ReadContinuation* aContinuation) {
+    mRecord = aRecord;
+    mContinuation = aContinuation;
+    return mRecord->Open();
+  }
+
+  virtual void OpenComplete(GMPErr aStatus) MOZ_OVERRIDE {
+    auto err = mRecord->Read();
+    if (GMP_FAILED(err)) {
+      mContinuation->ReadComplete(err, "");
+      delete this;
+    }
+  }
+
+  virtual void ReadComplete(GMPErr aStatus,
+                            const uint8_t* aData,
+                            uint32_t aDataSize) MOZ_OVERRIDE {
+    // Note: Call Close() before running continuation, in case the
+    // continuation tries to open the same record; if we call Close()
+    // after running the continuation, the Close() call will arrive
+    // just after the Open() call succeeds, immediately closing the
+    // record we just opened.
+    mRecord->Close();
+    std::string data((const char*)aData, aDataSize);
+    mContinuation->ReadComplete(GMPNoErr, data);
+    delete this;
+  }
+
+  virtual void WriteComplete(GMPErr aStatus) MOZ_OVERRIDE {
+  }
+
+private:
+  GMPRecord* mRecord;
+  ReadContinuation* mContinuation;
+};
+
+GMPErr
+ReadRecord(const std::string& aRecordName,
+           ReadContinuation* aContinuation)
+{
+  assert(aContinuation);
+  GMPRecord* record;
+  ReadRecordClient* client = new ReadRecordClient();
+  auto err = GMPOpenRecord(aRecordName.c_str(),
+                           aRecordName.size(),
+                           &record,
+                           client);
+  if (GMP_FAILED(err)) {
+    return err;
+  }
+  return client->Init(record, aContinuation);
+}
+
+extern GMPPlatformAPI* g_platform_api; // Defined in gmp-fake.cpp
+
+GMPErr
+GMPOpenRecord(const char* aName,
+              uint32_t aNameLength,
+              GMPRecord** aOutRecord,
+              GMPRecordClient* aClient)
+{
+  assert(g_platform_api);
+  return g_platform_api->createrecord(aName, aNameLength, aOutRecord, aClient);
+}
+
+GMPErr
+GMPRunOnMainThread(GMPTask* aTask)
+{
+  assert(g_platform_api);
+  return g_platform_api->runonmainthread(aTask);
+}
+
+class OpenRecordClient : public GMPRecordClient {
+public:
+  GMPErr Init(GMPRecord* aRecord,
+              OpenContinuation* aContinuation) {
+    mRecord = aRecord;
+    mContinuation = aContinuation;
+    return mRecord->Open();
+  }
+
+  virtual void OpenComplete(GMPErr aStatus) MOZ_OVERRIDE {
+    mContinuation->OpenComplete(aStatus, mRecord);
+    delete this;
+  }
+
+  virtual void ReadComplete(GMPErr aStatus,
+                            const uint8_t* aData,
+                            uint32_t aDataSize) MOZ_OVERRIDE { }
+
+  virtual void WriteComplete(GMPErr aStatus) MOZ_OVERRIDE { }
+
+private:
+  GMPRecord* mRecord;
+  OpenContinuation* mContinuation;
+};
+
+GMPErr
+GMPOpenRecord(const std::string& aRecordName,
+              OpenContinuation* aContinuation)
+{
+  assert(aContinuation);
+  GMPRecord* record;
+  OpenRecordClient* client = new OpenRecordClient();
+  auto err = GMPOpenRecord(aRecordName.c_str(),
+                           aRecordName.size(),
+                           &record,
+                           client);
+  if (GMP_FAILED(err)) {
+    return err;
+  }
+  return client->Init(record, aContinuation);
+}
new file mode 100644
--- /dev/null
+++ b/dom/media/gmp-plugin/gmp-test-storage.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TEST_GMP_STORAGE_H__
+#define TEST_GMP_STORAGE_H__
+
+#include "gmp-errors.h"
+#include "gmp-platform.h"
+#include <string>
+
+class ReadContinuation {
+public:
+  virtual void ReadComplete(GMPErr aErr, const std::string& aData) = 0;
+};
+
+// Reads a record to storage using GMPRecord.
+// Calls ReadContinuation with read data.
+GMPErr
+ReadRecord(const std::string& aRecordName,
+           ReadContinuation* aContinuation);
+
+// Writes a record to storage using GMPRecord.
+// Runs continuation when data is written.
+GMPErr
+WriteRecord(const std::string& aRecordName,
+            const std::string& aData,
+            GMPTask* aContinuation);
+
+GMPErr
+WriteRecord(const std::string& aRecordName,
+            const uint8_t* aData,
+            uint32_t aNumBytes,
+            GMPTask* aContinuation);
+
+GMPErr
+GMPOpenRecord(const char* aName,
+              uint32_t aNameLength,
+              GMPRecord** aOutRecord,
+              GMPRecordClient* aClient);
+
+GMPErr
+GMPRunOnMainThread(GMPTask* aTask);
+
+class OpenContinuation {
+public:
+  virtual void OpenComplete(GMPErr aStatus, GMPRecord* aRecord) = 0;
+};
+
+GMPErr
+GMPOpenRecord(const std::string& aRecordName,
+           OpenContinuation* aContinuation);
+
+#endif // TEST_GMP_STORAGE_H__
--- a/dom/media/gmp-plugin/moz.build
+++ b/dom/media/gmp-plugin/moz.build
@@ -1,17 +1,19 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 NO_DIST_INSTALL = True
 SOURCES += [
-        'gmp-fake.cpp'
+        'gmp-fake.cpp',
+        'gmp-test-decryptor.cpp',
+        'gmp-test-storage.cpp',
 ]
 
 SharedLibrary("fake")
 
 USE_STATIC_LIBS = True
 NO_VISIBILITY_FLAGS = True
 # Don't use STL wrappers; this isn't Gecko code
 DISABLE_STL_WRAPPING = True