// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ppapi/tests/test_instance_deprecated.h" #include <assert.h> #include <iostream> #include "ppapi/c/ppb_var.h" #include "ppapi/cpp/module.h" #include "ppapi/cpp/dev/scriptable_object_deprecated.h" #include "ppapi/tests/testing_instance.h" namespace { static const char kSetValueFunction[] = "SetValue"; static const char kSetExceptionFunction[] = "SetException"; static const char kReturnValueFunction[] = "ReturnValue"; // ScriptableObject used by instance. class InstanceSO : public pp::deprecated::ScriptableObject { public: explicit InstanceSO(TestInstance* i); virtual ~InstanceSO(); // pp::deprecated::ScriptableObject overrides. bool HasMethod(const pp::Var& name, pp::Var* exception); pp::Var Call(const pp::Var& name, const std::vector<pp::Var>& args, pp::Var* exception); private: TestInstance* test_instance_; // For out-of-process, the InstanceSO might be deleted after the instance was // already destroyed, so we can't rely on test_instance_->testing_interface() // being valid. Therefore we store our own. const PPB_Testing_Private* testing_interface_; }; InstanceSO::InstanceSO(TestInstance* i) : test_instance_(i), testing_interface_(i->testing_interface()) { // Set up a post-condition for the test so that we can ensure our destructor // is called. This only works reliably in-process. Out-of-process, it only // can work when the renderer stays alive a short while after the plugin // instance is destroyed. If the renderer is being shut down, too much happens // asynchronously for the out-of-process case to work reliably. In // particular: // - The Var ReleaseObject message is asynchronous. // - The PPB_Var_Deprecated host-side proxy posts a task to actually release // the object when the ReleaseObject message is received. // - The PPP_Class Deallocate message is asynchronous. // At time of writing this comment, if you modify the code so that the above // happens synchronously, and you remove the restriction that the plugin can't // be unblocked by a sync message, then this check actually passes reliably // for out-of-process. But we don't want to make any of those changes, so we // just skip the check. if (testing_interface_->IsOutOfProcess() == PP_FALSE) { i->instance()->AddPostCondition( "window.document.getElementById('container').instance_object_destroyed" ); } } InstanceSO::~InstanceSO() { if (testing_interface_->IsOutOfProcess() == PP_FALSE) { // TODO(dmichael): It would probably be best to make in-process consistent // with out-of-process. That would mean that the instance // would already be destroyed at this point. pp::Var ret = test_instance_->instance()->ExecuteScript( "document.getElementById('container').instance_object_destroyed=true;"); } else { // Out-of-process, this destructor might not actually get invoked. See the // comment in InstanceSO's constructor for an explanation. Also, instance() // has already been destroyed :-(. So we can't really do anything here. } } bool InstanceSO::HasMethod(const pp::Var& name, pp::Var* exception) { if (!name.is_string()) return false; return name.AsString() == kSetValueFunction || name.AsString() == kSetExceptionFunction || name.AsString() == kReturnValueFunction; } pp::Var InstanceSO::Call(const pp::Var& method_name, const std::vector<pp::Var>& args, pp::Var* exception) { if (!method_name.is_string()) return false; std::string name = method_name.AsString(); if (name == kSetValueFunction) { if (args.size() != 1 || !args[0].is_string()) *exception = pp::Var("Bad argument to SetValue(<string>)"); else test_instance_->set_string(args[0].AsString()); } else if (name == kSetExceptionFunction) { if (args.size() != 1 || !args[0].is_string()) *exception = pp::Var("Bad argument to SetException(<string>)"); else *exception = args[0]; } else if (name == kReturnValueFunction) { if (args.size() != 1) *exception = pp::Var("Need single arg to call ReturnValue"); else return args[0]; } else { *exception = pp::Var("Bad function call"); } return pp::Var(); } } // namespace REGISTER_TEST_CASE(Instance); TestInstance::TestInstance(TestingInstance* instance) : TestCase(instance) { } bool TestInstance::Init() { return true; } TestInstance::~TestInstance() { // Save the fact that we were destroyed in sessionStorage. This tests that // we can ExecuteScript at instance destruction without crashing. It also // allows us to check that ExecuteScript will run and succeed in certain // cases. In particular, when the instance is destroyed by normal DOM // deletion, ExecuteScript will actually work. See // TestExecuteScriptInInstanceShutdown for that test. Note, however, that // ExecuteScript will *not* have an effect when the instance is destroyed // because the renderer was shut down. pp::Var ret = instance()->ExecuteScript( "sessionStorage.setItem('instance_destroyed', 'true');"); } void TestInstance::RunTests(const std::string& filter) { RUN_TEST(ExecuteScript, filter); RUN_TEST(RecursiveObjects, filter); RUN_TEST(LeakedObjectDestructors, filter); RUN_TEST(SetupExecuteScriptAtInstanceShutdown, filter); RUN_TEST(ExecuteScriptAtInstanceShutdown, filter); } void TestInstance::LeakReferenceAndIgnore(const pp::Var& leaked) { static const PPB_Var* var_interface = static_cast<const PPB_Var*>( pp::Module::Get()->GetBrowserInterface(PPB_VAR_INTERFACE)); var_interface->AddRef(leaked.pp_var()); IgnoreLeakedVar(leaked.pp_var().value.as_id); } pp::deprecated::ScriptableObject* TestInstance::CreateTestObject() { return new InstanceSO(this); } std::string TestInstance::TestExecuteScript() { // Simple call back into the plugin. pp::Var exception; pp::Var ret = instance_->ExecuteScript( "document.getElementById('plugin').SetValue('hello, world');", &exception); ASSERT_TRUE(ret.is_undefined()); ASSERT_TRUE(exception.is_undefined()); ASSERT_TRUE(string_ == "hello, world"); // Return values from the plugin should be returned. ret = instance_->ExecuteScript( "document.getElementById('plugin').ReturnValue('return value');", &exception); ASSERT_TRUE(ret.is_string() && ret.AsString() == "return value"); ASSERT_TRUE(exception.is_undefined()); // Exception thrown by the plugin should be caught. ret = instance_->ExecuteScript( "document.getElementById('plugin').SetException('plugin exception');", &exception); ASSERT_TRUE(ret.is_undefined()); ASSERT_TRUE(exception.is_string()); // Due to a limitation in the implementation of TryCatch, it doesn't actually // pass the strings up. Since this is a trusted only interface, we've decided // not to bother fixing this for now. // Exception caused by string evaluation should be caught. exception = pp::Var(); ret = instance_->ExecuteScript("document.doesntExist()", &exception); ASSERT_TRUE(ret.is_undefined()); ASSERT_TRUE(exception.is_string()); // Don't know exactly what it will say. PASS(); } // A scriptable object that contains other scriptable objects recursively. This // is used to help verify that our scriptable object clean-up code works // properly. class ObjectWithChildren : public pp::deprecated::ScriptableObject { public: ObjectWithChildren(TestInstance* i, int num_descendents) { if (num_descendents > 0) { child_ = pp::VarPrivate(i->instance(), new ObjectWithChildren(i, num_descendents - 1)); } } struct IgnoreLeaks {}; ObjectWithChildren(TestInstance* i, int num_descendents, IgnoreLeaks) { if (num_descendents > 0) { child_ = pp::VarPrivate(i->instance(), new ObjectWithChildren(i, num_descendents - 1, IgnoreLeaks())); i->IgnoreLeakedVar(child_.pp_var().value.as_id); } } private: pp::VarPrivate child_; }; std::string TestInstance::TestRecursiveObjects() { // These should be deleted when we exit scope, so should not leak. pp::VarPrivate not_leaked(instance(), new ObjectWithChildren(this, 50)); // Leak some, but tell TestCase to ignore the leaks. This test is run and then // reloaded (see ppapi_uitest.cc). If these aren't cleaned up when the first // run is torn down, they will show up as leaks in the second run. // NOTE: The ScriptableObjects are actually leaked, but they should be removed // from the tracker. See below for a test that verifies that the // destructor is not run. pp::VarPrivate leaked( instance(), new ObjectWithChildren(this, 50, ObjectWithChildren::IgnoreLeaks())); // Now leak a reference to the root object. This should force the root and // all its descendents to stay in the tracker. LeakReferenceAndIgnore(leaked); PASS(); } // A scriptable object that should cause a crash if its destructor is run. We // don't run the destructor for objects which the plugin leaks. This is to // prevent them doing dangerous things at cleanup time, such as executing script // or creating new objects. class BadDestructorObject : public pp::deprecated::ScriptableObject { public: BadDestructorObject() {} ~BadDestructorObject() { assert(false); } }; std::string TestInstance::TestLeakedObjectDestructors() { pp::VarPrivate leaked(instance(), new BadDestructorObject()); // Leak a reference so it gets deleted on instance shutdown. LeakReferenceAndIgnore(leaked); PASS(); } std::string TestInstance::TestSetupExecuteScriptAtInstanceShutdown() { // This test only exists so that it can be run before // TestExecuteScriptAtInstanceShutdown. See the comment for that test. pp::Var exception; pp::Var result = instance()->ExecuteScript( "sessionStorage.removeItem('instance_destroyed');", &exception); ASSERT_TRUE(exception.is_undefined()); ASSERT_TRUE(result.is_undefined()); PASS(); } std::string TestInstance::TestExecuteScriptAtInstanceShutdown() { // This test relies on the previous test being run in the same browser // session, but in such a way that the instance is destroyed. See // chrome/test/ppapi/ppapi_browsertest.cc for how the navigation happens. // // Given those constraints, ~TestInstance should have been invoked to set // instance_destroyed in sessionStorage. So all we have to do is make sure // that it was set as expected. pp::Var result = instance()->ExecuteScript( "sessionStorage.getItem('instance_destroyed');"); ASSERT_TRUE(result.is_string()); ASSERT_EQ(std::string("true"), result.AsString()); instance()->ExecuteScript("sessionStorage.removeItem('instance_destroyed');"); PASS(); }