From 1c13218b4006ffb01f0cf617eb1a657cfe575f3e Mon Sep 17 00:00:00 2001 From: Taritsyn Date: Fri, 17 Mar 2023 17:51:39 +0300 Subject: [PATCH] Added a module based on the Topaz --- JavaScriptEngineSwitcher.NoSamples.sln | 11 +- global.json | 2 +- .../DefaultBuiltinObjectsInitializer.cs | 37 ++ .../JavaScriptEngineSwitcher.Topaz.csproj | 26 + .../JsEngineFactoryCollectionExtensions.cs | 79 +++ .../TopazJsEngine.cs | 539 ++++++++++++++++++ .../TopazJsEngineFactory.cs | 53 ++ .../TopazSettings.cs | 29 + .../HostObjectsEmbeddingBenchmark.cs | 16 + ...JavaScriptEngineSwitcher.Benchmarks.csproj | 4 + .../Interop/BundleTable.cs | 2 + .../Interop/Logging/DefaultLogger.cs | 1 + .../InteropTestsBase.cs | 24 +- .../JavaScriptEngineSwitcher.Tests.csproj | 4 + .../JsEngineSwitcherInitializer.cs | 6 + .../Topaz/CommonTests.cs | 269 +++++++++ .../Topaz/Es5Tests.cs | 91 +++ .../Topaz/InteropTests.cs | 482 ++++++++++++++++ .../Topaz/MultithreadingTests.cs | 12 + .../Topaz/PrecompilationTests.cs | 12 + 20 files changed, 1690 insertions(+), 9 deletions(-) create mode 100644 src/JavaScriptEngineSwitcher.Topaz/DefaultBuiltinObjectsInitializer.cs create mode 100644 src/JavaScriptEngineSwitcher.Topaz/JavaScriptEngineSwitcher.Topaz.csproj create mode 100644 src/JavaScriptEngineSwitcher.Topaz/JsEngineFactoryCollectionExtensions.cs create mode 100644 src/JavaScriptEngineSwitcher.Topaz/TopazJsEngine.cs create mode 100644 src/JavaScriptEngineSwitcher.Topaz/TopazJsEngineFactory.cs create mode 100644 src/JavaScriptEngineSwitcher.Topaz/TopazSettings.cs create mode 100644 test/JavaScriptEngineSwitcher.Tests/Topaz/CommonTests.cs create mode 100644 test/JavaScriptEngineSwitcher.Tests/Topaz/Es5Tests.cs create mode 100644 test/JavaScriptEngineSwitcher.Tests/Topaz/InteropTests.cs create mode 100644 test/JavaScriptEngineSwitcher.Tests/Topaz/MultithreadingTests.cs create mode 100644 test/JavaScriptEngineSwitcher.Tests/Topaz/PrecompilationTests.cs diff --git a/JavaScriptEngineSwitcher.NoSamples.sln b/JavaScriptEngineSwitcher.NoSamples.sln index f0274683..7e619876 100644 --- a/JavaScriptEngineSwitcher.NoSamples.sln +++ b/JavaScriptEngineSwitcher.NoSamples.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{19575E10-6B8E-4CF0-B7D2-898FFF47E157}" ProjectSection(SolutionItems) = preProject @@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaScriptEngineSwitcher.Ni EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaScriptEngineSwitcher.Node", "src\JavaScriptEngineSwitcher.Node\JavaScriptEngineSwitcher.Node.csproj", "{89F9DDDD-5236-4D9A-99E4-3C1358B81149}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaScriptEngineSwitcher.Topaz", "src\JavaScriptEngineSwitcher.Topaz\JavaScriptEngineSwitcher.Topaz.csproj", "{C5FB0DE1-496F-4A78-A6DC-448649E77172}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaScriptEngineSwitcher.Benchmarks", "test\JavaScriptEngineSwitcher.Benchmarks\JavaScriptEngineSwitcher.Benchmarks.csproj", "{24A8F6A6-EA4E-43A6-A2D7-E1916F8CB4EE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JavaScriptEngineSwitcher.Tests", "test\JavaScriptEngineSwitcher.Tests\JavaScriptEngineSwitcher.Tests.csproj", "{E95FDEF6-18A0-4E26-8FDF-B4B590E6EDAF}" @@ -185,6 +187,10 @@ Global {E95FDEF6-18A0-4E26-8FDF-B4B590E6EDAF}.Debug|Any CPU.Build.0 = Debug|Any CPU {E95FDEF6-18A0-4E26-8FDF-B4B590E6EDAF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E95FDEF6-18A0-4E26-8FDF-B4B590E6EDAF}.Release|Any CPU.Build.0 = Release|Any CPU + {C5FB0DE1-496F-4A78-A6DC-448649E77172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5FB0DE1-496F-4A78-A6DC-448649E77172}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5FB0DE1-496F-4A78-A6DC-448649E77172}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5FB0DE1-496F-4A78-A6DC-448649E77172}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -212,6 +218,7 @@ Global {89F9DDDD-5236-4D9A-99E4-3C1358B81149} = {0C281F46-F1D2-4A1C-8560-375EDA65D680} {24A8F6A6-EA4E-43A6-A2D7-E1916F8CB4EE} = {53B43213-2E66-42C2-8476-600A2FD2DA75} {E95FDEF6-18A0-4E26-8FDF-B4B590E6EDAF} = {53B43213-2E66-42C2-8476-600A2FD2DA75} + {C5FB0DE1-496F-4A78-A6DC-448649E77172} = {0C281F46-F1D2-4A1C-8560-375EDA65D680} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8184BE59-ACBC-4CD1-9419-D59A0FAC6131} diff --git a/global.json b/global.json index b1980314..8344741a 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "7.0.201" + "version": "7.0.202" } } diff --git a/src/JavaScriptEngineSwitcher.Topaz/DefaultBuiltinObjectsInitializer.cs b/src/JavaScriptEngineSwitcher.Topaz/DefaultBuiltinObjectsInitializer.cs new file mode 100644 index 00000000..158871eb --- /dev/null +++ b/src/JavaScriptEngineSwitcher.Topaz/DefaultBuiltinObjectsInitializer.cs @@ -0,0 +1,37 @@ +using IOriginalEngine = Tenray.Topaz.ITopazEngine; +using OriginalArray = Tenray.Topaz.API.JsArray; +using OriginalGlobalThis = Tenray.Topaz.API.GlobalThis; +using OriginalJSONObject = Tenray.Topaz.API.JSONObject; +using OriginalConcurrentObject = Tenray.Topaz.API.ConcurrentJsObject; +using OriginalObject = Tenray.Topaz.API.JsObject; +using OriginalUndefined = Tenray.Topaz.Undefined; +using OriginalVariableKind = Tenray.Topaz.VariableKind; + +namespace JavaScriptEngineSwitcher.Topaz +{ + /// + /// Default built-in objects initializer + /// + public static class DefaultBuiltinObjectsInitializer + { + /// + /// Initializes a built-in objects in the global scope + /// + /// Original JS engine + public static void Initialize(IOriginalEngine engine) + { + // Constants + engine.SetValueAndKind("Infinity", double.PositiveInfinity, OriginalVariableKind.Const); + engine.SetValueAndKind("NaN", double.NaN, OriginalVariableKind.Const); + engine.SetValueAndKind("undefined", OriginalUndefined.Value, OriginalVariableKind.Const); + + // Constructors + engine.AddType(engine.IsThreadSafe ? typeof(OriginalConcurrentObject) :typeof(OriginalObject), "Object"); + engine.AddType(typeof(OriginalArray), "Array"); + + // Objects + engine.SetValue("globalThis", new OriginalGlobalThis(engine.GlobalScope)); + engine.SetValue("JSON", new OriginalJSONObject()); + } + } +} \ No newline at end of file diff --git a/src/JavaScriptEngineSwitcher.Topaz/JavaScriptEngineSwitcher.Topaz.csproj b/src/JavaScriptEngineSwitcher.Topaz/JavaScriptEngineSwitcher.Topaz.csproj new file mode 100644 index 00000000..9cbb92a4 --- /dev/null +++ b/src/JavaScriptEngineSwitcher.Topaz/JavaScriptEngineSwitcher.Topaz.csproj @@ -0,0 +1,26 @@ + + + + JS Engine Switcher: Topaz + 3.20.10 + net6.0;net7.0 + Library + true + $(NoWarn);CS1591 + true + + + + + + JavaScriptEngineSwitcher.Topaz contains adapter `TopazJsEngine` (wrapper for the Topaz JavaScript Engine (https://github.com/koculu/Topaz) version 1.3.0). + $(PackageCommonTags);Topaz + + + + + + + + + \ No newline at end of file diff --git a/src/JavaScriptEngineSwitcher.Topaz/JsEngineFactoryCollectionExtensions.cs b/src/JavaScriptEngineSwitcher.Topaz/JsEngineFactoryCollectionExtensions.cs new file mode 100644 index 00000000..2c911f0e --- /dev/null +++ b/src/JavaScriptEngineSwitcher.Topaz/JsEngineFactoryCollectionExtensions.cs @@ -0,0 +1,79 @@ +using System; + +using JavaScriptEngineSwitcher.Core; + +namespace JavaScriptEngineSwitcher.Topaz +{ + /// + /// JS engine factory collection extensions + /// + public static class JsEngineFactoryCollectionExtensions + { + /// + /// Adds a instance of to + /// the specified + /// + /// Instance of + /// Instance of + public static JsEngineFactoryCollection AddTopaz(this JsEngineFactoryCollection source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + return source.AddTopaz(new TopazSettings()); + } + + /// + /// Adds a instance of to + /// the specified + /// + /// Instance of + /// The delegate to configure the provided + /// Instance of + public static JsEngineFactoryCollection AddTopaz(this JsEngineFactoryCollection source, + Action configure) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var settings = new TopazSettings(); + configure(settings); + + return source.AddTopaz(settings); + } + + /// + /// Adds a instance of to + /// the specified + /// + /// Instance of + /// Settings of the Topaz JS engine + /// Instance of + public static JsEngineFactoryCollection AddTopaz(this JsEngineFactoryCollection source, + TopazSettings settings) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + source.Add(new TopazJsEngineFactory(settings)); + + return source; + } + } +} \ No newline at end of file diff --git a/src/JavaScriptEngineSwitcher.Topaz/TopazJsEngine.cs b/src/JavaScriptEngineSwitcher.Topaz/TopazJsEngine.cs new file mode 100644 index 00000000..86ffe610 --- /dev/null +++ b/src/JavaScriptEngineSwitcher.Topaz/TopazJsEngine.cs @@ -0,0 +1,539 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; + +using OriginalAssignmentWithoutDefinitionBehavior = Tenray.Topaz.Options.AssignmentWithoutDefinitionBehavior; +using OriginalEngine = Tenray.Topaz.TopazEngine; +using OriginalEngineOptions = Tenray.Topaz.Options.TopazEngineOptions; +using OriginalEngineSetup = Tenray.Topaz.TopazEngineSetup; +using OriginalException = Tenray.Topaz.TopazException; +using OriginalParserException = Esprima.ParserException; +using OriginalUndefined = Tenray.Topaz.Undefined; +using OriginalVarScopeBehavior = Tenray.Topaz.Options.VarScopeBehavior; + +using JavaScriptEngineSwitcher.Core; +using JavaScriptEngineSwitcher.Core.Constants; +using JavaScriptEngineSwitcher.Core.Helpers; +using JavaScriptEngineSwitcher.Core.Utilities; + +using CoreStrings = JavaScriptEngineSwitcher.Core.Resources.Strings; +using WrapperCompilationException = JavaScriptEngineSwitcher.Core.JsCompilationException; +using WrapperException = JavaScriptEngineSwitcher.Core.JsException; +using WrapperInterruptedException = JavaScriptEngineSwitcher.Core.JsInterruptedException; +using WrapperRuntimeException = JavaScriptEngineSwitcher.Core.JsRuntimeException; +using WrapperScriptException = JavaScriptEngineSwitcher.Core.JsScriptException; + +namespace JavaScriptEngineSwitcher.Topaz +{ + /// + /// Adapter for the Topaz JS engine + /// + public sealed class TopazJsEngine : JsEngineBase + { + /// + /// Name of JS engine + /// + public const string EngineName = "TopazJsEngine"; + + /// + /// Version of original JS engine + /// + private const string EngineVersion = "1.3.0"; + + /// + /// Regular expression for working with the error message with type + /// + private static readonly Regex _errorMessageWithTypeRegex = + new Regex(@"^(?" + CommonRegExps.JsFullNamePattern + @"):\s+(?[\s\S]+?)$"); + + /// + /// Topaz JS engine + /// + private OriginalEngine _jsEngine; + + /// + /// Token source for canceling of script execution + /// + private CancellationTokenSource _cancellationTokenSource; + + + /// + /// Constructs an instance of adapter for the Topaz JS engine + /// + public TopazJsEngine() + : this(new TopazSettings()) + { } + + /// + /// Constructs an instance of adapter for the Topaz JS engine + /// + /// Settings of the Topaz JS engine + public TopazJsEngine(TopazSettings settings) + { + _cancellationTokenSource = new CancellationTokenSource(); + + TopazSettings topazSettings = settings ?? new TopazSettings(); + + try + { + _jsEngine = new OriginalEngine(new OriginalEngineSetup + { + Options = new OriginalEngineOptions + { + AllowNullReferenceMemberAccess = false, + AllowUndefinedReferenceAccess = true, + AllowUndefinedReferenceMemberAccess = false, + AssignmentWithoutDefinitionBehavior = OriginalAssignmentWithoutDefinitionBehavior.DefineAsVarInGlobalScope, + DefineSpecialArgumentsObjectOnEachFunctionCall = true, + NoUndefined = false, + VarScopeBehavior = OriginalVarScopeBehavior.FunctionScope, + UseThreadSafeJsObjects = true + } + }); + topazSettings.BuiltinObjectsInitializer?.Invoke(_jsEngine); + } + catch (Exception e) + { + throw JsErrorHelpers.WrapEngineLoadException(e, EngineName, EngineVersion, true); + } + } + + + #region Mapping + + /// + /// Makes a mapping of value from the host type to a script type + /// + /// The source value + /// The mapped value + private static object MapToScriptType(object value) + { + if (value is Undefined) + { + return OriginalUndefined.Value; + } + + return value; + } + + /// + /// Makes a mapping of array items from the host type to a script type + /// + /// The source array + /// The mapped array + private static object[] MapToScriptType(object[] args) + { + return args.Select(MapToScriptType).ToArray(); + } + + /// + /// Makes a mapping of value from the script type to a host type + /// + /// The source value + /// The mapped value + private static object MapToHostType(object value) + { + if (value is OriginalUndefined) + { + return Undefined.Value; + } + + return value; + } + + private static WrapperCompilationException WrapParserException(OriginalParserException originalParserException) + { + string description = originalParserException.Description; + string type = JsErrorType.Syntax; + int lineNumber = originalParserException.LineNumber; + int columnNumber = originalParserException.Column; + string message = JsErrorHelpers.GenerateScriptErrorMessage(type, description, string.Empty, lineNumber, + columnNumber); + + var wrapperCompilationException = new WrapperCompilationException(message, EngineName, EngineVersion, + originalParserException) + { + Description = description, + Type = type, + LineNumber = lineNumber, + ColumnNumber = columnNumber + }; + + return wrapperCompilationException; + } + + private static WrapperException WrapTopazException(OriginalException originalException) + { + WrapperScriptException wrapperScriptException; + string message = originalException.Message; + string description = message; + string type = string.Empty; + + Match messageWithTypeMatch = _errorMessageWithTypeRegex.Match(message); + if (messageWithTypeMatch.Success) + { + GroupCollection messageWithTypeGroups = messageWithTypeMatch.Groups; + type = messageWithTypeGroups["type"].Value; + description = messageWithTypeGroups["description"].Value; + } + else + { + type = JsErrorType.Common; + } + + message = JsErrorHelpers.GenerateScriptErrorMessage(type, description, string.Empty); + + if (type == JsErrorType.Syntax) + { + wrapperScriptException = new WrapperCompilationException(message, EngineName, EngineVersion, + originalException); + } + else + { + wrapperScriptException = new WrapperRuntimeException(message, EngineName, EngineVersion, + originalException); + } + + wrapperScriptException.Description = description; + wrapperScriptException.Type = type; + + return wrapperScriptException; + } + + private static WrapperInterruptedException WrapOperationCanceledException( + OperationCanceledException originalOperationCanceledException) + { + string message = CoreStrings.Runtime_ScriptInterrupted; + string description = message; + + var wrapperInterruptedException = new WrapperInterruptedException(message, EngineName, EngineVersion, + originalOperationCanceledException) + { + Description = description + }; + + return wrapperInterruptedException; + } + + private static WrapperException WrapTargetInvocationException(TargetInvocationException originalTargetInvocationException) + { + Exception innerException = originalTargetInvocationException.InnerException; + if (innerException == null) + { + return null; + } + + var wrapperException = innerException as WrapperException; + if (wrapperException == null) + { + wrapperException = new WrapperException(innerException.Message, EngineName, EngineVersion, + innerException) + { + Description = innerException.Message + }; + } + + return wrapperException; + } + + #endregion + + #region JsEngineBase overrides + + protected override IPrecompiledScript InnerPrecompile(string code) + { + throw new NotSupportedException(); + } + + protected override IPrecompiledScript InnerPrecompile(string code, string documentName) + { + throw new NotSupportedException(); + } + + protected override object InnerEvaluate(string expression) + { + return InnerEvaluate(expression, null); + } + + protected override object InnerEvaluate(string expression, string documentName) + { + object result; + + try + { + result = _jsEngine.ExecuteExpression(expression, _cancellationTokenSource.Token); + } + catch (OriginalParserException e) + { + throw WrapParserException(e); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + catch (OperationCanceledException e) + { + throw WrapOperationCanceledException(e); + } + catch (TargetInvocationException e) + { + var wrapperException = WrapTargetInvocationException(e); + if (wrapperException != null) + { + throw wrapperException; + } + + throw; + } + + result = MapToHostType(result); + + return result; + } + + protected override T InnerEvaluate(string expression) + { + return InnerEvaluate(expression, null); + } + + protected override T InnerEvaluate(string expression, string documentName) + { + object result = InnerEvaluate(expression, documentName); + + return TypeConverter.ConvertToType(result); + } + + protected override void InnerExecute(string code) + { + InnerExecute(code, null); + } + + protected override void InnerExecute(string code, string documentName) + { + try + { + _jsEngine.ExecuteScript(code, _cancellationTokenSource.Token); + } + catch (OriginalParserException e) + { + throw WrapParserException(e); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + catch (OperationCanceledException e) + { + throw WrapOperationCanceledException(e); + } + catch (TargetInvocationException e) + { + var wrapperException = WrapTargetInvocationException(e); + if (wrapperException != null) + { + throw wrapperException; + } + + throw; + } + } + + protected override void InnerExecute(IPrecompiledScript precompiledScript) + { + throw new NotSupportedException(); + } + + protected override object InnerCallFunction(string functionName, params object[] args) + { + object result; + object[] processedArgs = MapToScriptType(args); + + try + { + result = _jsEngine.InvokeFunction(functionName, _cancellationTokenSource.Token, processedArgs); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + catch (OperationCanceledException e) + { + throw WrapOperationCanceledException(e); + } + catch (TargetInvocationException e) + { + var wrapperException = WrapTargetInvocationException(e); + if (wrapperException != null) + { + throw wrapperException; + } + + throw; + } + + result = MapToHostType(result); + + return result; + } + + protected override T InnerCallFunction(string functionName, params object[] args) + { + object result = InnerCallFunction(functionName, args); + + return TypeConverter.ConvertToType(result); + } + + protected override bool InnerHasVariable(string variableName) + { + bool result; + + try + { + object variableValue = _jsEngine.GetValue(variableName); + result = variableValue != OriginalUndefined.Value; + } + catch (OriginalException) + { + result = false; + } + + return result; + } + + protected override object InnerGetVariableValue(string variableName) + { + object variableValue; + + try + { + variableValue = _jsEngine.GetValue(variableName); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + + object result = MapToHostType(variableValue); + + return result; + } + + protected override T InnerGetVariableValue(string variableName) + { + object result = InnerGetVariableValue(variableName); + + return TypeConverter.ConvertToType(result); + } + + protected override void InnerSetVariableValue(string variableName, object value) + { + object processedValue = MapToScriptType(value); + + try + { + _jsEngine.SetValue(variableName, processedValue); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + } + + protected override void InnerRemoveVariable(string variableName) + { + try + { + _jsEngine.SetValue(variableName, OriginalUndefined.Value); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + } + + protected override void InnerEmbedHostObject(string itemName, object value) + { + try + { + _jsEngine.SetValue(itemName, value); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + } + + protected override void InnerEmbedHostType(string itemName, Type type) + { + try + { + _jsEngine.AddType(type, itemName); + } + catch (OriginalException e) + { + throw WrapTopazException(e); + } + } + + protected override void InnerInterrupt() + { + _cancellationTokenSource.Cancel(); + } + + protected override void InnerCollectGarbage() + { + throw new NotSupportedException(); + } + + #region IJsEngine implementation + + public override string Name + { + get { return EngineName; } + } + + public override string Version + { + get { return EngineVersion; } + } + + public override bool SupportsScriptPrecompilation + { + get { return false; } + } + + public override bool SupportsScriptInterruption + { + get { return true; } + } + + public override bool SupportsGarbageCollection + { + get { return false; } + } + + #endregion + + #region IDisposable implementation + + public override void Dispose() + { + if (_disposedFlag.Set()) + { + _jsEngine = null; + + if (_cancellationTokenSource != null) + { + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + } + } + + #endregion + + #endregion + } +} \ No newline at end of file diff --git a/src/JavaScriptEngineSwitcher.Topaz/TopazJsEngineFactory.cs b/src/JavaScriptEngineSwitcher.Topaz/TopazJsEngineFactory.cs new file mode 100644 index 00000000..bdb942b8 --- /dev/null +++ b/src/JavaScriptEngineSwitcher.Topaz/TopazJsEngineFactory.cs @@ -0,0 +1,53 @@ +using JavaScriptEngineSwitcher.Core; + +namespace JavaScriptEngineSwitcher.Topaz +{ + /// + /// Topaz JS engine factory + /// + public sealed class TopazJsEngineFactory : IJsEngineFactory + { + /// + /// Settings of the Topaz JS engine + /// + private readonly TopazSettings _settings; + + + /// + /// Constructs an instance of the Topaz JS engine factory + /// + public TopazJsEngineFactory() + : this(new TopazSettings()) + { } + + /// + /// Constructs an instance of the Topaz JS engine factory + /// + /// Settings of the Topaz JS engine + public TopazJsEngineFactory(TopazSettings settings) + { + _settings = settings; + } + + + #region IJsEngineFactory implementation + + /// + public string EngineName + { + get { return TopazJsEngine.EngineName; } + } + + + /// + /// Creates a instance of the Topaz JS engine + /// + /// Instance of the Topaz JS engine + public IJsEngine CreateEngine() + { + return new TopazJsEngine(_settings); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/JavaScriptEngineSwitcher.Topaz/TopazSettings.cs b/src/JavaScriptEngineSwitcher.Topaz/TopazSettings.cs new file mode 100644 index 00000000..a81b10f0 --- /dev/null +++ b/src/JavaScriptEngineSwitcher.Topaz/TopazSettings.cs @@ -0,0 +1,29 @@ +using System; + +using IOriginalEngine = Tenray.Topaz.ITopazEngine; + +namespace JavaScriptEngineSwitcher.Topaz +{ + /// + /// Settings of the Topaz JS engine + /// + public sealed class TopazSettings + { + /// + /// Gets or sets a delegate invoked to initialize a built-in Javascript objects + /// + /// + /// When this property is set to null, the global scope remains empty. + /// + public Action BuiltinObjectsInitializer; + + + /// + /// Constructs an instance of the Topaz settings + /// + public TopazSettings() + { + BuiltinObjectsInitializer = DefaultBuiltinObjectsInitializer.Initialize; + } + } +} \ No newline at end of file diff --git a/test/JavaScriptEngineSwitcher.Benchmarks/HostObjectsEmbeddingBenchmark.cs b/test/JavaScriptEngineSwitcher.Benchmarks/HostObjectsEmbeddingBenchmark.cs index 0c0bf796..0b792e69 100644 --- a/test/JavaScriptEngineSwitcher.Benchmarks/HostObjectsEmbeddingBenchmark.cs +++ b/test/JavaScriptEngineSwitcher.Benchmarks/HostObjectsEmbeddingBenchmark.cs @@ -12,6 +12,11 @@ using JavaScriptEngineSwitcher.Msie; #if NET461 || NETCOREAPP3_1_OR_GREATER using JavaScriptEngineSwitcher.NiL; +#endif +#if NET6_0_OR_GREATER +using JavaScriptEngineSwitcher.Topaz; +#endif +#if NET461 || NETCOREAPP3_1_OR_GREATER using JavaScriptEngineSwitcher.V8; #endif @@ -169,6 +174,17 @@ public void NiL() Func createJsEngine = () => new NiLJsEngine(); EmbedAndUseHostObjects(createJsEngine); } +#endif +#if NET6_0_OR_GREATER + + [Benchmark] + public void Topaz() + { + Func createJsEngine = () => new TopazJsEngine(); + EmbedAndUseHostObjects(createJsEngine); + } +#endif +#if NET461 || NETCOREAPP3_1_OR_GREATER [Benchmark] [Arguments(false)] diff --git a/test/JavaScriptEngineSwitcher.Benchmarks/JavaScriptEngineSwitcher.Benchmarks.csproj b/test/JavaScriptEngineSwitcher.Benchmarks/JavaScriptEngineSwitcher.Benchmarks.csproj index e3d71d94..5dc91dd7 100644 --- a/test/JavaScriptEngineSwitcher.Benchmarks/JavaScriptEngineSwitcher.Benchmarks.csproj +++ b/test/JavaScriptEngineSwitcher.Benchmarks/JavaScriptEngineSwitcher.Benchmarks.csproj @@ -45,6 +45,10 @@ + + + + diff --git a/test/JavaScriptEngineSwitcher.Tests/Interop/BundleTable.cs b/test/JavaScriptEngineSwitcher.Tests/Interop/BundleTable.cs index 36673361..b9e477dd 100644 --- a/test/JavaScriptEngineSwitcher.Tests/Interop/BundleTable.cs +++ b/test/JavaScriptEngineSwitcher.Tests/Interop/BundleTable.cs @@ -4,6 +4,8 @@ public static class BundleTable { private static bool _enableOptimizations = true; + public static object SyncRoot = new object(); + public static bool EnableOptimizations { get diff --git a/test/JavaScriptEngineSwitcher.Tests/Interop/Logging/DefaultLogger.cs b/test/JavaScriptEngineSwitcher.Tests/Interop/Logging/DefaultLogger.cs index 18728c43..b4c8fdeb 100644 --- a/test/JavaScriptEngineSwitcher.Tests/Interop/Logging/DefaultLogger.cs +++ b/test/JavaScriptEngineSwitcher.Tests/Interop/Logging/DefaultLogger.cs @@ -2,6 +2,7 @@ { public class DefaultLogger { + public static object SyncRoot = new object(); public static ILogger Current = new NullLogger(); } } \ No newline at end of file diff --git a/test/JavaScriptEngineSwitcher.Tests/InteropTestsBase.cs b/test/JavaScriptEngineSwitcher.Tests/InteropTestsBase.cs index 0527daf2..4c7332a2 100644 --- a/test/JavaScriptEngineSwitcher.Tests/InteropTestsBase.cs +++ b/test/JavaScriptEngineSwitcher.Tests/InteropTestsBase.cs @@ -1123,7 +1123,9 @@ public virtual void EmbeddingOfCustomReferenceTypeWithField() // Arrange Type defaultLoggerType = typeof(DefaultLogger); Type throwExceptionLoggerType = typeof(ThrowExceptionLogger); - const string updateCode = "DefaultLogger.Current = new ThrowExceptionLogger();"; + const string updateCode = @"var oldLogger = DefaultLogger.Current; +DefaultLogger.Current = new ThrowExceptionLogger();"; + const string rollbackCode = "DefaultLogger.Current = oldLogger;"; const string input = "DefaultLogger.Current.ToString()"; const string targetOutput = "[throw exception logger]"; @@ -1135,9 +1137,13 @@ public virtual void EmbeddingOfCustomReferenceTypeWithField() { jsEngine.EmbedHostType("DefaultLogger", defaultLoggerType); jsEngine.EmbedHostType("ThrowExceptionLogger", throwExceptionLoggerType); - jsEngine.Execute(updateCode); - output = jsEngine.Evaluate(input); + lock (DefaultLogger.SyncRoot) + { + jsEngine.Execute(updateCode); + output = jsEngine.Evaluate(input); + jsEngine.Execute(rollbackCode); + } } // Assert @@ -1230,7 +1236,9 @@ public virtual void EmbeddingOfCustomReferenceTypeWithProperty() { // Arrange Type bundleTableType = typeof(BundleTable); - const string updateCode = "BundleTable.EnableOptimizations = false;"; + const string updateCode = @"var oldEnableOptimizationsValue = BundleTable.EnableOptimizations; +BundleTable.EnableOptimizations = false;"; + const string rollbackCode = "BundleTable.EnableOptimizations = oldEnableOptimizationsValue;"; const string input = "BundleTable.EnableOptimizations"; const bool targetOutput = false; @@ -1241,9 +1249,13 @@ public virtual void EmbeddingOfCustomReferenceTypeWithProperty() using (var jsEngine = CreateJsEngine()) { jsEngine.EmbedHostType("BundleTable", bundleTableType); - jsEngine.Execute(updateCode); - output = jsEngine.Evaluate(input); + lock (BundleTable.SyncRoot) + { + jsEngine.Execute(updateCode); + output = jsEngine.Evaluate(input); + jsEngine.Execute(rollbackCode); + } } // Assert diff --git a/test/JavaScriptEngineSwitcher.Tests/JavaScriptEngineSwitcher.Tests.csproj b/test/JavaScriptEngineSwitcher.Tests/JavaScriptEngineSwitcher.Tests.csproj index d623ee74..da95b050 100644 --- a/test/JavaScriptEngineSwitcher.Tests/JavaScriptEngineSwitcher.Tests.csproj +++ b/test/JavaScriptEngineSwitcher.Tests/JavaScriptEngineSwitcher.Tests.csproj @@ -92,6 +92,10 @@ + + + + diff --git a/test/JavaScriptEngineSwitcher.Tests/JsEngineSwitcherInitializer.cs b/test/JavaScriptEngineSwitcher.Tests/JsEngineSwitcherInitializer.cs index f10dcb87..2ef72a4c 100644 --- a/test/JavaScriptEngineSwitcher.Tests/JsEngineSwitcherInitializer.cs +++ b/test/JavaScriptEngineSwitcher.Tests/JsEngineSwitcherInitializer.cs @@ -16,6 +16,9 @@ #if !NET452 using JavaScriptEngineSwitcher.Node; #endif +#if NET6_0_OR_GREATER +using JavaScriptEngineSwitcher.Topaz; +#endif #if NETFRAMEWORK || NETCOREAPP3_1_OR_GREATER using JavaScriptEngineSwitcher.V8; #endif @@ -52,6 +55,9 @@ public static void Initialize() #if !NET452 .AddNode() #endif +#if NET6_0_OR_GREATER + .AddTopaz() +#endif #if NETFRAMEWORK || NETCOREAPP3_1_OR_GREATER .AddV8() #endif diff --git a/test/JavaScriptEngineSwitcher.Tests/Topaz/CommonTests.cs b/test/JavaScriptEngineSwitcher.Tests/Topaz/CommonTests.cs new file mode 100644 index 00000000..2cd3d03c --- /dev/null +++ b/test/JavaScriptEngineSwitcher.Tests/Topaz/CommonTests.cs @@ -0,0 +1,269 @@ +#if NET6_0_OR_GREATER +using System; + +using Xunit; + +using JavaScriptEngineSwitcher.Core; + +namespace JavaScriptEngineSwitcher.Tests.Topaz +{ + public class CommonTests : CommonTestsBase + { + protected override string EngineName + { + get { return "TopazJsEngine"; } + } + + #region Evaluation of scripts + + [Fact] + public override void EvaluationOfExpressionWithBooleanResult() + { } + + [Fact] + public override void EvaluationOfExpressionWithDoubleResult() + { + // Math object not implemented + } + + #endregion + + #region Error handling + + #region Mapping of errors + + [Fact] + public void MappingCompilationErrorDuringEvaluationOfExpression() + { + // Arrange + const string inputCode = @"var $variable1 = 611; +var _variable2 = 711; +var variable3 = 678;"; + const string inputExpression = "$variable1 + _variable2 - @variable3;"; + + JsCompilationException exception = null; + + // Act + using (var jsEngine = CreateJsEngine()) + { + try + { + jsEngine.Execute(inputCode); + int result = jsEngine.Evaluate(inputExpression, "variables.js"); + } + catch (JsCompilationException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Compilation error", exception.Category); + Assert.Equal("Unexpected token ILLEGAL", exception.Description); + Assert.Equal("SyntaxError", exception.Type); + Assert.Empty(exception.DocumentName); + Assert.Equal(1, exception.LineNumber); + Assert.Equal(27, exception.ColumnNumber); + Assert.Empty(exception.SourceFragment); + } + + [Fact] + public void MappingRuntimeErrorDuringEvaluationOfExpression() + { + // Arrange + const string inputCode = @"var $variable1 = 611; +var _variable2 = 711; +var variable3 = 678;"; + const string inputExpression = @"$variable1 + _variable2() - variable3;"; + + JsRuntimeException exception = null; + + // Act + using (var jsEngine = CreateJsEngine()) + { + try + { + jsEngine.Execute(inputCode); + int result = jsEngine.Evaluate(inputExpression, "variables.js"); + } + catch (JsRuntimeException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Runtime error", exception.Category); + Assert.Equal("Can not call 711 as a function.", exception.Description); + Assert.Equal("Error", exception.Type); + Assert.Empty(exception.DocumentName); + Assert.Equal(0, exception.LineNumber); + Assert.Equal(0, exception.ColumnNumber); + Assert.Empty(exception.SourceFragment); + Assert.Empty(exception.CallStack); + } + + [Fact] + public void MappingCompilationErrorDuringExecutionOfCode() + { + // Arrange + const string input = @"function factorial(value) { + if (value <= 0) { + throw new Error(""The value must be greater than or equal to zero.""); + } + + return value !== 1 ? value * factorial(value - 1) : 1; +} + +factorial(5); +factorial(2%); +factorial(0);"; + + JsCompilationException exception = null; + + // Act + using (var jsEngine = CreateJsEngine()) + { + try + { + jsEngine.Execute(input, "factorial.js"); + } + catch (JsCompilationException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Compilation error", exception.Category); + Assert.Equal("Unexpected token )", exception.Description); + Assert.Equal("SyntaxError", exception.Type); + Assert.Empty(exception.DocumentName); + Assert.Equal(10, exception.LineNumber); + Assert.Equal(13, exception.ColumnNumber); + Assert.Empty(exception.SourceFragment); + } + + [Fact] + public void MappingRuntimeErrorDuringExecutionOfCode() + { + // Arrange + const string input = @"function factorial(value) { + if (value <= 0) { + throw new Error(""The value must be greater than or equal to zero.""); + } + + return value !== 1 ? value * factorial(value - 1) : 1; +} + +factorial(5); +factorial(-1); +factorial(0);"; + + JsRuntimeException exception = null; + + // Act + using (var jsEngine = CreateJsEngine()) + { + try + { + jsEngine.Execute(input, "factorial.js"); + } + catch (JsRuntimeException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Runtime error", exception.Category); + Assert.Equal("Constructor call on undefined is not defined.", exception.Description); + Assert.Equal("Error", exception.Type); + Assert.Empty(exception.DocumentName); + Assert.Equal(0, exception.LineNumber); + Assert.Equal(0, exception.ColumnNumber); + Assert.Empty(exception.SourceFragment); + Assert.Empty(exception.CallStack); + } + + #endregion + + #region Generation of error messages + + [Fact] + public void GenerationOfCompilationErrorMessage() + { + // Arrange + const string input = @"var arr = []; +var obj = {}; +var foo = 'Browser's bar';"; + string targetOutput = "SyntaxError: Unexpected identifier" + Environment.NewLine + + " at 3:20"; + + JsCompilationException exception = null; + + // Act + using (var jsEngine = CreateJsEngine()) + { + try + { + jsEngine.Execute(input, "variables.js"); + } + catch (JsCompilationException e) + { + exception = e; + } + } + + Assert.NotNull(exception); + Assert.Equal(targetOutput, exception.Message); + } + + [Fact] + public void GenerationOfRuntimeErrorMessage() + { + // Arrange + const string input = @"function foo(x, y) { + var z = x + y; + if (z > 20) { + bar(); + } +} + +(function (foo) { + var a = 8; + var b = 15; + + foo(a, b); +})(foo);"; + string targetOutput = "Error: Can not call undefined as a function."; + + JsRuntimeException exception = null; + + // Act + using (var jsEngine = CreateJsEngine()) + { + try + { + jsEngine.Execute(input, "functions.js"); + } + catch (JsRuntimeException e) + { + exception = e; + } + } + + Assert.NotNull(exception); + Assert.Equal(targetOutput, exception.Message); + } + + #endregion + + #endregion + } +} +#endif \ No newline at end of file diff --git a/test/JavaScriptEngineSwitcher.Tests/Topaz/Es5Tests.cs b/test/JavaScriptEngineSwitcher.Tests/Topaz/Es5Tests.cs new file mode 100644 index 00000000..1e4cec2f --- /dev/null +++ b/test/JavaScriptEngineSwitcher.Tests/Topaz/Es5Tests.cs @@ -0,0 +1,91 @@ +#if NET6_0_OR_GREATER +using System; + +using Xunit; + +using JavaScriptEngineSwitcher.Core; + +namespace JavaScriptEngineSwitcher.Tests.Topaz +{ + public class Es5Tests : Es5TestsBase + { + protected override string EngineName + { + get { return "TopazJsEngine"; } + } + + + #region Array methods + + [Fact] + public override void SupportsArrayEveryMethod() + { } + + [Fact] + public override void SupportsArrayFilterMethod() + { } + + [Fact] + public override void SupportsArrayIndexOfMethod() + { } + + [Fact] + public override void SupportsArrayIsArrayMethod() + { } + + [Fact] + public override void SupportsArrayLastIndexOfMethod() + { } + + [Fact] + public override void SupportsArraySomeMethod() + { } + + #endregion + + #region Date methods + + [Fact] + public override void SupportsDateNowMethod() + { } + + [Fact] + public override void SupportsDateToIsoStringMethod() + { } + + #endregion + + #region Function methods + + [Fact] + public override void SupportsFunctionBindMethod() + { } + + #endregion + + #region Object methods + + [Fact] + public override void SupportsObjectCreateMethod() + { } + + [Fact] + public override void SupportsObjectKeysMethod() + { } + + #endregion + + #region String methods + + [Fact] + public override void SupportsStringSplitMethod() + { } + + [Fact] + public override void SupportsStringTrimMethod() + { } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/test/JavaScriptEngineSwitcher.Tests/Topaz/InteropTests.cs b/test/JavaScriptEngineSwitcher.Tests/Topaz/InteropTests.cs new file mode 100644 index 00000000..3545480d --- /dev/null +++ b/test/JavaScriptEngineSwitcher.Tests/Topaz/InteropTests.cs @@ -0,0 +1,482 @@ +#if NET6_0_OR_GREATER +using System; +using System.IO; + +using Xunit; + +using JavaScriptEngineSwitcher.Core; +using JavaScriptEngineSwitcher.Tests.Interop; +using JavaScriptEngineSwitcher.Tests.Interop.Animals; +using JavaScriptEngineSwitcher.Tests.Interop.Logging; + +namespace JavaScriptEngineSwitcher.Tests.Topaz +{ + public class InteropTests : InteropTestsBase + { + protected override string EngineName + { + get { return "TopazJsEngine"; } + } + + + #region Embedding of objects + + #region Objects with properties + + [Fact] + public override void EmbeddingOfInstanceOfCustomReferenceTypeWithProperties() + { + // Arrange + var person = new Person("Vanya", "Ivanov"); + const string updateCode = @"person.LastName = 'Ivanoff'; +person.Patronymic = null;"; + + const string input1 = "person.FirstName"; + const string targetOutput1 = "Vanya"; + + const string input2 = "person.LastName"; + const string targetOutput2 = "Ivanoff"; + + // Act + string output1; + string output2; + + using (var jsEngine = CreateJsEngine()) + { + jsEngine.EmbedHostObject("person", person); + jsEngine.Execute(updateCode); + + output1 = jsEngine.Evaluate(input1); + output2 = jsEngine.Evaluate(input2); + } + + // Assert + Assert.Equal(targetOutput1, output1); + Assert.Equal(targetOutput2, output2); + } + + #endregion + + #region Objects with methods + + [Fact] + public override void EmbeddingOfInstanceOfCustomValueTypeWithMethods() + { + // Arrange + var programmerDayDate = new Date(2015, 9, 13); + + const string input1 = "programmerDay.GetDayOfYear()"; + const int targetOutput1 = 256; + + const string input2 = @"programmerDay.AddDays(6).GetDayOfYear();"; + const int targetOutput2 = 262; + + // Act + int output1; + int output2; + + using (var jsEngine = CreateJsEngine()) + { + jsEngine.EmbedHostObject("programmerDay", programmerDayDate); + output1 = jsEngine.Evaluate(input1); + output2 = jsEngine.Evaluate(input2); + } + + // Assert + Assert.Equal(targetOutput1, output1); + Assert.Equal(targetOutput2, output2); + } + + #endregion + + #region Delegates + + [Fact] + public override void EmbeddingOfInstanceOfDelegateAndCheckingItsPrototype() + { } + + [Fact] + public override void EmbeddingOfInstanceOfDelegateAndCallingItWithExtraParameter() + { + // Arrange + var sumFunc = new Func((a, b) => a + b); + + const string input = "sum(678, 711, 611)"; + + // Act + JsRuntimeException exception = null; + + using (var jsEngine = CreateJsEngine()) + { + try + { + jsEngine.EmbedHostObject("sum", sumFunc); + jsEngine.Evaluate(input); + } + catch (JsRuntimeException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Runtime error", exception.Category); + Assert.Equal( + "Type Error: Delegate method call argument mismatch. delegate(678, 711, 611)", + exception.Description + ); + } + + [Fact] + public override void EmbeddingOfInstanceOfDelegateAndGettingItsMethodProperty() + { + // Arrange + var cat = new Cat(); + var cryFunc = new Func(cat.Cry); + + const string input = "cry.Method;"; + string targetOutput = "System.String Cry()"; + + // Act + string output; + + using (var jsEngine = CreateJsEngine()) + { + jsEngine.EmbedHostObject("cry", cryFunc); + output = jsEngine.Evaluate(input); + } + + // Assert + Assert.Equal(targetOutput, output); + } + + #endregion + + #region Recursive calls + + #region Mapping of errors + + [Fact] + public void MappingCompilationErrorDuringRecursiveEvaluationOfFiles() + { + // Arrange + const string directoryPath = "Files/recursive-evaluation/compilation-error"; + const string input = "evaluateFile('index').calculateResult();"; + + // Act + JsCompilationException exception = null; + + using (var jsEngine = CreateJsEngine()) + { + try + { + Func evaluateFile = path => { + string absolutePath = Path.Combine(directoryPath, $"{path}.js"); + string code = File.ReadAllText(absolutePath); + object result = jsEngine.Evaluate(code, absolutePath); + + return result; + }; + + jsEngine.EmbedHostObject("evaluateFile", evaluateFile); + double output = jsEngine.Evaluate(input); + } + catch (JsCompilationException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Compilation error", exception.Category); + Assert.Equal("Unexpected token ,", exception.Description); + Assert.Equal("SyntaxError", exception.Type); + Assert.Empty(exception.DocumentName); + Assert.Equal(25, exception.LineNumber); + Assert.Equal(11, exception.ColumnNumber); + Assert.Empty(exception.SourceFragment); + } + + [Fact] + public void MappingRuntimeErrorDuringRecursiveEvaluationOfFiles() + { + // Arrange + const string directoryPath = "Files/recursive-evaluation/runtime-error"; + const string input = "evaluateFile('index').calculateResult();"; + + // Act + JsRuntimeException exception = null; + + using (var jsEngine = CreateJsEngine()) + { + try + { + Func evaluateFile = path => { + string absolutePath = Path.Combine(directoryPath, $"{path}.js"); + string code = File.ReadAllText(absolutePath); + object result = jsEngine.Evaluate(code, absolutePath); + + return result; + }; + + jsEngine.EmbedHostObject("evaluateFile", evaluateFile); + double output = jsEngine.Evaluate(input); + } + catch (JsRuntimeException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Runtime error", exception.Category); + Assert.Equal("argumens is not defined", exception.Description); + Assert.Equal("ReferenceError", exception.Type); + Assert.Empty(exception.DocumentName); + Assert.Equal(0, exception.LineNumber); + Assert.Equal(0, exception.ColumnNumber); + Assert.Empty(exception.SourceFragment); + Assert.Empty(exception.CallStack); + } + + [Fact] + public void MappingHostErrorDuringRecursiveEvaluationOfFiles() + { + // Arrange + const string directoryPath = "Files/recursive-evaluation/host-error"; + const string input = "evaluateFile('index').calculateResult();"; + + // Act + JsException exception = null; + + using (var jsEngine = CreateJsEngine()) + { + try + { + Func evaluateFile = path => { + string absolutePath = Path.Combine(directoryPath, $"{path}.js"); + string code = File.ReadAllText(absolutePath); + object result = jsEngine.Evaluate(code, absolutePath); + + return result; + }; + + jsEngine.EmbedHostObject("evaluateFile", evaluateFile); + double output = jsEngine.Evaluate(input); + } + catch (JsException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Unknown error", exception.Category); + Assert.StartsWith("Could not find file ", exception.Description); + } + + [Fact] + public void MappingCompilationErrorDuringRecursiveExecutionOfFiles() + { + // Arrange + const string directoryPath = "Files/recursive-execution/compilation-error"; + const string variableName = "num"; + + // Act + JsCompilationException exception = null; + + using (var jsEngine = CreateJsEngine()) + { + try + { + Action executeFile = path => jsEngine.ExecuteFile(path); + + jsEngine.SetVariableValue("directoryPath", directoryPath); + jsEngine.EmbedHostObject("executeFile", executeFile); + jsEngine.ExecuteFile(Path.Combine(directoryPath, "main-file.js")); + + int output = jsEngine.GetVariableValue(variableName); + } + catch (JsCompilationException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Compilation error", exception.Category); + Assert.Equal("Unexpected token ILLEGAL", exception.Description); + Assert.Equal("SyntaxError", exception.Type); + Assert.Empty(exception.DocumentName); + Assert.Equal(1, exception.LineNumber); + Assert.Equal(6, exception.ColumnNumber); + Assert.Empty(exception.SourceFragment); + } + + [Fact] + public void MappingRuntimeErrorDuringRecursiveExecutionOfFiles() + { + // Arrange + const string directoryPath = "Files/recursive-execution/runtime-error"; + const string variableName = "num"; + + // Act + int output; + + using (var jsEngine = CreateJsEngine()) + { + Action executeFile = path => jsEngine.ExecuteFile(path); + + jsEngine.SetVariableValue("directoryPath", directoryPath); + jsEngine.EmbedHostObject("executeFile", executeFile); + jsEngine.ExecuteFile(Path.Combine(directoryPath, "main-file.js")); + + output = jsEngine.GetVariableValue(variableName); + } + + // Assert + Assert.Equal(15, output); + } + + [Fact] + public void MappingHostErrorDuringRecursiveExecutionOfFiles() + { + // Arrange + const string directoryPath = "Files/recursive-execution/host-error"; + const string variableName = "num"; + + // Act + JsException exception = null; + + using (var jsEngine = CreateJsEngine()) + { + try + { + Action executeFile = path => jsEngine.ExecuteFile(path); + + jsEngine.SetVariableValue("directoryPath", directoryPath); + jsEngine.EmbedHostObject("executeFile", executeFile); + jsEngine.ExecuteFile(Path.Combine(directoryPath, "main-file.js")); + + int output = jsEngine.GetVariableValue(variableName); + } + catch (JsException e) + { + exception = e; + } + } + + // Assert + Assert.NotNull(exception); + Assert.Equal("Unknown error", exception.Category); + Assert.StartsWith("File ", exception.Description); + } + + #endregion + + #endregion + + #endregion + + + #region Embedding of types + + #region Creating of instances + + [Fact] + public override void CreatingAnInstanceOfEmbeddedBuiltinReferenceType() + { + // Arrange + Type uriType = typeof(Uri); + + const string inputCode = @"var baseUri = new Uri('https://github.com'), + relativeUri = 'Taritsyn/MsieJavaScriptEngine' + ;"; + const string inputExpression = @"(new Uri(baseUri, relativeUri)).ToString()"; + const string targetOutput = "https://github.com/Taritsyn/MsieJavaScriptEngine"; + + // Act + string output; + + using (var jsEngine = CreateJsEngine()) + { + jsEngine.EmbedHostType("Uri", uriType); + jsEngine.Execute(inputCode); + output = jsEngine.Evaluate(inputExpression); + } + + // Assert + Assert.Equal(targetOutput, output); + } + + #endregion + + #region Types with fields + + [Fact] + public override void EmbeddingOfCustomReferenceTypeWithField() + { + // Arrange + Type defaultLoggerType = typeof(DefaultLogger); + + const string input = "DefaultLogger.Current.ToString()"; + const string targetOutput = "[null logger]"; + + // Act + string output; + + using (var jsEngine = CreateJsEngine()) + { + jsEngine.EmbedHostType("DefaultLogger", defaultLoggerType); + + lock (DefaultLogger.SyncRoot) + { + output = jsEngine.Evaluate(input); + } + } + + // Assert + Assert.Equal(targetOutput, output); + } + + #endregion + + #region Types with properties + + [Fact] + public override void EmbeddingOfCustomReferenceTypeWithProperty() + { + // Arrange + Type bundleTableType = typeof(BundleTable); + + const string input = "BundleTable.EnableOptimizations"; + const bool targetOutput = true; + + // Act + bool output; + + using (var jsEngine = CreateJsEngine()) + { + jsEngine.EmbedHostType("BundleTable", bundleTableType); + + lock (BundleTable.SyncRoot) + { + output = jsEngine.Evaluate(input); + } + } + + // Assert + Assert.Equal(targetOutput, output); + } + + #endregion + + #endregion + } +} +#endif \ No newline at end of file diff --git a/test/JavaScriptEngineSwitcher.Tests/Topaz/MultithreadingTests.cs b/test/JavaScriptEngineSwitcher.Tests/Topaz/MultithreadingTests.cs new file mode 100644 index 00000000..0da06425 --- /dev/null +++ b/test/JavaScriptEngineSwitcher.Tests/Topaz/MultithreadingTests.cs @@ -0,0 +1,12 @@ +#if NET6_0_OR_GREATER +namespace JavaScriptEngineSwitcher.Tests.Topaz +{ + public class MultithreadingTests : MultithreadingTestsBase + { + protected override string EngineName + { + get { return "TopazJsEngine"; } + } + } +} +#endif \ No newline at end of file diff --git a/test/JavaScriptEngineSwitcher.Tests/Topaz/PrecompilationTests.cs b/test/JavaScriptEngineSwitcher.Tests/Topaz/PrecompilationTests.cs new file mode 100644 index 00000000..6b11d628 --- /dev/null +++ b/test/JavaScriptEngineSwitcher.Tests/Topaz/PrecompilationTests.cs @@ -0,0 +1,12 @@ +#if NET6_0_OR_GREATER +namespace JavaScriptEngineSwitcher.Tests.Topaz +{ + public class PrecompilationTests : PrecompilationTestsBase + { + protected override string EngineName + { + get { return "TopazJsEngine"; } + } + } +} +#endif \ No newline at end of file