// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.ClearScript.Util; using Microsoft.ClearScript.V8; using Microsoft.ClearScript.Windows; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.ClearScript.Test { [TestClass] [DeploymentItem("ClearScriptV8-64.dll")] [DeploymentItem("ClearScriptV8-32.dll")] [DeploymentItem("v8-x64.dll")] [DeploymentItem("v8-ia32.dll")] [DeploymentItem("v8-base-x64.dll")] [DeploymentItem("v8-base-ia32.dll")] [DeploymentItem("v8-zlib-x64.dll")] [DeploymentItem("v8-zlib-ia32.dll")] [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Test classes use TestCleanupAttribute for deterministic teardown.")] public class PropertyBagTest : ClearScriptTest { #region setup / teardown private ScriptEngine engine; [TestInitialize] public void TestInitialize() { engine = new JScriptEngine(WindowsScriptEngineFlags.EnableDebugging); } [TestCleanup] public void TestCleanup() { engine.Dispose(); BaseTestCleanup(); } #endregion #region test methods // ReSharper disable InconsistentNaming [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_Property() { var host = new HostFunctions(); var bag = new PropertyBag { { "host", host } }; engine.AddHostObject("bag", bag); Assert.AreSame(host, engine.Evaluate("bag.host")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_Property_Scalar() { const int value = 123; var bag = new PropertyBag { { "value", value } }; engine.AddHostObject("bag", bag); Assert.AreEqual(value, engine.Evaluate("bag.value")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_Property_Struct() { var date = new DateTime(2007, 5, 22, 6, 15, 43); var bag = new PropertyBag { { "date", date } }; engine.AddHostObject("bag", bag); Assert.AreEqual(date, engine.Evaluate("bag.date")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_Property_GlobalMembers() { var host = new HostFunctions(); var bag = new PropertyBag { { "host", host } }; engine.AddHostObject("bag", HostItemFlags.GlobalMembers, bag); Assert.AreSame(host, engine.Evaluate("host")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_HostDelegate() { var methodInvoked = false; Action method = () => methodInvoked = true; var bag = new PropertyBag { { "method", method } }; engine.AddHostObject("bag", bag); engine.Execute("bag.method()"); Assert.IsTrue(methodInvoked); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_ScriptProperty() { engine.Execute("foo = { bar: false }"); var bag = new PropertyBag { { "foo", engine.Script.foo } }; engine.AddHostObject("bag", bag); engine.Execute("bag.foo.bar = true"); Assert.IsTrue(engine.Script.foo.bar); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_ScriptMethod() { engine.Execute("methodInvoked = false; function method() { methodInvoked = true }"); var bag = new PropertyBag { { "method", engine.Script.method } }; engine.AddHostObject("bag", bag); engine.Execute("bag.method()"); Assert.IsTrue(engine.Script.methodInvoked); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_IScriptableObject_OnExpose() { var host = new HostFunctions(); var bag = new PropertyBag { { "host", host } }; engine.AddHostObject("bag", bag); Assert.AreSame(engine, host.GetEngine()); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_IScriptableObject_OnModify() { var bag = new PropertyBag(); engine.AddHostObject("bag", bag); var host = new HostFunctions(); bag.Add("host", host); Assert.AreSame(engine, host.GetEngine()); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_Expando() { var bag = new PropertyBag(); engine.AddHostObject("bag", bag); engine.Execute("bag.foo = 123"); Assert.AreEqual(123, bag["foo"]); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_Writable() { var bag = new PropertyBag { { "foo", false } }; engine.AddHostObject("bag", bag); engine.Execute("bag.foo = true"); Assert.IsTrue((bool)bag["foo"]); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_Writable_Delete() { var bag = new PropertyBag { { "foo", false } }; engine.AddHostObject("bag", bag); engine.Execute("delete bag.foo"); Assert.IsFalse(bag.ContainsKey("foo")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_ReadOnly() { var bag = new PropertyBag(true); bag.SetPropertyNoCheck("foo", false); engine.AddHostObject("bag", bag); TestUtil.AssertException(() => engine.Execute("bag.foo = true")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_ReadOnly_Delete() { var bag = new PropertyBag(true); bag.SetPropertyNoCheck("foo", false); engine.AddHostObject("bag", bag); TestUtil.AssertException(() => engine.Execute("delete bag.foo")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_ExternalModification_AddProperty() { var bag = new PropertyBag { { "foo", 123 } }; engine.AddHostObject("bag", bag); Assert.AreEqual(123, engine.Evaluate("bag.foo")); bag.Add("bar", 456); Assert.AreEqual(456, engine.Evaluate("bag.bar")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_ExternalModification_ChangeProperty() { var bag = new PropertyBag { { "foo", 123 } }; engine.AddHostObject("bag", bag); Assert.AreEqual(123, engine.Evaluate("bag.foo")); bag["foo"] = 456; Assert.AreEqual(456, engine.Evaluate("bag.foo")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_ExternalModification_DeleteProperty() { var bag = new PropertyBag { { "foo", 123 }, { "bar", 456 } }; engine.AddHostObject("bag", bag); Assert.AreEqual(456, engine.Evaluate("bag.bar")); bag.Remove("bar"); Assert.AreSame(Undefined.Value, engine.Evaluate("bag.bar")); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_MultiEngine() { var bag = new PropertyBag(); engine.AddHostObject("bag", bag); Action innerTest = () => { // The Visual Studio 2013 debugging stack fails to release the engine properly, // resulting in test failure. Visual Studio 2012 does not have this bug. using (var scriptEngine = new VBScriptEngine()) { scriptEngine.AddHostObject("bag", bag); Assert.AreEqual(2, bag.EngineCount); } }; innerTest(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); Assert.AreEqual(1, bag.EngineCount); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_MultiEngine_Property() { var outerBag = new PropertyBag(); engine.AddHostObject("bag", outerBag); var innerBag = new PropertyBag(); Action innerTest = () => { // The Visual Studio 2013 debugging stack fails to release the engine properly, // resulting in test failure. Visual Studio 2012 does not have this bug. using (var scriptEngine = new VBScriptEngine()) { scriptEngine.AddHostObject("bag", outerBag); outerBag.Add("innerBag", innerBag); Assert.AreEqual(2, innerBag.EngineCount); } }; innerTest(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); Assert.AreEqual(1, innerBag.EngineCount); } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_MultiEngine_HostFunctions() { using (var scriptEngine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging)) { const string code = "bag.host.func(0, function () { return bag.func(); })"; var bag = new PropertyBag { { "host", new HostFunctions() }, { "func", new Func(() => ScriptEngine.Current) } }; engine.AddHostObject("bag", bag); scriptEngine.AddHostObject("bag", bag); var func = (Func)engine.Evaluate(code); Assert.AreSame(engine, func()); func = (Func)scriptEngine.Evaluate(code); Assert.AreSame(scriptEngine, func()); } } [TestMethod, TestCategory("PropertyBag")] public void PropertyBag_MultiEngine_Parallel() { // This is a torture test for ConcurrentWeakSet and general engine teardown/cleanup. // It has exposed some very tricky engine bugs. var bag = new PropertyBag(); engine.AddHostObject("bag", bag); const int threadCount = 256; var engineCount = 0; // 32-bit V8 starts failing requests to create new contexts rather quickly. This is // because each V8 isolate requires (among other things) a 32MB address space // reservation. 64-bit V8 reserves much larger blocks but benefits from the enormous // available address space. var maxV8Count = Environment.Is64BitProcess ? 128 : 16; var maxJScriptCount = (threadCount - maxV8Count) / 2; var startEvent = new ManualResetEventSlim(false); var checkpointEvent = new ManualResetEventSlim(false); var continueEvent = new ManualResetEventSlim(false); var stopEvent = new ManualResetEventSlim(false); ParameterizedThreadStart body = arg => { // ReSharper disable AccessToDisposedClosure var index = (int)arg; startEvent.Wait(); ScriptEngine scriptEngine; if (index < maxV8Count) { scriptEngine = new V8ScriptEngine(); } else if (index < (maxV8Count + maxJScriptCount)) { scriptEngine = new JScriptEngine(); } else { scriptEngine = new VBScriptEngine(); } scriptEngine.AddHostObject("bag", bag); if (Interlocked.Increment(ref engineCount) == threadCount) { checkpointEvent.Set(); } continueEvent.Wait(); scriptEngine.Dispose(); if (Interlocked.Decrement(ref engineCount) == 0) { stopEvent.Set(); } // ReSharper restore AccessToDisposedClosure }; var threads = Enumerable.Range(0, threadCount).Select(index => new Thread(body)).ToArray(); threads.ForEach((thread, index) => thread.Start(index)); startEvent.Set(); checkpointEvent.Wait(); Assert.AreEqual(threadCount + 1, bag.EngineCount); continueEvent.Set(); stopEvent.Wait(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); Assert.AreEqual(1, bag.EngineCount); Array.ForEach(threads, thread => thread.Join()); startEvent.Dispose(); checkpointEvent.Dispose(); continueEvent.Dispose(); stopEvent.Dispose(); } // ReSharper restore InconsistentNaming #endregion } }