public void InvokeFromStringAsync_WithTypeParameter_WithRawStringModule_IsThreadSafe()
        {
            // Arrange
            const string      dummyArg    = "success";
            HttpNodeJSService testSubject = CreateHttpNodeJSService();

            // Act
            var       results    = new ConcurrentQueue <DummyResult>();
            const int numThreads = 5;
            var       threads    = new List <Thread>();

            for (int i = 0; i < numThreads; i++)
            {
                var thread = new Thread(() => results.Enqueue(testSubject.
                                                              InvokeFromStringAsync <DummyResult>(_dummyReturnsArgModule, 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);
            }
        }
        public void InvokeFromStringAsync_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 <DummyResult>();
            const int numThreads = 5;
            var       threads    = new List <Thread>();

            for (int i = 0; i < numThreads; i++)
            {
                var thread = new Thread(() => results.Enqueue(testSubject.InvokeFromStringAsync <DummyResult>(dummyModule, args: new[] { dummyResultString }).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(dummyResultString, result.Result);
            }
        }
Example #3
0
        public void HttpNodeJSService_FileWatching_GracefulShutdownEnabled_MoveToNewProcess_Setup()
        {
            var services = new ServiceCollection();

            services.AddNodeJS();
            services.Configure <OutOfProcessNodeJSServiceOptions>(options => options.EnableFileWatching = true);
            _serviceProvider   = services.BuildServiceProvider();
            _httpNodeJSService = _serviceProvider.GetRequiredService <INodeJSService>() as HttpNodeJSService;

            // Warmup. First run starts a Node.js process.
            _httpNodeJSService.InvokeFromStringAsync(DUMMY_WARMUP_MODULE).GetAwaiter().GetResult();
        }
        public async void AllInvokeMethods_InvokeAsyncJavascriptMethods()
        {
            // Arrange
            const string      dummyArg    = "success";
            HttpNodeJSService testSubject = CreateHttpNodeJSService();

            // Act
            DummyResult result = await testSubject.
                                 InvokeFromStringAsync <DummyResult>("module.exports = async (resultString) => {return {result: resultString};}", args : new[] { dummyArg }).ConfigureAwait(false);

            // Assert
            Assert.Equal(dummyArg, result.Result);
        }
        public async void InvokeFromStringAsync_InvokesJavascript()
        {
            // Arrange
            const string      dummyResultString = "success";
            HttpNodeJSService testSubject       = CreateHttpNodeJSService();

            // Act
            DummyResult result = await testSubject.
                                 InvokeFromStringAsync <DummyResult>("module.exports = (callback, resultString) => callback(null, {result: resultString});", args : new[] { dummyResultString }).ConfigureAwait(false);

            // Assert
            Assert.Equal(dummyResultString, result.Result);
        }
        public async void InvokeFromStringAsync_WithTypeParameter_WithRawStringModule_InvokesFromString()
        {
            // Arrange
            const string      dummyArg    = "success";
            HttpNodeJSService testSubject = CreateHttpNodeJSService();

            // Act
            DummyResult result = await testSubject.
                                 InvokeFromStringAsync <DummyResult>(_dummyReturnsArgModule, args : new[] { dummyArg }).ConfigureAwait(false);

            // Assert
            Assert.Equal(dummyArg, result.Result);
        }
        public async void TryInvokeFromCacheAsync_WithTypeParameter_InvokesFromCacheIfModuleIsCached()
        {
            // Arrange
            const string      dummyArg    = "success";
            HttpNodeJSService testSubject = CreateHttpNodeJSService();
            await testSubject.InvokeFromStringAsync <DummyResult>(_dummyReturnsArgModule, DUMMY_CACHE_IDENTIFIER, args : new[] { dummyArg }).ConfigureAwait(false);

            // Act
            (bool success, DummyResult value) = await testSubject.TryInvokeFromCacheAsync <DummyResult>(DUMMY_CACHE_IDENTIFIER, args : new[] { dummyArg }).ConfigureAwait(false);

            // Assert
            Assert.True(success);
            Assert.Equal(dummyArg, value.Result);
        }
        public async void AllInvokeMethods_InvokeASpecificExportIfExportNameIsNotNull()
        {
            // Arrange
            const string      dummyArg        = "success";
            const string      dummyExportName = "dummyExportName";
            HttpNodeJSService testSubject     = CreateHttpNodeJSService();

            // Act
            DummyResult result = await testSubject.
                                 InvokeFromStringAsync <DummyResult>($"module.exports = {{ {dummyExportName}: (callback, resultString) => callback(null, {{result: resultString}}) }};", exportName : dummyExportName, args : new[] { dummyArg }).ConfigureAwait(false);

            // Assert
            Assert.Equal(dummyArg, result.Result);
        }
        public async void InMemoryInvokeMethods_LoadRequiredModulesFromFilesInProjectDirectory()
        {
            // Arrange
            HttpNodeJSService testSubject = CreateHttpNodeJSService(projectPath: _projectPath); // Current directory is <test project path>/bin/debug/<framework>

            // Act
            int result = await testSubject.InvokeFromStringAsync <int>(@"const value = require('./dummyReturnsValueModule.js');

module.exports = (callback) => {
    callback(null, value);
};").ConfigureAwait(false);

            // Assert
            Assert.Equal(10, result); // dummyReturnsValueModule.js just exports 10
        }
        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 <int>(DUMMY_CACHE_IDENTIFIER, "getNumber").ConfigureAwait(false);

            Assert.True(success);
            Assert.Equal(1, result);
        }
        public async void InvokeFromStringAsync_WithTypeParameter_WithModuleFactory_InvokesFromStringAndCachesModuleIfModuleIsNotCached()
        {
            // Arrange
            const string      dummyArg    = "success";
            HttpNodeJSService testSubject = CreateHttpNodeJSService();

            // Act
            // Module hasn't been cached, so if this returns the expected value, string was sent over
            DummyResult result1 = await testSubject.
                                  InvokeFromStringAsync <DummyResult>(() => _dummyReturnsArgModule, DUMMY_CACHE_IDENTIFIER, args : new[] { dummyArg }).ConfigureAwait(false);

            // Assert
            // Ensure module was cached
            (bool success, DummyResult result2) = await testSubject.TryInvokeFromCacheAsync <DummyResult>(DUMMY_CACHE_IDENTIFIER, args : new[] { dummyArg }).ConfigureAwait(false);

            Assert.Equal(dummyArg, result1.Result);
            Assert.True(success);
            Assert.Equal(dummyArg, result2.Result);
        }
        public async void AllInvokeMethods_ReceiveAndLogStderrOutput(string dummyLogArgument, string expectedResult)
        {
            // Arrange
            var resultStringBuilder       = new StringBuilder();
            HttpNodeJSService testSubject = CreateHttpNodeJSService(resultStringBuilder);

            // Act
            await testSubject.
            InvokeFromStringAsync <string>($@"module.exports = (callback) => {{ 
            console.error({dummyLogArgument}); 
            callback();
        }}").ConfigureAwait(false);

            Thread.Sleep(1000);
            ((IDisposable)_serviceProvider).Dispose();
            string result = resultStringBuilder.ToString();

            // Assert
            Assert.Equal($"{nameof(LogLevel.Error)}: {expectedResult}\n", result, ignoreLineEndingDifferences: true);
        }
Example #13
0
        public async void AllInvokeMethods_ReceiveAndLogMessages()
        {
            // Arrange
            const string      dummySinglelineString = "dummySingleLineString";
            const string      dummyMultilineString  = "dummy\nMultiline\nString\n";
            var               resultStringBuilder   = new StringBuilder();
            HttpNodeJSService testSubject           = CreateHttpNodeService(resultStringBuilder);

            // Act
            await testSubject.
            InvokeFromStringAsync <string>($"module.exports = (callback) => {{ console.log('{dummySinglelineString}'); console.log(`{dummyMultilineString}`); callback();}}").ConfigureAwait(false);

            // Disposing of HttpNodeServices causes Process.WaitForExit(500) to be called on the node process, this give it time to flush its output.
            // This isn't super clean.
            ((IDisposable)_serviceProvider).Dispose();
            string result = resultStringBuilder.ToString();

            // Assert
            Assert.Equal($"{dummySinglelineString}\n{dummyMultilineString}", result, ignoreLineEndingDifferences: true);
        }
        public async void TryInvokeFromCacheAsync_InvokesJavascriptIfModuleIsCached()
        {
            // Arrange
            const string      dummyResultString    = "success";
            const string      dummyCacheIdentifier = "dummyCacheIdentifier";
            HttpNodeJSService testSubject          = CreateHttpNodeJSService();

            // Cache
            await testSubject.
            InvokeFromStringAsync <DummyResult>("module.exports = (callback, resultString) => callback(null, {result: resultString});",
                                                dummyCacheIdentifier,
                                                args : new[] { dummyResultString }).
            ConfigureAwait(false);

            // Act
            (bool success, DummyResult value) = await testSubject.TryInvokeFromCacheAsync <DummyResult>(dummyCacheIdentifier, args : new[] { dummyResultString }).ConfigureAwait(false);

            // Assert
            Assert.True(success);
            Assert.Equal(dummyResultString, value.Result);
        }
        public async void TryInvokeFromCacheAsync_IsThreadSafe()
        {
            // Arrange
            const string      dummyResultString    = "success";
            const string      dummyCacheIdentifier = "dummyCacheIdentifier";
            HttpNodeJSService testSubject          = CreateHttpNodeJSService();

            // Cache
            await testSubject.
            InvokeFromStringAsync <DummyResult>("module.exports = (callback, resultString) => callback(null, {result: resultString});",
                                                dummyCacheIdentifier,
                                                args : new[] { dummyResultString }).
            ConfigureAwait(false);

            // Act
            var       results    = new ConcurrentQueue <(bool, DummyResult)>();
            const int numThreads = 5;
            var       threads    = new List <Thread>();

            for (int i = 0; i < numThreads; i++)
            {
                var thread = new Thread(() => results.Enqueue(testSubject.TryInvokeFromCacheAsync <DummyResult>(dummyCacheIdentifier, args: new[] { dummyResultString }).GetAwaiter().GetResult()));
                threads.Add(thread);
                thread.Start();
            }
            foreach (Thread thread in threads)
            {
                thread.Join();
            }

            // Assert
            Assert.Equal(numThreads, results.Count);
            foreach ((bool success, DummyResult value) in results)
            {
                Assert.True(success);
                Assert.Equal(dummyResultString, value.Result);
            }
        }
        public async void AllInvokeMethods_ReceiveAndLogMessages()
        {
            // Arrange
            const string      dummySinglelineString = "dummySingleLineString";
            const string      dummyMultilineString  = "dummy\nMultiline\nString\n";
            var               resultStringBuilder   = new StringBuilder();
            HttpNodeJSService testSubject           = CreateHttpNodeJSService(resultStringBuilder);

            // Act
            await testSubject.
            InvokeFromStringAsync <string>($@"module.exports = (callback) => {{ 
    console.log('{dummySinglelineString}'); 
    console.log(`{dummyMultilineString}`);
    callback();

    // Does not work
    // process.stdout.end();
    // process.on(finish, () => callback());

    // 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(500) works almost all the time, but isn't a clean solution.
            Thread.Sleep(500);
            ((IDisposable)_serviceProvider).Dispose();
            string result = resultStringBuilder.ToString();

            // Assert
            Assert.Equal($"{nameof(LogLevel.Information)}: {dummySinglelineString}\n{nameof(LogLevel.Information)}: {dummyMultilineString}", result, ignoreLineEndingDifferences: true);
        }
        public async void AllInvokeMethods_HandleHttpClientErrorsSuchAsMalformedRequests()
        {
            // Arrange
            HttpNodeJSService testSubject = CreateHttpNodeJSService();
            await testSubject.InvokeFromStringAsync("module.exports = callback => callback();").ConfigureAwait(false); // Starts the Node.js process

            Uri dummyEndpoint    = testSubject.Endpoint;
            var dummyJsonService = new JsonService();

            // Act
            using (var dummyHttpClient = new HttpClient())
                // Send a request with an invalid HTTP method. NodeJS drops the connection halfway through and fires the clientError event - https://nodejs.org/api/http.html#http_event_clienterror
                using (var dummyHttpRequestMessage = new HttpRequestMessage(new HttpMethod("INVALID"), dummyEndpoint))
                    using (HttpResponseMessage dummyHttpResponseMessage = await dummyHttpClient.SendAsync(dummyHttpRequestMessage).ConfigureAwait(false))
                        using (Stream dummyStream = await dummyHttpResponseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
                        {
                            InvocationError result = await dummyJsonService.DeserializeAsync <InvocationError>(dummyStream).ConfigureAwait(false);

                            // Assert
                            Assert.Equal(HttpStatusCode.InternalServerError, dummyHttpResponseMessage.StatusCode);
                            Assert.False(string.IsNullOrWhiteSpace(result.ErrorMessage));
                            Assert.False(string.IsNullOrWhiteSpace(result.ErrorStack));
                        }
        }
        public async void AllInvokeMethods_ThrowInvocationExceptionIfInvokedAsyncMethodThrowsError()
        {
            // Arrange
            const string      dummyErrorString = "error";
            HttpNodeJSService testSubject      = CreateHttpNodeJSService();

            // Act
            InvocationException result = await Assert.ThrowsAsync <InvocationException>(() =>
                                                                                        testSubject.InvokeFromStringAsync <DummyResult>("module.exports = async (errorString) => {throw new Error(errorString);}", args: new[] { dummyErrorString })).
                                         ConfigureAwait(false);

            // Assert
            Assert.StartsWith(dummyErrorString, result.Message); // Complete message includes the stack
        }
        public async void AllInvokeMethods_ThrowInvocationExceptionIfNoExportNameSpecifiedAndModuleExportsIsNotAFunction()
        {
            // Arrange
            HttpNodeJSService testSubject = CreateHttpNodeJSService();

            // Act
            InvocationException result = await Assert.ThrowsAsync <InvocationException>(() =>
                                                                                        testSubject.InvokeFromStringAsync <DummyResult>("module.exports = {result: 'not a function'};")).
                                         ConfigureAwait(false);

            // Assert
            Assert.StartsWith("The module \"module.exports = {result:...\" does not export a function.", result.Message);
        }
        public async void AllInvokeMethods_ThrowInvocationExceptionIfModuleExportWithSpecifiedExportNameIsNotAFunction()
        {
            // Arrange
            const string      dummyExportName = "dummyExportName";
            HttpNodeJSService testSubject     = CreateHttpNodeJSService();

            // Act
            InvocationException result = await Assert.ThrowsAsync <InvocationException>(() =>
                                                                                        testSubject.InvokeFromStringAsync <DummyResult>($"module.exports = {{{dummyExportName}: 'not a function'}};", exportName: dummyExportName)).
                                         ConfigureAwait(false);

            // Assert
            Assert.StartsWith($"The export named {dummyExportName} from module \"module.exports = {{dummyEx...\" is not a function.", result.Message);
        }
        public async void AllInvokeMethods_ThrowInvocationExceptionIfThereIsNoModuleExportWithSpecifiedExportName()
        {
            // Arrange
            const string      dummyExportName = "dummyExportName";
            HttpNodeJSService testSubject     = CreateHttpNodeJSService();

            // Act
            InvocationException result = await Assert.ThrowsAsync <InvocationException>(() =>
                                                                                        testSubject.InvokeFromStringAsync <DummyResult>("module.exports = (callback) => callback(null, {result: 'success'});", DUMMY_CACHE_IDENTIFIER, dummyExportName)).
                                         ConfigureAwait(false);

            // Assert
            Assert.StartsWith($"The module {DUMMY_CACHE_IDENTIFIER} has no export named {dummyExportName}.", result.Message);
        }
        public async void AllInvokeMethods_ThrowInvocationExceptionIfModuleHasNoExports()
        {
            // Arrange
            const string      dummyModule = "return null;";
            HttpNodeJSService testSubject = CreateHttpNodeJSService();

            // Act
            InvocationException result = await Assert.ThrowsAsync <InvocationException>(() =>
                                                                                        testSubject.InvokeFromStringAsync <DummyResult>(dummyModule)).
                                         ConfigureAwait(false);

            // Assert
            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);
        }