// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.ClearScript.V8;
using Microsoft.ClearScript.Util;
using Microsoft.CSharp.RuntimeBinder;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace Microsoft.ClearScript.Test
{
// ReSharper disable once PartialTypeWithSinglePart
[TestClass]
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Test classes use TestCleanupAttribute for deterministic teardown.")]
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "Typos in test code are acceptable.")]
[SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Typos in test code are acceptable.")]
public partial class BugFixTest : ClearScriptTest
{
#region setup / teardown
private ScriptEngine engine;
[TestInitialize]
public void TestInitialize()
{
engine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging);
}
[TestCleanup]
public void TestCleanup()
{
engine.Dispose();
BaseTestCleanup();
}
#endregion
#region test methods
// ReSharper disable InconsistentNaming
[TestMethod, TestCategory("BugFix")]
public void BugFix_NullArgBinding()
{
var value = 123.456 as IConvertible;
engine.AddRestrictedHostObject("value", value);
Assert.AreEqual(value.ToString(null), engine.Evaluate("value.ToString(null)"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_NullArgBinding_Ambiguous()
{
engine.AddHostObject("lib", new HostTypeCollection("mscorlib"));
TestUtil.AssertException(() => engine.Execute("lib.System.Console.WriteLine(null)"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_DelegateConstructionSyntax()
{
engine.AddHostObject("lib", HostItemFlags.GlobalMembers, new HostTypeCollection("mscorlib"));
var func = (Func)engine.Evaluate("new (System.Func(System.Int32, System.Int32))(function (x) {return x * x; })");
Assert.AreEqual(25, func(5));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_DelegateConstructionSyntax_DoubleWrap()
{
engine.AddHostObject("lib", HostItemFlags.GlobalMembers, new HostTypeCollection("mscorlib"));
engine.Execute("cb = new (System.Func(System.Int32, System.Int32))(function (x) {return x * x; })");
var func = (Func)engine.Evaluate("new (System.Func(System.Int32, System.Int32))(cb)");
Assert.AreEqual(25, func(5));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8ScriptInterruptCrash()
{
// run the test several times to verify post-interrupt engine functionality
for (var iteration = 0; iteration < 16; iteration++)
{
var context = new PropertyBag();
engine.AddHostObject("context", context);
using (var startEvent = new ManualResetEventSlim(false))
{
context["startEvent"] = startEvent;
var interrupted = false;
var thread = new Thread(() =>
{
try
{
engine.Execute("while (true) { context.startEvent.Set(); }");
}
catch (ScriptInterruptedException)
{
interrupted = true;
}
});
thread.Start();
startEvent.Wait();
engine.Interrupt();
thread.Join();
Assert.IsTrue(interrupted);
}
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_InheritedInterfaceMethod()
{
var list = new List { 123 };
var enumerator = list.AsEnumerable().GetEnumerator();
engine.AddRestrictedHostObject("enumerator", enumerator);
Assert.IsTrue((bool)engine.Evaluate("enumerator.MoveNext()"));
Assert.AreEqual(123, engine.Evaluate("enumerator.Current"));
Assert.IsFalse((bool)engine.Evaluate("enumerator.MoveNext()"));
engine.Execute("enumerator.Dispose()");
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_EnumEquality()
{
engine.AddHostObject("host", new HostFunctions());
engine.AddHostType("DayOfWeek", typeof(DayOfWeek));
engine.AddHostType("BindingFlags", typeof(BindingFlags));
Assert.IsTrue((bool)engine.Evaluate("DayOfWeek.Wednesday == DayOfWeek.Wednesday"));
Assert.IsTrue((bool)engine.Evaluate("host.flags(BindingFlags.Public, BindingFlags.Instance) == host.flags(BindingFlags.Public, BindingFlags.Instance)"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_FloatParameterBinding()
{
engine.AddHostType("Convert", typeof(Convert));
engine.Script.list = new List();
const float floatPi = (float)Math.PI;
engine.Execute("list.Add(Convert.ToSingle(Math.PI))");
Assert.AreEqual(floatPi, engine.Script.list[0]);
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_UInt32RoundTrip()
{
engine.AddHostType("UInt32", typeof(uint));
Assert.AreEqual((long)uint.MaxValue, engine.Evaluate("UInt32.MaxValue"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_AutoInt64FromDouble()
{
engine.AddHostType("Int32", typeof(int));
Assert.AreEqual((long)int.MaxValue * 123 + 1, engine.Evaluate("Int32.MaxValue * 123 + 1"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8CompiledScriptResult()
{
engine.Script.host = new HostFunctions();
using (var script = ((V8ScriptEngine)engine).Compile("host"))
{
Assert.IsInstanceOfType(((V8ScriptEngine)engine).Evaluate(script), typeof(HostFunctions));
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_GenericDelegateConstructorWithArgument()
{
engine.AddHostType("Func", "System.Func");
engine.AddHostType("StringT", typeof(string));
engine.AddHostType("IntT", typeof(int));
var func = (Func)engine.Evaluate("new Func(StringT, IntT, function (s) { return s.length; })");
const string testString = "floccinaucinihilipilification";
Assert.AreEqual(testString.Length, func(testString));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_HostMethodAsArgument()
{
var testObject = new TestObject();
engine.Script.testObject = testObject;
engine.Script.expando = new ExpandoObject();
engine.AddHostType("DateTime", typeof(DateTime));
engine.Execute("expando.method = testObject.Method");
Assert.AreEqual(testObject.Method("foo", 123), engine.Evaluate("expando.method('foo', 123)"));
Assert.AreEqual(testObject.Method(456), engine.Evaluate("expando.method(DateTime, 456)"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_HostIndexedPropertyAsArgument()
{
var testObject = new TestObject();
engine.Script.testObject = testObject;
engine.Script.expando = new ExpandoObject();
engine.Execute("expando.property = testObject.Item");
Assert.AreEqual("foo", engine.Evaluate("expando.property.set(123, 'foo')"));
Assert.AreEqual("foo", engine.Evaluate("expando.property.get(123)"));
Assert.AreEqual("foo", engine.Evaluate("expando.property(123)"));
Assert.AreEqual(456, engine.Evaluate("expando.property.set('bar', 456)"));
Assert.AreEqual(456, engine.Evaluate("expando.property.get('bar')"));
Assert.AreEqual(456, engine.Evaluate("expando.property('bar')"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_Nullable_Field()
{
engine.Script.test = new NullableTest();
Assert.IsNull(engine.Evaluate("test.Field"));
Assert.IsTrue((bool)engine.Evaluate("test.Field === null"));
engine.Execute("test.Field = 123");
Assert.AreEqual(123, engine.Evaluate("test.Field"));
Assert.IsTrue((bool)engine.Evaluate("test.Field === 123"));
engine.Execute("test.Field = null");
Assert.IsNull(engine.Evaluate("test.Field"));
Assert.IsTrue((bool)engine.Evaluate("test.Field === null"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_Nullable_Property()
{
engine.Script.test = new NullableTest();
Assert.IsNull(engine.Evaluate("test.Property"));
Assert.IsTrue((bool)engine.Evaluate("test.Property === null"));
engine.Execute("test.Property = 123");
Assert.AreEqual(123, engine.Evaluate("test.Property"));
Assert.IsTrue((bool)engine.Evaluate("test.Property === 123"));
engine.Execute("test.Property = null");
Assert.IsNull(engine.Evaluate("test.Property"));
Assert.IsTrue((bool)engine.Evaluate("test.Property === null"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_Nullable_ArgBinding()
{
var test = new NullableTest();
engine.Script.test = test;
Assert.IsNull(engine.Evaluate("test.Method(null)"));
Assert.AreEqual(test.Method(5), engine.Evaluate("test.Method(5)"));
Assert.AreEqual(test.Method(5.1), engine.Evaluate("test.Method(5.1)"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_GlobalObjectCrash()
{
engine.AddHostObject("random", HostItemFlags.GlobalMembers, new Random());
Assert.AreEqual("[object Object]", engine.ExecuteCommand("this"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_VBDynamicArgMarshaling_Numeric()
{
var result = TestUtil.InvokeVBTestFunction(@"
Using engine As New V8ScriptEngine
engine.Execute(""data = [5, 4, 'qux', 2, 1]; function getElement(i1, i2, i3) { return data[i1 + i2 - i3]; }"")
TestFunction = engine.Script.getElement(CShort(1), CLng(99), CStr(98))
End Using
");
Assert.AreEqual("qux", result);
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_VBDynamicArgMarshaling_String()
{
var result = TestUtil.InvokeVBTestFunction(@"
Using engine As New V8ScriptEngine
engine.Execute(""data = { foo26: 123, bar97: 456.789 }; function getElement(i1, i2) { return data[i1 + i2]; }"")
TestFunction = engine.Script.getElement(""bar"", CLng(97))
End Using
");
Assert.AreEqual(456.789, result);
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_AmbiguousAttribute()
{
engine.Script.foo = new AmbiguousAttributeTest();
engine.Execute("foo.Foo()");
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_CoreBindCache()
{
HostItem.ResetCoreBindCache();
engine.Dispose();
for (var i = 0; i < 10; i++)
{
using (engine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging))
{
engine.Script.host = new HostFunctions();
Assert.AreEqual(' ', engine.Evaluate("host.toChar(32)"));
Assert.AreEqual('A', engine.Evaluate("host.toChar(65)"));
Assert.AreEqual(0.0F, engine.Evaluate("host.toSingle(0)"));
Assert.AreEqual(1.0F, engine.Evaluate("host.toSingle(1)"));
}
}
Assert.AreEqual(2L, HostItem.GetCoreBindCount());
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_StringWithNullChar()
{
const string value = "abc\0def";
const string value2 = "ghi\0jkl";
dynamic func = engine.Evaluate("(function (x) { return x.length; }).valueOf()");
Assert.AreEqual(value.Length, func(value));
Assert.AreEqual(value, engine.Evaluate(@"'abc\0def'"));
dynamic obj = engine.Evaluate(@"({ 'abc\0def': 'ghi\0jkl' })");
Assert.IsTrue(((DynamicObject)obj).GetDynamicMemberNames().Contains(value));
Assert.AreEqual(value2, obj[value]);
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8ScriptItemWeakBinding()
{
// This test verifies that V8 script items no longer prevent their isolates from being
// destroyed. Previously it exhausted address space and crashed in 32-bit mode.
for (var i = 0; i < 128; i++)
{
using (var tempEngine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging))
{
tempEngine.Evaluate("(function () {}).valueOf()");
}
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8ScriptItemDispose()
{
dynamic item1;
using (var tempEngine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging))
{
item1 = tempEngine.Evaluate("(function () { return 123; }).valueOf()");
Assert.AreEqual(123, item1());
dynamic item2 = tempEngine.Evaluate("(function () { return 456; }).valueOf()");
using (item2 as IDisposable)
{
Assert.AreEqual(456, item2());
}
}
using (item1 as IDisposable)
{
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8CompiledScriptWeakBinding()
{
// This test verifies that V8 compiled scripts no longer prevent their isolates from
// being destroyed. Previously it exhausted address space and crashed in 32-bit mode.
for (var i = 0; i < 128; i++)
{
using (var tempEngine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging))
{
tempEngine.Compile("(function () {}).valueOf()");
}
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8CompiledScriptDispose()
{
V8Script script1;
using (var tempEngine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDebugging))
{
script1 = tempEngine.Compile("(function () { return 123; })()");
Assert.AreEqual(123, tempEngine.Evaluate(script1));
V8Script script2 = tempEngine.Compile("(function () { return 456; })()");
using (script2)
{
Assert.AreEqual(456, tempEngine.Evaluate(script2));
}
}
using (script1)
{
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8ArrayBufferAllocator()
{
engine.Execute("buffer = new ArrayBuffer(12345)");
Assert.AreEqual(12345, engine.Script.buffer.byteLength);
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8CachedObjectLeak()
{
WeakReference wr = null;
new Action(() =>
{
// ReSharper disable RedundantAssignment
object x = null;
using (var tempEngine = new V8ScriptEngine())
{
tempEngine.AddHostType("Action", typeof(Action));
x = tempEngine.Evaluate("action = new Action(function () {})");
wr = new WeakReference(tempEngine);
}
Assert.IsInstanceOfType(x, typeof(Action));
TestUtil.AssertException((Action)x);
x = null;
// ReSharper restore RedundantAssignment
})();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
GC.WaitForPendingFinalizers();
Assert.IsFalse(wr.IsAlive);
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_Resurrection_V8ScriptEngine()
{
for (var index = 0; index < 256; index++)
{
// ReSharper disable UnusedVariable
var wrapper = new ResurrectionTestWrapper(new V8ScriptEngine());
GC.Collect();
// ReSharper restore UnusedVariable
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_Resurrection_V8Runtime()
{
for (var index = 0; index < 256; index++)
{
// ReSharper disable UnusedVariable
var wrapper = new ResurrectionTestWrapper(new V8Runtime());
GC.Collect();
// ReSharper restore UnusedVariable
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_Resurrection_V8Script()
{
for (var index = 0; index < 256; index++)
{
// ReSharper disable UnusedVariable
var tempEngine = new V8ScriptEngine();
var wrapper = new ResurrectionTestWrapper(tempEngine.Compile("function foo() {}"));
GC.Collect();
// ReSharper restore UnusedVariable
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_Resurrection_V8ScriptItem()
{
for (var index = 0; index < 256; index++)
{
// ReSharper disable UnusedVariable
var tempEngine = new V8ScriptEngine();
var wrapper = new ResurrectionTestWrapper(tempEngine.Script.Math);
GC.Collect();
// ReSharper restore UnusedVariable
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8GlobalMembers_ReadOnlyPropertyCrash()
{
// this test is for a crash that occurred only on debug V8 builds
engine.AddHostObject("bag", HostItemFlags.GlobalMembers, new PropertyBag());
engine.AddHostObject("test", HostItemFlags.GlobalMembers, new { foo = 123 });
TestUtil.AssertException(() => engine.Execute("foo = 456"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8GlobalMembers_ReadOnlyPropertyCrash_Index()
{
// this test is for a crash that occurred only on debug V8 builds
engine.AddHostObject("bag", HostItemFlags.GlobalMembers, new PropertyBag());
engine.AddHostObject("test", HostItemFlags.GlobalMembers, new ReadOnlyCollection(new[] { 5, 4, 3, 2, 1 }));
TestUtil.AssertException(() => engine.Execute("this[2] = 123"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8GlobalMembers_NativeFunctionHiding()
{
engine.Execute("function toString() { return 'ABC'; }");
engine.AddHostObject("bag", HostItemFlags.GlobalMembers, new PropertyBag());
Assert.AreEqual("ABC", engine.Evaluate("toString()"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_CallHostObjectFunctionAsConstructor()
{
engine.Script.random = new Random();
engine.AddHostType("Random", typeof(Random));
var result = engine.Evaluate(@"
(function () {
var x = new Random().NextDouble();
try {
return new random.constructor();
}
catch (ex) {
return new Random().NextDouble() * x;
}
return false;
})()
");
Assert.IsInstanceOfType(result, typeof(double));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_HostItemCachingForHostVariables()
{
var foo = new HostFunctions().newVar(new object());
engine.Script.foo1 = foo;
engine.Script.foo2 = foo;
Assert.IsTrue(Convert.ToBoolean(engine.Evaluate("foo1 === foo2")));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_MetaScriptItem_GetDynamicMemberNames()
{
var dmop = (IDynamicMetaObjectProvider)engine.Evaluate("({ foo: 123, bar: 456, baz: 789 })");
var dmo = dmop.GetMetaObject(Expression.Constant(dmop));
var names = dmo.GetDynamicMemberNames().ToArray();
Assert.IsTrue(names.Contains("foo"));
Assert.IsTrue(names.Contains("bar"));
Assert.IsTrue(names.Contains("baz"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_AmbiguousIndexer()
{
IAmbiguousIndexer indexer = new AmbiguousIndexer();
engine.AddRestrictedHostObject("indexer", indexer);
engine.AddHostType("DayOfWeek", typeof(DayOfWeek));
engine.Execute("indexer.Item.set(123, 456)");
Assert.AreEqual(456, engine.Evaluate("indexer.Item(123)"));
Assert.IsNull(engine.Evaluate("indexer.Item(789)"));
engine.Execute("indexer.Item.set(DayOfWeek.Thursday, DayOfWeek.Sunday)");
Assert.AreEqual(DayOfWeek.Sunday, engine.Evaluate("indexer.Item(DayOfWeek.Thursday)"));
Assert.IsNull(engine.Evaluate("indexer.Item(DayOfWeek.Tuesday)"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_InaccessiblePropertyAccessors()
{
engine.Script.foo = new InaccessiblePropertyAccessors();
TestUtil.AssertException(() => engine.Evaluate("foo.NoGetter"));
Assert.AreEqual(123, engine.Evaluate("foo.NoGetter = 123"));
TestUtil.AssertException(() => engine.Evaluate("foo.PrivateGetter"));
Assert.AreEqual(456, engine.Evaluate("foo.PrivateGetter = 456"));
Assert.AreEqual(456, engine.Evaluate("foo.NoSetter"));
TestUtil.AssertException(() => engine.Evaluate("foo.NoSetter = 789"));
Assert.AreEqual(456, engine.Evaluate("foo.PrivateSetter"));
TestUtil.AssertException(() => engine.Evaluate("foo.PrivateSetter = 789"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_InaccessiblePropertyAccessors_Static()
{
engine.AddHostType("foo", typeof(InaccessiblePropertyAccessorsStatic));
TestUtil.AssertException(() => engine.Evaluate("foo.NoGetter"));
Assert.AreEqual(123, engine.Evaluate("foo.NoGetter = 123"));
TestUtil.AssertException(() => engine.Evaluate("foo.PrivateGetter"));
Assert.AreEqual(456, engine.Evaluate("foo.PrivateGetter = 456"));
Assert.AreEqual(456, engine.Evaluate("foo.NoSetter"));
TestUtil.AssertException(() => engine.Evaluate("foo.NoSetter = 789"));
Assert.AreEqual(456, engine.Evaluate("foo.PrivateSetter"));
TestUtil.AssertException(() => engine.Evaluate("foo.PrivateSetter = 789"));
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_V8RuntimeConstraintScale()
{
const int maxNewSpaceSize = 16;
const int maxOldSpaceSize = 512;
const double tolerance = .05;
var constraints = new V8RuntimeConstraints
{
MaxNewSpaceSize = maxNewSpaceSize,
MaxOldSpaceSize = maxOldSpaceSize
};
using (var tempEngine = new V8ScriptEngine(constraints))
{
Assert.AreEqual(Math.PI, tempEngine.Evaluate("Math.PI"));
var expected = Convert.ToDouble(maxNewSpaceSize * 2 + maxOldSpaceSize);
var actual = Convert.ToDouble(tempEngine.GetRuntimeHeapInfo().HeapSizeLimit / (1024 * 1024));
var ratio = actual / expected;
Assert.IsTrue((ratio >= 1 - tolerance) && (ratio <= 1 + tolerance));
}
constraints = new V8RuntimeConstraints
{
MaxNewSpaceSize = maxNewSpaceSize * 1024 * 1024,
MaxOldSpaceSize = maxOldSpaceSize * 1024 * 1024
};
using (var tempEngine = new V8ScriptEngine(constraints))
{
Assert.AreEqual(Math.E, tempEngine.Evaluate("Math.E"));
var expected = Convert.ToDouble(maxNewSpaceSize * 2 + maxOldSpaceSize);
var actual = Convert.ToDouble(tempEngine.GetRuntimeHeapInfo().HeapSizeLimit / (1024 * 1024));
var ratio = actual / expected;
Assert.IsTrue((ratio >= 1 - tolerance) && (ratio <= 1 + tolerance));
}
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_AssemblyHelpersClass()
{
Assert.IsTrue(typeof(AssemblyHelpers).IsStatic());
Assert.IsNull(typeof(AssemblyHelpers).TypeInitializer);
}
[TestMethod, TestCategory("BugFix")]
public void BugFix_FinalizerHang_V8ScriptItem()
{
engine.Script.foo = new Action