From 30ecd70b9d6132e49dc6ab793e0724fbf68a216c Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Mon, 2 Dec 2019 16:04:06 +0800 Subject: [PATCH 01/11] Improved API - Added module factory overloads that atomically attempt to invoke from cache/cache if unable to invoke from cache. - Added overloads that don't return any values. --- src/NodeJS/INodeJSService.cs | 227 +++++++++++++++++++++++++++++------ 1 file changed, 193 insertions(+), 34 deletions(-) diff --git a/src/NodeJS/INodeJSService.cs b/src/NodeJS/INodeJSService.cs index 1ac591c..2464719 100644 --- a/src/NodeJS/INodeJSService.cs +++ b/src/NodeJS/INodeJSService.cs @@ -11,73 +11,232 @@ namespace Jering.Javascript.NodeJS public interface INodeJSService : IDisposable { /// - /// Invokes a function exported by a NodeJS module on disk. + /// Invokes a function from a NodeJS module on disk. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// NodeJS caches the module using the module's path as cache identifier. This means subsequent invocations won't reread and recompile the module. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The path to the module (i.e., JavaScript file) relative to . - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The path to the module relative to . This value mustn't be null, whitespace or an empty string. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null, whitespace or an empty string. /// Thrown if NodeJS cannot be initialized. /// Thrown if the invocation request times out. /// Thrown if a NodeJS error occurs. - /// Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. /// Thrown if is cancelled. Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); /// - /// Invokes a function exported by a NodeJS module in string form. + /// Invokes a function from a NodeJS module on disk. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// NodeJS caches the module using the module's path as cache identifier. This means subsequent invocations won't re-read and re-compile the module. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The module in string form. - /// The module's cache identifier in the NodeJS module cache. If unspecified, the module will not be cached. - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The path to the module relative to . This value mustn't be null, whitespace or an empty string. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. + /// Thrown if is null, whitespace or an empty string. /// Thrown if NodeJS cannot be initialized. /// Thrown if the invocation request times out. /// Thrown if a NodeJS error occurs. - /// Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Invokes a function from a NodeJS module in string form. + /// If is null, the module string is sent to NodeJS and compiled for one time use. + /// If isn't null, the module string and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module string is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module string to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The module in string form. This value mustn't be null, whitespace or an empty string. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. /// Thrown if is cancelled. Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); /// - /// Invokes a function exported by a NodeJS module in Stream form. + /// Invokes a function from a NodeJS module in string form. + /// If is null, the module string is sent to NodeJS and compiled for one time use. + /// If isn't null, the module string and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module string is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module string to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The module in string form. This value mustn't be null, whitespace or an empty string. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Invokes a function from a NodeJS module in string form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module string using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The factory that creates the module string. This value mustn't be null and it mustn't return null, whitespace or an empty string. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Invokes a function from a NodeJS module in string form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module string using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The factory that creates the module string. This value mustn't be null and it mustn't return null, whitespace or an empty string. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Invokes a function from a NodeJS module in stream form. + /// If is null, the module stream is sent to NodeJS and compiled for one time use. + /// If isn't null, the module stream and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module stream is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module stream to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The module in Stream form. - /// The module's cache identifier in the NodeJS module cache. If unspecified, the module will not be cached. - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The module in stream form. This value mustn't be null. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null. /// Thrown if NodeJS cannot be initialized. /// Thrown if the invocation request times out. /// Thrown if a NodeJS error occurs. - /// Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. /// Thrown if is cancelled. Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); /// - /// Attempts to invoke a function exported by a NodeJS module cached by NodeJS. + /// Invokes a function from a NodeJS module in stream form. + /// If is null, the module stream is sent to NodeJS and compiled for one time use. + /// If isn't null, the module stream and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module stream is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module stream to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The module in stream form. This value mustn't be null. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Invokes a function from a NodeJS module in stream form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module stream using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The factory that creates the module stream. This value mustn't be null and it mustn't return null. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Invokes a function from a NodeJS module in stream form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module stream using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The factory that creates the module stream. This value mustn't be null and it mustn't return null. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Attempts to invoke a function from a module in NodeJS's cache. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The cache identifier of the module. - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. On completion, the task returns a (bool, T) with the bool set to true on + /// The representing the asynchronous operation. On completion, the task returns a (bool, T) with the bool set to true on /// success and false otherwise. + /// Thrown if is null. /// Thrown if NodeJS cannot be initialized. /// Thrown if the invocation request times out. /// Thrown if a NodeJS error occurs. - /// Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. /// Thrown if is cancelled. Task<(bool, T)> TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); } From 1ef49cedae672cdb920644f04c03b13a8bed6c78 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Mon, 2 Dec 2019 16:04:18 +0800 Subject: [PATCH 02/11] Updated benchmarks to use new API. --- perf/NodeJS/ConcurrencyBenchmarks.cs | 20 +++++++++---------- perf/NodeJS/LatencyBenchmarks.cs | 28 +++++++++++++++------------ perf/NodeJS/RealWorkloadBenchmarks.cs | 18 +++++++++++------ src/NodeJS/INodeJSService.cs | 16 +++++++++++++++ 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/perf/NodeJS/ConcurrencyBenchmarks.cs b/perf/NodeJS/ConcurrencyBenchmarks.cs index cdd83cd..69a7a29 100644 --- a/perf/NodeJS/ConcurrencyBenchmarks.cs +++ b/perf/NodeJS/ConcurrencyBenchmarks.cs @@ -8,11 +8,11 @@ namespace Jering.Javascript.NodeJS.Performance { - // TODO after adding invoke method with no return value (analagous to execute method in js engines), remove string type parameters [MemoryDiagnoser] public class ConcurrencyBenchmarks { - private const string DUMMY_CONCURRENCY_MODULE = "dummyConcurrencyModule.js"; + private const string DUMMY_WARMUP_MODULE = "module.exports = (callback) => callback()"; + private const string DUMMY_CONCURRENCY_MODULE_FILE = "dummyConcurrencyModule.js"; private ServiceProvider _serviceProvider; private INodeJSService _nodeJSService; @@ -29,11 +29,11 @@ public void INodeJSService_Concurrency_MultiProcess_Setup() _serviceProvider = services.BuildServiceProvider(); _nodeJSService = _serviceProvider.GetRequiredService(); - // Warm up. First few runs start Node.js processes, so they take longer. If we don't manually warm up, BenchmarkDotNet erroneously complains + // Warmup. First few runs start Node.js processes, so they take longer. If we don't manually warm up, BenchmarkDotNet erroneously complains // about iteration time being too low for (int i = 0; i < Environment.ProcessorCount; i++) { - _nodeJSService.InvokeFromStringAsync("module.exports = (callback) => callback(null, null)", "warmup").GetAwaiter().GetResult(); + _nodeJSService.InvokeFromStringAsync(DUMMY_WARMUP_MODULE).GetAwaiter().GetResult(); } } @@ -45,7 +45,7 @@ public async Task INodeJSService_Concurrency_MultiProcess() var results = new Task[numTasks]; for (int i = 0; i < numTasks; i++) { - results[i] = _nodeJSService.InvokeFromFileAsync(DUMMY_CONCURRENCY_MODULE); + results[i] = _nodeJSService.InvokeFromFileAsync(DUMMY_CONCURRENCY_MODULE_FILE); } return await Task.WhenAll(results); @@ -60,8 +60,8 @@ public void INodeJSService_Concurrency_None_Setup() _serviceProvider = services.BuildServiceProvider(); _nodeJSService = _serviceProvider.GetRequiredService(); - // Warm up. First run starts a Node.js processes. - _nodeJSService.InvokeFromStringAsync("module.exports = (callback) => callback(null, null)", "warmup").GetAwaiter().GetResult(); + // Warmup. First run starts a Node.js processes. + _nodeJSService.InvokeFromStringAsync(DUMMY_WARMUP_MODULE).GetAwaiter().GetResult(); } [Benchmark] @@ -72,7 +72,7 @@ public async Task INodeJSService_Concurrency_None() var results = new Task[numTasks]; for (int i = 0; i < numTasks; i++) { - results[i] = _nodeJSService.InvokeFromFileAsync(DUMMY_CONCURRENCY_MODULE); + results[i] = _nodeJSService.InvokeFromFileAsync(DUMMY_CONCURRENCY_MODULE_FILE); } return await Task.WhenAll(results); @@ -91,7 +91,7 @@ public void INodeServices_Concurrency_Setup() _serviceProvider = services.BuildServiceProvider(); _nodeServices = _serviceProvider.GetRequiredService(); - // Warm up. First run starts a Node.js processes. + // Warmup. First run starts a Node.js processes. _nodeServices.InvokeAsync("dummyLatencyModule.js", 0).GetAwaiter().GetResult(); } @@ -104,7 +104,7 @@ public async Task INodeServices_Concurrency() var results = new Task[numTasks]; for (int i = 0; i < numTasks; i++) { - results[i] = _nodeServices.InvokeAsync(DUMMY_CONCURRENCY_MODULE); + results[i] = _nodeServices.InvokeAsync(DUMMY_CONCURRENCY_MODULE_FILE); } return await Task.WhenAll(results); diff --git a/perf/NodeJS/LatencyBenchmarks.cs b/perf/NodeJS/LatencyBenchmarks.cs index b936a29..d431ee5 100644 --- a/perf/NodeJS/LatencyBenchmarks.cs +++ b/perf/NodeJS/LatencyBenchmarks.cs @@ -11,7 +11,8 @@ namespace Jering.Javascript.NodeJS.Performance [MemoryDiagnoser] public class LatencyBenchmarks { - private const string DUMMY_LATENCY_MODULE = "dummyLatencyModule.js"; + private const string DUMMY_WARMUP_MODULE = "module.exports = (callback) => callback()"; + private const string DUMMY_LATENCY_MODULE_FILE = "dummyLatencyModule.js"; private const string DUMMY_MODULE_IDENTIFIER = "dummyLatencyModuleIdentifier"; private ServiceProvider _serviceProvider; @@ -30,15 +31,14 @@ public void INodeJSService_Latency_InvokeFromFile_Setup() _nodeJSService = _serviceProvider.GetRequiredService(); _counter = 0; - // Warm up. First run starts a Node.js process. - _nodeJSService.InvokeFromStringAsync("module.exports = (callback) => callback(null, null)", "warmup").GetAwaiter().GetResult(); + // Warmup. First run starts a Node.js process. + _nodeJSService.InvokeFromStringAsync(DUMMY_WARMUP_MODULE).GetAwaiter().GetResult(); } [Benchmark] public async Task INodeJSService_Latency_InvokeFromFile() { - DummyResult result = await _nodeJSService.InvokeFromFileAsync(DUMMY_LATENCY_MODULE, args: new object[] { _counter++ }); - return result; + return await _nodeJSService.InvokeFromFileAsync(DUMMY_LATENCY_MODULE_FILE, args: new object[] { _counter++ }); } [GlobalSetup(Target = nameof(INodeJSService_Latency_InvokeFromCache))] @@ -50,15 +50,19 @@ public void INodeJSService_Latency_InvokeFromCache_Setup() _nodeJSService = _serviceProvider.GetRequiredService(); _counter = 0; - // Cache module/warmup - _nodeJSService.InvokeFromStringAsync("module.exports = (callback, result) => callback(null, { result: result });", DUMMY_MODULE_IDENTIFIER, args: new object[] { _counter++ }).GetAwaiter().GetResult(); + // Warmup/cache. + _nodeJSService.InvokeFromStringAsync(DummyModuleFactory, DUMMY_MODULE_IDENTIFIER, args: new object[] { _counter++ }).GetAwaiter().GetResult(); } [Benchmark] public async Task INodeJSService_Latency_InvokeFromCache() { - (bool _, DummyResult result) = await _nodeJSService.TryInvokeFromCacheAsync(DUMMY_MODULE_IDENTIFIER, args: new object[] { _counter++ }); - return result; + return await _nodeJSService.InvokeFromStringAsync(DummyModuleFactory, DUMMY_MODULE_IDENTIFIER, args: new object[] { _counter++ }); + } + + private string DummyModuleFactory() + { + return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "../../../..", DUMMY_LATENCY_MODULE_FILE)); } [Obsolete] @@ -75,15 +79,15 @@ public void INodeServices_Latency_Setup() _nodeServices = _serviceProvider.GetRequiredService(); _counter = 0; - // Warm up. First run starts a Node.js process. - _nodeServices.InvokeAsync(DUMMY_LATENCY_MODULE, 0).GetAwaiter().GetResult(); + // Warmup. First run starts a Node.js process. + _nodeServices.InvokeAsync(DUMMY_LATENCY_MODULE_FILE, 0).GetAwaiter().GetResult(); } [Obsolete] [Benchmark] public async Task INodeServices_Latency() { - DummyResult result = await _nodeServices.InvokeAsync(DUMMY_LATENCY_MODULE, _counter++); + DummyResult result = await _nodeServices.InvokeAsync(DUMMY_LATENCY_MODULE_FILE, _counter++); return result; } diff --git a/perf/NodeJS/RealWorkloadBenchmarks.cs b/perf/NodeJS/RealWorkloadBenchmarks.cs index 140b21d..a94551c 100644 --- a/perf/NodeJS/RealWorkloadBenchmarks.cs +++ b/perf/NodeJS/RealWorkloadBenchmarks.cs @@ -11,7 +11,8 @@ namespace Jering.Javascript.NodeJS.Performance [MemoryDiagnoser] public class RealWorkloadBenchmarks { - private const string DUMMY_REAL_WORKLOAD_MODULE = "dummyRealWorkloadModule.js"; + private const string DUMMY_CACHE_IDENTIFIER = "dummyRealWorkloadModuleIdentifier"; + private const string DUMMY_REAL_WORKLOAD_MODULE_FILE = "dummyRealWorkloadModule.js"; // Realistically, you aren't going to pass the same string for highlighting every time, so use a format that we alter slightly every interation private const string DUMMY_CODE_FORMAT = @"public class HelloWorld {{ @@ -39,11 +40,11 @@ public void INodeJSService_RealWorkload_Setup() _nodeJSService = _serviceProvider.GetRequiredService(); _counter = 0; - // Warm up. First few runs start Node.js processes, so they take longer. If we don't manually warm up, BenchmarkDotNet erroneously complains + // Warmup/cache. First few runs start Node.js processes, so they take longer. If we don't manually warm up, BenchmarkDotNet erroneously complains // about iteration time being too low for (int i = 0; i < Environment.ProcessorCount; i++) { - _nodeJSService.InvokeFromStringAsync("module.exports = (callback) => callback(null, null)", "warmup").GetAwaiter().GetResult(); + _nodeJSService.InvokeFromStringAsync(DummyModuleFactory, DUMMY_CACHE_IDENTIFIER, args: new object[] { string.Format(DUMMY_CODE_FORMAT, _counter++) }).GetAwaiter().GetResult(); } } @@ -56,12 +57,17 @@ public async Task INodeJSService_RealWorkload() for (int i = 0; i < numTasks; i++) { // The module uses Prism.js to perform syntax highlighting - results[i] = _nodeJSService.InvokeFromFileAsync(DUMMY_REAL_WORKLOAD_MODULE, args: new object[] { string.Format(DUMMY_CODE_FORMAT, _counter++) }); + results[i] = _nodeJSService.InvokeFromStringAsync(DummyModuleFactory, DUMMY_CACHE_IDENTIFIER, args: new object[] { string.Format(DUMMY_CODE_FORMAT, _counter++) }); } return await Task.WhenAll(results); } + private string DummyModuleFactory() + { + return File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "../../../..", DUMMY_REAL_WORKLOAD_MODULE_FILE)); + } + [Obsolete] [GlobalSetup(Target = nameof(INodeServices_RealWorkload))] public void INodeServices_RealWorkload_Setup() @@ -76,7 +82,7 @@ public void INodeServices_RealWorkload_Setup() _nodeServices = _serviceProvider.GetRequiredService(); _counter = 0; - // Warm up. First run starts a Node.js process. + // Warmup. First run starts a Node.js process. _nodeServices.InvokeAsync("dummyLatencyModule.js", 0).GetAwaiter().GetResult(); // Doesn't support invoke from string, so this is the simplest/quickest } @@ -89,7 +95,7 @@ public async Task INodeServices_RealWorkload() var results = new Task[numTasks]; for (int i = 0; i < numTasks; i++) { - results[i] = _nodeServices.InvokeAsync(DUMMY_REAL_WORKLOAD_MODULE, string.Format(DUMMY_CODE_FORMAT, _counter++)); + results[i] = _nodeServices.InvokeAsync(DUMMY_REAL_WORKLOAD_MODULE_FILE, string.Format(DUMMY_CODE_FORMAT, _counter++)); } return await Task.WhenAll(results); diff --git a/src/NodeJS/INodeJSService.cs b/src/NodeJS/INodeJSService.cs index 2464719..0b0886b 100644 --- a/src/NodeJS/INodeJSService.cs +++ b/src/NodeJS/INodeJSService.cs @@ -239,5 +239,21 @@ public interface INodeJSService : IDisposable /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. /// Thrown if is cancelled. Task<(bool, T)> TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); + + /// + /// Attempts to invoke a function from a module in NodeJS's cache. + /// + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. On completion, the task returns true on success and false otherwise. + /// Thrown if is null. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + Task TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default); } } From 67b9cae222d5335330fe9165bffc1c93a18442d2 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Mon, 2 Dec 2019 16:05:25 +0800 Subject: [PATCH 03/11] Added Void type. --- src/NodeJS/NodeJSServiceImplementations/Void.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/NodeJS/NodeJSServiceImplementations/Void.cs diff --git a/src/NodeJS/NodeJSServiceImplementations/Void.cs b/src/NodeJS/NodeJSServiceImplementations/Void.cs new file mode 100644 index 0000000..2226581 --- /dev/null +++ b/src/NodeJS/NodeJSServiceImplementations/Void.cs @@ -0,0 +1,9 @@ +namespace Jering.Javascript.NodeJS +{ + /// + /// Represents non-existence. + /// + public sealed class Void + { + } +} From 523b67f6219a77d60f5e2926e074b4309448a5a1 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Mon, 2 Dec 2019 18:36:47 +0800 Subject: [PATCH 04/11] Implemented new INodeJSService API in OutOfProcessNodeJSService - Made implemented methods virtual. Previously all they did was call TryInvokeCoreAsync, now they have logic, so they should be exposed for overriding. --- .../OutOfProcess/OutOfProcessNodeJSService.cs | 112 ++++- .../OutOfProcessNodeJSServiceUnitTests.cs | 386 +++++++++++++++--- 2 files changed, 424 insertions(+), 74 deletions(-) diff --git a/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/OutOfProcessNodeJSService.cs b/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/OutOfProcessNodeJSService.cs index 30652f4..31dcbc4 100644 --- a/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/OutOfProcessNodeJSService.cs +++ b/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/OutOfProcessNodeJSService.cs @@ -88,51 +88,119 @@ protected OutOfProcessNodeJSService(INodeJSProcessFactory nodeProcessFactory, protected abstract void OnConnectionEstablishedMessageReceived(string connectionEstablishedMessage); /// - public async Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + public virtual async Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { - var invocationRequest = new InvocationRequest(ModuleSourceType.File, - modulePath, - exportName: exportName, - args: args); + var invocationRequest = new InvocationRequest(ModuleSourceType.File, modulePath, exportName: exportName, args: args); return (await TryInvokeCoreAsync(invocationRequest, cancellationToken).ConfigureAwait(false)).Item2; } /// - public async Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + public virtual Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { - var invocationRequest = new InvocationRequest(ModuleSourceType.String, - moduleString, - newCacheIdentifier, - exportName, - args); + // Task extends Task + return InvokeFromFileAsync(modulePath, exportName, args, cancellationToken); + } + + /// + public virtual async Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + var invocationRequest = new InvocationRequest(ModuleSourceType.String, moduleString, newCacheIdentifier, exportName, args); + + return (await TryInvokeCoreAsync(invocationRequest, cancellationToken).ConfigureAwait(false)).Item2; + } + + /// + public virtual Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return InvokeFromStringAsync(moduleString, newCacheIdentifier, exportName, args, cancellationToken); + } + + /// + public virtual async Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + (bool success, T result) = await TryInvokeFromCacheAsync(cacheIdentifier, exportName, args, cancellationToken).ConfigureAwait(false); + + if (success) + { + return result; + } + + if (moduleFactory == null) + { + throw new ArgumentNullException(nameof(moduleFactory)); + } + + // If module doesn't exist in cache, create module string and send it to the NodeJS process + var invocationRequest = new InvocationRequest(ModuleSourceType.String, moduleFactory(), cacheIdentifier, exportName, args); return (await TryInvokeCoreAsync(invocationRequest, cancellationToken).ConfigureAwait(false)).Item2; } /// - public async Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + public virtual Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return InvokeFromStringAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + public virtual async Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { - var invocationRequest = new InvocationRequest(ModuleSourceType.Stream, - newCacheIdentifier: newCacheIdentifier, - exportName: exportName, - args: args, - moduleStreamSource: moduleStream); + var invocationRequest = new InvocationRequest(ModuleSourceType.Stream, null, newCacheIdentifier, exportName, args, moduleStream); return (await TryInvokeCoreAsync(invocationRequest, cancellationToken).ConfigureAwait(false)).Item2; } /// - public Task<(bool, T)> TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + public virtual Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { - var invocationRequest = new InvocationRequest(ModuleSourceType.Cache, - moduleCacheIdentifier, - exportName: exportName, - args: args); + return InvokeFromStreamAsync(moduleStream, newCacheIdentifier, exportName, args, cancellationToken); + } + + /// + public virtual async Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + (bool success, T result) = await TryInvokeFromCacheAsync(cacheIdentifier, exportName, args, cancellationToken).ConfigureAwait(false); + + if (success) + { + return result; + } + + if (moduleFactory == null) + { + throw new ArgumentNullException(nameof(moduleFactory)); + } + + using (Stream moduleStream = moduleFactory()) + { + // If module doesn't exist in cache, create module stream and send it to the NodeJS process + var invocationRequest = new InvocationRequest(ModuleSourceType.Stream, null, cacheIdentifier, exportName, args, moduleStream); + + return (await TryInvokeCoreAsync(invocationRequest, cancellationToken).ConfigureAwait(false)).Item2; + } + } + + /// + public virtual Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return InvokeFromStreamAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + public virtual Task<(bool, T)> TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + var invocationRequest = new InvocationRequest(ModuleSourceType.Cache, moduleCacheIdentifier, exportName: exportName, args: args); return TryInvokeCoreAsync(invocationRequest, cancellationToken); } + /// + public virtual async Task TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return (await TryInvokeFromCacheAsync(moduleCacheIdentifier, exportName, args, cancellationToken).ConfigureAwait(false)).Item1; + } + internal virtual async Task<(bool, T)> TryInvokeCoreAsync(InvocationRequest invocationRequest, CancellationToken cancellationToken) { int numRetries = _options.NumRetries; diff --git a/test/NodeJS/OutOfProcessNodeJSServiceUnitTests.cs b/test/NodeJS/OutOfProcessNodeJSServiceUnitTests.cs index a284d04..d3566e3 100644 --- a/test/NodeJS/OutOfProcessNodeJSServiceUnitTests.cs +++ b/test/NodeJS/OutOfProcessNodeJSServiceUnitTests.cs @@ -25,7 +25,7 @@ public class OutOfProcessNodeJSServiceUnitTests : IDisposable private readonly MockRepository _mockRepository = new MockRepository(MockBehavior.Default); private readonly ITestOutputHelper _testOutputHelper; private IServiceProvider _serviceProvider; - private const int _timeoutMS = 60000; + private const int TIMEOUT_MS = 60000; public OutOfProcessNodeJSServiceUnitTests(ITestOutputHelper testOutputHelper) { @@ -33,7 +33,7 @@ public OutOfProcessNodeJSServiceUnitTests(ITestOutputHelper testOutputHelper) } [Fact] - public async void InvokeFromFileAsync_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + public async void InvokeFromFileAsync_WithTypeParameter_InvokesFromFile() { // Arrange const int dummyResult = 1; @@ -43,26 +43,49 @@ public async void InvokeFromFileAsync_CreatesInvocationRequestAndCallsTryInvokeC var dummyCancellationToken = new CancellationToken(); Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); mockTestSubject.CallBase = true; - mockTestSubject.Setup(t => t.TryInvokeCoreAsync(It.IsAny(), It.IsAny())).ReturnsAsync((true, dummyResult)); + mockTestSubject. + Setup(t => t.TryInvokeCoreAsync(It.Is( + invocationRequest => + invocationRequest.ModuleSourceType == ModuleSourceType.File && + invocationRequest.ModuleSource == dummyModulePath && + invocationRequest.NewCacheIdentifier == null && + invocationRequest.ExportName == dummyExportName && + invocationRequest.Args == dummyArgs && + invocationRequest.ModuleStreamSource == null), + dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); // Act int result = await mockTestSubject.Object.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); // Assert + _mockRepository.VerifyAll(); Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromFileAsync_WithoutTypeParameter_InvokesFromFile() + { + // Arrange + const string dummyModulePath = "dummyModulePath"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((Void)null); + + // Act + await mockTestSubject.Object.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert _mockRepository.VerifyAll(); - mockTestSubject.Verify(t => t.TryInvokeCoreAsync( - It.Is( - invocationRequest => - invocationRequest.ModuleSourceType == ModuleSourceType.File && - invocationRequest.ModuleSource == dummyModulePath && - invocationRequest.ExportName == dummyExportName && - invocationRequest.Args == dummyArgs), - dummyCancellationToken)); } [Fact] - public async void InvokeFromStringAsync_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + public async void InvokeFromStringAsync_WithTypeParameter_WithRawStringModule_InvokesFromString() { // Arrange const int dummyResult = 1; @@ -73,7 +96,17 @@ public async void InvokeFromStringAsync_CreatesInvocationRequestAndCallsTryInvok var dummyCancellationToken = new CancellationToken(); Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); mockTestSubject.CallBase = true; - mockTestSubject.Setup(t => t.TryInvokeCoreAsync(It.IsAny(), It.IsAny())).ReturnsAsync((true, dummyResult)); + mockTestSubject. + Setup(t => t.TryInvokeCoreAsync(It.Is( + invocationRequest => + invocationRequest.ModuleSourceType == ModuleSourceType.String && + invocationRequest.ModuleSource == dummyModuleString && + invocationRequest.NewCacheIdentifier == dummyNewCacheIdentifier && + invocationRequest.ExportName == dummyExportName && + invocationRequest.Args == dummyArgs && + invocationRequest.ModuleStreamSource == null), + dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); // Act int result = await mockTestSubject.Object.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); @@ -81,19 +114,126 @@ public async void InvokeFromStringAsync_CreatesInvocationRequestAndCallsTryInvok // Assert Assert.Equal(dummyResult, result); _mockRepository.VerifyAll(); - mockTestSubject.Verify(t => t.TryInvokeCoreAsync( - It.Is( - invocationRequest => - invocationRequest.ModuleSourceType == ModuleSourceType.String && - invocationRequest.ModuleSource == dummyModuleString && - invocationRequest.NewCacheIdentifier == dummyNewCacheIdentifier && - invocationRequest.ExportName == dummyExportName && - invocationRequest.Args == dummyArgs), - dummyCancellationToken)); } [Fact] - public async void InvokeFromStreamAsync_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + public async void InvokeFromStringAsync_WithoutTypeParameter_WithRawStringModule_InvokesFromString() + { + // Arrange + const string dummyModuleString = "dummyModuleString"; + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((Void)null); + + // Act + await mockTestSubject.Object.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_ThrowsArgumentNullExceptionIfModuleIsNotCachedButModuleFactoryIsNull() + { + // Arrange + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.TryInvokeFromCacheAsync("dummyCacheIdentifier", null, null, default)). + ReturnsAsync((false, 0)); + + // Act and assert + await Assert.ThrowsAsync(async () => await mockTestSubject.Object.InvokeFromStringAsync((Func)null, "dummyCacheIdentifier").ConfigureAwait(false)).ConfigureAwait(false); + } + + [Fact] + public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_InvokesFromCacheIfModuleIsCached() + { + // Arrange + const int dummyResult = 1; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.TryInvokeFromCacheAsync(dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); + + // Act + int result = await mockTestSubject.Object.InvokeFromStringAsync(() => "dummyModule", dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_InvokesFromStringAndCachesModuleIfModuleIsNotCached() + { + // Arrange + const int dummyResult = 1; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + const string dummyModule = "dummyModule"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.TryInvokeFromCacheAsync(dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((false, 0)); + mockTestSubject. + Setup(t => t.TryInvokeCoreAsync(It.Is( + invocationRequest => + invocationRequest.ModuleSourceType == ModuleSourceType.String && + invocationRequest.ModuleSource == dummyModule && + invocationRequest.NewCacheIdentifier == dummyCacheIdentifier && + invocationRequest.ExportName == dummyExportName && + invocationRequest.Args == dummyArgs && + invocationRequest.ModuleStreamSource == null), + dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); + + // Act + int result = await mockTestSubject.Object.InvokeFromStringAsync(() => dummyModule, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromString() + { + // Arrange + Func dummyFactory = () => "dummyModule"; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.InvokeFromStringAsync(dummyFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((Void)null); + + // Act + await mockTestSubject.Object.InvokeFromStringAsync(dummyFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithRawStreamModule_InvokesFromStream() { // Arrange const int dummyResult = 1; @@ -104,7 +244,17 @@ public async void InvokeFromStreamAsync_CreatesInvocationRequestAndCallsTryInvok var dummyCancellationToken = new CancellationToken(); Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); mockTestSubject.CallBase = true; - mockTestSubject.Setup(t => t.TryInvokeCoreAsync(It.IsAny(), It.IsAny())).ReturnsAsync((true, dummyResult)); + mockTestSubject. + Setup(t => t.TryInvokeCoreAsync(It.Is( + invocationRequest => + invocationRequest.ModuleSourceType == ModuleSourceType.Stream && + invocationRequest.ModuleSource == null && + invocationRequest.NewCacheIdentifier == dummyNewCacheIdentifier && + invocationRequest.ExportName == dummyExportName && + invocationRequest.Args == dummyArgs && + invocationRequest.ModuleStreamSource == dummyModuleStream), + dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); // Act int result = await mockTestSubject.Object.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); @@ -112,19 +262,127 @@ public async void InvokeFromStreamAsync_CreatesInvocationRequestAndCallsTryInvok // Assert Assert.Equal(dummyResult, result); _mockRepository.VerifyAll(); - mockTestSubject.Verify(t => t.TryInvokeCoreAsync( - It.Is( - invocationRequest => - invocationRequest.ModuleSourceType == ModuleSourceType.Stream && - invocationRequest.ModuleStreamSource == dummyModuleStream && - invocationRequest.NewCacheIdentifier == dummyNewCacheIdentifier && - invocationRequest.ExportName == dummyExportName && - invocationRequest.Args == dummyArgs), - dummyCancellationToken)); } [Fact] - public async void TryInvokeFromCacheAsync_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithRawStreamModule_InvokesFromStream() + { + // Arrange + var dummyModuleStream = new MemoryStream(); + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((Void)null); + + // Act + await mockTestSubject.Object.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_ThrowsArgumentNullExceptionIfModuleIsNotCachedButModuleFactoryIsNull() + { + // Arrange + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.TryInvokeFromCacheAsync("dummyCacheIdentifier", null, null, default)). + ReturnsAsync((false, 0)); + + // Act and assert + await Assert.ThrowsAsync(async () => await mockTestSubject.Object.InvokeFromStreamAsync((Func)null, "dummyCacheIdentifier").ConfigureAwait(false)).ConfigureAwait(false); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_InvokesFromCacheIfModuleIsCached() + { + // Arrange + const int dummyResult = 1; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.TryInvokeFromCacheAsync(dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); + + // Act + int result = await mockTestSubject.Object.InvokeFromStreamAsync(() => new MemoryStream(), dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_InvokesFromStreamIfModuleIsNotCached() + { + // Arrange + const int dummyResult = 1; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; +#pragma warning disable IDE0067 + var dummyModule = new MemoryStream(); +#pragma warning disable IDE0067 + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.TryInvokeFromCacheAsync(dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((false, 0)); + mockTestSubject. + Setup(t => t.TryInvokeCoreAsync(It.Is( + invocationRequest => + invocationRequest.ModuleSourceType == ModuleSourceType.Stream && + invocationRequest.ModuleSource == null && + invocationRequest.NewCacheIdentifier == dummyCacheIdentifier && + invocationRequest.ExportName == dummyExportName && + invocationRequest.Args == dummyArgs && + invocationRequest.ModuleStreamSource == dummyModule), + dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); + + // Act + int result = await mockTestSubject.Object.InvokeFromStreamAsync(() => dummyModule, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromStream() + { + // Arrange + Func dummyFactory = () => new MemoryStream(); + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.InvokeFromStreamAsync(dummyFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)).ReturnsAsync((Void)null); + + // Act + await mockTestSubject.Object.InvokeFromStreamAsync(dummyFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void TryInvokeFromCacheAsync_WithTypeParameter_InvokesFromCache() { // Arrange const int dummyResult = 1; @@ -134,7 +392,17 @@ public async void TryInvokeFromCacheAsync_CreatesInvocationRequestAndCallsTryInv var dummyCancellationToken = new CancellationToken(); Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); mockTestSubject.CallBase = true; - mockTestSubject.Setup(t => t.TryInvokeCoreAsync(It.IsAny(), It.IsAny())).ReturnsAsync((true, dummyResult)); + mockTestSubject. + Setup(t => t.TryInvokeCoreAsync(It.Is( + invocationRequest => + invocationRequest.ModuleSourceType == ModuleSourceType.Cache && + invocationRequest.ModuleSource == dummyModuleCacheIdentifier && + invocationRequest.NewCacheIdentifier == null && + invocationRequest.ExportName == dummyExportName && + invocationRequest.Args == dummyArgs && + invocationRequest.ModuleStreamSource == null), + dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); // Act (bool success, int result) = await mockTestSubject.Object.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); @@ -143,14 +411,28 @@ public async void TryInvokeFromCacheAsync_CreatesInvocationRequestAndCallsTryInv Assert.True(success); Assert.Equal(dummyResult, result); _mockRepository.VerifyAll(); - mockTestSubject.Verify(t => t.TryInvokeCoreAsync( - It.Is( - invocationRequest => - invocationRequest.ModuleSourceType == ModuleSourceType.Cache && - invocationRequest.ModuleSource == dummyModuleCacheIdentifier && - invocationRequest.ExportName == dummyExportName && - invocationRequest.Args == dummyArgs), - dummyCancellationToken)); + } + + [Fact] + public async void TryInvokeFromCacheAsync_WithoutTypeParameter_InvokesFromCache() + { + // Arrange + const string dummyModuleCacheIdentifier = "dummyModuleCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockTestSubject = CreateMockOutOfProcessNodeJSService(); + mockTestSubject.CallBase = true; + mockTestSubject. + Setup(t => t.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((true, null)); + + // Act + bool success = await mockTestSubject.Object.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.True(success); + _mockRepository.VerifyAll(); } [Fact] @@ -166,7 +448,7 @@ public async void TryInvokeCoreAsync_ThrowsObjectDisposedExceptionIfObjectHasBee Assert.Equal($"Cannot access a disposed object.\nObject name: '{nameof(OutOfProcessNodeJSService)}'.", result.Message, ignoreLineEndingDifferences: true); } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_FirstThreadLazilyCreatesNodeJSProcessBeforeInvoking() { // Arrange @@ -227,7 +509,7 @@ public void TryInvokeCoreAsync_FirstThreadLazilyCreatesNodeJSProcessBeforeInvoki } } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void TryInvokeCoreAsync_IfNodeJSProcessIsNotConnectedFirstThreadCreatesNodeJSProcessBeforeInvoking() { // Arrange @@ -290,7 +572,7 @@ public async void TryInvokeCoreAsync_IfNodeJSProcessIsNotConnectedFirstThreadCre } } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_RetriesConnectionIfConnectionAttemptTimesoutAndThrowsInvocationExceptionIfNoRetriesRemain() { // Arrange @@ -366,7 +648,7 @@ public void TryInvokeCoreAsync_RetriesConnectionIfConnectionAttemptTimesoutAndTh } } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_DoesNotRetryInvocationsThatAreCanceledAndThrowsOperationCanceledException() { // Arrange @@ -441,7 +723,7 @@ public void TryInvokeCoreAsync_DoesNotRetryInvocationsThatAreCanceledAndThrowsOp } } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_RetriesInvocationsThatTimeoutAndThrowsInvocationExceptionIfNoRetriesRemain() { // Arrange @@ -523,7 +805,7 @@ public void TryInvokeCoreAsync_RetriesInvocationsThatTimeoutAndThrowsInvocationE } } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_RetriesInvocationsThatThrowExceptionsAndThrowsExceptionIfNoRetriesRemain() { // Arrange @@ -595,7 +877,7 @@ public void TryInvokeCoreAsync_RetriesInvocationsThatThrowExceptionsAndThrowsExc } } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_DoesNotRetryInvocationIfModuleSourceIsAnUnseekableStream() { // Arrange @@ -636,7 +918,7 @@ public void TryInvokeCoreAsync_DoesNotRetryInvocationIfModuleSourceIsAnUnseekabl Verify(t => t.TryInvokeAsync(dummyInvocationRequest, It.IsAny()), Times.Once()); // No retries } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_ResetsStreamPositionAndRetriesInvocationIfModuleSourceIsASeekableStreamThatIsNotAtItsInitialPosition() { // Arrange @@ -680,7 +962,7 @@ public void TryInvokeCoreAsync_ResetsStreamPositionAndRetriesInvocationIfModuleS mockStream.VerifySet(s => s.Position = dummyStreamInitialPosition); } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void TryInvokeCoreAsync_RetriesInvocationIfModuleSourceIsASeekableStream() { // Arrange From 6e4deb85d1b20b1624b874b33cea389d572258ba Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Mon, 2 Dec 2019 21:25:29 +0800 Subject: [PATCH 05/11] Updated HttpNodeJSService - Added shortcut for requests with return type Void to HttpNodeJSService. - Updated integration tests. --- .../OutOfProcess/Http/HttpNodeJSService.cs | 5 + .../HttpNodeJSServiceIntegrationTests.cs | 787 +++++++++++++----- test/NodeJS/HttpNodeJSServiceUnitTests.cs | 173 ++-- .../dummyExportsMultipleFunctionsModule.js | 19 + ...ummyModule.js => dummyReturnsArgModule.js} | 0 .../Jering.Javascript.NodeJS.Tests.csproj | 1 - 6 files changed, 704 insertions(+), 281 deletions(-) create mode 100644 test/NodeJS/Javascript/dummyExportsMultipleFunctionsModule.js rename test/NodeJS/Javascript/{dummyModule.js => dummyReturnsArgModule.js} (100%) diff --git a/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSService.cs b/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSService.cs index 8386194..b968052 100644 --- a/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSService.cs +++ b/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSService.cs @@ -93,6 +93,11 @@ public HttpNodeJSService(IOptions outOfProcess if (httpResponseMessage.StatusCode == HttpStatusCode.OK) { + if(typeof(T) == typeof(Void)) // Returned value doesn't matter + { + return (true, default); + } + if (typeof(T) == typeof(string)) { string result = await httpResponseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); diff --git a/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs b/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs index ee75a9e..7f188dc 100644 --- a/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs +++ b/test/NodeJS/HttpNodeJSServiceIntegrationTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using System.Threading; using Xunit; @@ -13,82 +14,146 @@ namespace Jering.Javascript.NodeJS.Tests { /// - /// These tests are de facto tests for HttpServer.ts. They serve the additional role of verifying that IPC works. + /// These tests are de facto tests for HttpServer.ts. They serve the additional role of verifying that IPC works properly. /// public class HttpNodeJSServiceIntegrationTests : IDisposable { + // Set to true to break in NodeJS (see CreateHttpNodeJSService) + private const bool DEBUG_NODEJS = false; + // Set to -1 when debugging in NodeJS + private const int TIMEOUT_MS = 60000; + private const string DUMMY_RETURNS_ARG_MODULE_FILE = "dummyReturnsArgModule.js"; + private const string DUMMY_EXPORTS_MULTIPLE_FUNCTIONS_MODULE_FILE = "dummyExportsMultipleFunctionsModule.js"; + private const string DUMMY_CACHE_IDENTIFIER = "dummyCacheIdentifier"; + + private static readonly string _projectPath = Path.Combine(Directory.GetCurrentDirectory(), "../../../Javascript"); // Current directory is /bin/debug/ + private static readonly string _dummyReturnsArgModule = File.ReadAllText(Path.Combine(_projectPath, DUMMY_RETURNS_ARG_MODULE_FILE)); + private static readonly string _dummyExportsMultipleFunctionsModule = File.ReadAllText(Path.Combine(_projectPath, DUMMY_EXPORTS_MULTIPLE_FUNCTIONS_MODULE_FILE)); + private readonly ITestOutputHelper _testOutputHelper; private IServiceProvider _serviceProvider; - private const int _timeoutMS = 60000; - // Set to true to break in NodeJS (see CreateHttpNodeJSService) - private const bool _debugNodeJS = false; public HttpNodeJSServiceIntegrationTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } - [Fact(Timeout = _timeoutMS)] - public async void TryInvokeFromCacheAsync_InvokesJavascriptIfModuleIsCached() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromFileAsync_WithTypeParameter_InvokesFromFile() + { + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); + + // Act + DummyResult result = await testSubject. + InvokeFromFileAsync(DUMMY_RETURNS_ARG_MODULE_FILE, args: new[] { dummyArg }).ConfigureAwait(false); + + // Assert + Assert.Equal(dummyArg, result.Result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public void InvokeFromFileAsync_WithTypeParameter_IsThreadSafe() { // Arrange - const string dummyResultString = "success"; - const string dummyCacheIdentifier = "dummyCacheIdentifier"; - HttpNodeJSService testSubject = CreateHttpNodeJSService(); + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); - // Cache - await testSubject. - InvokeFromStringAsync("module.exports = (callback, resultString) => callback(null, {result: resultString});", - dummyCacheIdentifier, - args: new[] { dummyResultString }). - ConfigureAwait(false); + // Act + var results = new ConcurrentQueue(); + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) + { + var thread = new Thread(() => results.Enqueue(testSubject. + InvokeFromFileAsync(DUMMY_RETURNS_ARG_MODULE_FILE, args: new[] { dummyArg }).GetAwaiter().GetResult())); + threads.Add(thread); + thread.Start(); + } + foreach (Thread thread in threads) + { + thread.Join(); + } + + // Assert + Assert.Equal(numThreads, results.Count); + foreach (DummyResult result in results) + { + Assert.Equal(dummyArg, result.Result); + } + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromFileAsync_WithoutTypeParameter_InvokesFromFile() + { + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); // Act - (bool success, DummyResult value) = await testSubject.TryInvokeFromCacheAsync(dummyCacheIdentifier, args: new[] { dummyResultString }).ConfigureAwait(false); + await testSubject.InvokeFromFileAsync(DUMMY_EXPORTS_MULTIPLE_FUNCTIONS_MODULE_FILE, "setString", new[] { dummyArg }).ConfigureAwait(false); // Assert - Assert.True(success); - Assert.Equal(dummyResultString, value.Result); + DummyResult result = await testSubject. + InvokeFromFileAsync(DUMMY_EXPORTS_MULTIPLE_FUNCTIONS_MODULE_FILE, "getString").ConfigureAwait(false); + Assert.Equal(dummyArg, result.Result); } - [Fact(Timeout = _timeoutMS)] - public async void TryInvokeFromCacheAsync_ReturnsFalseIfModuleIsNotCached() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromFileAsync_WithoutTypeParameter_IsThreadSafe() { // Arrange - const string dummyResultString = "success"; - const string dummyCacheIdentifier = "dummyCacheIdentifier"; - HttpNodeJSService testSubject = CreateHttpNodeJSService(); + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); // Act - (bool success, DummyResult value) = await testSubject.TryInvokeFromCacheAsync(dummyCacheIdentifier, args: new[] { dummyResultString }).ConfigureAwait(false); + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) + { + var thread = new Thread(() => testSubject. + InvokeFromFileAsync(DUMMY_EXPORTS_MULTIPLE_FUNCTIONS_MODULE_FILE, "incrementNumber").GetAwaiter().GetResult()); + threads.Add(thread); + thread.Start(); + } + foreach (Thread thread in threads) + { + thread.Join(); + } // Assert - Assert.False(success); - Assert.Null(value); + int result = await testSubject.InvokeFromFileAsync(DUMMY_EXPORTS_MULTIPLE_FUNCTIONS_MODULE_FILE, "getNumber").ConfigureAwait(false); + Assert.Equal(numThreads, result); } - [Fact(Timeout = _timeoutMS)] - public async void TryInvokeFromCacheAsync_IsThreadSafe() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithTypeParameter_WithRawStringModule_InvokesFromString() { // Arrange - const string dummyResultString = "success"; - const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyArg = "success"; HttpNodeJSService testSubject = CreateHttpNodeJSService(); - // Cache - await testSubject. - InvokeFromStringAsync("module.exports = (callback, resultString) => callback(null, {result: resultString});", - dummyCacheIdentifier, - args: new[] { dummyResultString }). - ConfigureAwait(false); + // Act + DummyResult result = await testSubject. + InvokeFromStringAsync(_dummyReturnsArgModule, args: new[] { dummyArg }).ConfigureAwait(false); + + // Assert + Assert.Equal(dummyArg, result.Result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public void InvokeFromStringAsync_WithTypeParameter_WithRawStringModule_IsThreadSafe() + { + // Arrange + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act - var results = new ConcurrentQueue<(bool, DummyResult)>(); + var results = new ConcurrentQueue(); const int numThreads = 5; var threads = new List(); for (int i = 0; i < numThreads; i++) { - var thread = new Thread(() => results.Enqueue(testSubject.TryInvokeFromCacheAsync(dummyCacheIdentifier, args: new[] { dummyResultString }).GetAwaiter().GetResult())); + var thread = new Thread(() => results.Enqueue(testSubject. + InvokeFromStringAsync(_dummyReturnsArgModule, args: new[] { dummyArg }).GetAwaiter().GetResult())); threads.Add(thread); thread.Start(); } @@ -99,110 +164,202 @@ await testSubject. // Assert Assert.Equal(numThreads, results.Count); - foreach ((bool success, DummyResult value) in results) + foreach (DummyResult result in results) { - Assert.True(success); - Assert.Equal(dummyResultString, value.Result); + Assert.Equal(dummyArg, result.Result); } } - [Fact(Timeout = _timeoutMS)] - public async void InvokeFromStreamAsync_InvokesJavascript() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithRawStringModule_InvokesFromString() + { + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); + + // Act + await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "setString", new[] { dummyArg }).ConfigureAwait(false); + + // Assert + DummyResult result = await testSubject. + InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "getString").ConfigureAwait(false); + Assert.Equal(dummyArg, result.Result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithRawStringModule_IsThreadSafe() { // Arrange - const string dummyResultString = "success"; - HttpNodeJSService testSubject = CreateHttpNodeJSService(); + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); - DummyResult result; - using (var memoryStream = new MemoryStream()) - using (var streamWriter = new StreamWriter(memoryStream)) + // Act + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) { - streamWriter.Write("module.exports = (callback, resultString) => callback(null, {result: resultString});"); - streamWriter.Flush(); - memoryStream.Position = 0; - - // Act - result = await testSubject.InvokeFromStreamAsync(memoryStream, args: new[] { dummyResultString }).ConfigureAwait(false); + var thread = new Thread(() => testSubject. + InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "incrementNumber").GetAwaiter().GetResult()); + threads.Add(thread); + thread.Start(); + } + foreach (Thread thread in threads) + { + thread.Join(); } // Assert - Assert.Equal(dummyResultString, result.Result); + int result = await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.Equal(numThreads, result); } - [Fact(Timeout = _timeoutMS)] - public async void InvokeFromStreamAsync_LoadsRequiredModulesFromNodeModulesInProjectDirectory() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_InvokesFromCacheIfModuleIsCached() { // Arrange - const string dummyCode = @"public string ExampleFunction(string arg) -{ - // Example comment - return arg + ""dummyString""; -}"; - HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: Directory.GetCurrentDirectory() + "/../../../Javascript"); // Current directory is /bin/debug/ + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "setString", new[] { dummyArg }).ConfigureAwait(false); // Act - string result; - using (var memoryStream = new MemoryStream()) - using (var streamWriter = new StreamWriter(memoryStream)) - { - streamWriter.Write(@"const prismjs = require('prismjs'); -require('prismjs/components/prism-csharp'); + DummyResult result = await testSubject. + InvokeFromStringAsync(() => null, DUMMY_CACHE_IDENTIFIER, "getString").ConfigureAwait(false); -module.exports = (callback, code) => { - var result = prismjs.highlight(code, prismjs.languages.csharp, 'csharp'); + // Assert + Assert.Equal(dummyArg, result.Result); + } - callback(null, result); -};"); - streamWriter.Flush(); - memoryStream.Position = 0; + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_InvokesFromStringAndCachesModuleIfModuleIsNotCached() + { + // Arrange + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(); - // Act - result = await testSubject.InvokeFromStreamAsync(memoryStream, args: new[] { dummyCode }).ConfigureAwait(false); - } + // Act + // Module hasn't been cached, so if this returns the expected value, string was sent over + DummyResult result1 = await testSubject. + InvokeFromStringAsync(() => _dummyReturnsArgModule, DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).ConfigureAwait(false); // Assert - const string expectedResult = @"public string ExampleFunction(string arg) -{ - // Example comment - return arg + ""dummyString""; -}"; - Assert.Equal(expectedResult, result); + // Ensure module was cached + (bool success, DummyResult result2) = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).ConfigureAwait(false); + Assert.Equal(dummyArg, result1.Result); + Assert.True(success); + Assert.Equal(dummyArg, result2.Result); } - [Fact(Timeout = _timeoutMS)] - public async void InvokeFromStreamAsync_LoadsRequiredModuleFromFileInProjectDirectory() + [Fact(Timeout = TIMEOUT_MS)] + public void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_IsThreadSafe() { // Arrange - HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: Directory.GetCurrentDirectory() + "/../../../Javascript"); // Current directory is /bin/debug/ + HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act - int result; - using (var memoryStream = new MemoryStream()) - using (var streamWriter = new StreamWriter(memoryStream)) + var results = new ConcurrentQueue(); + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) + { + var thread = new Thread(() => results.Enqueue(testSubject. + InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "incrementAndGetNumber").GetAwaiter().GetResult())); + threads.Add(thread); + thread.Start(); + } + foreach (Thread thread in threads) { - streamWriter.Write(@"const value = require('./dummyReturnsValueModule.js'); + thread.Join(); + } -module.exports = (callback) => { + // Assert + Assert.Equal(numThreads, results.Count); + // Module shouldn't get cached more than once, we should get exactly [1,2,3,4,5] + List resultsList = results.ToList(); + resultsList.Sort(); + for (int i = 0; i < numThreads; i++) + { + Assert.Equal(resultsList[i], i + 1); + } + } - callback(null, value); -};"); - streamWriter.Flush(); - memoryStream.Position = 0; + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithModuleFactory_InvokesFromCacheIfModuleIsCached() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); + + // Act + await testSubject. + InvokeFromStringAsync(() => null, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); + + // Assert + int result = await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.Equal(2, result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithModuleFactory_InvokesFromStringAndCachesModuleIfModuleIsNotCached() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + + // Act + await testSubject. + InvokeFromStringAsync(() => _dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); + + // Assert + // Ensure module was cached + (bool success, int result) = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.True(success); + Assert.Equal(1, result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithModuleFactory_IsThreadSafe() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); - // Act - result = await testSubject.InvokeFromStreamAsync(memoryStream).ConfigureAwait(false); + // Act + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) + { + var thread = new Thread(() => testSubject. + InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "incrementNumber").GetAwaiter().GetResult()); + threads.Add(thread); + thread.Start(); + } + foreach (Thread thread in threads) + { + thread.Join(); } // Assert - Assert.Equal(10, result); // dummyReturnsValueModule.js just exports 10 + (bool success, int result) = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.True(success); + Assert.Equal(numThreads, result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithTypeParameter_WithRawStreamModule_InvokesFromStream() + { + // Arrange + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + MemoryStream memoryStream = CreateMemoryStream(_dummyReturnsArgModule); + + // Act + DummyResult result = await testSubject.InvokeFromStreamAsync(memoryStream, args: new[] { dummyArg }).ConfigureAwait(false); + + // Assert + Assert.Equal(dummyArg, result.Result); } - [Fact(Timeout = _timeoutMS)] - public void InvokeFromStreamAsync_IsThreadSafe() + [Fact(Timeout = TIMEOUT_MS)] + public void InvokeFromStreamAsync_WithTypeParameter_WithRawStreamModule_IsThreadSafe() { // Arrange - const string dummyModule = "module.exports = (callback, resultString) => callback(null, {result: resultString});"; - const string dummyResultString = "success"; + const string dummyArg = "success"; HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act @@ -213,14 +370,8 @@ public void InvokeFromStreamAsync_IsThreadSafe() { var thread = new Thread(() => { - using (var memoryStream = new MemoryStream()) - using (var streamWriter = new StreamWriter(memoryStream)) - { - streamWriter.Write(dummyModule); - streamWriter.Flush(); - memoryStream.Position = 0; - results.Enqueue(testSubject.InvokeFromStreamAsync(memoryStream, args: new[] { dummyResultString }).GetAwaiter().GetResult()); - } + MemoryStream memoryStream = CreateMemoryStream(_dummyReturnsArgModule); + results.Enqueue(testSubject.InvokeFromStreamAsync(memoryStream, args: new[] { dummyArg }).GetAwaiter().GetResult()); }); threads.Add(thread); thread.Start(); @@ -234,88 +385,183 @@ public void InvokeFromStreamAsync_IsThreadSafe() Assert.Equal(numThreads, results.Count); foreach (DummyResult result in results) { - Assert.Equal(dummyResultString, result.Result); + Assert.Equal(dummyArg, result.Result); + } + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithRawStreamModule_InvokesFromStream() + { + // Arrange + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); + MemoryStream memoryStream = CreateMemoryStream(_dummyExportsMultipleFunctionsModule); + + // Act + await testSubject.InvokeFromStreamAsync(memoryStream, DUMMY_CACHE_IDENTIFIER, "setString", new[] { dummyArg }).ConfigureAwait(false); + + // Assert + DummyResult result = await testSubject. + InvokeFromStreamAsync(memoryStream, DUMMY_CACHE_IDENTIFIER, "getString").ConfigureAwait(false); + Assert.Equal(dummyArg, result.Result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithRawStreamModule_IsThreadSafe() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); + + // Act + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) + { + var thread = new Thread(() => + { + MemoryStream memoryStream = CreateMemoryStream(_dummyExportsMultipleFunctionsModule); + testSubject.InvokeFromStreamAsync(memoryStream, DUMMY_CACHE_IDENTIFIER, "incrementNumber").GetAwaiter().GetResult(); + }); + threads.Add(thread); + thread.Start(); } + foreach (Thread thread in threads) + { + thread.Join(); + } + + // Assert + int result = await testSubject. + InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.Equal(numThreads, result); } - [Fact(Timeout = _timeoutMS)] - public async void InvokeFromStringAsync_InvokesJavascript() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_InvokesFromCacheIfModuleIsCached() { // Arrange - const string dummyResultString = "success"; + const string dummyArg = "success"; HttpNodeJSService testSubject = CreateHttpNodeJSService(); + MemoryStream memoryStream = CreateMemoryStream(_dummyExportsMultipleFunctionsModule); + await testSubject.InvokeFromStreamAsync(memoryStream, DUMMY_CACHE_IDENTIFIER, "setString", new[] { dummyArg }).ConfigureAwait(false); // Act DummyResult result = await testSubject. - InvokeFromStringAsync("module.exports = (callback, resultString) => callback(null, {result: resultString});", args: new[] { dummyResultString }).ConfigureAwait(false); + InvokeFromStreamAsync(() => null, DUMMY_CACHE_IDENTIFIER, "getString").ConfigureAwait(false); // Assert - Assert.Equal(dummyResultString, result.Result); + Assert.Equal(dummyArg, result.Result); } - [Fact(Timeout = _timeoutMS)] - public async void InvokeFromStringAsync_LoadsRequiredModulesFromNodeModulesInProjectDirectory() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_InvokesFromStreamAndCachesModuleIfModuleIsNotCached() { // Arrange - const string dummyCode = @"public string ExampleFunction(string arg) -{ - // Example comment - return arg + ""dummyString""; -}"; - HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: Directory.GetCurrentDirectory() + "/../../../Javascript"); // Current directory is /bin/debug/ + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + MemoryStream memoryStream = CreateMemoryStream(_dummyReturnsArgModule); // Act - string result = await testSubject.InvokeFromStringAsync(@"const prismjs = require('prismjs'); -require('prismjs/components/prism-csharp'); + // Module hasn't been cached, so if this returns the expected value, stream was sent over + DummyResult result1 = await testSubject. + InvokeFromStreamAsync(() => memoryStream, DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).ConfigureAwait(false); -module.exports = (callback, code) => { - var result = prismjs.highlight(code, prismjs.languages.csharp, 'csharp'); + // Assert + // Ensure module was cached + (bool success, DummyResult result2) = await testSubject. + TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).ConfigureAwait(false); + Assert.Equal(dummyArg, result1.Result); + Assert.True(success); + Assert.Equal(dummyArg, result2.Result); + } - callback(null, result); -};", args: new[] { dummyCode }).ConfigureAwait(false); + [Fact(Timeout = TIMEOUT_MS)] + public void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_IsThreadSafe() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + + // Act + var results = new ConcurrentQueue(); + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) + { + var thread = new Thread(() => + { + MemoryStream memoryStream = CreateMemoryStream(_dummyExportsMultipleFunctionsModule); + results.Enqueue(testSubject.InvokeFromStreamAsync(() => memoryStream, DUMMY_CACHE_IDENTIFIER, "incrementAndGetNumber").GetAwaiter().GetResult()); + }); + threads.Add(thread); + thread.Start(); + } + foreach (Thread thread in threads) + { + thread.Join(); + } // Assert - const string expectedResult = @"public string ExampleFunction(string arg) -{ - // Example comment - return arg + ""dummyString""; -}"; - Assert.Equal(expectedResult, result); + Assert.Equal(numThreads, results.Count); + // Module shouldn't get cached more than once, we should get exactly [1,2,3,4,5] + List resultsList = results.ToList(); + resultsList.Sort(); + for (int i = 0; i < numThreads; i++) + { + Assert.Equal(resultsList[i], i + 1); + } } - [Fact(Timeout = _timeoutMS)] - public async void InvokeFromStringAsync_LoadsRequiredModuleFromFileInProjectDirectory() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithModuleFactory_InvokesFromCacheIfModuleIsCached() { // Arrange - HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: Directory.GetCurrentDirectory() + "/../../../Javascript"); // Current directory is /bin/debug/ + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + MemoryStream memoryStream = CreateMemoryStream(_dummyExportsMultipleFunctionsModule); + await testSubject.InvokeFromStreamAsync(memoryStream, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); // Act - int result = await testSubject.InvokeFromStringAsync(@"const value = require('./dummyReturnsValueModule.js'); + await testSubject. + InvokeFromStreamAsync(() => null, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); -module.exports = (callback) => { + // Assert + int result = await testSubject.InvokeFromStreamAsync(memoryStream, DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.Equal(2, result); + } - callback(null, value); -};").ConfigureAwait(false); + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithModuleFactory_InvokesFromStreamAndCachesModuleIfModuleIsNotCached() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + MemoryStream memoryStream = CreateMemoryStream(_dummyExportsMultipleFunctionsModule); + + // Act + await testSubject. + InvokeFromStreamAsync(() => memoryStream, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); // Assert - Assert.Equal(10, result); // dummyReturnsValueModule.js just exports 10 + // Ensure module was cached + (bool success, int result) = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.True(success); + Assert.Equal(1, result); } - [Fact(Timeout = _timeoutMS)] - public void InvokeFromStringAsync_IsThreadSafe() + [Fact(Timeout = TIMEOUT_MS)] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithModuleFactory_IsThreadSafe() { // Arrange - const string dummyModule = "module.exports = (callback, resultString) => callback(null, {result: resultString});"; - const string dummyResultString = "success"; HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act - var results = new ConcurrentQueue(); const int numThreads = 5; var threads = new List(); for (int i = 0; i < numThreads; i++) { - var thread = new Thread(() => results.Enqueue(testSubject.InvokeFromStringAsync(dummyModule, args: new[] { dummyResultString }).GetAwaiter().GetResult())); + var thread = new Thread(() => + { + MemoryStream memoryStream = CreateMemoryStream(_dummyExportsMultipleFunctionsModule); + testSubject.InvokeFromStreamAsync(memoryStream, DUMMY_CACHE_IDENTIFIER, "incrementNumber").GetAwaiter().GetResult(); + }); threads.Add(thread); thread.Start(); } @@ -325,42 +571,57 @@ public void InvokeFromStringAsync_IsThreadSafe() } // Assert - Assert.Equal(numThreads, results.Count); - foreach (DummyResult result in results) - { - Assert.Equal(dummyResultString, result.Result); - } + (bool success, int result) = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.True(success); + Assert.Equal(numThreads, result); } - [Fact(Timeout = _timeoutMS)] - public async void InvokeFromFileAsync_InvokesJavascript() + [Fact(Timeout = TIMEOUT_MS)] + public async void TryInvokeFromCacheAsync_WithTypeParameter_InvokesFromCacheIfModuleIsCached() { - const string dummyResultString = "success"; - HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: Directory.GetCurrentDirectory() + "/Javascript"); + // Arrange + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + await testSubject.InvokeFromStringAsync(_dummyReturnsArgModule, DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).ConfigureAwait(false); // Act - DummyResult result = await testSubject. - InvokeFromFileAsync("dummyModule.js", args: new[] { dummyResultString }).ConfigureAwait(false); + (bool success, DummyResult value) = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).ConfigureAwait(false); // Assert - Assert.Equal(dummyResultString, result.Result); + Assert.True(success); + Assert.Equal(dummyArg, value.Result); } - [Fact(Timeout = _timeoutMS)] - public void InvokeFromFileAsync_IsThreadSafe() + [Fact(Timeout = TIMEOUT_MS)] + public async void TryInvokeFromCacheAsync_WithTypeParameter_ReturnsFalseIfModuleIsNotCached() { // Arrange - const string dummyModule = "dummyModule.js"; - const string dummyResultString = "success"; - HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: Directory.GetCurrentDirectory() + "/Javascript"); + HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act - var results = new ConcurrentQueue(); + (bool success, DummyResult value) = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, args: new[] { "success" }).ConfigureAwait(false); + + // Assert + Assert.False(success); + Assert.Null(value); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void TryInvokeFromCacheAsync_WithTypeParameter_IsThreadSafe() + { + // Arrange + const string dummyArg = "success"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + await testSubject.InvokeFromStringAsync(_dummyReturnsArgModule, DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).ConfigureAwait(false); + + // Act + var results = new ConcurrentQueue<(bool, DummyResult)>(); const int numThreads = 5; var threads = new List(); for (int i = 0; i < numThreads; i++) { - var thread = new Thread(() => results.Enqueue(testSubject.InvokeFromFileAsync(dummyModule, args: new[] { dummyResultString }).GetAwaiter().GetResult())); + var thread = new Thread(() => results.Enqueue(testSubject. + TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, args: new[] { dummyArg }).GetAwaiter().GetResult())); threads.Add(thread); thread.Start(); } @@ -371,13 +632,116 @@ public void InvokeFromFileAsync_IsThreadSafe() // Assert Assert.Equal(numThreads, results.Count); - foreach (DummyResult result in results) + foreach ((bool success, DummyResult value) in results) + { + Assert.True(success); + Assert.Equal(dummyArg, value.Result); + } + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void TryInvokeFromCacheAsync_WithoutTypeParameter_InvokesFromCacheIfModuleIsCached() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); + + // Act + bool success = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); + + // Assert + Assert.True(success); + int result = await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.Equal(2, result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void TryInvokeFromCacheAsync_WithoutTypeParameter_ReturnsFalseIfModuleIsNotCached() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + + // Act + bool success = await testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER).ConfigureAwait(false); + + // Assert + Assert.False(success); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void TryInvokeFromCacheAsync_WithoutTypeParameter_IsThreadSafe() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(); + await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "incrementNumber").ConfigureAwait(false); + + // Act + const int numThreads = 5; + var threads = new List(); + for (int i = 0; i < numThreads; i++) + { + var thread = new Thread(() => testSubject.TryInvokeFromCacheAsync(DUMMY_CACHE_IDENTIFIER, "incrementNumber").GetAwaiter().GetResult()); + threads.Add(thread); + thread.Start(); + } + foreach (Thread thread in threads) { - Assert.Equal(dummyResultString, result.Result); + thread.Join(); } + + // Assert + int result = await testSubject.InvokeFromStringAsync(_dummyExportsMultipleFunctionsModule, DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false); + Assert.Equal(numThreads + 1, result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InMemoryInvokeMethods_LoadRequiredModulesFromNodeModulesInProjectDirectory() + { + // Arrange + const string dummyCode = @"public string ExampleFunction(string arg) +{ + // Example comment + return arg + ""dummyString""; +}"; + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); + + // Act + string result = await testSubject.InvokeFromStringAsync(@"const prismjs = require('prismjs'); +require('prismjs/components/prism-csharp'); + +module.exports = (callback, code) => { + var result = prismjs.highlight(code, prismjs.languages.csharp, 'csharp'); + + callback(null, result); +};", args: new[] { dummyCode }).ConfigureAwait(false); + + // Assert + const string expectedResult = @"public string ExampleFunction(string arg) +{ + // Example comment + return arg + ""dummyString""; +}"; + Assert.Equal(expectedResult, result); + } + + [Fact(Timeout = TIMEOUT_MS)] + public async void InMemoryInvokeMethods_LoadRequiredModulesFromFilesInProjectDirectory() + { + // Arrange + HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); // Current directory is /bin/debug/ + + // Act + int result = await testSubject.InvokeFromStringAsync(@"const value = require('./dummyReturnsValueModule.js'); + +module.exports = (callback) => { + callback(null, value); +};").ConfigureAwait(false); + + // Assert + Assert.Equal(10, result); // dummyReturnsValueModule.js just exports 10 } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void AllInvokeMethods_ThrowInvocationExceptionIfModuleHasNoExports() { // Arrange @@ -393,24 +757,23 @@ public async void AllInvokeMethods_ThrowInvocationExceptionIfModuleHasNoExports( Assert.StartsWith($"The module \"{dummyModule}...\" has no exports. Ensure that the module assigns a function or an object containing functions to module.exports.", result.Message); } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void AllInvokeMethods_ThrowInvocationExceptionIfThereIsNoModuleExportWithSpecifiedExportName() { // Arrange const string dummyExportName = "dummyExportName"; - const string dummyCacheIdentifier = "dummyCacheIdentifier"; HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act InvocationException result = await Assert.ThrowsAsync(() => - testSubject.InvokeFromStringAsync("module.exports = (callback) => callback(null, {result: 'success'});", dummyCacheIdentifier, dummyExportName)). + testSubject.InvokeFromStringAsync("module.exports = (callback) => callback(null, {result: 'success'});", DUMMY_CACHE_IDENTIFIER, dummyExportName)). ConfigureAwait(false); // Assert - Assert.StartsWith($"The module {dummyCacheIdentifier} has no export named {dummyExportName}.", result.Message); + Assert.StartsWith($"The module {DUMMY_CACHE_IDENTIFIER} has no export named {dummyExportName}.", result.Message); } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void AllInvokeMethods_ThrowInvocationExceptionIfModuleExportWithSpecifiedExportNameIsNotAFunction() { // Arrange @@ -426,7 +789,7 @@ public async void AllInvokeMethods_ThrowInvocationExceptionIfModuleExportWithSpe Assert.StartsWith($"The export named {dummyExportName} from module \"module.exports = {{dummyEx...\" is not a function.", result.Message); } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void AllInvokeMethods_ThrowInvocationExceptionIfNoExportNameSpecifiedAndModuleExportsIsNotAFunction() { // Arrange @@ -441,7 +804,7 @@ public async void AllInvokeMethods_ThrowInvocationExceptionIfNoExportNameSpecifi Assert.StartsWith("The module \"module.exports = {result:...\" does not export a function.", result.Message); } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void AllInvokeMethods_ThrowInvocationExceptionIfInvokedMethodCallsCallbackWithError() { // Arrange @@ -457,7 +820,7 @@ public async void AllInvokeMethods_ThrowInvocationExceptionIfInvokedMethodCallsC Assert.StartsWith(dummyErrorString, result.Message); // Complete message includes the stack } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void AllInvokeMethods_ThrowInvocationExceptionIfInvokedAsyncMethodThrowsError() { // Arrange @@ -473,41 +836,41 @@ public async void AllInvokeMethods_ThrowInvocationExceptionIfInvokedAsyncMethodT Assert.StartsWith(dummyErrorString, result.Message); // Complete message includes the stack } - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public async void AllInvokeMethods_InvokeAsyncJavascriptMethods() { // Arrange - const string dummyResultString = "success"; + const string dummyArg = "success"; HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act DummyResult result = await testSubject. - InvokeFromStringAsync("module.exports = async (resultString) => {return {result: resultString};}", args: new[] { dummyResultString }).ConfigureAwait(false); + InvokeFromStringAsync("module.exports = async (resultString) => {return {result: resultString};}", args: new[] { dummyArg }).ConfigureAwait(false); // Assert - Assert.Equal(dummyResultString, result.Result); + Assert.Equal(dummyArg, result.Result); } - [Fact(Timeout = _timeoutMS)] - public async void AllInvokeMethods_InvokeASpecificExportIfExportNameIsProvided() + [Fact(Timeout = TIMEOUT_MS)] + public async void AllInvokeMethods_InvokeASpecificExportIfExportNameIsNotNull() { // Arrange - const string dummyResultString = "success"; + const string dummyArg = "success"; const string dummyExportName = "dummyExportName"; HttpNodeJSService testSubject = CreateHttpNodeJSService(); // Act DummyResult result = await testSubject. - InvokeFromStringAsync($"module.exports = {{ {dummyExportName}: (callback, resultString) => callback(null, {{result: resultString}}) }};", exportName: dummyExportName, args: new[] { dummyResultString }).ConfigureAwait(false); + InvokeFromStringAsync($"module.exports = {{ {dummyExportName}: (callback, resultString) => callback(null, {{result: resultString}}) }};", exportName: dummyExportName, args: new[] { dummyArg }).ConfigureAwait(false); // Assert - Assert.Equal(dummyResultString, result.Result); + Assert.Equal(dummyArg, result.Result); } - // TODO these tests don't pass reliably because Node.js randomly truncates stdout/stderr - https://github.com/nodejs/node/issues/6456. - // They're are still useful for diagnosing issues with output piping. - // Tests the interaction between the Http server and OutOfProcessNodeJSService.TryCreateMessage - [Theory(Timeout = _timeoutMS, Skip = "Node.js randomly truncates stdout/stderr")] + // TODO doesn't pass reliably because Node.js randomly truncates stdout/stderr - https://github.com/nodejs/node/issues/6456. + // Still useful for diagnosing issues with output piping. + // We're using Thread.Sleep(1000) to minimize issues, but it doesn't work all the time. + [Theory(Timeout = TIMEOUT_MS, Skip = "Node.js randomly truncates stdout/stderr")] [MemberData(nameof(AllInvokeMethods_ReceiveAndLogStdoutOutput_Data))] public async void AllInvokeMethods_ReceiveAndLogStdoutOutput(string dummyLogArgument, string expectedResult) { @@ -528,13 +891,6 @@ await testSubject. // Does not work // process.stdout.write('', 'utf8', () => callback()); }}").ConfigureAwait(false); - // Disposing of HttpNodeServices causes Process.Kill and Process.WaitForExit(500) to be called on the node process, this gives it time for it to flush its output. - // - // TODO On Linux and macOS, Node.js does not flush stdout completely when Process.Kill is called, even if Process.WaitForExit is called immediately after. - // Note that console.log just writes to stdout under the hood -https://nodejs.org/docs/latest-v8.x/api/console.html#console_console_log_data_args. - // There flakiness causes this test to fail randomly. The whole stdout flushing issue seems like a persistent Node.js problem - https://github.com/nodejs/node/issues/6456. - // Several attempts have been made to flush/write to stdout synchronously in the js above, to no avail. - // The following Thread.Sleep(1000) works almost all the time, but isn't a clean solution. Thread.Sleep(1000); ((IDisposable)_serviceProvider).Dispose(); string result = resultStringBuilder.ToString(); @@ -558,7 +914,10 @@ public static IEnumerable AllInvokeMethods_ReceiveAndLogStdoutOutput_D }; } - [Theory(Timeout = _timeoutMS, Skip = "Node.js randomly truncates stdout/stderr")] + // TODO doesn't pass reliably because Node.js randomly truncates stdout/stderr - https://github.com/nodejs/node/issues/6456. + // Still useful for diagnosing issues with output piping. + // We're using Thread.Sleep(1000) to minimize issues, but it doesn't work all the time. + [Theory(Timeout = TIMEOUT_MS, Skip = "Node.js randomly truncates stdout/stderr")] [MemberData(nameof(AllInvokeMethods_ReceiveAndLogStderrOutput_Data))] public async void AllInvokeMethods_ReceiveAndLogStderrOutput(string dummyLogArgument, string expectedResult) { @@ -572,14 +931,7 @@ await testSubject. console.error({dummyLogArgument}); callback(); }}").ConfigureAwait(false); - // Disposing of HttpNodeServices causes Process.Kill and Process.WaitForExit(500) to be called on the node process, this gives it time for it to flush its output. - // - // TODO On Linux and macOS, Node.js does not flush stderr completely when Process.Kill is called, even if Process.WaitForExit is called immediately after. - // Note that console.log just writes to stderr under the hood -https://nodejs.org/docs/latest-v8.x/api/console.html#console_console_log_data_args. - // There flakiness causes this test to fail randomly. The whole stderr flushing issue seems like a persistent Node.js problem - https://github.com/nodejs/node/issues/6456. - // Several attempts have been made to flush/write to stderr synchronously in the js above, to no avail. - // The following Thread.Sleep(500) works almost all the time, but isn't a clean solution. - Thread.Sleep(500); + Thread.Sleep(1000); ((IDisposable)_serviceProvider).Dispose(); string result = resultStringBuilder.ToString(); @@ -628,7 +980,7 @@ private HttpNodeJSService CreateHttpNodeJSService(StringBuilder loggerStringBuil } }); - if (Debugger.IsAttached && _debugNodeJS) + if (Debugger.IsAttached && DEBUG_NODEJS) { services.Configure(options => options.NodeAndV8Options = "--inspect-brk"); // An easy way to step through NodeJS code is to use Chrome. Consider option 1 from this list https://nodejs.org/en/docs/guides/debugging-getting-started/#chrome-devtools-55. services.Configure(options => options.TimeoutMS = -1); @@ -639,6 +991,19 @@ private HttpNodeJSService CreateHttpNodeJSService(StringBuilder loggerStringBuil return _serviceProvider.GetRequiredService() as HttpNodeJSService; } + private MemoryStream CreateMemoryStream(string value) + { +#pragma warning disable IDE0067 + var memoryStream = new MemoryStream(); + var streamWriter = new StreamWriter(memoryStream); +#pragma warning disable IDE0067 + streamWriter.Write(value); + streamWriter.Flush(); + memoryStream.Position = 0; + + return memoryStream; + } + private class DummyResult { public string Result { get; set; } diff --git a/test/NodeJS/HttpNodeJSServiceUnitTests.cs b/test/NodeJS/HttpNodeJSServiceUnitTests.cs index dec9d1a..22a799e 100644 --- a/test/NodeJS/HttpNodeJSServiceUnitTests.cs +++ b/test/NodeJS/HttpNodeJSServiceUnitTests.cs @@ -22,7 +22,7 @@ public class HttpNodeJSServiceUnitTests public async Task TryInvokeAsync_ReturnsTupleContainingFalseAndDefaultIfHttpResponseHas404StatusCode() { // Arrange - var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, moduleSource: "dummyModuleSource"); + var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, "dummyModuleSource"); Mock mockRequestHttpContent = _mockRepository.Create(); // HttpContent is an abstract class Mock mockHttpContentFactory = _mockRepository.Create(); mockHttpContentFactory.Setup(h => h.Create(dummyInvocationRequest)).Returns(mockRequestHttpContent.Object); @@ -32,23 +32,24 @@ public async Task TryInvokeAsync_ReturnsTupleContainingFalseAndDefaultIfHttpResp HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)). ReturnsAsync(dummyHttpResponseMessage); - using (ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, httpClientService: mockHttpClientService.Object)) - { - // Act - (bool success, string value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, httpClientService: mockHttpClientService.Object); +#pragma warning disable IDE0067 - // Assert - _mockRepository.VerifyAll(); - Assert.False(success); - Assert.Null(value); - } + // Act + (bool success, string value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.False(success); + Assert.Null(value); } [Fact] public async Task TryInvokeAsync_ThrowsInvocationExceptionIfHttpResponseHas500StatusCode() { // Arrange - var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, moduleSource: "dummyModuleSource"); + var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, "dummyModuleSource"); Mock mockRequestHttpContent = _mockRepository.Create(); // HttpContent is an abstract class Mock mockHttpContentFactory = _mockRepository.Create(); mockHttpContentFactory.Setup(h => h.Create(dummyInvocationRequest)).Returns(mockRequestHttpContent.Object); @@ -61,22 +62,51 @@ public async Task TryInvokeAsync_ThrowsInvocationExceptionIfHttpResponseHas500St var dummyInvocationError = new InvocationError("dummyErrorMessage", "dummyErrorStack"); Mock mockJsonService = _mockRepository.Create(); mockJsonService.Setup(j => j.DeserializeAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(dummyInvocationError); - using (ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, httpClientService: mockHttpClientService.Object, - jsonService: mockJsonService.Object)) - { - // Act and assert - InvocationException result = await Assert.ThrowsAsync(() => testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None)).ConfigureAwait(false); - _mockRepository.VerifyAll(); - Assert.Equal(dummyInvocationError.ErrorMessage + Environment.NewLine + dummyInvocationError.ErrorStack, result.Message, ignoreLineEndingDifferences: true); - } + jsonService: mockJsonService.Object); +#pragma warning disable IDE0067 + + // Act and assert + InvocationException result = await Assert.ThrowsAsync(() => testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None)).ConfigureAwait(false); + _mockRepository.VerifyAll(); + Assert.Equal(dummyInvocationError.ErrorMessage + Environment.NewLine + dummyInvocationError.ErrorStack, result.Message, ignoreLineEndingDifferences: true); + } + + [Fact] + public async Task TryInvokeAsync_ReturnsTupleContainingTrueAndNullIfHttpResponseHas200StatusCodeAndTypeParameterIsVoid() + { + // Arrange + var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, "dummyModuleSource"); + Mock mockRequestHttpContent = _mockRepository.Create(); // HttpContent is an abstract class + Mock mockHttpContentFactory = _mockRepository.Create(); + mockHttpContentFactory.Setup(h => h.Create(dummyInvocationRequest)).Returns(mockRequestHttpContent.Object); + var dummyHttpResponseMessage = new HttpResponseMessage(HttpStatusCode.OK); + Mock mockHttpClientService = _mockRepository.Create(); + mockHttpClientService.Setup(h => h.SendAsync(It.Is(hr => ReferenceEquals(hr.Content, mockRequestHttpContent.Object)), + HttpCompletionOption.ResponseHeadersRead, + CancellationToken.None)). + ReturnsAsync(dummyHttpResponseMessage); +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, + httpClientService: mockHttpClientService.Object); +#pragma warning disable IDE0067 + + // Act + (bool success, Void value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.True(success); + Assert.Null(value); } [Fact] public async Task TryInvokeAsync_ReturnsTupleContainingTrueAndAStreamIfHttpResponseHas200StatusCodeAndTypeParameterIsStream() { // Arrange - var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, moduleSource: "dummyModuleSource"); + var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, "dummyModuleSource"); Mock mockRequestHttpContent = _mockRepository.Create(); // HttpContent is an abstract class Mock mockHttpContentFactory = _mockRepository.Create(); mockHttpContentFactory.Setup(h => h.Create(dummyInvocationRequest)).Returns(mockRequestHttpContent.Object); @@ -86,24 +116,25 @@ public async Task TryInvokeAsync_ReturnsTupleContainingTrueAndAStreamIfHttpRespo HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)). ReturnsAsync(dummyHttpResponseMessage); - using (ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, - httpClientService: mockHttpClientService.Object)) - { - // Act - (bool success, Stream value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, + httpClientService: mockHttpClientService.Object); +#pragma warning disable IDE0067 - // Assert - _mockRepository.VerifyAll(); - Assert.True(success); - Assert.NotNull(value); - } + // Act + (bool success, Stream value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.True(success); + Assert.NotNull(value); } [Fact] public async Task TryInvokeAsync_ReturnsTupleContainingTrueAndAStringIfHttpResponseHas200StatusCodeAndTypeParameterIsString() { // Arrange - var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, moduleSource: "dummyModuleSource"); + var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, "dummyModuleSource"); Mock mockRequestHttpContent = _mockRepository.Create(); // HttpContent is an abstract class Mock mockHttpContentFactory = _mockRepository.Create(); mockHttpContentFactory.Setup(h => h.Create(dummyInvocationRequest)).Returns(mockRequestHttpContent.Object); @@ -115,24 +146,25 @@ public async Task TryInvokeAsync_ReturnsTupleContainingTrueAndAStringIfHttpRespo HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)). ReturnsAsync(dummyHttpResponseMessage); - using (ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, - httpClientService: mockHttpClientService.Object)) - { - // Act - (bool success, string value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, + httpClientService: mockHttpClientService.Object); +#pragma warning disable IDE0067 - // Assert - _mockRepository.VerifyAll(); - Assert.True(success); - Assert.Equal(dummyValue, value); - } + // Act + (bool success, string value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.True(success); + Assert.Equal(dummyValue, value); } [Fact] public async Task TryInvokeAsync_ReturnsTupleContainingTrueAndAnObjectIfHttpResponseHas200StatusCodeAndTypeParameterIsAnObject() { // Arrange - var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, moduleSource: "dummyModuleSource"); + var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, "dummyModuleSource"); Mock mockRequestHttpContent = _mockRepository.Create(); // HttpContent is an abstract class Mock mockHttpContentFactory = _mockRepository.Create(); mockHttpContentFactory.Setup(h => h.Create(dummyInvocationRequest)).Returns(mockRequestHttpContent.Object); @@ -145,25 +177,26 @@ public async Task TryInvokeAsync_ReturnsTupleContainingTrueAndAnObjectIfHttpResp var dummyObject = new DummyClass(); Mock mockJsonService = _mockRepository.Create(); mockJsonService.Setup(j => j.DeserializeAsync(It.IsAny(), CancellationToken.None)).ReturnsAsync(dummyObject); - using (ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, httpClientService: mockHttpClientService.Object, - jsonService: mockJsonService.Object)) - { - // Act - (bool success, DummyClass value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); + jsonService: mockJsonService.Object); +#pragma warning disable IDE0067 - // Assert - _mockRepository.VerifyAll(); - Assert.True(success); - Assert.Same(dummyObject, value); - } + // Act + (bool success, DummyClass value) = await testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.True(success); + Assert.Same(dummyObject, value); } [Fact] public async Task TryInvokeAsync_ThrowsInvocationExceptionIfHttpResponseHasAnUnexpectedStatusCode() { // Arrange - var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, moduleSource: "dummyModuleSource"); + var dummyInvocationRequest = new InvocationRequest(ModuleSourceType.Cache, "dummyModuleSource"); Mock mockRequestHttpContent = _mockRepository.Create(); // HttpContent is an abstract class Mock mockHttpContentFactory = _mockRepository.Create(); mockHttpContentFactory.Setup(h => h.Create(dummyInvocationRequest)).Returns(mockRequestHttpContent.Object); @@ -174,15 +207,16 @@ public async Task TryInvokeAsync_ThrowsInvocationExceptionIfHttpResponseHasAnUne HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)). ReturnsAsync(dummyHttpResponseMessage); - using (ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, httpClientService: mockHttpClientService.Object)) - { - // Act and assert - InvocationException result = await Assert.ThrowsAsync(() => testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None)).ConfigureAwait(false); +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(httpContentFactory: mockHttpContentFactory.Object, httpClientService: mockHttpClientService.Object); +#pragma warning disable IDE0067 - // Assert - _mockRepository.VerifyAll(); - Assert.Equal(string.Format(Strings.InvocationException_HttpNodeJSService_UnexpectedStatusCode, dummyHttpStatusCode), result.Message); - } + // Act and assert + InvocationException result = await Assert.ThrowsAsync(() => testSubject.ExposedTryInvokeAsync(dummyInvocationRequest, CancellationToken.None)).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(string.Format(Strings.InvocationException_HttpNodeJSService_UnexpectedStatusCode, dummyHttpStatusCode), result.Message); } [Theory] @@ -191,14 +225,15 @@ public void OnConnectionEstablishedMessageReceived_ExtractsEndPoint(string dummy { // Arrange string dummyConnectionEstablishedMessage = $"[Jering.Javascript.NodeJS: Listening on IP - {dummyIP} Port - {dummyPort}]"; - using (ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService()) - { - // Act - testSubject.ExposedOnConnectionEstablishedMessageReceived(dummyConnectionEstablishedMessage); +#pragma warning disable IDE0067 + ExposedHttpNodeJSService testSubject = CreateHttpNodeJSService(); +#pragma warning disable IDE0067 - // Assert - Assert.Equal(expectedResult, testSubject.Endpoint.AbsoluteUri); - } + // Act + testSubject.ExposedOnConnectionEstablishedMessageReceived(dummyConnectionEstablishedMessage); + + // Assert + Assert.Equal(expectedResult, testSubject.Endpoint.AbsoluteUri); } public static IEnumerable OnConnectionEstablishedMessageReceived_ExtractsEndPoint_Data() @@ -222,7 +257,7 @@ private ExposedHttpNodeJSService CreateHttpNodeJSService(IOptions mockLogger = _mockRepository.Create(); Mock mockLoggerFactory = _mockRepository.Create(); diff --git a/test/NodeJS/Javascript/dummyExportsMultipleFunctionsModule.js b/test/NodeJS/Javascript/dummyExportsMultipleFunctionsModule.js new file mode 100644 index 0000000..fc0115d --- /dev/null +++ b/test/NodeJS/Javascript/dummyExportsMultipleFunctionsModule.js @@ -0,0 +1,19 @@ +// Used by HttpNodeJSServiceIntegrationTests +var persistentString; +var persistentNumber = 0; + +module.exports = { + setString: (callback, value) => { + persistentString = value; + callback(); + }, + getString: (callback) => callback(null, { result: persistentString }), + incrementNumber: (callback) => { + persistentNumber++; + callback(); + }, + getNumber: (callback) => callback(null, persistentNumber), + incrementAndGetNumber: (callback) => { + callback(null, ++persistentNumber); + } +}; diff --git a/test/NodeJS/Javascript/dummyModule.js b/test/NodeJS/Javascript/dummyReturnsArgModule.js similarity index 100% rename from test/NodeJS/Javascript/dummyModule.js rename to test/NodeJS/Javascript/dummyReturnsArgModule.js diff --git a/test/NodeJS/Jering.Javascript.NodeJS.Tests.csproj b/test/NodeJS/Jering.Javascript.NodeJS.Tests.csproj index ed806b3..cfb0a41 100644 --- a/test/NodeJS/Jering.Javascript.NodeJS.Tests.csproj +++ b/test/NodeJS/Jering.Javascript.NodeJS.Tests.csproj @@ -39,7 +39,6 @@ - From 69119aa8f5b2182b00651e5906de04c7d83edf22 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Mon, 2 Dec 2019 21:28:16 +0800 Subject: [PATCH 06/11] Fixed InvocationRequest constructor throwing ArgumentExceptions when it should have been throwing ArgumentNullExceptions. --- .../InvocationData/InvocationRequest.cs | 50 ++++++++----------- test/NodeJS/InvocationRequestUnitTests.cs | 11 ++-- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/src/NodeJS/InvocationData/InvocationRequest.cs b/src/NodeJS/InvocationData/InvocationRequest.cs index 1e3e3c8..1b55a23 100644 --- a/src/NodeJS/InvocationData/InvocationRequest.cs +++ b/src/NodeJS/InvocationData/InvocationRequest.cs @@ -14,27 +14,21 @@ public class InvocationRequest /// /// Creates an instance. /// - /// The source type of the module to be invoked. - /// The source of the module to be invoked. - /// The source can be the path of the module relative to , - /// the module as a string, or the cache identifier of the module. - /// If is not , this parameter must be specified. - /// Additionally, if is or , this parameter must not be an empty string - /// or whitespace. + /// The source type of the module. + /// + /// The module's source. + /// This value may be the path of the module relative to , the module as a string, or the module's cache identifier. + /// If is not , this value must not be null. Additionally, if + /// is or , this value must not be an empty string or whitespace. /// - /// The new cache identifier for the module to be invoked. - /// If this parameter is not specified, the module will not be cached. If it is specified, this parameter must not be an empty string or whitespace. - /// The name of the function in the module's exports to invoke. - /// If this value is not specified, the module's exports object is assumed to be a function, and that function is invoked. - /// If it is specified, it must not be an empty string or whitespace. - /// The arguments for the function to invoke. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module's exports. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The module as a . - /// Thrown if is but - /// is null. - /// Thrown if is or - /// but is null, whitespace or an empty string. - /// Thrown if is but - /// is null. + /// Thrown if is but is null. + /// Thrown if is but is null. + /// Thrown if is or but + /// is null, whitespace or an empty string. public InvocationRequest(ModuleSourceType moduleSourceType, string moduleSource = null, string newCacheIdentifier = null, @@ -47,7 +41,7 @@ public InvocationRequest(ModuleSourceType moduleSourceType, // moduleSourceType is stream but moduleStreamSource is null if (moduleStreamSource == null) { - throw new ArgumentException(Strings.ArgumentException_InvocationRequest_ModuleStreamSourceCannotBeNull, nameof(moduleStreamSource)); + throw new ArgumentNullException(nameof(moduleStreamSource), Strings.ArgumentException_InvocationRequest_ModuleStreamSourceCannotBeNull); } if (moduleStreamSource.CanSeek) @@ -61,12 +55,11 @@ public InvocationRequest(ModuleSourceType moduleSourceType, if (moduleSource == null) { // moduleSourceType is cache but moduleSource is null - throw new ArgumentException(Strings.ArgumentException_InvocationRequest_ModuleSourceCannotBeNull, nameof(moduleSource)); + throw new ArgumentNullException(nameof(moduleSource), Strings.ArgumentException_InvocationRequest_ModuleSourceCannotBeNull); } } - else if (string.IsNullOrWhiteSpace(moduleSource)) + else if (string.IsNullOrWhiteSpace(moduleSource)) // moduleSourceType is file or string but moduleSource is null, whitespace or an empty string { - // moduleSourceType is file or string but moduleSource is null, whitespace or an empty string throw new ArgumentException(Strings.ArgumentException_InvocationRequest_ModuleSourceCannotBeNullWhitespaceOrAnEmptyString, nameof(moduleSource)); } @@ -102,7 +95,8 @@ public void ResetStreamPosition() /// /// Thrown if is null. /// Thrown if is an unseekable . - public bool CheckStreamAtInitialPosition() { + public bool CheckStreamAtInitialPosition() + { if (ModuleStreamSource == null) { throw new InvalidOperationException(Strings.InvalidOperationException_InvocationRequest_StreamIsNull); @@ -117,17 +111,17 @@ public bool CheckStreamAtInitialPosition() { } /// - /// Gets the source type of the module to be invoked. + /// Gets the source type of the module. /// public ModuleSourceType ModuleSourceType { get; } /// - /// Gets the source of the module to be invoked. + /// Gets the module's source /// public string ModuleSource { get; } /// - /// Gets the new cache identifier for the module to be invoked. + /// Gets the module's cache identifier. /// public string NewCacheIdentifier { get; } @@ -137,7 +131,7 @@ public bool CheckStreamAtInitialPosition() { public string ExportName { get; } /// - /// Gets the arguments for the function to invoke. + /// Gets the sequence of JSON-serializable arguments to pass to the function to invoke. /// public object[] Args { get; } diff --git a/test/NodeJS/InvocationRequestUnitTests.cs b/test/NodeJS/InvocationRequestUnitTests.cs index 02c6475..c5629d7 100644 --- a/test/NodeJS/InvocationRequestUnitTests.cs +++ b/test/NodeJS/InvocationRequestUnitTests.cs @@ -11,11 +11,10 @@ public class InvocationRequestUnitTests private readonly MockRepository _mockRepository = new MockRepository(MockBehavior.Default); [Fact] - public void Constructor_ThrowsArgumentExceptionIfModuleSourceTypeIsStreamButModuleStreamSourceIsNull() + public void Constructor_ThrowsArgumentNullExceptionIfModuleSourceTypeIsStreamButModuleStreamSourceIsNull() { // Act and assert - ArgumentException result = Assert.Throws(() => new InvocationRequest(ModuleSourceType.Stream)); - Assert.Equal(Strings.ArgumentException_InvocationRequest_ModuleStreamSourceCannotBeNull + "\nParameter name: moduleStreamSource", result.Message, ignoreLineEndingDifferences: true); + ArgumentNullException result = Assert.Throws(() => new InvocationRequest(ModuleSourceType.Stream)); } [Theory] @@ -24,7 +23,6 @@ public void Constructor_ThrowsArgumentExceptionIfModuleSourceTypeIsFileOrStringB { // Act and assert ArgumentException result = Assert.Throws(() => new InvocationRequest(dummyModuleSourceType, dummyModuleSource)); - Assert.Equal(Strings.ArgumentException_InvocationRequest_ModuleSourceCannotBeNullWhitespaceOrAnEmptyString + "\nParameter name: moduleSource", result.Message, ignoreLineEndingDifferences: true); } public static IEnumerable Constructor_ThrowsArgumentExceptionIfModuleSourceTypeIsFileOrStringButModuleSourceIsNullWhitespaceOrAnEmptyString_Data() @@ -41,11 +39,10 @@ public static IEnumerable Constructor_ThrowsArgumentExceptionIfModuleS } [Fact] - public void Constructor_ThrowsArgumentExceptionIfModuleSourceTypeIsCacheButModuleSourceIsNull() + public void Constructor_ThrowsArgumentNullExceptionIfModuleSourceTypeIsCacheButModuleSourceIsNull() { // Act and assert - ArgumentException result = Assert.Throws(() => new InvocationRequest(ModuleSourceType.Cache)); - Assert.Equal(Strings.ArgumentException_InvocationRequest_ModuleSourceCannotBeNull + "\nParameter name: moduleSource", result.Message, ignoreLineEndingDifferences: true); + ArgumentNullException result = Assert.Throws(() => new InvocationRequest(ModuleSourceType.Cache)); } [Fact] From efbf8d36ce14de60280c3b01ad6d45541b1eac3c Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Mon, 2 Dec 2019 21:46:23 +0800 Subject: [PATCH 07/11] Implemented new INodeJSService API in HttpNodeJSPoolService. --- .../Http/HttpNodeJSPoolService.cs | 49 +++ .../HttpNodeJSPoolServiceIntegrationTests.cs | 9 +- test/NodeJS/HttpNodeJSPoolServiceUnitTests.cs | 369 ++++++++++++++++-- 3 files changed, 393 insertions(+), 34 deletions(-) diff --git a/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSPoolService.cs b/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSPoolService.cs index 162f764..29aa13c 100644 --- a/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSPoolService.cs +++ b/src/NodeJS/NodeJSServiceImplementations/OutOfProcess/Http/HttpNodeJSPoolService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.ObjectModel; using System.IO; using System.Threading; @@ -38,24 +39,72 @@ public Task InvokeFromFileAsync(string modulePath, string exportName = nul return GetHttpNodeJSService().InvokeFromFileAsync(modulePath, exportName, args, cancellationToken); } + /// + public Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().InvokeFromFileAsync(modulePath, exportName, args, cancellationToken); + } + /// public Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { return GetHttpNodeJSService().InvokeFromStringAsync(moduleString, newCacheIdentifier, exportName, args, cancellationToken); } + /// + public Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().InvokeFromStringAsync(moduleString, newCacheIdentifier, exportName, args, cancellationToken); + } + + /// + public Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().InvokeFromStringAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + public Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().InvokeFromStringAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + /// public Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { return GetHttpNodeJSService().InvokeFromStreamAsync(moduleStream, newCacheIdentifier, exportName, args, cancellationToken); } + /// + public Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().InvokeFromStreamAsync(moduleStream, newCacheIdentifier, exportName, args, cancellationToken); + } + + /// + public Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().InvokeFromStreamAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + public Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().InvokeFromStreamAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + /// public Task<(bool, T)> TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { return GetHttpNodeJSService().TryInvokeFromCacheAsync(moduleCacheIdentifier, exportName, args, cancellationToken); } + /// + public Task TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetHttpNodeJSService().TryInvokeFromCacheAsync(moduleCacheIdentifier, exportName, args, cancellationToken); + } + internal HttpNodeJSService GetHttpNodeJSService() { int index = 0; diff --git a/test/NodeJS/HttpNodeJSPoolServiceIntegrationTests.cs b/test/NodeJS/HttpNodeJSPoolServiceIntegrationTests.cs index c3ea899..3301686 100644 --- a/test/NodeJS/HttpNodeJSPoolServiceIntegrationTests.cs +++ b/test/NodeJS/HttpNodeJSPoolServiceIntegrationTests.cs @@ -16,11 +16,12 @@ namespace Jering.Javascript.NodeJS.Tests public class HttpNodeJSPoolServiceIntegrationTests : IDisposable { private IServiceProvider _serviceProvider; - private const int _timeoutMS = 60000; // Set to true to break in NodeJS (see CreateHttpNodeJSPoolService) - private const bool _debugNodeJS = false; + private const bool DEBUG_NODEJS = false; + // Set to -1 when debugging in NodeJS + private const int TIMEOUT_MS = 60000; - [Fact(Timeout = _timeoutMS)] + [Fact(Timeout = TIMEOUT_MS)] public void AllInvokeMethods_InvokeJavascriptInMultipleProcesses() { // Arrange @@ -75,7 +76,7 @@ private HttpNodeJSPoolService CreateHttpNodeJSPoolService(int numProcesses) options.ConcurrencyDegree = numProcesses; }); - if (Debugger.IsAttached && _debugNodeJS) + if (Debugger.IsAttached && DEBUG_NODEJS) { services.Configure(options => options.NodeAndV8Options = "--inspect-brk"); // An easy way to step through NodeJS code is to use Chrome. Consider option 1 from this list https://nodejs.org/en/docs/guides/debugging-getting-started/#chrome-devtools-55. services.Configure(options => options.TimeoutMS = -1); diff --git a/test/NodeJS/HttpNodeJSPoolServiceUnitTests.cs b/test/NodeJS/HttpNodeJSPoolServiceUnitTests.cs index 79902bb..ba4c581 100644 --- a/test/NodeJS/HttpNodeJSPoolServiceUnitTests.cs +++ b/test/NodeJS/HttpNodeJSPoolServiceUnitTests.cs @@ -2,61 +2,370 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Threading; using Xunit; namespace Jering.Javascript.NodeJS.Tests { + // Strategy + // - Ensure that GetHttpNodeJSService returns each HttpNodeJSServices in the pool an equal number of times. + // - Ensure INodeJSService member implementations call the right method on the returned HttpNodeJSService. public class HttpNodeJSPoolServiceUnitTests { private readonly MockRepository _mockRepository = new MockRepository(MockBehavior.Default); + [Fact] + public async void InvokeFromFileAsync_WithTypeParameter_InvokesFromFile() + { + // Arrange + const int dummyResult = 1; + const string dummyModulePath = "dummyModulePath"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); +#pragma warning disable IDE0067 + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); +#pragma warning disable IDE0067 + + // Act + int result = await testSubject.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromFileAsync_WithoutTypeParameter_InvokesFromFile() + { + // Arrange + const string dummyModulePath = "dummyModulePath"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken)); +#pragma warning disable IDE0067 + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); +#pragma warning disable IDE0067 + + // Act + await testSubject.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStringAsync_WithTypeParameter_WithRawStringModule_InvokesFromString() + { + // Arrange + const int dummyResult = 1; + const string dummyModuleString = "dummyModuleString"; + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + int result = await testSubject.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.Equal(dummyResult, result); + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithRawStringModule_InvokesFromString() + { + // Arrange + const string dummyModuleString = "dummyModuleString"; + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService.Setup(t => t.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + await testSubject.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromString() + { + // Arrange + const int dummyResult = 1; + Func dummyModuleFactory = () => "dummyModule"; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + int result = await testSubject.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromString() + { + // Arrange + Func dummyModuleFactory = () => "dummyModule"; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + await testSubject.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithRawStreamModule_InvokesFromStream() + { + // Arrange + const int dummyResult = 1; + var dummyModuleStream = new MemoryStream(); + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + int result = await testSubject.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.Equal(dummyResult, result); + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithRawStreamModule_InvokesFromStream() + { + // Arrange + var dummyModuleStream = new MemoryStream(); + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService.Setup(t => t.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + await testSubject.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromStream() + { + // Arrange + const int dummyResult = 1; + Func dummyModuleFactory = () => new MemoryStream(); + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + int result = await testSubject.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromStream() + { + // Arrange + Func dummyModuleFactory = () => new MemoryStream(); + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + await testSubject.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void TryInvokeFromCacheAsync_WithTypeParameter_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + { + // Arrange + const int dummyResult = 1; + const string dummyModuleCacheIdentifier = "dummyModuleCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); +#pragma warning disable IDE0067 + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); +#pragma warning disable IDE0067 + + // Act + (bool success, int result) = await testSubject.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.True(success); + Assert.Equal(dummyResult, result); + _mockRepository.VerifyAll(); + } + + [Fact] + public async void TryInvokeFromCacheAsync_WithoutTypeParameter_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + { + // Arrange + const string dummyModuleCacheIdentifier = "dummyModuleCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockHttpNodeJSService = CreateMockHttpNodeJSService(); + mockHttpNodeJSService. + Setup(t => t.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(true); + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(new[] { mockHttpNodeJSService.Object }); + + // Act + bool success = await testSubject.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.True(success); + _mockRepository.VerifyAll(); + } + [Fact] public void GetHttpNodeJSService_ReturnsEachHttpNodeJSServiceAnEqualNumberOfTimes() { // Arrange const int dummyNumHttpNodeJSServices = 5; - var dummyHttpNodeJSServices = new List(); + var dummyHttpNodeJSServices = new HttpNodeJSService[dummyNumHttpNodeJSServices]; for (int i = 0; i < dummyNumHttpNodeJSServices; i++) { - dummyHttpNodeJSServices.Add(CreateHttpNodeJSService()); + dummyHttpNodeJSServices[i] = CreateHttpNodeJSService(); } - using (var testSubject = new HttpNodeJSPoolService(new ReadOnlyCollection(dummyHttpNodeJSServices))) +#pragma warning disable IDE0067 + HttpNodeJSPoolService testSubject = CreateHttpNodeJSPoolService(dummyHttpNodeJSServices); +#pragma warning disable IDE0067 + + // Act + var results = new ConcurrentBag(); + const int numThreads = 5; + const int numGetsPerThread = 10; + var threads = new List(); + for (int i = 0; i < numThreads; i++) { - // Act - var results = new ConcurrentBag(); - const int numThreads = 5; - const int numGetsPerThread = 10; - var threads = new List(); - for (int i = 0; i < numThreads; i++) + var thread = new Thread(() => { - var thread = new Thread(() => + for (int j = 0; j < numGetsPerThread; j++) { - for (int j = 0; j < numGetsPerThread; j++) - { - results.Add(testSubject.GetHttpNodeJSService()); - } - }); - threads.Add(thread); - thread.Start(); - } - foreach (Thread thread in threads) - { - thread.Join(); - } - - // Assert - const int expectedNumPerHttpNodeJSService = numThreads * numGetsPerThread / dummyNumHttpNodeJSServices; - Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[0])); - Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[1])); - Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[2])); - Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[3])); - Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[4])); + results.Add(testSubject.GetHttpNodeJSService()); + } + }); + threads.Add(thread); + thread.Start(); } + foreach (Thread thread in threads) + { + thread.Join(); + } + + // Assert + const int expectedNumPerHttpNodeJSService = numThreads * numGetsPerThread / dummyNumHttpNodeJSServices; + Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[0])); + Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[1])); + Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[2])); + Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[3])); + Assert.Equal(expectedNumPerHttpNodeJSService, results.Count(httpNodeJSService => httpNodeJSService == dummyHttpNodeJSServices[4])); + } + + private HttpNodeJSPoolService CreateHttpNodeJSPoolService(HttpNodeJSService[] httpNodeJSServices) + { + return new HttpNodeJSPoolService(new ReadOnlyCollection(httpNodeJSServices)); + } + + private Mock CreateMockHttpNodeJSService(IOptions outOfProcessNodeHostOptionsAccessor = null, + IHttpContentFactory httpContentFactory = null, + IEmbeddedResourcesService embeddedResourcesService = null, + IHttpClientService httpClientService = null, + IJsonService jsonService = null, + INodeJSProcessFactory nodeProcessFactory = null, + ILoggerFactory loggerFactory = null) + { + if (loggerFactory == null) + { + Mock mockLogger = _mockRepository.Create(); + Mock mockLoggerFactory = _mockRepository.Create(); + mockLoggerFactory.Setup(l => l.CreateLogger(typeof(HttpNodeJSService).FullName)).Returns(mockLogger.Object); + loggerFactory = mockLoggerFactory.Object; + } + + return _mockRepository.Create(outOfProcessNodeHostOptionsAccessor, + httpContentFactory, + embeddedResourcesService, + httpClientService, + jsonService, + nodeProcessFactory, + loggerFactory); } private HttpNodeJSService CreateHttpNodeJSService(IOptions outOfProcessNodeHostOptionsAccessor = null, From fca8225dec5684000b4570356138a8fcb126988a Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Tue, 3 Dec 2019 21:46:12 +0800 Subject: [PATCH 08/11] Implemented new API for StaticNodeJSService. --- src/NodeJS/StaticNodeJSService.cs | 293 ++++++++++++++--- .../StaticNodeJSServiceIntegrationTests.cs | 179 +++------- test/NodeJS/StaticNodeJSServiceUnitTests.cs | 306 ++++++++++++++++++ 3 files changed, 613 insertions(+), 165 deletions(-) create mode 100644 test/NodeJS/StaticNodeJSServiceUnitTests.cs diff --git a/src/NodeJS/StaticNodeJSService.cs b/src/NodeJS/StaticNodeJSService.cs index 2afcc7b..293ca7a 100644 --- a/src/NodeJS/StaticNodeJSService.cs +++ b/src/NodeJS/StaticNodeJSService.cs @@ -28,7 +28,7 @@ private static INodeJSService GetOrCreateNodeJSService() _serviceProvider?.Dispose(); // Create new service provider - (_services ?? (_services = new ServiceCollection())).AddNodeJS(); + _services = _services ?? new ServiceCollection().AddNodeJS(); _serviceProvider = _services.BuildServiceProvider(); _nodeJSService = _serviceProvider.GetRequiredService(); @@ -60,82 +60,303 @@ public static void DisposeServiceProvider() /// The action that configures the options. public static void Configure(Action configureOptions) where T : class { - (_services ?? (_services = new ServiceCollection())).Configure(configureOptions); + _services = (_services ?? new ServiceCollection().AddNodeJS()).Configure(configureOptions); } /// - /// Invokes a function exported by a NodeJS module on disk. + /// Sets the used to create an . + /// This method is not thread safe. + /// + /// + /// The used to create an . + /// If this value doesn't contain a valid service for , s are + /// thrown on subsequent invocations. + /// + public static void SetServices(ServiceCollection services) + { + _services = services; + } + + /// + /// Invokes a function from a NodeJS module on disk. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// NodeJS caches the module using the module's path as cache identifier. This means subsequent invocations won't reread and recompile the module. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The path to the module (i.e., JavaScript file) relative to . - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The path to the module relative to . This value mustn't be null, whitespace or an empty string. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. - /// Thrown if a NodeJS error occurs. - /// Thrown if the invocation request times out. + /// The representing the asynchronous operation. + /// Thrown if is null, whitespace or an empty string. /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. public static Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { return GetOrCreateNodeJSService().InvokeFromFileAsync(modulePath, exportName, args, cancellationToken); } /// - /// Invokes a function exported by a NodeJS module in string form. + /// Invokes a function from a NodeJS module on disk. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// NodeJS caches the module using the module's path as cache identifier. This means subsequent invocations won't re-read and re-compile the module. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The module in string form. - /// The module's cache identifier in the NodeJS module cache. If unspecified, the module will not be cached. - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The path to the module relative to . This value mustn't be null, whitespace or an empty string. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. - /// Thrown if a NodeJS error occurs. + /// Thrown if is null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().InvokeFromFileAsync(modulePath, exportName, args, cancellationToken); + } + + /// + /// Invokes a function from a NodeJS module in string form. + /// If is null, the module string is sent to NodeJS and compiled for one time use. + /// If isn't null, the module string and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module string is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module string to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The module in string form. This value mustn't be null, whitespace or an empty string. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null, whitespace or an empty string. /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. public static Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { return GetOrCreateNodeJSService().InvokeFromStringAsync(moduleString, newCacheIdentifier, exportName, args, cancellationToken); } /// - /// Invokes a function exported by a NodeJS module in Stream form. + /// Invokes a function from a NodeJS module in string form. + /// If is null, the module string is sent to NodeJS and compiled for one time use. + /// If isn't null, the module string and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module string is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module string to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The module in Stream form. - /// The module's cache identifier in the NodeJS module cache. If unspecified, the module will not be cached. - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The module in string form. This value mustn't be null, whitespace or an empty string. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().InvokeFromStringAsync(moduleString, newCacheIdentifier, exportName, args, cancellationToken); + } + + /// + /// Invokes a function from a NodeJS module in string form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module string using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The factory that creates the module string. This value mustn't be null and it mustn't return null, whitespace or an empty string. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().InvokeFromStringAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + /// Invokes a function from a NodeJS module in string form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module string using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The factory that creates the module string. This value mustn't be null and it mustn't return null, whitespace or an empty string. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null, whitespace or an empty string. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().InvokeFromStringAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + /// Invokes a function from a NodeJS module in stream form. + /// If is null, the module stream is sent to NodeJS and compiled for one time use. + /// If isn't null, the module stream and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module stream is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module stream to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The module in stream form. This value mustn't be null. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if is null. /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. public static Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { return GetOrCreateNodeJSService().InvokeFromStreamAsync(moduleStream, newCacheIdentifier, exportName, args, cancellationToken); } /// - /// Attempts to invoke a function exported by a NodeJS module cached by NodeJS. + /// Invokes a function from a NodeJS module in stream form. + /// If is null, the module stream is sent to NodeJS and compiled for one time use. + /// If isn't null, the module stream and the cache identifier are both sent to NodeJS. If the module exists in NodeJS's cache, its reused. Otherwise, the module stream is compiled and cached. + /// On subsequent invocations, you may use to invoke directly from the cache, avoiding the overhead of sending the module stream to NodeJS. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. /// - /// The type of object this method will return. It can be a JSON-serializable type, , or . - /// The cache identifier of the module. - /// The name of the function in the module's exports to be invoked. If unspecified, the module's exports object - /// is assumed to be a function, and is invoked. - /// The sequence of JSON-serializable and/or string arguments to be passed to the function to invoke. + /// The module in stream form. This value mustn't be null. + /// The module's cache identifier. If this value is null, no attempt is made to retrieve or cache the module. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. /// The cancellation token for the asynchronous operation. - /// The task object representing the asynchronous operation. On completion, the task returns a (bool, T) with the bool set to true on - /// success and false otherwise. + /// The representing the asynchronous operation. + /// Thrown if is null. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().InvokeFromStreamAsync(moduleStream, newCacheIdentifier, exportName, args, cancellationToken); + } + + /// + /// Invokes a function from a NodeJS module in stream form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module stream using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The factory that creates the module stream. This value mustn't be null and it mustn't return null. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null. + /// Thrown if NodeJS cannot be initialized. /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().InvokeFromStreamAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + /// Invokes a function from a NodeJS module in stream form. + /// Initially, only sends the module's cache identifier to NodeJS. If the module exists in NodeJS's cache, its reused. If the module doesn't exist in NodeJS's cache, creates the module stream using + /// and sends it, together with the module's cache identifier, to NodeJS for compilation and caching. + /// If is null, the module's exports is assumed to be a function and is invoked. Otherwise, invokes the function named in the module's exports. + /// + /// The factory that creates the module stream. This value mustn't be null and it mustn't return null. + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. + /// Thrown if module is not cached but is null. + /// Thrown if is null. + /// Thrown if returns null. /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().InvokeFromStreamAsync(moduleFactory, cacheIdentifier, exportName, args, cancellationToken); + } + + /// + /// Attempts to invoke a function from a module in NodeJS's cache. + /// + /// The type of value returned. This may be a JSON-serializable type, , or . + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. On completion, the task returns a (bool, T) with the bool set to true on + /// success and false otherwise. + /// Thrown if is null. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. public static Task<(bool, T)> TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) { return GetOrCreateNodeJSService().TryInvokeFromCacheAsync(moduleCacheIdentifier, exportName, args, cancellationToken); } + + /// + /// Attempts to invoke a function from a module in NodeJS's cache. + /// + /// The module's cache identifier. This value mustn't be null. + /// The name of the function in the module's exports to invoke. If this value is null, the module's exports is assumed to be a function and is invoked. + /// The sequence of JSON-serializable arguments to pass to the function to invoke. If this value is null, no arguments are passed. + /// The cancellation token for the asynchronous operation. + /// The representing the asynchronous operation. On completion, the task returns true on success and false otherwise. + /// Thrown if is null. + /// Thrown if NodeJS cannot be initialized. + /// Thrown if the invocation request times out. + /// Thrown if a NodeJS error occurs. + /// Thrown if this instance is disposed or if it attempts to use a disposed dependency. + /// Thrown if is cancelled. + public static Task TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default) + { + return GetOrCreateNodeJSService().TryInvokeFromCacheAsync(moduleCacheIdentifier, exportName, args, cancellationToken); + } } } diff --git a/test/NodeJS/StaticNodeJSServiceIntegrationTests.cs b/test/NodeJS/StaticNodeJSServiceIntegrationTests.cs index 67071af..f573438 100644 --- a/test/NodeJS/StaticNodeJSServiceIntegrationTests.cs +++ b/test/NodeJS/StaticNodeJSServiceIntegrationTests.cs @@ -1,168 +1,97 @@ -using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; +using System.Linq; using System.Threading; using Xunit; namespace Jering.Javascript.NodeJS.Tests { + [Collection(nameof(StaticNodeJSService))] public class StaticNodeJSServiceIntegrationTests { - private const int _timeoutMS = 60000; + private const int TIMEOUT_MS = 60000; - [Fact] - public async void Configure_ConfiguresOptions() - { - // Arrange - const int dummyInitialInvocationResult = 1; - const string dummyTestVariableName1 = "TEST_VARIABLE_1"; - const string dummyTestVariableValue1 = "testVariableValue1"; - const string dummyTestVariableName2 = "TEST_VARIABLE_2"; - const string dummyTestVariableValue2 = "testVariableValue2"; - - // Act - // Invoke javascript once to ensure that an initial NodeJSService is created. The invocation after configuration should properly dispose of this initial instance and create a new one with the - // specified options. - int initialInvocationResult = await StaticNodeJSService. - InvokeFromStringAsync($"module.exports = (callback) => callback(null, {dummyInitialInvocationResult});").ConfigureAwait(false); - StaticNodeJSService. - Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName1, dummyTestVariableValue1)); - StaticNodeJSService. - Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName2, dummyTestVariableValue2)); - - // Assert - Assert.Equal(dummyInitialInvocationResult, initialInvocationResult); - DummyResult result = await StaticNodeJSService. - InvokeFromStringAsync($"module.exports = (callback) => callback(null, {{result: process.env.{dummyTestVariableName1} + process.env.{dummyTestVariableName2}}});"). - ConfigureAwait(false); - Assert.Equal(dummyTestVariableValue1 + dummyTestVariableValue2, result.Result); - } - - [Fact] - public async void DisposeServiceProvider_DisposesServiceProvider() + [Fact(Timeout = TIMEOUT_MS)] + public async void DisposeServiceProvider_RestartsNodeJSProcess() { // Arrange const string dummyTestVariableName = "TEST_VARIABLE"; const string dummyTestVariableValue = "testVariableValue"; - StaticNodeJSService. - Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName, dummyTestVariableValue)); - DummyResult initialInvocationResult = await StaticNodeJSService. - InvokeFromStringAsync($"module.exports = (callback) => callback(null, {{result: process.env.{dummyTestVariableName}}});"). - ConfigureAwait(false); + StaticNodeJSService.Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName, dummyTestVariableValue)); + string result1 = await StaticNodeJSService. + InvokeFromStringAsync($"module.exports = (callback) => callback(null, process.env.{dummyTestVariableName});").ConfigureAwait(false); // Act StaticNodeJSService.DisposeServiceProvider(); // Dispose, environment variable should not be set in the next call - DummyResult result = await StaticNodeJSService. - InvokeFromStringAsync($"module.exports = (callback) => callback(null, {{result: process.env.{dummyTestVariableName}}});"). - ConfigureAwait(false); - - // Assert - Assert.Equal(dummyTestVariableValue, initialInvocationResult.Result); - Assert.Null(result.Result); - } - - [Fact] - public async void TryInvokeFromCacheAsync_InvokesJavascriptIfModuleIsCached() - { - // Arrange - const string dummyResultString = "success"; - const string dummyCacheIdentifier = "dummyCacheIdentifier"; - - // Cache - await StaticNodeJSService. - InvokeFromStringAsync("module.exports = (callback, resultString) => callback(null, {result: resultString});", - dummyCacheIdentifier, - args: new[] { dummyResultString }). - ConfigureAwait(false); - - // Act - (bool success, DummyResult value) = await StaticNodeJSService.TryInvokeFromCacheAsync(dummyCacheIdentifier, args: new[] { dummyResultString }).ConfigureAwait(false); // Assert - Assert.True(success); - Assert.Equal(dummyResultString, value.Result); + string result2 = await StaticNodeJSService. + InvokeFromStringAsync($"module.exports = (callback) => callback(null, process.env.{dummyTestVariableName});").ConfigureAwait(false); + Assert.Equal(dummyTestVariableValue, result1); + Assert.Equal(string.Empty, result2); } - [Fact] - public async void TryInvokeFromCacheAsync_ReturnsFalseIfModuleIsNotCached() + [Fact(Timeout = TIMEOUT_MS)] + public async void Configure_RestartsNodeJSProcessWithNewOptions() { // Arrange - const string dummyResultString = "success"; - const string dummyCacheIdentifier = "dummyCacheIdentifier"; - - // Act - (bool success, DummyResult value) = await StaticNodeJSService.TryInvokeFromCacheAsync(dummyCacheIdentifier, args: new[] { dummyResultString }).ConfigureAwait(false); - - // Assert - Assert.False(success); - Assert.Null(value); - } - - [Fact] - public async void InvokeFromStreamAsync_InvokesJavascript() - { - // Arrange - const string dummyResultString = "success"; - - DummyResult result; - using (var memoryStream = new MemoryStream()) - using (var streamWriter = new StreamWriter(memoryStream)) - { - streamWriter.Write("module.exports = (callback, resultString) => callback(null, {result: resultString});"); - streamWriter.Flush(); - memoryStream.Position = 0; - - // Act - result = await StaticNodeJSService.InvokeFromStreamAsync(memoryStream, args: new[] { dummyResultString }).ConfigureAwait(false); - } - - // Assert - Assert.Equal(dummyResultString, result.Result); - } - - [Fact] - public async void InvokeFromStringAsync_InvokesJavascript() - { - // Arrange - const string dummyResultString = "success"; + const string dummyTestVariableName = "TEST_VARIABLE"; + const string dummyTestVariableValue1 = "testVariableValue1"; + const string dummyTestVariableValue2 = "testVariableValue2"; + StaticNodeJSService.Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName, dummyTestVariableValue1)); + string result1 = await StaticNodeJSService. + InvokeFromStringAsync($"module.exports = (callback) => callback(null, process.env.{dummyTestVariableName});").ConfigureAwait(false); // Act - DummyResult result = await StaticNodeJSService. - InvokeFromStringAsync("module.exports = (callback, resultString) => callback(null, {result: resultString});", args: new[] { dummyResultString }).ConfigureAwait(false); + StaticNodeJSService.Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName, dummyTestVariableValue2)); // Assert - Assert.Equal(dummyResultString, result.Result); + string result2 = await StaticNodeJSService. + InvokeFromStringAsync($"module.exports = (callback) => callback(null, process.env.{dummyTestVariableName});").ConfigureAwait(false); + Assert.Equal(dummyTestVariableValue1, result1); + Assert.Equal(dummyTestVariableValue2, result2); } - [Fact] - public async void InvokeFromFileAsync_InvokesJavascript() + [Fact(Timeout = TIMEOUT_MS)] + public async void SetServices_RestartsNodeJSProcessWithNewServices() { // Arrange - const string dummyResultString = "success"; - StaticNodeJSService. - Configure(options => options.ProjectPath = "./Javascript"); + const string dummyTestVariableName = "TEST_VARIABLE_1"; + const string dummyTestVariableValue1 = "testVariableValue1"; + const string dummyTestVariableValue2 = "testVariableValue2"; + StaticNodeJSService.Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName, dummyTestVariableValue1)); + string result1 = await StaticNodeJSService. + InvokeFromStringAsync($"module.exports = (callback) => callback(null, process.env.{dummyTestVariableName});").ConfigureAwait(false); + var dummyServices = new ServiceCollection(); + dummyServices. + AddNodeJS(). + Configure(options => options.EnvironmentVariables.Add(dummyTestVariableName, dummyTestVariableValue2)); // Act - DummyResult result = await StaticNodeJSService. - InvokeFromFileAsync("dummyModule.js", args: new[] { dummyResultString }).ConfigureAwait(false); + StaticNodeJSService.SetServices(dummyServices); // Assert - Assert.Equal(dummyResultString, result.Result); + string result2 = await StaticNodeJSService. + InvokeFromStringAsync($"module.exports = (callback) => callback(null, process.env.{dummyTestVariableName});").ConfigureAwait(false); + Assert.Equal(dummyTestVariableValue1, result1); + Assert.Equal(dummyTestVariableValue2, result2); } - [Fact(Timeout = _timeoutMS)] + // This test ensures that private method GetOrCreateNodeJSService properly handles multiple concurrent requests + [Fact(Timeout = TIMEOUT_MS)] public void AllInvokeMethods_AreThreadSafe() { // Arrange - const string dummyResultString = "success"; + StaticNodeJSService.DisposeServiceProvider(); // In case previous test registered a custom service // Act - var results = new ConcurrentQueue(); + var results = new ConcurrentQueue(); const int numThreads = 5; var threads = new List(); for (int i = 0; i < numThreads; i++) { - var thread = new Thread(() => results.Enqueue(StaticNodeJSService.InvokeFromStringAsync("module.exports = (callback, resultString) => callback(null, {result: resultString});", args: new[] { dummyResultString }).GetAwaiter().GetResult())); + var thread = new Thread(() => results.Enqueue(StaticNodeJSService.InvokeFromStringAsync("module.exports = (callback) => callback(null, process.pid);").GetAwaiter().GetResult())); threads.Add(thread); thread.Start(); } @@ -173,15 +102,7 @@ public void AllInvokeMethods_AreThreadSafe() // Assert Assert.Equal(numThreads, results.Count); - foreach (DummyResult result in results) - { - Assert.Equal(dummyResultString, result.Result); - } - } - - private class DummyResult - { - public string Result { get; set; } + Assert.Single(results.Distinct()); // All invocations should run in process started by first invocation } } } diff --git a/test/NodeJS/StaticNodeJSServiceUnitTests.cs b/test/NodeJS/StaticNodeJSServiceUnitTests.cs new file mode 100644 index 0000000..a246526 --- /dev/null +++ b/test/NodeJS/StaticNodeJSServiceUnitTests.cs @@ -0,0 +1,306 @@ +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System; +using System.IO; +using System.Threading; +using Xunit; + +namespace Jering.Javascript.NodeJS.Tests +{ + [Collection(nameof(StaticNodeJSService))] + public class StaticNodeJSServiceUnitTests + { + private readonly MockRepository _mockRepository = new MockRepository(MockBehavior.Default); + + [Fact] + public async void InvokeFromFileAsync_WithTypeParameter_InvokesFromFile() + { + // Arrange + const int dummyResult = 1; + const string dummyModulePath = "dummyModulePath"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + int result = await StaticNodeJSService.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromFileAsync_WithoutTypeParameter_InvokesFromFile() + { + // Arrange + const string dummyModulePath = "dummyModulePath"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken)); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + await StaticNodeJSService.InvokeFromFileAsync(dummyModulePath, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStringAsync_WithTypeParameter_WithRawStringModule_InvokesFromString() + { + // Arrange + const int dummyResult = 1; + const string dummyModuleString = "dummyModuleString"; + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + int result = await StaticNodeJSService.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.Equal(dummyResult, result); + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithRawStringModule_InvokesFromString() + { + // Arrange + const string dummyModuleString = "dummyModuleString"; + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService.Setup(t => t.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + await StaticNodeJSService.InvokeFromStringAsync(dummyModuleString, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromString() + { + // Arrange + const int dummyResult = 1; + Func dummyModuleFactory = () => "dummyModule"; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + int result = await StaticNodeJSService.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStringAsync_WithoutTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromString() + { + // Arrange + Func dummyModuleFactory = () => "dummyModule"; + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + await StaticNodeJSService.InvokeFromStringAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithRawStreamModule_InvokesFromStream() + { + // Arrange + const int dummyResult = 1; + var dummyModuleStream = new MemoryStream(); + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + int result = await StaticNodeJSService.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.Equal(dummyResult, result); + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithRawStreamModule_InvokesFromStream() + { + // Arrange + var dummyModuleStream = new MemoryStream(); + const string dummyNewCacheIdentifier = "dummyNewCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService.Setup(t => t.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + await StaticNodeJSService.InvokeFromStreamAsync(dummyModuleStream, dummyNewCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void InvokeFromStreamAsync_WithTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromStream() + { + // Arrange + const int dummyResult = 1; + Func dummyModuleFactory = () => new MemoryStream(); + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(dummyResult); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + int result = await StaticNodeJSService.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + Assert.Equal(dummyResult, result); + } + + [Fact] + public async void InvokeFromStreamAsync_WithoutTypeParameter_WithModuleFactory_IfModuleIsCachedInvokesFromCacheOtherwiseInvokesFromStream() + { + // Arrange + Func dummyModuleFactory = () => new MemoryStream(); + const string dummyCacheIdentifier = "dummyCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + await StaticNodeJSService.InvokeFromStreamAsync(dummyModuleFactory, dummyCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + _mockRepository.VerifyAll(); + } + + [Fact] + public async void TryInvokeFromCacheAsync_WithTypeParameter_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + { + // Arrange + const int dummyResult = 1; + const string dummyModuleCacheIdentifier = "dummyModuleCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync((true, dummyResult)); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + (bool success, int result) = await StaticNodeJSService.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.True(success); + Assert.Equal(dummyResult, result); + _mockRepository.VerifyAll(); + } + + [Fact] + public async void TryInvokeFromCacheAsync_WithoutTypeParameter_CreatesInvocationRequestAndCallsTryInvokeCoreAsync() + { + // Arrange + const string dummyModuleCacheIdentifier = "dummyModuleCacheIdentifier"; + const string dummyExportName = "dummyExportName"; + var dummyArgs = new object[0]; + var dummyCancellationToken = new CancellationToken(); + Mock mockNodeJSService = _mockRepository.Create(); + mockNodeJSService. + Setup(t => t.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken)). + ReturnsAsync(true); + var dummyServices = new ServiceCollection(); + dummyServices.AddSingleton(typeof(INodeJSService), mockNodeJSService.Object); + StaticNodeJSService.SetServices(dummyServices); + + // Act + bool success = await StaticNodeJSService.TryInvokeFromCacheAsync(dummyModuleCacheIdentifier, dummyExportName, dummyArgs, dummyCancellationToken).ConfigureAwait(false); + + // Assert + Assert.True(success); + _mockRepository.VerifyAll(); + } + } +} From bc0e7dce9812a7eda8bffed08e8c91cc62417911 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Tue, 3 Dec 2019 21:49:35 +0800 Subject: [PATCH 09/11] Misc formatting. --- .../InvocationData/InvocationException.cs | 4 ++-- .../NodeJSServiceCollectionExtensions.cs | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/NodeJS/InvocationData/InvocationException.cs b/src/NodeJS/InvocationData/InvocationException.cs index 4dadbf1..82125fa 100644 --- a/src/NodeJS/InvocationData/InvocationException.cs +++ b/src/NodeJS/InvocationData/InvocationException.cs @@ -4,7 +4,7 @@ namespace Jering.Javascript.NodeJS { /// - /// Represents an exception caused by an error caught in NodeJS. + /// Represents an unrecoverable issue encountered when trying to invoke javascript in NodeJS. /// [Serializable] public class InvocationException : Exception @@ -12,7 +12,7 @@ public class InvocationException : Exception /// /// Creates an instance. /// - public InvocationException() : base() + public InvocationException() { } diff --git a/src/NodeJS/NodeJSServiceCollectionExtensions.cs b/src/NodeJS/NodeJSServiceCollectionExtensions.cs index a2d89dc..aefd467 100644 --- a/src/NodeJS/NodeJSServiceCollectionExtensions.cs +++ b/src/NodeJS/NodeJSServiceCollectionExtensions.cs @@ -17,21 +17,23 @@ public static class NodeJSServiceCollectionExtensions /// Adds NodeJS services to the an . /// /// The target . - public static void AddNodeJS(this IServiceCollection services) + public static IServiceCollection AddNodeJS(this IServiceCollection services) { // Third party services - services.AddLogging(); - services.AddOptions(); + services. + AddLogging(). + AddOptions(); services.TryAddSingleton(typeof(IHttpClientService), IHttpClientServiceFactory); // Services defined in this project - services.AddSingleton, ConfigureNodeJSProcessOptions>(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(typeof(INodeJSService), INodeJSServiceFactory); - services.AddSingleton(); - services.AddSingleton(); + return services. + AddSingleton, ConfigureNodeJSProcessOptions>(). + AddSingleton(). + AddSingleton(). + AddSingleton(). + AddSingleton(typeof(INodeJSService), INodeJSServiceFactory). + AddSingleton(). + AddSingleton(); } internal static IHttpClientService IHttpClientServiceFactory(IServiceProvider serviceProvider) @@ -52,7 +54,7 @@ internal static INodeJSService INodeJSServiceFactory(IServiceProvider servicePro int concurrencyDegree = outOfProcessNodeJSServiceOptions.ConcurrencyDegree; int processorCount = environmentService.ProcessorCount; // TODO to be safe we should ensure that this is >= 1 - if(outOfProcessNodeJSServiceOptions.Concurrency == Concurrency.None || + if (outOfProcessNodeJSServiceOptions.Concurrency == Concurrency.None || concurrencyDegree == 1 || // MultiProcess mode but only 1 process concurrencyDegree <= 0 && processorCount == 1) // Machine has only 1 logical processor { From 3440efa6c068c88670e2df04fce6ccbe3c20a291 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Wed, 4 Dec 2019 20:55:08 +0800 Subject: [PATCH 10/11] Updated ReadMe. --- ReadMe.md | 226 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 142 insertions(+), 84 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index 6f1ba93..d370483 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -22,7 +22,7 @@ ## Overview Jering.Javascript.NodeJS enables you to invoke javascript in [NodeJS](https://nodejs.org/en/), from C#. With this ability, you can use javascript libraries and scripts from your C# projects. -> You can use this library as a replacement for the recently deprecated [Microsoft.AspNetCore.NodeServices](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.NodeServices). +> You can use this library as a replacement for the recently obsoleted [Microsoft.AspNetCore.NodeServices](https://github.com/aspnet/JavaScriptServices/tree/master/src/Microsoft.AspNetCore.NodeServices). [`InvokeFromFileAsync`](#inodejsserviceinvokefromfileasync) replaces `INodeService`'s `InvokeAsync` and `InvokeExportAsync`. This library is flexible; you can use a dependency injection (DI) based API or a static API, also, you can invoke both in-memory and on-disk javascript. @@ -52,7 +52,7 @@ module.exports = (callback, x, y) => { // Module must export a function that ta callback(null /* If an error occurred, provide an error object or message */, result); // Call the callback when you're done. }"; -// Create INodeJSService instance +// Create an INodeJSService var services = new ServiceCollection(); services.AddNodeJS(); ServiceProvider serviceProvider = services.BuildServiceProvider(); @@ -87,21 +87,19 @@ Using .Net CLI: ### Creating INodeJSService This library provides a DI based API to facilitate [extensibility](#extensibility) and testability. You can use any DI framework that has adapters for [Microsoft.Extensions.DependencyInjection](https://github.com/aspnet/DependencyInjection). -Here, we'll use the vanilla Microsoft.Extensions.DependencyInjection framework: +Here, we'll use vanilla Microsoft.Extensions.DependencyInjection: ```csharp var services = new ServiceCollection(); services.AddNodeJS(); ServiceProvider serviceProvider = services.BuildServiceProvider(); INodeJSService nodeJSService = serviceProvider.GetRequiredService(); ``` -The default implementation of `INodeJSService` is `HttpNodeJSService`. It starts a Http server in a NodeJS process and sends invocation requests -over Http. For simplicty's sake, this ReadMe assumes that `INodeJSService`'s default implementation is used. - +The default implementation of `INodeJSService` is `HttpNodeJSService`, which manages a NodeJS process that it sends javascript invocations to via HTTP. `INodeJSService` is a singleton service and `INodeJSService`'s members are thread safe. -Where possible, inject `INodeJSService` into your types or keep a reference to a shared `INodeJSService` instance. +Where possible, inject `INodeJSService` into your types or share an `INodeJSService`. This avoids the overhead of killing and creating NodeJS processes repeatedly. -When you're done, you can manually dispose of an `INodeJSService` instance by calling +When you're done, you can dispose of an `INodeJSService` by calling ```csharp nodeJSService.Dispose(); ``` @@ -109,17 +107,16 @@ or ```csharp serviceProvider.Dispose(); // Calls Dispose on objects it has instantiated that are disposable ``` -`Dispose` kills the spawned NodeJS process. -Note that even if `Dispose` isn't called manually, `INodeJSService` will kill the -NodeJS process when the application shuts down - if the application shuts down gracefully. If the application doesn't shutdown gracefully, the NodeJS process will kill -itself when it detects that its parent has been killed. -Essentially, manually disposing of `INodeJSService` instances is not mandatory. +Disposing of an `INodeJSService` kills its associated NodeJS process. +Note that even if `Dispose` isn't called, the NodeJS process is killed when the application shuts down - if the application shuts down gracefully. +If the application doesn't shutdown gracefully, the NodeJS process will kill itself when it detects that its parent has been killed. +Essentially, manually disposing of `INodeJSService`s isn't mandatory. #### Static API -This library also provides a static API as an alternative. The `StaticNodeJSService` type wraps an `INodeJSService` instance, exposing most of its [public members](#api) statically. -Whether you use the static API or the DI based API depends on your development needs. If you are already using DI, if you want to mock +This library provides a static API as an alternative. The `StaticNodeJSService` type wraps an `INodeJSService`, exposing most of its [public members](#api). +Whether you use the static API or the DI based API depends on your development needs. If you're already using DI, if you want to mock out javascript invocations in your tests or if you want to [overwrite](#extensibility) services, use the DI based API. Otherwise, -use the static API. An example usage: +use the static API. Example usage: ```csharp string result = await StaticNodeJSService @@ -127,21 +124,21 @@ string result = await StaticNodeJSService Assert.Equal("success", result); ``` -The following section on using `INodeJSService` applies to usage of `StaticNodeJSService`. ### Using INodeJSService #### Basics -To invoke javascript, we'll first need to create a [NodeJS module](#nodejs-modules) that exports a function or an object containing functions. Exported functions can be of two forms: +To invoke javascript, you'll need a [NodeJS module](#nodejs-modules) that exports either a function or an object containing functions. Exported functions can be of two forms: ##### Function With Callback Parameter -These functions must take a callback as their first argument, and they must call the callback. +These functions take a callback as their first argument, and call the callback when they're done. + The callback takes two optional arguments: -- The first argument must be an error or an error message. It must be an instance of type [`Error`](https://nodejs.org/api/errors.html#errors_class_error) or a `string`. -- The second argument is the result. It must be an instance of a JSON-serializable type, a `string`, or a [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable). +- The first argument is an error or an error message. It must be of type [`Error`](https://nodejs.org/api/errors.html#errors_class_error) or `string`. +- The second argument is the result. It must be a JSON-serializable type, a `string`, or a [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable). -This sort of callback is known as an [error-first callback](https://nodejs.org/api/errors.html#errors_error_first_callbacks). -Such callbacks are commonly used for [error handling](https://nodejs.org/api/errors.html#errors_error_propagation_and_interception) in NodeJS asynchronous code (check out the [NodeJS event loop](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/) -if you'd like to learn more about how asynchrony works in NodeJS). +This is known as an [error-first callback](https://nodejs.org/api/errors.html#errors_error_first_callbacks). +Such callbacks are commonly used for [error handling](https://nodejs.org/api/errors.html#errors_error_propagation_and_interception) in NodeJS asynchronous code (check out [NodeJS Event Loop](https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/) +for more information on asynchrony in NodeJS). This is a module that exports a valid function: ```javascript @@ -152,7 +149,7 @@ module.exports = (callback, arg1, arg2, arg3) => { } ``` -And this is a module that exports an object containing valid functions: +This is a module that exports an object containing valid functions: ```javascript module.exports = { doSomething: (callback, arg1) => { @@ -169,8 +166,8 @@ module.exports = { ``` ##### Async Function -Async functions are really just syntactic sugar for functions with callback parameters. -[Callbacks, Promises and Async/Await](https://medium.com/front-end-weekly/callbacks-promises-and-async-await-ad4756e01d90) provides a nice summary on how callbacks, promises and async/await work. +Async functions are syntactic sugar for [functions with callback parameters](#function-with-callback-parameter) (check out +[Callbacks, Promises and Async/Await](https://medium.com/front-end-weekly/callbacks-promises-and-async-await-ad4756e01d90) for a summary on how callbacks, promises and async/await are related). This is a module that exports a valid function: ```javascript @@ -200,7 +197,7 @@ module.exports = { } ``` -If an error is thrown it is caught and handled by the caller (error message is sent back to the calling .Net process): +If an error is thrown in an async function, the error message is sent back to the calling .Net process, where an `InvocationException` is thrown: ```javascript module.exports = async () => { throw new Error('error message'); @@ -208,88 +205,112 @@ module.exports = async () => { ``` #### Invoking Javascript From a File -If we have a file named `exampleModule.js` (located in [`NodeJSProcessOptions.ProjectPath`](#nodejsprocessoptions)), with contents: +If you have a javascript file named `exampleModule.js` (located in [`NodeJSProcessOptions.ProjectPath`](#nodejsprocessoptions)): ```javascript module.exports = (callback, message) => callback(null, { resultMessage: message }); ``` -And we have the class `Result`: +And a .Net class `Result`: ```csharp public class Result { - public string ResultMessage { get; set; } + public string Message { get; set; } } ``` -We can invoke the javascript using [`InvokeFromFileAsync`](#inodejsserviceinvokefromfileasync) ): +You can invoke the javascript using [`InvokeFromFileAsync`](#inodejsserviceinvokefromfileasync): ```csharp Result result = await nodeJSService.InvokeFromFileAsync("exampleModule.js", args: new[] { "success" }); -Assert.Equal("success", result.ResultMessage); +Assert.Equal("success", result.Message); ``` -If we change `exampleModule.js` to export an object containing functions: +If you change `exampleModule.js` to export an object containing functions: ```javascript module.exports = { appendExclamationMark: (callback, message) => callback(null, { resultMessage: message + '!' }), appendFullStop: (callback, message) => callback(null, { resultMessage: message + '.' }) } ``` -We can invoke javascript by providing an export's name to `InvokeFromFileAsync`: +You can invoke a specific function by providing an export's name: ```csharp Result result = await nodeJSService.InvokeFromFileAsync("exampleModule.js", "appendExclamationMark", args: new[] { "success" }); -Assert.Equal("success!", result.ResultMessage); +Assert.Equal("success!", result.Message); ``` -When using `InvokeFromFileAsync`, NodeJS always caches the module, using the absolute path of the `.js` file as the module's cache identifier. This is great for -performance, since the file will not be read more than once. +When using `InvokeFromFileAsync`, NodeJS always caches the module using the `.js` file's absolute path as cache identifier. This is great for +performance, since the file will not be reread or recompiled on subsequent invocations. #### Invoking Javascript in String Form -We can invoke javascript in string form using [`InvokeFromStringAsync`](#inodejsserviceinvokefromstringasync) : +You can invoke javascript in string form using [`InvokeFromStringAsync`](#inodejsserviceinvokefromstringasync): ```csharp -Result result = await nodeJSService.InvokeFromStringAsync("module.exports = (callback, message) => callback(null, { resultMessage: message });", - args: new[] { "success" }); +string module = "module.exports = (callback, message) => callback(null, { resultMessage: message });"; -Assert.Equal("success", result.ResultMessage); +// Invoke javascript +Result result = await nodeJSService.InvokeFromStringAsync(module, args: new[] { "success" }); + +Assert.Equal("success", result.Message); ``` -If we're going to invoke the module repeatedly, it would make sense to have NodeJS cache the module so that it doesn't need to be kept in -memory and sent with every invocation. To cache the module, we must specify a custom cache identifier, since unlike a file, a string has no -"absolute file path" for NodeJS to use as a cache identifier. Once NodeJS has cached the module, we should invoke logic directly from the NodeJS cache: +In the above example, the module string is sent to NodeJS and recompiled on every invocation. If you're going to invoke a module repeatedly, +to avoid resending and recompiling, you'll want to have NodeJS cache the module. +To do this, you must specify a custom cache identifier, since unlike a file, a string has no "absolute file path" for NodeJS to use as cache identifier. +Once NodeJS has cached the module, invoke directly from the NodeJS cache: + ```csharp string cacheIdentifier = "exampleModule"; // Try to invoke from the NodeJS cache (bool success, Result result) = await nodeJSService.TryInvokeFromCacheAsync(cacheIdentifier, args: new[] { "success" }); -// If the NodeJS process dies and restarts, the module will have to be re-cached, so we must always check whether success is false + +// If the module hasn't been cached, cache it. If the NodeJS process dies and restarts, the cache will be invalidated, so always check whether success is false. if(!success) { - // Retrieve the module string, this is a trivialized example for demonstration purposes + // This is a trivialized example. In practice, to avoid holding large module strings in memory, you might retrieve the module + // string from an on-disk or remote source, like a file. string moduleString = "module.exports = (callback, message) => callback(null, { resultMessage: message });"; - // Cache and invoke the module + + // Send the module string to NodeJS where it's compiled, invoked and cached. result = await nodeJSService.InvokeFromStringAsync(moduleString, cacheIdentifier, args: new[] { "success" }); } Assert.Equal("success", result.ResultMessage); ``` -Like when [invoking javascript form a file](#invoking-javascript-from-a-file), if the module exports an object containing functions, we can invoke a function by specifying -an export's name. +We recommend using the following [`InvokeFromStringAsync`](#inodejsserviceinvokefromstringasync) overload to perform the above example's operations. +The above example is really there to explain what this overload does. +If you've enabled [concurrency](#concurrency), [you must use this overload](#concurrency): + +```csharp +string module = "module.exports = (callback, message) => callback(null, { resultMessage: message });"; +string cacheIdentifier = "exampleModule"; + +// This is a trivialized example. In practice, to avoid holding large module strings in memory, you might retrieve the module +// string from an on-disk or remote source, like a file. +Func moduleFactory = () => module; + +// Initially, sends only cacheIdentifier to NodeJS, in an attempt to invoke from the NodeJS cache. If the module hasn't been cached, creates the module string using moduleFactory and +// sends it to NodeJS where it's compiled, invoked and cached. +Result result = await nodeJSService.InvokeFromStringAsync(moduleFactory, cacheIdentifier, args: new[] { "success" }); + +Assert.Equal("success", result.Message); +``` + +Like when [invoking javascript form a file](#invoking-javascript-from-a-file), if the module exports an object containing functions, you can invoke a specific function by specifying +its name. + #### Invoking Javascript in Stream Form -We can invoke javascript in Stream form using [`InvokeFromStreamAsync`](#inodejsserviceinvokefromstreamasync) : +You can invoke javascript in stream form using [`InvokeFromStreamAsync`](#inodejsserviceinvokefromstreamasync) : ```csharp -using (var memoryStream = new MemoryStream()) -using (var streamWriter = new StreamWriter(memoryStream)) -{ - // Write the module to a MemoryStream for demonstration purposes. - streamWriter.Write("module.exports = (callback, message) => callback(null, {resultMessage: message});"); - streamWriter.Flush(); - memoryStream.Position = 0; +// Write the module to a MemoryStream for demonstration purposes. +streamWriter.Write("module.exports = (callback, message) => callback(null, {resultMessage: message});"); +streamWriter.Flush(); +memoryStream.Position = 0; - Result result = await nodeJSService.InvokeFromStreamAsync(memoryStream, args: new[] { "success" }); +Result result = await nodeJSService.InvokeFromStreamAsync(memoryStream, args: new[] { "success" }); - Assert.Equal("success", result.ResultMessage); -} +Assert.Equal("success", result.Message); ``` + `InvokeFromStreamAsync` behaves in a similar manner to `InvokeFromStringAsync`, refer to [Invoking Javascript in String Form](#invoking-javascript-in-string-form) for details on caching and more. -The utility of this method is in providing a way to avoid allocating a string if the source of the module is a Stream. Avoiding `string` allocations can improve performance. +This method provides a way to avoid allocating a string if the source of the module is a stream. Avoiding `string` allocations can improve performance. ### Configuring INodeJSService This library uses the [ASP.NET Core options pattern](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-2.1). While developed for ASP.NET Core, @@ -317,7 +338,7 @@ StaticNodeJSService.Configure(options => optio Configurations made using `StaticNodeJSService.Configure` only apply to javascript invocations made using the static API. Ideally, such configurations should be done before the first javascript invocation. Any existing NodeJS process is killed and a new one is created in the first javascript invocation after every `StaticNodeJSService.Configure` call. -Re-creating the NodeJS process is resource intensive. Also, if you are using the static API from multiple threads and +Re-creating the NodeJS process is resource intensive. Also, if you're using the static API from multiple threads and the NodeJS process is performing invocations for other threads, you might get unexpected results. The next two sections list all available options. @@ -334,7 +355,7 @@ The next two sections list all available options. | Option | Type | Description | Default | | ------ | ---- | ----------- | ------- | | TimeoutMS | `int` | The maximum duration to wait for the NodeJS process to connect and to wait for responses to invocations. If this value is negative, the maximum duration is infinite. | `60000` | -| NumRetries | `int` | The number of times an invocation is retried. If set to a negative value, invocations are retried indefinitely. If the module source of an invocation is an unseekable stream, the invocation is not retried. If you require retries for such streams, copy their contents to a `MemoryStream`.| `1` | +| NumRetries | `int` | The number of times an invocation is retried. If set to a negative value, invocations are retried indefinitely. If the module source of an invocation is an unseekable stream, the invocation isn't retried. If you require retries for such streams, copy their contents to a `MemoryStream`.| `1` | | Concurrency | `Concurrency` | The concurrency mode for invocations.

By default, this value is `Concurrency.None` and invocations are executed synchronously by a single NodeJS process; mode pros: lower memory overhead and supports all modules, cons: less performant.

If this value is `Concurrency.MultiProcess`, `ConcurrencyDegree` NodeJS processes are created and invocations are distributed among them using round-robin load balancing; mode pros: more performant, cons: higher memory overhead and doesn't work with modules that have persistent state. | `Concurrency.None` | | ConcurrencyDegree | `int` | The concurrency degree. If `Concurrency` is `Concurrency.MultiProcess`, this value is the number of NodeJS processes. If this value is less than or equal to 0, concurrency degree is the number of logical processors the current machine has. This value does nothing if `Concurrency` is `Concurrency.None`. | `0` | @@ -359,7 +380,7 @@ services.Configure(options => { ``` (see [Configuring INodeJSService](#configuring-inodejsservice) for more information on configuring) -All invocations will be distributed among multiple NodeJS processes using round-robin load balancing. +All invocations will be distributed among multiple NodeJS processes using round-robin load balancing. ##### Why Bother? Enabling concurrency significantly speeds up CPU-bound workloads. For example, consider the following benchmarks: @@ -412,7 +433,7 @@ module.exports = (callback) => { For `INodeJSService` with `Concurrency.MultiProcessing`, multiple NodeJS processes perform invocations concurrently, so the benchmark takes ~400ms ((25 tasks x 100ms) / number-of-logical-processors + overhead-from-unrelated-processes). -In the other two benchmarks, a single NodeJS process performs invocations synchronously, so those benchmarks take ~2500ms (25 tasks x 100ms = 2500ms). +In the other two benchmarks, a single NodeJS process performs invocations synchronously, so those benchmarks take ~2500ms (25 tasks x 100ms). ##### Limitations 1. You can't use concurrency if you persist data between invocations. For example, with concurrency enabled: @@ -438,11 +459,48 @@ In the other two benchmarks, a single NodeJS process performs invocations synchr This should not be a problem in most cases. -2. Higher memory overhead. This isn't typically an issue - on a standard workstation you can start dozens of NodeJS processes, and in cloud scenarios you'll typically have memory proportional to +2. Higher memory overhead. This isn't typically an issue - a standard workstation can host dozens of NodeJS processes, and in cloud scenarios you'll typically have memory proportional to the number of logical processors. -3. Concurrency may not speed up workloads with lots of asynchronous operations, for example if your workload spends lots of time waiting on a databases. +3. Concurrency may not speed up workloads with lots of asynchronous operations. For example if your workload spends lots of time waiting on a databases, + more NodeJS processes will not speed things up significantly. +4. With concurrency enabled, you can't use the following [pattern](#invoking-javascript-in-string-form) to invoke from NodeJS's cache: + + ```csharp + string cacheIdentifier = "exampleModule"; + + // If you have an even number of NodeJS processes, success will always be false since the resulting caching attempt is + // sent to the next NodeJS process. + (bool success, Result result) = await nodeJSService.TryInvokeFromCacheAsync(cacheIdentifier, args: new[] { "success" }); + + // False, so we attempt to cache + if(!success) + { + string moduleString = "module.exports = (callback, message) => callback(null, { resultMessage: message });"; + + // Because of round-robin load balancing, this caching attempt is sent to the next NodeJS process. + result = await nodeJSService.InvokeFromStringAsync(moduleString, cacheIdentifier, args: new[] { "success" }); + } + + Assert.Equal("success", result.ResultMessage); + ``` + + Instead, call an overload that atomically handles caching and invoking: + + ```csharp + string module = "module.exports = (callback, message) => callback(null, { resultMessage: message });"; + string cacheIdentifier = "exampleModule"; + + // This is a trivialized example. In practice, to avoid holding large module strings in memory, you might retrieve the module + // string from an on-disk or remote source, like a file. + Func moduleFactory = () => module; + // Initially, sends only cacheIdentifier to NodeJS, in an attempt to invoke from the NodeJS cache. If the module hasn't been cached, creates the module string using moduleFactory and + // sends it to NodeJS where it's compiled, invoked and cached. + Result result = await nodeJSService.InvokeFromStringAsync(moduleFactory, cacheIdentifier, args: new[] { "success" }); + + Assert.Equal("success", result.Message); + ``` ## API ### INodeJSService.InvokeFromFileAsync #### Signature @@ -471,14 +529,14 @@ Invokes a function exported by a NodeJS module on disk. - Type: `CancellationToken` - Description: The cancellation token for the asynchronous operation. #### Returns -The task object representing the asynchronous operation. +The task representing the asynchronous operation. #### Exceptions - `InvocationException` - Thrown if a NodeJS error occurs. - Thrown if the invocation request times out. - Thrown if NodeJS cannot be initialized. - `ObjectDisposedException` - - Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + - Thrown if this has been disposed or if it attempts to use one of its dependencies that has been disposed. - `OperationCanceledException` - Thrown if `cancellationToken` is cancelled. #### Example @@ -490,14 +548,14 @@ And we have the class `Result`: ```csharp public class Result { - public string ResultMessage { get; set; } + public string Message { get; set; } } ``` The following assertion will pass: ```csharp Result result = await nodeJSService.InvokeFromFileAsync("exampleModule.js", args: new[] { "success" }); -Assert.Equal("success", result.ResultMessage); +Assert.Equal("success", result.Message); ``` ### INodeJSService.InvokeFromStringAsync @@ -531,14 +589,14 @@ Invokes a function exported by a NodeJS module in string form. - Type: `CancellationToken` - Description: The cancellation token for the asynchronous operation. #### Returns -The task object representing the asynchronous operation. +The task representing the asynchronous operation. #### Exceptions - `InvocationException` - Thrown if a NodeJS error occurs. - Thrown if the invocation request times out. - Thrown if NodeJS cannot be initialized. - `ObjectDisposedException` - - Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + - Thrown if this has been disposed or if it attempts to use one of its dependencies that has been disposed. - `OperationCanceledException` - Thrown if `cancellationToken` is cancelled. #### Example @@ -546,7 +604,7 @@ Using the class `Result`: ```csharp public class Result { - public string ResultMessage { get; set; } + public string Message { get; set; } } ``` The following assertion will pass: @@ -554,7 +612,7 @@ The following assertion will pass: Result result = await nodeJSService.InvokeFromStringAsync("module.exports = (callback, message) => callback(null, { resultMessage: message });", args: new[] { "success" }); -Assert.Equal("success", result.ResultMessage); +Assert.Equal("success", result.Message); ``` ### INodeJSService.InvokeFromStreamAsync #### Signature @@ -587,14 +645,14 @@ Invokes a function exported by a NodeJS module in Stream form. - Type: `CancellationToken` - Description: The cancellation token for the asynchronous operation. #### Returns -The task object representing the asynchronous operation. +The task representing the asynchronous operation. #### Exceptions - `InvocationException` - Thrown if a NodeJS error occurs. - Thrown if the invocation request times out. - Thrown if NodeJS cannot be initialized. - `ObjectDisposedException` - - Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + - Thrown if this has been disposed or if it attempts to use one of its dependencies that has been disposed. - `OperationCanceledException` - Thrown if `cancellationToken` is cancelled. #### Example @@ -602,7 +660,7 @@ Using the class `Result`: ```csharp public class Result { - public string ResultMessage { get; set; } + public string Message { get; set; } } ``` The following assertion will pass: @@ -617,7 +675,7 @@ using (var streamWriter = new StreamWriter(memoryStream)) Result result = await nodeJSService.InvokeFromStreamAsync(memoryStream, args: new[] { "success" }); - Assert.Equal("success", result.ResultMessage); + Assert.Equal("success", result.Message); } ``` ### INodeJSService.TryInvokeFromCacheAsync @@ -647,7 +705,7 @@ Attempts to invoke a function exported by a NodeJS module cached by NodeJS. - Type: `CancellationToken` - Description: The cancellation token for the asynchronous operation. #### Returns -The task object representing the asynchronous operation. On completion, the task returns a `(bool, T)` with the bool set to true on +The task representing the asynchronous operation. On completion, the task returns a `(bool, T)` with the bool set to true on success and false otherwise. #### Exceptions - `InvocationException` @@ -655,7 +713,7 @@ success and false otherwise. - Thrown if the invocation request times out. - Thrown if NodeJS cannot be initialized. - `ObjectDisposedException` - - Thrown if this instance has been disposed or if an attempt is made to use one of its dependencies that has been disposed. + - Thrown if this has been disposed or if it attempts to use one of its dependencies that has been disposed. - `OperationCanceledException` - Thrown if `cancellationToken` is cancelled. #### Example @@ -663,7 +721,7 @@ Using the class `Result`: ```csharp public class Result { - public string ResultMessage { get; set; } + public string Message { get; set; } } ``` The following assertion will pass: @@ -678,7 +736,7 @@ await nodeJSService.InvokeFromStringAsync("module.exports = (callback, m (bool success, Result result) = await nodeJSService.TryInvokeFromCacheAsync(cacheIdentifier, args: new[] { "success" }); Assert.True(success); -Assert.Equal("success", result.ResultMessage); +Assert.Equal("success", result.Message); ``` ## Extensibility From aa5b8060673c8f8aeb94ae88aa27b6a900fb54a6 Mon Sep 17 00:00:00 2001 From: JeremyTCD Date: Wed, 4 Dec 2019 21:00:01 +0800 Subject: [PATCH 11/11] Added release 5.2.0. --- Changelog.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 2f2fe92..98ccec2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,7 +3,20 @@ This project uses [semantic versioning](http://semver.org/spec/v2.0.0.html). Ref *[Semantic Versioning in Practice](https://www.jering.tech/articles/semantic-versioning-in-practice)* for an overview of semantic versioning. -## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/5.1.1...HEAD) +## [Unreleased](https://github.com/JeringTech/Javascript.NodeJS/compare/5.2.0...HEAD) + +## [5.2.0](https://github.com/JeringTech/Javascript.NodeJS/compare/5.1.1...5.2.0) - Dec 4, 2019 +### Fixes +- Expanded API. ([#57](https://github.com/JeringTech/Javascript.NodeJS/pull/57)). Added `INodeJSService` members for invocations without return values and + atomic/simplified caching-invoking: + - `Task InvokeFromFileAsync(string modulePath, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` + - `Task InvokeFromStringAsync(string moduleString, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` + - `Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` + - `Task InvokeFromStringAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` + - `Task InvokeFromStreamAsync(Stream moduleStream, string newCacheIdentifier = null, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` + - `Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` + - `Task InvokeFromStreamAsync(Func moduleFactory, string cacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` + - `Task TryInvokeFromCacheAsync(string moduleCacheIdentifier, string exportName = null, object[] args = null, CancellationToken cancellationToken = default);` ## [5.1.1](https://github.com/JeringTech/Javascript.NodeJS/compare/5.1.0...5.1.1) - Nov 29, 2019 ### Fixes