diff --git a/.incrementalist/testsOnlyNetFx.json b/.incrementalist/testsOnlyNetFx.json new file mode 100644 index 00000000000..8494a5d096c --- /dev/null +++ b/.incrementalist/testsOnlyNetFx.json @@ -0,0 +1,19 @@ +{ + "outputFile": "bin/output/incrementalist.txt", + "gitBranch": "dev", + "verbose": false, + "timeoutMinutes": 20, + "continueOnError": true, + "runInParallel": false, + "failOnNoProjects": false, + "skip": [ + "**/.Tests.MultiNode.csproj", + "**/Akka.MultiNode.TestAdapter.Tests.csproj", + "src/examples/**" + ], + "target": [ + "**/*.Tests.csproj", + "**/**/Akka.Streams.Tests.TCK.csproj", + "**/.Tests.fsproj" + ] +} \ No newline at end of file diff --git a/.slopwatch/baseline.json b/.slopwatch/baseline.json new file mode 100644 index 00000000000..97764d70d35 --- /dev/null +++ b/.slopwatch/baseline.json @@ -0,0 +1,737 @@ +{ + "version": 1, + "createdAt": "2026-03-25T14:09:31.5934528+00:00", + "updatedAt": "2026-03-25T14:09:31.5956824+00:00", + "description": "Initial baseline created by 'slopwatch init' on 2026-03-25 14:09:31 UTC", + "entries": [ + { + "hash": "51a77db094a515e7", + "ruleId": "SW005", + "filePath": "Directory.Build.props", + "lineNumber": 16, + "codeSnippet": "$(NoWarn);CS1591;xUnit1013;xUnit1031;CS8632;xUnit1051", + "message": "Adding warnings to NoWarn: CS1591;xUnit1013;xUnit1031;CS8632;xUnit1051", + "baselinedAt": "2026-03-25T14:09:31.5954649+00:00" + }, + { + "hash": "573cc6dd28e7382e", + "ruleId": "SW002", + "filePath": "src/benchmark/PingPong/ClientAsyncActor.cs", + "lineNumber": 12, + "codeSnippet": "#pragma warning disable 1998 //async method lacks an await", + "message": "#pragma warning disable for warnings 1998 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5955137+00:00" + }, + { + "hash": "5c346f8d07ea6c1d", + "ruleId": "SW002", + "filePath": "src/benchmark/PingPong/ClientAsyncActor.cs", + "lineNumber": 13, + "codeSnippet": "#pragma warning disable AK1003 // ReceiveAsync lacks an await", + "message": "#pragma warning disable for warnings AK1003 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5955172+00:00" + }, + { + "hash": "f61b8f3283ee0922", + "ruleId": "SW003", + "filePath": "src/contrib/cluster/Akka.Cluster.Metrics/Collectors/DefaultCollector.cs", + "lineNumber": 89, + "codeSnippet": "catch (Exception)\r\n {\r\n // MaxWorkingSet may throw on some platforms (e.g., Linux/Mono)\r\n // Ignore and continue without this metric\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.595525+00:00" + }, + { + "hash": "dfaea02bf2c86baf", + "ruleId": "SW003", + "filePath": "src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs", + "lineNumber": 1072, + "codeSnippet": "catch\r\n {\r\n // no-op, we're shutting down anyway.\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955298+00:00" + }, + { + "hash": "1ea9daaa12cb519b", + "ruleId": "SW003", + "filePath": "src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/MultiNodeClusterShardingSpec.cs", + "lineNumber": 198, + "codeSnippet": "catch (Exception)\r\n {\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955321+00:00" + }, + { + "hash": "c277e2108b358daa", + "ruleId": "SW003", + "filePath": "src/contrib/cluster/Akka.Cluster.Tools/Singleton/ClusterSingleton.cs", + "lineNumber": 74, + "codeSnippet": "catch (InvalidActorNameException ex) when (ex.Message.EndsWith(\"is not unique!\"))\r\n {\r\n // This is fine. We just wanted to make sure it is running and it already is\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955346+00:00" + }, + { + "hash": "845a9df7b790d887", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/ActorCell.DefaultMessages.cs", + "lineNumber": 390, + "codeSnippet": "catch\r\n {\r\n //TODO: Hmmm?\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955372+00:00" + }, + { + "hash": "4178214ea8948200", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/ActorCell.FaultHandling.cs", + "lineNumber": 475, + "codeSnippet": "catch\r\n {\r\n //TODO: Hmmm?\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955392+00:00" + }, + { + "hash": "ce7efe5072243fc9", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/ActorRef.cs", + "lineNumber": 79, + "codeSnippet": "catch\r\n {\r\n // we don't do error handling for this - we do not care\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955419+00:00" + }, + { + "hash": "bd149108a88e3e51", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/ActorRef.cs", + "lineNumber": 1182, + "codeSnippet": "catch (Exception) { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955439+00:00" + }, + { + "hash": "c5f9337f769c1e9d", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/SupervisorStrategy.cs", + "lineNumber": 179, + "codeSnippet": "catch (Exception)\r\n {\r\n // swallow any exceptions\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955459+00:00" + }, + { + "hash": "e19886191172e32e", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/Internal/ActorSystemImpl.cs", + "lineNumber": 239, + "codeSnippet": "catch\r\n {\r\n // ignored\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955479+00:00" + }, + { + "hash": "d6fa4922660bdbd4", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/Scheduler/HashedWheelTimerScheduler.cs", + "lineNumber": 522, + "codeSnippet": "catch (SchedulerException)\r\n {\r\n // ignore, this is from terminated actors\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955535+00:00" + }, + { + "hash": "0b95a01799e7e19c", + "ruleId": "SW003", + "filePath": "src/core/Akka/Actor/Scheduler/HashedWheelTimerScheduler.cs", + "lineNumber": 782, + "codeSnippet": "catch\r\n {\r\n // suppress any errors thrown during logging\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955557+00:00" + }, + { + "hash": "63ddf742f71dea2d", + "ruleId": "SW003", + "filePath": "src/core/Akka/Dispatch/ChannelSchedulerExtension.cs", + "lineNumber": 324, + "codeSnippet": "catch\r\n {\r\n //ignore error\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955575+00:00" + }, + { + "hash": "8f246a0e213ae0d5", + "ruleId": "SW003", + "filePath": "src/core/Akka/Dispatch/Dispatchers.cs", + "lineNumber": 167, + "codeSnippet": "catch\r\n {\r\n // suppress exceptions\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955596+00:00" + }, + { + "hash": "136828c217cc3915", + "ruleId": "SW003", + "filePath": "src/core/Akka/IO/Tcp.cs", + "lineNumber": 898, + "codeSnippet": "catch (Exception) { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955615+00:00" + }, + { + "hash": "412124630e61d3c7", + "ruleId": "SW003", + "filePath": "src/core/Akka/IO/TcpConnection.cs", + "lineNumber": 741, + "codeSnippet": "catch\r\n {\r\n /* ignore */\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955634+00:00" + }, + { + "hash": "0c7c28bc324a7a7b", + "ruleId": "SW003", + "filePath": "src/core/Akka/IO/TcpOutgoingConnection.cs", + "lineNumber": 68, + "codeSnippet": "catch (InvalidOperationException)\r\n {\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955668+00:00" + }, + { + "hash": "30b2a94386c0da0f", + "ruleId": "SW002", + "filePath": "src/core/Akka.Cluster.Tests/MemberOrderingModelBasedTests.cs", + "lineNumber": 20, + "codeSnippet": "#pragma warning disable xUnit1028", + "message": "#pragma warning disable for warnings xUnit1028 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5955687+00:00" + }, + { + "hash": "5a60d012890f5d94", + "ruleId": "SW002", + "filePath": "src/core/Akka.Docs.Tests/Testkit/ParentSampleTest.cs", + "lineNumber": 14, + "codeSnippet": "#pragma warning disable CS0414 // Field is assigned but its value is never used. This is for documentation purposes, its fine. ", + "message": "#pragma warning disable for warnings CS0414 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5955709+00:00" + }, + { + "hash": "9d0193b3ead7f0de", + "ruleId": "SW003", + "filePath": "src/core/Akka.MultiNode.RemoteHost/RemoteHost.cs", + "lineNumber": 50, + "codeSnippet": "catch{}", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955724+00:00" + }, + { + "hash": "dc281fe084e8fc9a", + "ruleId": "SW001", + "filePath": "src/core/Akka.MultiNode.SampleMultiNodeTests/IgnoredXunitTest.cs", + "lineNumber": 9, + "codeSnippet": "Fact(Skip = \"This test should be ignored by MNTR\")", + "message": "Test method 'Ignored_test' is disabled: This test should be ignored by MNTR", + "baselinedAt": "2026-03-25T14:09:31.5955739+00:00" + }, + { + "hash": "c128afaabd09216e", + "ruleId": "SW003", + "filePath": "src/core/Akka.MultiNode.TestAdapter/Configuration/OptionsReader.cs", + "lineNumber": 36, + "codeSnippet": "catch\r\n { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955757+00:00" + }, + { + "hash": "161c46d53af3a81f", + "ruleId": "SW003", + "filePath": "src/core/Akka.MultiNode.TestAdapter/Configuration/OptionsReader.cs", + "lineNumber": 80, + "codeSnippet": "catch { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955773+00:00" + }, + { + "hash": "efd03e5f2d3f8292", + "ruleId": "SW003", + "filePath": "src/core/Akka.MultiNode.TestAdapter/NodeRunner/Executor.cs", + "lineNumber": 179, + "codeSnippet": "catch { /* well... at least we have tried */ }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955798+00:00" + }, + { + "hash": "68261c2dc0a451fa", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.TestKit/ConnectionInterceptors.cs", + "lineNumber": 97, + "codeSnippet": "catch (OperationCanceledException)\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955858+00:00" + }, + { + "hash": "0a2630229d63ab46", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.TestKit/ConnectionInterceptors.cs", + "lineNumber": 101, + "codeSnippet": "catch (TimeoutException)\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955875+00:00" + }, + { + "hash": "13dbfd621fcd7591", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.TestKit/Journal/JournalInterceptors.cs", + "lineNumber": 127, + "codeSnippet": "catch (OperationCanceledException)\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.59559+00:00" + }, + { + "hash": "61fd6e63e6d74c52", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.TestKit/Journal/JournalInterceptors.cs", + "lineNumber": 131, + "codeSnippet": "catch (TimeoutException)\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955917+00:00" + }, + { + "hash": "3ebe4f00788a15eb", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.TestKit/SnapshotStore/SnapshotStoreInterceptors.cs", + "lineNumber": 98, + "codeSnippet": "catch (OperationCanceledException)\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955936+00:00" + }, + { + "hash": "0dc21eb186747457", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.TestKit/SnapshotStore/SnapshotStoreInterceptors.cs", + "lineNumber": 102, + "codeSnippet": "catch (TimeoutException)\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955955+00:00" + }, + { + "hash": "13f21bca41231e30", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.Tests/PersistenceSpec.cs", + "lineNumber": 121, + "codeSnippet": "catch (IOException) { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5955971+00:00" + }, + { + "hash": "daf1343e8543ba73", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.Tests/ReceivePersistentActorAsyncAwaitSpec.cs", + "lineNumber": 615, + "codeSnippet": "catch (Exception)\r\n {\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.595599+00:00" + }, + { + "hash": "1bf7bf24c01f18dc", + "ruleId": "SW003", + "filePath": "src/core/Akka.Persistence.Tests/SnapshotDirectoryFailureSpec.cs", + "lineNumber": 66, + "codeSnippet": "catch { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956004+00:00" + }, + { + "hash": "3a24c8bc72c2bdfe", + "ruleId": "SW002", + "filePath": "src/core/Akka.Remote/Endpoint.cs", + "lineNumber": 7, + "codeSnippet": "#pragma warning disable AK1004", + "message": "#pragma warning disable for warnings AK1004 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.595602+00:00" + }, + { + "hash": "032823f713deea61", + "ruleId": "SW002", + "filePath": "src/core/Akka.Remote/EndpointManager.cs", + "lineNumber": 7, + "codeSnippet": "#pragma warning disable AK1004", + "message": "#pragma warning disable for warnings AK1004 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956039+00:00" + }, + { + "hash": "8e6754b39cbbbef5", + "ruleId": "SW002", + "filePath": "src/core/Akka.Remote/RemoteWatcher.cs", + "lineNumber": 7, + "codeSnippet": "#pragma warning disable AK1004", + "message": "#pragma warning disable for warnings AK1004 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956054+00:00" + }, + { + "hash": "3f30d9efd91f5ae6", + "ruleId": "SW003", + "filePath": "src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs", + "lineNumber": 234, + "codeSnippet": "catch\r\n {\r\n // ignore errors occurring during shutdown\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956074+00:00" + }, + { + "hash": "6d6dfc7c835742e6", + "ruleId": "SW003", + "filePath": "src/core/Akka.Remote.Tests/Transport/DotNettyCertificateValidationSpec.cs", + "lineNumber": 101, + "codeSnippet": "catch { /* ignore */ }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956089+00:00" + }, + { + "hash": "dc73244806271cf5", + "ruleId": "SW003", + "filePath": "src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs", + "lineNumber": 103, + "codeSnippet": "catch { /* ignore */ }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956106+00:00" + }, + { + "hash": "28b142cc54b1eaf9", + "ruleId": "SW003", + "filePath": "src/core/Akka.Remote.Tests/Transport/DotNettyTlsHandshakeFailureSpec.cs", + "lineNumber": 187, + "codeSnippet": "catch\r\n {\r\n // Connection might be closed by server, that's expected\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956129+00:00" + }, + { + "hash": "7ddd220c868e2611", + "ruleId": "SW002", + "filePath": "src/core/Akka.Streams/StreamRefs.cs", + "lineNumber": 16, + "codeSnippet": "#pragma warning disable 628", + "message": "#pragma warning disable for warnings 628 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956142+00:00" + }, + { + "hash": "26b2f4088086abac", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams/Dsl/Hub.cs", + "lineNumber": 731, + "codeSnippet": "catch\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956161+00:00" + }, + { + "hash": "2b5b2f6cdd1a6ff8", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams/Implementation/ActorPublisher.cs", + "lineNumber": 269, + "codeSnippet": "catch (Exception exception)\r\n when (exception is ISpecViolation)\r\n {\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956184+00:00" + }, + { + "hash": "5ad3e20bef0c50f0", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams/Implementation/CompletedPublishers.cs", + "lineNumber": 40, + "codeSnippet": "catch (Exception e)\r\n when (e is ISpecViolation)\r\n {\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956206+00:00" + }, + { + "hash": "055953950152623a", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams/Implementation/Fusing/ActorGraphInterpreter.cs", + "lineNumber": 357, + "codeSnippet": "catch (Exception) { /* swallow? */ }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956253+00:00" + }, + { + "hash": "c94c0f36e7b14586", + "ruleId": "SW002", + "filePath": "src/core/Akka.Streams/Implementation/Fusing/GraphStages.cs", + "lineNumber": 532, + "codeSnippet": "SuppressMessage(\"ReSharper\", \"MethodSupportsCancellation\")", + "message": "SuppressMessage attribute suppressing ReSharper:MethodSupportsCancellation", + "baselinedAt": "2026-03-25T14:09:31.5956285+00:00" + }, + { + "hash": "6c216acbb6961bb6", + "ruleId": "SW002", + "filePath": "src/core/Akka.Streams/Implementation/IO/FilePublisher.cs", + "lineNumber": 19, + "codeSnippet": "#pragma warning disable 1587", + "message": "#pragma warning disable for warnings 1587 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956301+00:00" + }, + { + "hash": "2367574532adc0be", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams.Tests/Dsl/FlowRecoverWithSpec.cs", + "lineNumber": 291, + "codeSnippet": "catch (AggregateException) { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956315+00:00" + }, + { + "hash": "150b8b26fb03a8b3", + "ruleId": "SW002", + "filePath": "src/core/Akka.Streams.Tests/Dsl/FlowSelectAsyncSpec.cs", + "lineNumber": 34, + "codeSnippet": "#pragma warning disable 162", + "message": "#pragma warning disable for warnings 162 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956332+00:00" + }, + { + "hash": "fcb317c47a611f54", + "ruleId": "SW002", + "filePath": "src/core/Akka.Streams.Tests/Dsl/FlowSelectAsyncUnorderedSpec.cs", + "lineNumber": 29, + "codeSnippet": "#pragma warning disable 162", + "message": "#pragma warning disable for warnings 162 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956349+00:00" + }, + { + "hash": "3624807874bb4d89", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams.Tests/Dsl/LazySinkSpec.cs", + "lineNumber": 205, + "codeSnippet": "catch (AggregateException) { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956363+00:00" + }, + { + "hash": "af2a82b4dcd11067", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams.Tests/Dsl/LazySourceSpec.cs", + "lineNumber": 208, + "codeSnippet": "catch (AggregateException) { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956376+00:00" + }, + { + "hash": "59f8995ddd6c2ef9", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs", + "lineNumber": 549, + "codeSnippet": "catch\r\n {\r\n // Expected: at least one should fail\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956396+00:00" + }, + { + "hash": "7400d07d2a8a42a0", + "ruleId": "SW003", + "filePath": "src/core/Akka.Streams.Tests/Implementation/Fusing/KeepGoingStageSpec.cs", + "lineNumber": 304, + "codeSnippet": "catch { }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956411+00:00" + }, + { + "hash": "a609db5f57106cbd", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit/TestKitBase_AwaitAssert.cs", + "lineNumber": 75, + "codeSnippet": "Task.Delay(t, cancellationToken)", + "message": "Test uses Task.Delay(t) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956428+00:00" + }, + { + "hash": "d5c9710b9beb8ed9", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit/TestKitBase_AwaitConditions.cs", + "lineNumber": 314, + "codeSnippet": "Task.Delay(sleepDuration, cancellationToken)", + "message": "Test uses Task.Delay(sleepDuration) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956453+00:00" + }, + { + "hash": "84080843d9d3bd4e", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit/TestKitBase_Receive.cs", + "lineNumber": 426, + "codeSnippet": "Task.Delay(maxDuration, cancellationToken)", + "message": "Test uses Task.Delay(maxDuration) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956468+00:00" + }, + { + "hash": "5923f4149699d248", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit/TestKitBase_Within.cs", + "lineNumber": 309, + "codeSnippet": "Task.Delay(max + TimeSpan.FromMilliseconds(200), cts.Token)", + "message": "Test uses Task.Delay(max + TimeSpan.FromMilliseconds(200)) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956482+00:00" + }, + { + "hash": "ec95e3aaf5589b04", + "ruleId": "SW003", + "filePath": "src/core/Akka.TestKit/EventFilter/TestEventListener.cs", + "lineNumber": 128, + "codeSnippet": "catch\r\n {\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956497+00:00" + }, + { + "hash": "a9d628c17aff3ff6", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit.Tests/TestKitAsyncCancellationSpec.cs", + "lineNumber": 63, + "codeSnippet": "Task.Delay(100)", + "message": "Test uses Task.Delay(100) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956515+00:00" + }, + { + "hash": "54434ed7e0a9b021", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit.Tests/TestKitAsyncCancellationSpec.cs", + "lineNumber": 130, + "codeSnippet": "Task.Delay(TimeSpan.FromSeconds(1))", + "message": "Test uses Task.Delay(TimeSpan.FromSeconds(1)) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956527+00:00" + }, + { + "hash": "2965783b3d651c63", + "ruleId": "SW002", + "filePath": "src/core/Akka.TestKit.Tests/TestEventListenerTests/AllTestForEventFilterBase.cs", + "lineNumber": 236, + "codeSnippet": "#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed", + "message": "#pragma warning disable for warnings CS4014 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956546+00:00" + }, + { + "hash": "4322712ce7ce9c41", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs", + "lineNumber": 154, + "codeSnippet": "Task.Delay(halfMax)", + "message": "Test uses Task.Delay(halfMax) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.595656+00:00" + }, + { + "hash": "c419aab74e2bc6b8", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit.Tests/TestKitBaseTests/ReceiveTests.cs", + "lineNumber": 158, + "codeSnippet": "Task.Delay(doubleMax)", + "message": "Test uses Task.Delay(doubleMax) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956585+00:00" + }, + { + "hash": "c92171bee5deb8c7", + "ruleId": "SW003", + "filePath": "src/core/Akka.TestKit.Tests/TestKitBaseTests/WithinTests.cs", + "lineNumber": 96, + "codeSnippet": "catch (System.Threading.Tasks.TaskCanceledException)\r\n {\r\n // This is expected if the test completes in time\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956605+00:00" + }, + { + "hash": "adf89ad86f045ca3", + "ruleId": "SW004", + "filePath": "src/core/Akka.TestKit.Tests/TestKitBaseTests/WithinTests.cs", + "lineNumber": 92, + "codeSnippet": "Task.Delay(shortTimeout.Add(300.Milliseconds()), timerCts.Token)", + "message": "Test uses Task.Delay(shortTimeout.Add(300.Milliseconds())) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956616+00:00" + }, + { + "hash": "1a628586626eddde", + "ruleId": "SW002", + "filePath": "src/core/Akka.Tests/Actor/RemotePathParsingSpec.cs", + "lineNumber": 18, + "codeSnippet": "#pragma warning disable xUnit1028", + "message": "#pragma warning disable for warnings xUnit1028 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956636+00:00" + }, + { + "hash": "0b85d5bcb03a7024", + "ruleId": "SW004", + "filePath": "src/core/Akka.Tests/Actor/Scheduler/TaskBasedScheduler_ActionScheduler_Schedule_Tests.cs", + "lineNumber": 293, + "codeSnippet": "Task.Delay(200)", + "message": "Test uses Task.Delay(200) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956653+00:00" + }, + { + "hash": "83f4deffa6900e17", + "ruleId": "SW001", + "filePath": "src/core/Akka.Tests/Configuration/HoconTests.cs", + "lineNumber": 748, + "codeSnippet": "Fact(Skip = \"we currently do not make any distinction between quoted and unquoted strings once parsed\")", + "message": "Test method 'Can_assign_quoted_null_string_to_field' is disabled: we currently do not make any distinction between quoted and unquoted strings once parsed", + "baselinedAt": "2026-03-25T14:09:31.595667+00:00" + }, + { + "hash": "c14f9352e155f981", + "ruleId": "SW001", + "filePath": "src/core/Akka.Tests/Configuration/HoconTests.cs", + "lineNumber": 836, + "codeSnippet": "Fact(Skip = \"Not allowed according to current HOCON spec\")", + "message": "Test method 'Can_parse_unquoted_ipv6' is disabled: Not allowed according to current HOCON spec", + "baselinedAt": "2026-03-25T14:09:31.5956682+00:00" + }, + { + "hash": "87a59931dbccfe59", + "ruleId": "SW001", + "filePath": "src/core/Akka.Tests/Configuration/HoconTests.cs", + "lineNumber": 883, + "codeSnippet": "Fact(Skip = \"not working yet\")", + "message": "Test method 'Can_substitute_with_concated_string' is disabled: not working yet", + "baselinedAt": "2026-03-25T14:09:31.5956693+00:00" + }, + { + "hash": "50fb53e8b912daf7", + "ruleId": "SW003", + "filePath": "src/core/Akka.Tests/Dispatch/AsyncAwaitSpec.cs", + "lineNumber": 407, + "codeSnippet": "catch (Exception)\r\n {\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956713+00:00" + }, + { + "hash": "642c86e03d34588a", + "ruleId": "SW003", + "filePath": "src/core/Akka.Tests/Util/ResultSpec.cs", + "lineNumber": 167, + "codeSnippet": "catch\r\n {\r\n // no-op\r\n }", + "message": "Empty catch block swallows exceptions without handling", + "baselinedAt": "2026-03-25T14:09:31.5956731+00:00" + }, + { + "hash": "f4cc61a6d1e94d1f", + "ruleId": "SW002", + "filePath": "src/core/Akka.Tests/Util/StableListPriorityQueueSpec.cs", + "lineNumber": 16, + "codeSnippet": "#pragma warning disable xUnit1028", + "message": "#pragma warning disable for warnings xUnit1028 without matching restore in same scope", + "baselinedAt": "2026-03-25T14:09:31.5956762+00:00" + }, + { + "hash": "50cf9910b71e3945", + "ruleId": "SW002", + "filePath": "src/core/Akka.Tests/Util/TokenBucketSpec.cs", + "lineNumber": 221, + "codeSnippet": "SuppressMessage(\"ReSharper\", \"ConditionIsAlwaysTrueOrFalse\")", + "message": "SuppressMessage attribute suppressing ReSharper:ConditionIsAlwaysTrueOrFalse", + "baselinedAt": "2026-03-25T14:09:31.5956779+00:00" + }, + { + "hash": "e17f013523d0b47f", + "ruleId": "SW004", + "filePath": "src/core/Akka.Tests.Performance/Actor/Scheduler/DefaultSchedulerPerformanceTests.cs", + "lineNumber": 61, + "codeSnippet": "Thread.Sleep(40)", + "message": "Test uses Thread.Sleep(40) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956796+00:00" + }, + { + "hash": "069247e6789d6acc", + "ruleId": "SW004", + "filePath": "src/core/Akka.Tests.Performance/Actor/Scheduler/DefaultSchedulerPerformanceTests.cs", + "lineNumber": 77, + "codeSnippet": "Task.Delay(RunTime).Wait()", + "message": "Test uses Task.Delay(?) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956811+00:00" + }, + { + "hash": "ca00c698db9401b2", + "ruleId": "SW004", + "filePath": "src/core/Akka.Tests.Performance/Actor/Scheduler/DefaultSchedulerPerformanceTests.cs", + "lineNumber": 77, + "codeSnippet": "Task.Delay(RunTime)", + "message": "Test uses Task.Delay(RunTime) which may indicate a timing-dependent test", + "baselinedAt": "2026-03-25T14:09:31.5956823+00:00" + } + ] +} \ No newline at end of file diff --git a/Akka.slnx b/Akka.slnx index fc0ba4941fb..fcaf7b64b22 100644 --- a/Akka.slnx +++ b/Akka.slnx @@ -127,6 +127,10 @@ + + + + @@ -283,6 +287,10 @@ + + + + diff --git a/Directory.Build.props b/Directory.Build.props index 2ba90f0838d..a677747a0c7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -43,7 +43,6 @@ 3.3.2 2.0.3 6.0.1 - 1.5.25 [6.0.*,) [6.0.*,) 10.0.5 diff --git a/build-system/pr-validation.yaml b/build-system/pr-validation.yaml index 187429545ab..403b22c7481 100644 --- a/build-system/pr-validation.yaml +++ b/build-system/pr-validation.yaml @@ -59,7 +59,7 @@ jobs: name: "netfx_tests_windows" displayName: ".NET Framework Unit Tests (Windows)" vmImage: "windows-latest" - command: "dotnet incrementalist run --config .incrementalist/testsOnly.json --branch $(IncrementalistBaseBranch) -- test -c Release --no-build --framework net48 --logger:trx --results-directory TestResults" + command: "dotnet incrementalist run --config .incrementalist/testsOnlyNetFx.json --branch $(IncrementalistBaseBranch) -- test -c Release --no-build --framework net48 --logger:trx --results-directory TestResults" outputDirectory: "TestResults" artifactName: "netfx_tests_windows-$(Build.BuildId)" diff --git a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj index 1a83c99bc24..e1ae9f8b290 100644 --- a/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.Cluster.Metrics.Tests.MultiNode/Akka.Cluster.Metrics.Tests.MultiNode.csproj @@ -5,13 +5,12 @@ - + - diff --git a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj index efc72ea10e6..99e38963aa8 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.Cluster.Sharding.Tests.MultiNode/Akka.Cluster.Sharding.Tests.MultiNode.csproj @@ -5,6 +5,7 @@ + @@ -13,7 +14,6 @@ - diff --git a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj index 31ae09ac36d..7732a06fe1e 100644 --- a/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.Cluster.Tools.Tests.MultiNode/Akka.Cluster.Tools.Tests.MultiNode.csproj @@ -5,14 +5,15 @@ + - - + + diff --git a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj index 307c729a11e..679d35a2e73 100644 --- a/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj +++ b/src/contrib/cluster/Akka.DistributedData.Tests.MultiNode/Akka.DistributedData.Tests.MultiNode.csproj @@ -5,6 +5,7 @@ + @@ -12,7 +13,6 @@ - diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt index 38ea385f398..961ce25eac4 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.DotNet.verified.txt @@ -22,6 +22,7 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Xunit2.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests.MultiNode")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Streams")] diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt index b85c2c4b729..6d4995061ae 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveCore.Net.verified.txt @@ -22,6 +22,7 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Tests")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Xunit2.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests.MultiNode")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Streams")] diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt index 292021bd6e9..93a12f0fe31 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.DotNet.verified.txt @@ -11,6 +11,7 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Xunit2")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Xunit2.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests.MultiNode")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests.Performance")] diff --git a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt index 07717c4406f..c389e8c00e4 100644 --- a/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt +++ b/src/core/Akka.API.Tests/verify/CoreAPISpec.ApproveRemote.Net.verified.txt @@ -11,6 +11,7 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Xunit2")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.TestKit.Xunit2.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests.MultiNode")] [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.Remote.Tests.Performance")] diff --git a/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj b/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj index a824f218a12..becc4d493f8 100644 --- a/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj +++ b/src/core/Akka.Cluster.Tests.MultiNode/Akka.Cluster.Tests.MultiNode.csproj @@ -5,12 +5,12 @@ + - diff --git a/src/core/Akka.MultiNode.RemoteHost/Akka.MultiNode.RemoteHost.csproj b/src/core/Akka.MultiNode.RemoteHost/Akka.MultiNode.RemoteHost.csproj new file mode 100644 index 00000000000..4364f474b35 --- /dev/null +++ b/src/core/Akka.MultiNode.RemoteHost/Akka.MultiNode.RemoteHost.csproj @@ -0,0 +1,8 @@ + + + Exe + $(NetStandardLibVersion) + false + + + diff --git a/src/core/Akka.MultiNode.RemoteHost/RemoteHost.cs b/src/core/Akka.MultiNode.RemoteHost/RemoteHost.cs new file mode 100644 index 00000000000..142b6a13d08 --- /dev/null +++ b/src/core/Akka.MultiNode.RemoteHost/RemoteHost.cs @@ -0,0 +1,446 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Akka.MultiNode.RemoteHost +{ + public static class RemoteHost + { + #region Static functions + public static (Process, Task) RunProcessAsync( + Func> action, + string[] args, + Action configure = null, + CancellationToken token = default) + => Start(GetMethodInfo(action), args ?? throw new ArgumentNullException(nameof(args)), configure, token); + + private static (Process process, Task exitedTask) Start( + MethodInfo method, + string[] args, + Action configure, + CancellationToken token = default) + { + var process = new Process(); + RemoteHostOptions options; + try + { + options = new RemoteHostOptions(process.StartInfo); + ConfigureProcessStartInfoForMethodInvocation(method, args, options.StartInfo); + configure?.Invoke(options); + } + catch + { + process.Dispose(); + throw; + } + + var tcs = new TaskCompletionSource(); + if (token != default) + { + token.Register(() => + { + try + { + process.Kill(); + } catch{} + }); + } + + process.EnableRaisingEvents = true; + process.Exited += (_1, _2) => + { + options.OnExit(process); + + tcs.SetResult(true); + process.Dispose(); + }; + + if (options.OutputDataReceived != null) + { + process.OutputDataReceived += options.OutputDataReceived; + options.StartInfo.RedirectStandardOutput = true; + } + + if (options.ErrorDataReceived != null) + { + process.ErrorDataReceived += options.ErrorDataReceived; + options.StartInfo.RedirectStandardError = true; + } + + process.Start(); + + if (options.OutputDataReceived != null) + { + process.BeginOutputReadLine(); + } + + if (options.ErrorDataReceived != null) + { + process.BeginErrorReadLine(); + } + + return (process, tcs.Task); + } + + private static void ConfigureProcessStartInfoForMethodInvocation( + MethodInfo method, + string[] args, + ProcessStartInfo psi) + { + if (method.ReturnType != typeof(void) && + method.ReturnType != typeof(int) && + method.ReturnType != typeof(Task)) + { + throw new ArgumentException("method has an invalid return type", nameof(method)); + } + if (method.GetParameters().Length > 1) + { + throw new ArgumentException("method has more than one argument argument", nameof(method)); + } + if (method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType != typeof(string[])) + { + throw new ArgumentException("method has non string[] argument", nameof(method)); + } + + // If we need the host (if it exists), use it, otherwise target the console app directly. + var t = method.DeclaringType; + var a = t.GetTypeInfo().Assembly; + var programArgs = Utils.Paste(new [] { a.FullName, t.FullName, method.Name }); + var functionArgs = Utils.Paste(args); + var fullArgs = HostArguments + " " + " " + programArgs + " " + functionArgs; + + psi.FileName = HostFilename; + psi.Arguments = fullArgs; + } + + private static MethodInfo GetMethodInfo(Delegate d) + { + // RemoteInvoke doesn't support marshaling state on classes associated with + // the delegate supplied (often a display class of a lambda). If such fields + // are used, odd errors result, e.g. NullReferenceExceptions during the remote + // execution. Try to ward off the common cases by proactively failing early + // if it looks like such fields are needed. + if (d.Target != null) + { + // The only fields on the type should be compiler-defined (any fields of the compiler's own + // making generally include '<' and '>', as those are invalid in C# source). Note that this logic + // may need to be revised in the future as the compiler changes, as this relies on the specifics of + // actually how the compiler handles lifted fields for lambdas. + var targetType = d.Target.GetType(); + var fields = targetType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); + foreach (var fi in fields) + { + if (fi.Name.IndexOf('<') == -1) + { + throw new ArgumentException($"Field marshaling is not supported by {nameof(RemoteHost)}: {fi.Name}", "method"); + } + } + } + + return d.GetMethodInfo(); + } + + static RemoteHost() + { + HostFilename = "dotnet"; + + var execFunctionAssembly = typeof(RemoteHost).Assembly.Location; + var entryAssemblyWithoutExtension = Path.Combine( + Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), + Path.GetFileNameWithoutExtension(Assembly.GetEntryAssembly().Location)); + var appArguments = GetApplicationArguments(); + + var runtimeConfigFile = GetApplicationArgument(appArguments, "--runtimeconfig"); + if (runtimeConfigFile == null) + { + runtimeConfigFile = entryAssemblyWithoutExtension + ".runtimeconfig.json"; + } + + var depsFile = GetApplicationArgument(appArguments, "--depsfile"); + if (depsFile == null) + { + depsFile = entryAssemblyWithoutExtension + ".deps.json"; + } + + HostArguments = Utils.Paste(new [] { "exec", "--runtimeconfig", runtimeConfigFile, "--depsfile", depsFile, execFunctionAssembly }); + } + + private static string GetApplicationArgument(string[] arguments, string name) + { + for (var i = 0; i < arguments.Length - 1; i++) + { + if (arguments[i].ToLowerInvariant() == name) + { + return arguments[i + 1]; + } + } + return null; + } + + private static string[] GetOSXCommandLineArguments() + { + // The following logic is based on https://gist.github.com/nonowarn/770696 + // Set up the mib array and the query for process maximum args size + var mib = new int[3]; + var mibLength = 2; + mib[0] = MACOS_CTL_KERN; + mib[1] = MACOS_KERN_ARGMAX; + + var size = IntPtr.Size / 2; + var argmax = 0; + var argv = new List(); + + var mibHandle = GCHandle.Alloc(mib, GCHandleType.Pinned); + try + { + var mibPtr = mibHandle.AddrOfPinnedObject(); + + // Get the process args size + SysCtl(mibPtr, mibLength, ref argmax, ref size, IntPtr.Zero, 0); + + // Get the PID so we can query this process' args + var pid = Process.GetCurrentProcess().Id; + + // Now read the process args into the allocated space + var procargs = Marshal.AllocHGlobal(argmax); + try + { + mib[0] = MACOS_CTL_KERN; + mib[1] = MACOS_KERN_PROCARGS2; + mib[2] = pid; + mibLength = 3; + + SysCtl(mibPtr, mibLength, procargs, ref argmax, IntPtr.Zero, 0); + + // The memory block we're reading is a series of null-terminated strings + // that looks something like this: + // + // | argc | is always 4 bytes long even on 64bit architectures + // | exec_path | ... \0\0\0\0 * ? + // | argv[0] | ... \0 + // | argv[1] | ... \0 + // | argv[2] | ... \0 + // ... + // | env[0] | ... \0 (VALUE = SOMETHING\0) + + // Read argc + var argc = Marshal.ReadInt32(procargs); + + // Skip over argc + var argvPtr = IntPtr.Add(procargs, sizeof(int)); + + // Skip over exec_path + var offset = 0; + while (Marshal.ReadByte(argvPtr, offset) != 0) { offset++; } + while (Marshal.ReadByte(argvPtr, offset) == 0) { offset++; } + argvPtr = IntPtr.Add(argvPtr, offset); + + // Start reading argv + for (var i = 0; i < argc; i++) + { + offset = 0; + // Keep reading bytes until we find a null-terminated string + while (Marshal.ReadByte(argvPtr, offset) != 0) { offset++; } + var arg = Marshal.PtrToStringAnsi(argvPtr, offset); + argv.Add(arg); + + // Move pointer to the start of the next arg (= currentArg + \0) + argvPtr = IntPtr.Add(argvPtr, offset + sizeof(byte)); + } + } + finally + { + Marshal.FreeHGlobal(procargs); + } + } + finally + { + mibHandle.Free(); + } + + return argv.ToArray(); + } + + private static string[] GetApplicationArguments() + { + // Environment.GetCommandLineArgs doesn't include arguments passed to the runtime. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return File.ReadAllText($"/proc/{Process.GetCurrentProcess().Id}/cmdline").Split(new[] { '\0' }); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var ptr = GetCommandLine(); + var commandLine = Marshal.PtrToStringAuto(ptr); + return CommandLineToArgs(commandLine); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return GetOSXCommandLineArguments(); + } + + throw new PlatformNotSupportedException($"{nameof(GetApplicationArguments)} is unsupported on this platform"); + } + + private const int MACOS_CTL_KERN = 1; + private const int MACOS_KERN_ARGMAX = 8; + private const int MACOS_KERN_PROCARGS2 = 49; + + [DllImport("libc", + EntryPoint = "sysctl", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi, + SetLastError = true)] + private static extern int SysCtl(IntPtr mib, int mibLength, ref int oldp, ref int oldlenp, IntPtr newp, int newlenp); + + [DllImport("libc", + EntryPoint = "sysctl", + CallingConvention = CallingConvention.Cdecl, + CharSet = CharSet.Ansi, + SetLastError = true)] + private static extern int SysCtl(IntPtr mib, int mibLength, IntPtr oldp, ref int oldlenp, IntPtr newp, int newlenp); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr GetCommandLine(); + + [DllImport("shell32.dll", SetLastError = true)] + private static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs); + + public static string[] CommandLineToArgs(string commandLine) + { + var argv = CommandLineToArgvW(commandLine, out var argc); + if (argv == IntPtr.Zero) + throw new System.ComponentModel.Win32Exception(); + try + { + var args = new string[argc]; + for (var i = 0; i < args.Length; i++) + { + var p = Marshal.ReadIntPtr(argv, i * IntPtr.Size); + args[i] = Marshal.PtrToStringUni(p); + } + + return args; + } + finally + { + Marshal.FreeHGlobal(argv); + } + } + + private static readonly string HostFilename; + private static readonly string HostArguments; + + #endregion + + public const string CommandName = "remotehost"; + public static int UnhandledExceptionExitCode = 128 + 6; // SIGABRT exit code + + public static bool IsExecFunctionCommand(string[] args) + => args.Length >= 1 && args[0] == CommandName; + + /// + /// Provides an entry point in a new process that will load a specified method and invoke it. + /// + public static class Program + { + public static int Main(string[] args) + { + var argsLength = args.Length; + var argIdx = 0; + // Strip CommandName. + if (argsLength > 0 && args[0] == CommandName) + { + argsLength--; + argIdx++; + } + + // The program expects to be passed the target assembly name to load, the type + // from that assembly to find, and the method from that assembly to invoke. + // Any additional arguments are passed as strings to the method. + if (argsLength < 3) + { + Console.Error.WriteLine("Usage: {0} assemblyName typeName methodName [additionalArgs]", typeof(Program).GetTypeInfo().Assembly.GetName().Name); + Environment.Exit(-1); + return -1; + } + + var assemblyName = args[argIdx++]; + var typeName = args[argIdx++]; + var methodName = args[argIdx++]; + var additionalArgs = args.SubArray(3); + + // Load the specified assembly, type, and method, then invoke the method. + // The program's exit code is the return value of the invoked method. + object instance = null; + var exitCode = 0; + try + { + // Create the class if necessary + var a = Assembly.Load(new AssemblyName(assemblyName)); + var t = a.GetType(typeName); + var mi = t.GetTypeInfo().GetDeclaredMethod(methodName); + if (!mi.IsStatic) + { + instance = Activator.CreateInstance(t); + } + + // Invoke the method + object result; + if (mi.GetParameters().Length == 0) + { + result = mi.Invoke(instance, null); + } + else + { + result = mi.Invoke(instance, new object[] { additionalArgs }); + } + + if (result is Task task) + { + exitCode = task.GetAwaiter().GetResult(); + } + else if (result is int exit) + { + exitCode = exit; + } + } + catch (Exception exc) + { + if (exc is TargetInvocationException && exc.InnerException != null) + exc = exc.InnerException; + + Console.Error.Write("Unhandled exception: "); + Console.Error.WriteLine(exc); + + exitCode = UnhandledExceptionExitCode; + } + finally + { + (instance as IDisposable)?.Dispose(); + } + + return exitCode; + } + } + + private static T[] SubArray(this T[] data, int index) + { + var length = data.Length - index; + if (length == 0) + { + return Array.Empty(); + } + var result = new T[length]; + Array.Copy(data, index, result, 0, length); + return result; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.RemoteHost/RemoteHostOptions.cs b/src/core/Akka.MultiNode.RemoteHost/RemoteHostOptions.cs new file mode 100644 index 00000000000..37576c28136 --- /dev/null +++ b/src/core/Akka.MultiNode.RemoteHost/RemoteHostOptions.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics; + +namespace Akka.MultiNode.RemoteHost +{ + public class RemoteHostOptions + { + internal RemoteHostOptions(ProcessStartInfo psi) + { + StartInfo = psi; + } + + public ProcessStartInfo StartInfo { get; } + + public Action OnExit { get; set; } + + public DataReceivedEventHandler OutputDataReceived { get; set; } + + public DataReceivedEventHandler ErrorDataReceived { get; set; } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.RemoteHost/Utils.cs b/src/core/Akka.MultiNode.RemoteHost/Utils.cs new file mode 100644 index 00000000000..cca2f606be8 --- /dev/null +++ b/src/core/Akka.MultiNode.RemoteHost/Utils.cs @@ -0,0 +1,168 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace Akka.MultiNode.RemoteHost +{ + internal static class Utils + { + internal static void AppendArgument(StringBuilder stringBuilder, string argument) + { + if (stringBuilder.Length != 0) + { + stringBuilder.Append(' '); + } + + // Parsing rules for non-argv[0] arguments: + // - Backslash is a normal character except followed by a quote. + // - 2N backslashes followed by a quote ==> N literal backslashes followed by unescaped quote + // - 2N+1 backslashes followed by a quote ==> N literal backslashes followed by a literal quote + // - Parsing stops at first whitespace outside of quoted region. + // - (post 2008 rule): A closing quote followed by another quote ==> literal quote, and parsing remains in quoting mode. + if (argument.Length != 0 && ContainsNoWhitespaceOrQuotes(argument)) + { + // Simple case - no quoting or changes needed. + stringBuilder.Append(argument); + } + else + { + stringBuilder.Append(Quote); + var idx = 0; + while (idx < argument.Length) + { + var c = argument[idx++]; + if (c == Backslash) + { + var numBackSlash = 1; + while (idx < argument.Length && argument[idx] == Backslash) + { + idx++; + numBackSlash++; + } + + if (idx == argument.Length) + { + // We'll emit an end quote after this so must double the number of backslashes. + stringBuilder.Append(Backslash, numBackSlash * 2); + } + else if (argument[idx] == Quote) + { + // Backslashes will be followed by a quote. Must double the number of backslashes. + stringBuilder.Append(Backslash, numBackSlash * 2 + 1); + stringBuilder.Append(Quote); + idx++; + } + else + { + // Backslash will not be followed by a quote, so emit as normal characters. + stringBuilder.Append(Backslash, numBackSlash); + } + + continue; + } + + if (c == Quote) + { + // Escape the quote so it appears as a literal. This also guarantees that we won't end up generating a closing quote followed + // by another quote (which parses differently pre-2008 vs. post-2008.) + stringBuilder.Append(Backslash); + stringBuilder.Append(Quote); + continue; + } + + stringBuilder.Append(c); + } + + stringBuilder.Append(Quote); + } + } + + private static bool ContainsNoWhitespaceOrQuotes(string s) + { + for (var i = 0; i < s.Length; i++) + { + var c = s[i]; + if (char.IsWhiteSpace(c) || c == Quote) + { + return false; + } + } + + return true; + } + + /// + /// Repastes a set of arguments into a linear string that parses back into the originals under pre- or post-2008 VC parsing rules. + /// + internal static string Paste(IEnumerable arguments, bool pasteFirstArgumentUsingArgV0Rules = false) + { + /// On Windows: The rules for parsing the executable name (argv[0]) are special, so you must indicate whether the first argument actually is argv[0]. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var stringBuilder = new StringBuilder(); + + foreach (var argument in arguments) + { + if (pasteFirstArgumentUsingArgV0Rules) + { + pasteFirstArgumentUsingArgV0Rules = false; + + // Special rules for argv[0] + // - Backslash is a normal character. + // - Quotes used to include whitespace characters. + // - Parsing ends at first whitespace outside quoted region. + // - No way to get a literal quote past the parser. + + var hasWhitespace = false; + foreach (var c in argument) + { + if (c == Quote) + { + throw new ApplicationException("The argv[0] argument cannot include a double quote."); + } + if (char.IsWhiteSpace(c)) + { + hasWhitespace = true; + } + } + if (argument.Length == 0 || hasWhitespace) + { + stringBuilder.Append(Quote); + stringBuilder.Append(argument); + stringBuilder.Append(Quote); + } + else + { + stringBuilder.Append(argument); + } + } + else + { + AppendArgument(stringBuilder, argument); + } + } + + return stringBuilder.ToString(); + } + /// On Unix: the rules for parsing the executable name (argv[0]) are ignored. + else + { + var stringBuilder = new StringBuilder(); + foreach (var argument in arguments) + { + AppendArgument(stringBuilder, argument); + } + return stringBuilder.ToString(); + } + + } + + private const char Quote = '\"'; + private const char Backslash = '\\'; + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/Akka.MultiNode.SampleMultiNodeTests.csproj b/src/core/Akka.MultiNode.SampleMultiNodeTests/Akka.MultiNode.SampleMultiNodeTests.csproj new file mode 100644 index 00000000000..5e9e16d3b26 --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/Akka.MultiNode.SampleMultiNodeTests.csproj @@ -0,0 +1,20 @@ + + + $(NetTestVersion) + false + + + + + + + + + + + + + $(DefineConstants);RELEASE + + + diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/BadConfigMultiNodeSpec.cs b/src/core/Akka.MultiNode.SampleMultiNodeTests/BadConfigMultiNodeSpec.cs new file mode 100644 index 00000000000..505c58eda9e --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/BadConfigMultiNodeSpec.cs @@ -0,0 +1,32 @@ +using System; +using Akka.Cluster.TestKit; +using Akka.Remote.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; + +namespace Akka.MultiNode.TestAdapter.SampleTests +{ + public class BadMultiNodeSpecConfig : MultiNodeConfig + { + public BadMultiNodeSpecConfig() + { + throw new Exception("Some config creation exception"); + } + } + + // This spec should be skipped because failed to build it's config + public class BadConfigMultiNodeSpec : MultiNodeClusterSpec + { + public BadConfigMultiNodeSpec() : this(new BadMultiNodeSpecConfig()) + { + } + + private BadConfigMultiNodeSpec(BadMultiNodeSpecConfig config) : base(config, typeof(BadConfigMultiNodeSpec)) + { + } + + [MultiNodeFact] + public void Should_not_be_started() + { + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/FailedMultiNodeSpec.cs b/src/core/Akka.MultiNode.SampleMultiNodeTests/FailedMultiNodeSpec.cs new file mode 100644 index 00000000000..f184296f82b --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/FailedMultiNodeSpec.cs @@ -0,0 +1,39 @@ +using System; +using Akka.Cluster.TestKit; +using Akka.Remote.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; + +namespace Akka.MultiNode.TestAdapter.SampleTests +{ + public class FailedMultiNodeSpecConfig : MultiNodeConfig + { + public RoleName First { get; } + public RoleName Second { get; } + + public FailedMultiNodeSpecConfig() + { + First = Role("first"); + Second = Role("second"); + + CommonConfig = DebugConfig(true) + .WithFallback(MultiNodeClusterSpec.ClusterConfig()); + } + } + + public class FailedMultiNodeSpec : MultiNodeClusterSpec + { + public FailedMultiNodeSpec() : this(new FailedMultiNodeSpecConfig()) + { + } + + private FailedMultiNodeSpec(FailedMultiNodeSpecConfig config) : base(config, typeof(FailedMultiNodeSpec)) + { + } + + [MultiNodeFact] + public void Should_fail() + { + throw new Exception("Spec should fail"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/IgnoredXunitTest.cs b/src/core/Akka.MultiNode.SampleMultiNodeTests/IgnoredXunitTest.cs new file mode 100644 index 00000000000..64cb565a734 --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/IgnoredXunitTest.cs @@ -0,0 +1,15 @@ +using System; +using Xunit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; + +namespace Akka.MultiNode.TestAdapter.SampleTests +{ + public class IgnoredXunitTest + { + [Fact(Skip = "This test should be ignored by MNTR")] + public void Ignored_test() + { + throw new Exception("This test should be ignored by MNTR"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/Metadata/SampleTestsMetadata.cs b/src/core/Akka.MultiNode.SampleMultiNodeTests/Metadata/SampleTestsMetadata.cs new file mode 100644 index 00000000000..5cb97ae1ba8 --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/Metadata/SampleTestsMetadata.cs @@ -0,0 +1,20 @@ +using Xunit; + +[assembly: TestFramework("Akka.MultiNode.TestAdapter.MultiNodeTestFramework", "Akka.MultiNode.TestAdapter")] +namespace Akka.MultiNode.TestAdapter.SampleTests.Metadata +{ + /// + /// SampleTestsMetadata + /// + public static class SampleTestsMetadata + { + /// + /// Sample tests assembly path + /// + public static string AssemblyPath => typeof(SampleTestsMetadata).Assembly.Location; + /// + /// Gets assembly file name + /// + public static string AssemblyFileName => typeof(SampleTestsMetadata).Assembly.GetName().Name + ".dll"; + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/OneNodeFailedMultiNodeSpec.cs b/src/core/Akka.MultiNode.SampleMultiNodeTests/OneNodeFailedMultiNodeSpec.cs new file mode 100644 index 00000000000..30971bf711d --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/OneNodeFailedMultiNodeSpec.cs @@ -0,0 +1,42 @@ +using System; +using Akka.Cluster.TestKit; +using Akka.Remote.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; + +namespace Akka.MultiNode.TestAdapter.SampleTests +{ + public class OneNodeFailedMultiNodeSpecConfig : MultiNodeConfig + { + public RoleName First { get; } + public RoleName Second { get; } + + public OneNodeFailedMultiNodeSpecConfig() + { + First = Role("first"); + Second = Role("second"); + + CommonConfig = DebugConfig(true) + .WithFallback(MultiNodeClusterSpec.ClusterConfig()); + } + } + + public class OneNodeFailedMultiNodeSpec : MultiNodeClusterSpec + { + private readonly FailedMultiNodeSpecConfig _config; + + public OneNodeFailedMultiNodeSpec() : this(new FailedMultiNodeSpecConfig()) + { + } + + private OneNodeFailedMultiNodeSpec(FailedMultiNodeSpecConfig config) : base(config, typeof(FailedMultiNodeSpec)) + { + _config = config; + } + + [MultiNodeFact] + public void One_node_failed_should_fail() + { + RunOn(() => throw new Exception("Spec should fail"), _config.First); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/SampleMultiNodeSpec.cs b/src/core/Akka.MultiNode.SampleMultiNodeTests/SampleMultiNodeSpec.cs new file mode 100644 index 00000000000..f3195a8a64a --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/SampleMultiNodeSpec.cs @@ -0,0 +1,47 @@ +using Akka.Cluster.TestKit; +using Akka.Remote.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; + +namespace Akka.MultiNode.TestAdapter.SampleTests +{ + public class SampleMultiNodeSpecConfig : MultiNodeConfig + { + public RoleName First { get; } + public RoleName Second { get; } + + public SampleMultiNodeSpecConfig() + { + First = Role("first"); + Second = Role("second"); + + CommonConfig = DebugConfig(true) + .WithFallback(MultiNodeClusterSpec.ClusterConfig()); + } + } + + public class SampleMultiNodeSpec : MultiNodeClusterSpec + { + private readonly SampleMultiNodeSpecConfig _config; + + public SampleMultiNodeSpec() : this(new SampleMultiNodeSpecConfig()) + { + } + + private SampleMultiNodeSpec(SampleMultiNodeSpecConfig config) : base(config, typeof(SampleMultiNodeSpec)) + { + _config = config; + } + + [MultiNodeFact] + public void Should_start_and_join_cluster() + { + RunOn(StartClusterNode, _config.First); + + EnterBarrier("first-started"); + + RunOn(() => Cluster.Join(GetAddress(_config.First)), _config.Second); + + EnterBarrier("after"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.SampleMultiNodeTests/SkippedMultiNodeSpec.cs b/src/core/Akka.MultiNode.SampleMultiNodeTests/SkippedMultiNodeSpec.cs new file mode 100644 index 00000000000..1e06e23d4c3 --- /dev/null +++ b/src/core/Akka.MultiNode.SampleMultiNodeTests/SkippedMultiNodeSpec.cs @@ -0,0 +1,39 @@ +using System; +using Akka.Cluster.TestKit; +using Akka.Remote.TestKit; +using MultiNodeFactAttribute = Akka.MultiNode.TestAdapter.MultiNodeFactAttribute; + +namespace Akka.MultiNode.TestAdapter.SampleTests +{ + public class SkippedMultiNodeSpecConfig : MultiNodeConfig + { + public RoleName First { get; } + public RoleName Second { get; } + + public SkippedMultiNodeSpecConfig() + { + First = Role("first"); + Second = Role("second"); + + CommonConfig = DebugConfig(true) + .WithFallback(MultiNodeClusterSpec.ClusterConfig()); + } + } + + public class SkippedMultiNodeSpec : MultiNodeClusterSpec + { + public SkippedMultiNodeSpec() : this(new SkippedMultiNodeSpecConfig()) + { + } + + private SkippedMultiNodeSpec(SkippedMultiNodeSpecConfig config) : base(config, typeof(SkippedMultiNodeSpec)) + { + } + + [MultiNodeFact(Skip = "This spec should be skipped")] + public void Should_skip() + { + throw new NotImplementedException("This spec should be skipped"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Akka.MultiNode.TestAdapter.Tests.csproj b/src/core/Akka.MultiNode.TestAdapter.Tests/Akka.MultiNode.TestAdapter.Tests.csproj new file mode 100644 index 00000000000..28448635e2a --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Akka.MultiNode.TestAdapter.Tests.csproj @@ -0,0 +1,26 @@ + + + $(NetTestVersion) + true + false + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Helpers/TestCollections.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Helpers/TestCollections.cs new file mode 100644 index 00000000000..b67415951fa --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Helpers/TestCollections.cs @@ -0,0 +1,13 @@ +namespace Akka.MultiNode.TestAdapter.Tests.Helpers +{ + /// + /// TestCollections + /// + public static class TestCollections + { + /// + /// Collection of tests that are running MNTR + /// + public const string MultiNode = "MNTR Collection"; + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/MultiNodeTestRunnerDiscovery/DiscoveryCases.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/MultiNodeTestRunnerDiscovery/DiscoveryCases.cs new file mode 100644 index 00000000000..2147e75c3bf --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/MultiNodeTestRunnerDiscovery/DiscoveryCases.cs @@ -0,0 +1,199 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Linq; +using Akka.Remote.TestKit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal.MultiNodeTestRunnerDiscovery +{ + public class DiscoveryCases + { + public class NoAbstractClassesConfig : MultiNodeConfig + { + public RoleName Dummy { get; set; } + + public NoAbstractClassesConfig() + { + Dummy = Role("dummy"); + } + } + + public abstract class NoAbstractClassesSpec + { + public NoAbstractClassesSpec(NoAbstractClassesConfig config) + { + } + + [MultiNodeFact(Skip = "Only for discovery tests")] + public void Dummy() + { + } + } + + public class DeeplyInheritedConfig : MultiNodeConfig + { + public RoleName DeeplyInheritedChildRole { get; set; } + + public DeeplyInheritedConfig() + { + DeeplyInheritedChildRole = Role("DeeplyInheritedChildRole"); + } + } + + public abstract class DeeplyInheritedBaseSpec : MultiNodeSpec + { + public DeeplyInheritedBaseSpec(DeeplyInheritedConfig config) : base(config, typeof(DeeplyInheritedBaseSpec)) + { + } + + [MultiNodeFact(Skip = "Only for discovery tests")] + public void Dummy() + { + } + + /// + protected override int InitialParticipantsValueFactory { get; } + } + + public abstract class DeeplyInheritedMediumSpec : DeeplyInheritedBaseSpec + { + public DeeplyInheritedMediumSpec(DeeplyInheritedConfig config) : base(config) + { + } + } + + public class DeeplyInheritedChildSpec : DeeplyInheritedMediumSpec + { + public DeeplyInheritedChildSpec(DeeplyInheritedConfig config) : base(config) + { + } + } + + /// + /// According to the discovery rules, the concrete class doesn't explicitly need + /// an instance of its configuration. Only the sub-class does. + /// + public class DefaultConstructorOnDerivedClassSpec : DeeplyInheritedMediumSpec + { + public DefaultConstructorOnDerivedClassSpec() : base(new DeeplyInheritedConfig()) + { + + } + } + + public class FloodyConfig : MultiNodeConfig + { + public RoleName Role1 { get; set; } + public RoleName Role2 { get; set; } + public RoleName Role3 { get; set; } + public RoleName Role4 { get; set; } + public RoleName Role5 { get; set; } + + public FloodyConfig() + { + Role1 = Role("Role1"); + Role2 = Role("Role2"); + Role3 = Role("Role3"); + Role4 = Role("Role4"); + Role5 = Role("Role5"); + } + } + + public abstract class FloodyBaseSpec : MultiNodeSpec + { + + public FloodyBaseSpec(FloodyConfig config) : base(config, typeof(FloodyBaseSpec)) + { + } + + [MultiNodeFact(Skip = "Only for discovery tests")] + public void Dummy() + { + } + + /// + protected override int InitialParticipantsValueFactory { get; } + } + + public class FloodyChildSpec1 : FloodyBaseSpec + { + public FloodyChildSpec1(FloodyConfig config) : base(config) + { + } + } + + public class FloodyChildSpec2 : FloodyBaseSpec + { + public FloodyChildSpec2(FloodyConfig config) : base(config) + { + } + } + + public class FloodyChildSpec3 : FloodyBaseSpec + { + public FloodyChildSpec3(FloodyConfig config) : base(config) + { + } + } + + public class NoReflectionConfig : MultiNodeConfig + { + public NoReflectionConfig() + { + foreach(var i in Enumerable.Range(1, 10)) + { + Role("node-" + i); + } + } + } + + public class NoReflectionSpec : MultiNodeSpec + { + public NoReflectionSpec(NoReflectionConfig config): base(config, typeof(NoReflectionSpec)) + { + } + + [MultiNodeFact(Skip = "Only for discovery tests")] + public void Dummy() + { + } + + protected override int InitialParticipantsValueFactory { get; } + } + + public class DiverseConfig : MultiNodeConfig + { + public RoleName RoleProp { get; set; } + public readonly RoleName RoleField; + private RoleName RolePropPriv { get; set; } + protected readonly RoleName RoleFieldPriv; + + public DiverseConfig() + { + RoleProp = Role("RoleProp"); + RoleField = Role("RoleField"); + RolePropPriv = Role("RolePropPriv"); + RoleFieldPriv = Role("RoleFieldPriv"); + } + } + + public class DiverseSpec : MultiNodeSpec + { + public DiverseSpec(DiverseConfig config) : base(config, typeof(DiverseSpec)) + { + } + + [MultiNodeFact(Skip = "Only for discovery tests")] + public void Dummy() + { + } + + /// + protected override int InitialParticipantsValueFactory { get; } + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/MultiNodeTestRunnerDiscovery/DiscoverySpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/MultiNodeTestRunnerDiscovery/DiscoverySpec.cs new file mode 100644 index 00000000000..45bc757639a --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/MultiNodeTestRunnerDiscovery/DiscoverySpec.cs @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Akka.MultiNode.TestAdapter.Internal; +using FluentAssertions; +using Xunit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal.MultiNodeTestRunnerDiscovery +{ + public class DiscoverySpec + { + [Fact(DisplayName = "Abstract classes are not discoverable")] + public void No_abstract_classes() + { + var discoveredSpecs = DiscoverSpecs(); + Assert.False(discoveredSpecs.ContainsKey(KeyFromSpecName(nameof(DiscoveryCases.NoAbstractClassesSpec)))); + } + + [Fact(DisplayName = "Deeply inherited classes are discoverable")] + public void Deeply_inherited_are_ok() + { + var discoveredSpecs = DiscoverSpecs(); + Assert.Equal( + "DeeplyInheritedChildRole", + discoveredSpecs[KeyFromSpecName(nameof(DiscoveryCases.DeeplyInheritedChildSpec))].First().Role); + } + + [Fact(DisplayName = "Child test class with default constructors are ok")] + public void Child_class_with_default_constructor_are_ok() + { + Action testDelegate = () => + { + var testCase = typeof(DiscoveryCases.DefaultConstructorOnDerivedClassSpec); + var constructor = MultiNodeTestCase.FindConfigConstructor(testCase); + constructor.Should().NotBeNull(); + }; + + testDelegate.Should().NotThrow(); + } + + [Fact(DisplayName = "One test case per RoleName per Spec declaration with MultiNodeFact")] + public void Discovered_count_equals_number_of_roles_mult_specs() + { + var discoveredSpecs = DiscoverSpecs(); + Assert.Equal(5, discoveredSpecs[KeyFromSpecName(nameof(DiscoveryCases.FloodyChildSpec1))].Count); + Assert.Equal(5, discoveredSpecs[KeyFromSpecName(nameof(DiscoveryCases.FloodyChildSpec2))].Count); + Assert.Equal(5, discoveredSpecs[KeyFromSpecName(nameof(DiscoveryCases.FloodyChildSpec3))].Count); + } + + [Fact(DisplayName = "Only the MultiNodeConfig.Roles property is used to compute the number of Roles in MultiNodeFact")] + public void Only_MultiNodeConfig_role_count_used() + { + var discoveredSpecs = DiscoverSpecs(); + Assert.Equal(10, discoveredSpecs[KeyFromSpecName(nameof(DiscoveryCases.NoReflectionSpec))].Select(c => c.Role).Count()); + } + + private static Dictionary> DiscoverSpecs() + { + var assemblyPath = new Uri(typeof(DiscoveryCases).GetTypeInfo().Assembly.Location).LocalPath; + + using (var controller = new XunitFrontController(AppDomainSupport.IfAvailable, assemblyPath)) + { + using (var discovery = new Discovery(assemblyPath)) + { + controller.Find(false, discovery, TestFrameworkOptions.ForDiscovery()); + discovery.Finished.WaitOne(); + return discovery + .TestCases + .ToDictionary(t => t.TypeName, t => t.Nodes); + } + } + } + + private string KeyFromSpecName(string specName) + { + return typeof(DiscoveryCases).FullName + "+" + specName; + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/NodeDataActorSpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/NodeDataActorSpec.cs new file mode 100644 index 00000000000..c304a787355 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/NodeDataActorSpec.cs @@ -0,0 +1,126 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Xunit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal +{ + public class NodeDataActorSpec : TestKit.Xunit2.TestKit + { + [Fact] + public void NodeData_should_maintain_events_in_time_order() + { + var nodeIndex = 1; + var nodeRole = NodeMessageHelpers.DummyRoleFor + nodeIndex; + var nodeDataActor = Sys.ActorOf(Props.Create(() => new NodeDataActor(nodeIndex, nodeRole))); + var m1 = NodeMessageHelpers.GenerateMessageSequence(nodeIndex, 3); + + foreach(var m in m1) + nodeDataActor.Tell(m); + + var m2 = NodeMessageHelpers.GenerateMessageSequence(nodeIndex, 4); + foreach(var m in m2) + nodeDataActor.Tell(m); + + //union the two sets together + m1.UnionWith(m2); + + //Kill the node data actor and have it deliver its payload to TestActor + nodeDataActor.Tell(new EndSpec(), TestActor); + + var nodeData = ExpectMsg(); + + Assert.True(m1.SetEquals(nodeData.EventStream)); + Assert.Equal(nodeIndex, nodeData.NodeIndex); + Assert.Equal(nodeRole, nodeData.NodeRole); + Assert.False(nodeData.EndTime.HasValue); + Assert.False(nodeData.Passed.HasValue); + } + + [Fact] + public void NodeData_should_mark_as_complete_when_MultiNodeResultMessage_received() + { + var nodeIndex = 1; + var nodeRole = NodeMessageHelpers.DummyRoleFor + nodeIndex; + var nodeDataActor = Sys.ActorOf(Props.Create(() => new NodeDataActor(nodeIndex, nodeRole))); + + var m1 = NodeMessageHelpers.GenerateMessageSequence(nodeIndex, 3); + m1.UnionWith(NodeMessageHelpers.GenerateResultMessage(nodeIndex, true)); + + foreach (var m in m1) + nodeDataActor.Tell(m); + + //Kill the node data actor and have it deliver its payload to TestActor + nodeDataActor.Tell(new EndSpec(), TestActor); + + var nodeData = ExpectMsg(); + + Assert.True(m1.SetEquals(nodeData.EventStream)); + Assert.True(nodeData.Passed.Value); + Assert.True(nodeData.EndTime.HasValue); + Assert.True(nodeData.EndTime.Value >= nodeData.StartTime); + } + + [Fact] + public void NodeData_should_mark_as_failed_when_MultiNodeResultMessage_received() + { + var nodeIndex = 1; + var nodeRole = NodeMessageHelpers.DummyRoleFor + nodeIndex; + var nodeDataActor = Sys.ActorOf(Props.Create(() => new NodeDataActor(nodeIndex, nodeRole))); + + var m1 = NodeMessageHelpers.GenerateMessageSequence(nodeIndex, 3); + m1.UnionWith(NodeMessageHelpers.GenerateResultMessage(nodeIndex, false)); + + foreach (var m in m1) + nodeDataActor.Tell(m); + + //Kill the node data actor and have it deliver its payload to TestActor + nodeDataActor.Tell(new EndSpec(), TestActor); + + var nodeData = ExpectMsg(); + + Assert.True(m1.SetEquals(nodeData.EventStream)); + Assert.False(nodeData.Passed.Value); + Assert.True(nodeData.EndTime.HasValue); + Assert.True(nodeData.EndTime.Value >= nodeData.StartTime); + } + + [Fact] + public void NodeData_should_process_LogMessageFragments_into_timeline() + { + var nodeIndex = 1; + var nodeRole = NodeMessageHelpers.DummyRoleFor + nodeIndex; + var nodeDataActor = Sys.ActorOf(Props.Create(() => new NodeDataActor(nodeIndex, nodeRole))); + var m1 = NodeMessageHelpers.GenerateMessageSequence(nodeIndex, 3); + + foreach (var m in m1) + nodeDataActor.Tell(m); + + var m2 = NodeMessageHelpers.GenerateMessageFragmentSequence(nodeIndex, 4); + foreach (var m in m2) + nodeDataActor.Tell(m); + + //union the two sets together + m1.UnionWith(m2); + + //Kill the node data actor and have it deliver its payload to TestActor + nodeDataActor.Tell(new EndSpec(), TestActor); + + var nodeData = ExpectMsg(); + + Assert.True(m1.SetEquals(nodeData.EventStream)); + Assert.Equal(nodeIndex, nodeData.NodeIndex); + Assert.Equal(nodeRole, nodeData.NodeRole); + Assert.False(nodeData.EndTime.HasValue); + Assert.False(nodeData.Passed.HasValue); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/NodeMessageHelpers.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/NodeMessageHelpers.cs new file mode 100644 index 00000000000..ee254ec3ebd --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/NodeMessageHelpers.cs @@ -0,0 +1,232 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Akka.Event; +using Akka.MultiNode.TestAdapter.Internal; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using Akka.MultiNode.TestAdapter.Tests.Internal.Utils; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal +{ + /// + /// Helper class for creating + /// + public static class NodeMessageHelpers + { + internal const string DummyRoleFor = "Dummy_role_for_"; + internal static readonly Random Random = new Random(); + + public static MultiNodeTestCase BuildNodeTests(IEnumerable nodeIndicies) + => new MockMultiNodeTestCase(nodeIndicies); + + /// + /// Meta-function for generating a distribution of messages across multiple nodes + /// + private static SortedSet GenerateMessageDistributionForNodes(IEnumerable nodeIndices, + int count, Func> messageGenerator) + { + var nodes = nodeIndices.ToList(); + var messages = new SortedSet(); + + //special case for 1:1 distribution + if (nodes.Count == count) + { + foreach (var node in nodes) + { + messages.UnionWith(messageGenerator(node, node)); + } + return messages; + } + + // Key = nodeIndex, Value = # of allocated messages + var messageDistribution = new Dictionary(); + foreach (var node in nodes) + { + messageDistribution[node] = 0; + } + + var remainingMessages = count; + var nodeIterator = nodes.GetContinuousEnumerator(); + + while (remainingMessages > 0) + { + nodeIterator.MoveNext(); + var node = nodeIterator.Current; + var added = Random.Next(1, Math.Max(1, remainingMessages / 2)); + + //Don't go over the message count + if (added > remainingMessages) + added = remainingMessages; + + messageDistribution[node] += added; + remainingMessages -= added; + } + + //generate the assigned sequence for each node + foreach (var node in messageDistribution) + messages.UnionWith(messageGenerator(node.Key, node.Value)); + + return messages; + } + + public static SortedSet GenerateMessageSequence(IEnumerable nodeIndices, int count) + { + return GenerateMessageDistributionForNodes(nodeIndices, count, GenerateMessageSequence); + } + + public static SortedSet GenerateMessageSequence(int nodeIndex, int count) + { + var messages = new SortedSet(); + var startTime = DateTime.UtcNow; + foreach (var i in Enumerable.Range(0, count)) + { + messages.Add(new MultiNodeLogMessage( + GetTimeStamp(startTime, startTime + TimeSpan.FromSeconds(20)), + String.Format("Message {0}", i), nodeIndex, DummyRoleFor + nodeIndex, + "/foo", LogLevel.InfoLevel)); + } + return messages; + } + + public static SortedSet GenerateMessageFragmentSequence(IEnumerable nodeIndices, int count) + { + return GenerateMessageDistributionForNodes(nodeIndices, count, GenerateMessageFragmentSequence); + } + + public static SortedSet GenerateMessageFragmentSequence(int nodeIndex, int count) + { + var messages = new SortedSet(); + var startTime = DateTime.UtcNow; + foreach (var i in Enumerable.Range(0, count)) + { + messages.Add(new MultiNodeLogMessageFragment( + GetTimeStamp(startTime, startTime + TimeSpan.FromSeconds(20)), + String.Format("Message {0}", i), nodeIndex, DummyRoleFor + nodeIndex)); + } + return messages; + } + + public static SortedSet GenerateTestRunnerMessageSequence(int count) + { + var messages = new SortedSet(); + var startTime = DateTime.UtcNow; + foreach (var i in Enumerable.Range(0, count)) + { + messages.Add(new MultiNodeTestRunnerMessage(GetTimeStamp(startTime, startTime + TimeSpan.FromSeconds(20)), String.Format("Message {0}", i), + "/foo", LogLevel.InfoLevel)); + } + return messages; + } + + public static SortedSet GenerateResultMessage(IEnumerable nodeIndices, bool pass) + { + var messages = new SortedSet(); + var enumerable = nodeIndices as int[] ?? nodeIndices.ToArray(); + return GenerateMessageDistributionForNodes(enumerable, enumerable.Count(), + (i, i1) => GenerateResultMessage(i, pass)); + } + + public static SortedSet GenerateResultMessage(int nodeIndex, bool pass) + { + var messages = new SortedSet(); + var startTime = DateTime.UtcNow; + messages.Add( + new MultiNodeResultMessage( + GetTimeStamp(startTime, startTime + TimeSpan.FromSeconds(30)), + String.Format("Test passed? {0}", pass), nodeIndex, DummyRoleFor + nodeIndex, pass)); + return messages; + } + + #region Faker functions + private static DateTime GetDateTime(DateTime from, DateTime to) + { + TimeSpan timeSpan = new TimeSpan(to.Ticks - from.Ticks); + return from + new TimeSpan((long)(timeSpan.Ticks * Random.NextDouble())); + } + + private static DateTime GetDateTime() + { + return GetDateTime(DateTime.Now.AddYears(-70), DateTime.Now.AddYears(70)); + } + + private static long GetTimeStamp(DateTime when) + { + return (long)(when - new DateTime(1970, 1, 1, 0, 0, 0, 0).ToUniversalTime()).TotalSeconds; + } + + private static long GetTimeStamp(DateTime from, DateTime to) + { + return GetTimeStamp(GetDateTime(from, to)); + } + + private static long GetTimeStamp() + { + return GetTimeStamp(GetDateTime()); + } + + private static string AlphaNumericString(int minLength = 10, int maxLength = 40) + { + return new string( + Enumerable.Repeat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + Random.Next(minLength, maxLength)) + .Select(x => x[Random.Next(x.Length)]) + .ToArray()); + } + + private static int Range(int min = 0, int max = 2147483647) + { + return Random.Next(min, max); + } + #endregion + + internal class MockMultiNodeTestCase : MultiNodeTestCase + { +#pragma warning disable CS0618 // Type or member is obsolete + public MockMultiNodeTestCase() { } + + public MockMultiNodeTestCase(IEnumerable nodeIndices) + { + _indices = nodeIndices; + MethodName = AlphaNumericString(); + TypeName = AlphaNumericString(); + AssemblyPath = AlphaNumericString(); + } +#pragma warning restore CS0618 // Type or member is obsolete + + private readonly IEnumerable _indices; + + public override string MethodName { get; } + public override string TypeName { get; } + public override string AssemblyPath { get; protected set; } + + protected override void Initialize() + { + InternalNodes = LoadDetails(); + } + + protected override List LoadDetails() + { + return _indices.Select(i => new NodeTest(this, i, DummyRoleFor + i)).ToList(); + } + + public override Task RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, + ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) + { + return Task.FromResult(new RunSummary()); + } + } + + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/ParsingSpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/ParsingSpec.cs new file mode 100644 index 00000000000..ad35b067f6b --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/ParsingSpec.cs @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Reflection; +using Akka.Actor; +using Akka.Configuration; +using Akka.Event; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using FluentAssertions; +using Xunit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal +{ + /// + /// Used to test the 's ability to parse + /// + public class ParsingSpec : TestKit.Xunit2.TestKit + { + public ParsingSpec() + : base(ConfigurationFactory.ParseString(@" + akka { + loglevel = DEBUG + stdout-loglevel = DEBUG + } + ")) + { + + } + + #region Actor definitions + + public class LoggingActor : UntypedActor + { + protected override void OnReceive(object message) + { + Context.GetLogger().Debug("Received message {0}", message); + } + } + + #endregion + + [Fact] + public void MessageSink_should_parse_Node_log_message_fragment_correctly() + { + //format the a log fragment as would be recorded by the test runner + var message = "this is some message"; + var foundMessageStr = "[NODE1]" + message; + LogMessageFragmentForNode nodeMessage; + MessageSink.TryParseLogMessage(foundMessageStr, out nodeMessage).Should().BeTrue("should have been able to parse log message"); + + Assert.Equal(1, nodeMessage.NodeIndex); + Assert.Equal(message, nodeMessage.Message); + } + + [Fact] + public void MessageSink_should_parse_Runner_log_message_correctly() + { + var loggingActor = Sys.ActorOf(); + Sys.EventStream.Subscribe(TestActor, typeof(Debug)); + var message = "LOG ME... but like the test runner this time!"; + loggingActor.Tell(message); + + //capture the logged message + var foundMessage = ExpectMsg(); + + //format the string as it would appear when reported by multinode test runner + var foundMessageStr = foundMessage.ToString(); + LogMessageForTestRunner runnerMessage; + MessageSink.TryParseLogMessage(foundMessageStr, out runnerMessage).Should().BeTrue("should have been able to parse log message"); + + Assert.Equal(foundMessage.LogLevel(), runnerMessage.Level); + Assert.Equal(foundMessage.LogSource, runnerMessage.LogSource); + Assert.Equal(foundMessage.Message.ToString(), $"Received message {message}"); + } + + [Fact] + public void MessageSink_should_parse_Node_SpecPass_message_correctly() + { + var specPass = new SpecPass(1, "super_role_1", GetType().GetTypeInfo().Assembly.GetName().Name); + NodeCompletedSpecWithSuccess nodeCompletedSpecWithSuccess; + MessageSink.TryParseSuccessMessage(specPass.ToString(), out nodeCompletedSpecWithSuccess) + .Should().BeTrue("should have been able to parse node success message"); + + Assert.Equal(specPass.NodeIndex, nodeCompletedSpecWithSuccess.NodeIndex); + Assert.Equal(specPass.NodeRole, nodeCompletedSpecWithSuccess.NodeRole); + } + + [Fact] + public void MessageSink_should_parse_Node_SpecFail_message_correctly() + { + var specFail = new SpecFail(1, "super_role_1", GetType().GetTypeInfo().Assembly.GetName().Name); + NodeCompletedSpecWithFail nodeCompletedSpecWithFail; + MessageSink.TryParseFailureMessage(specFail.ToString(), out nodeCompletedSpecWithFail) + .Should().BeTrue("should have been able to parse node failure message"); + + Assert.Equal(specFail.NodeIndex, nodeCompletedSpecWithFail.NodeIndex); + Assert.Equal(specFail.NodeRole, nodeCompletedSpecWithFail.NodeRole); + } + + [Fact] + public void MessageSink_should_be_able_to_infer_message_type() + { + var specPass = new SpecPass(1, "super_role_1", GetType().GetTypeInfo().Assembly.GetName().Name); + var specFail = new SpecFail(1, "super_role_1", GetType().GetTypeInfo().Assembly.GetName().Name); + + var loggingActor = Sys.ActorOf(); + Sys.EventStream.Subscribe(TestActor, typeof(Debug)); + loggingActor.Tell("LOG ME!"); + + //capture the logged message + var foundMessage = ExpectMsg(); + + //format the string as it would appear when reported by multinode test runner + var nodeMessageFragment = "[NODE1:super_role_1] Only part of a message!"; + var runnerMessageStr = foundMessage.ToString(); + + MessageSink.DetermineMessageType(runnerMessageStr).Should().Be(MessageSink.MultiNodeTestRunnerMessageType.RunnerLogMessage); + MessageSink.DetermineMessageType(specPass.ToString()).Should().Be(MessageSink.MultiNodeTestRunnerMessageType.NodePassMessage); + MessageSink.DetermineMessageType(specFail.ToString()).Should().Be(MessageSink.MultiNodeTestRunnerMessageType.NodeFailMessage); + MessageSink.DetermineMessageType("[Node2][FAIL-EXCEPTION] Type: Xunit.Sdk.TrueException").Should().Be(MessageSink.MultiNodeTestRunnerMessageType.NodeFailureException); + MessageSink.DetermineMessageType(nodeMessageFragment).Should().Be(MessageSink.MultiNodeTestRunnerMessageType.NodeLogFragment); + MessageSink.DetermineMessageType("foo!").Should().Be(MessageSink.MultiNodeTestRunnerMessageType.Unknown); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/Persistence/JsonPersistentTestRunStoreSpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/Persistence/JsonPersistentTestRunStoreSpec.cs new file mode 100644 index 00000000000..8c5c5ceaefa --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/Persistence/JsonPersistentTestRunStoreSpec.cs @@ -0,0 +1,92 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Linq; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Persistence; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using FluentAssertions; +using Xunit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal.Persistence +{ + public class JsonPersistentTestRunStoreSpec : TestKit.Xunit2.TestKit + { + [Fact] + public void Should_save_TestRunTree_as_JSON() + { + var testRunStore = new JsonPersistentTestRunStore(); + var testRunCoordinator = Sys.ActorOf(Props.Create()); + var nodeIndexes = Enumerable.Range(1, 4).ToArray(); + + var beginSpec = new BeginNewSpec(NodeMessageHelpers.BuildNodeTests(nodeIndexes)); + + //begin a new spec + testRunCoordinator.Tell(beginSpec); + + // create some messages for each node, the test runner, and some result messages + // just like a real MultiNodeSpec + var allMessages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 300); + var runnerMessages = NodeMessageHelpers.GenerateTestRunnerMessageSequence(20); + allMessages.UnionWith(runnerMessages); + + foreach (var message in allMessages) + testRunCoordinator.Tell(message); + + //end the spec + testRunCoordinator.Tell(new EndTestRun(), TestActor); + var testRunData = ExpectMsg(); + + //save the test run + var file = Path.GetTempFileName(); + testRunStore.SaveTestRun(file, testRunData).Should().BeTrue("Should have been able to save test run"); + } + + [Fact] + public void Should_load_saved_JSON_TestRunTree() + { + var testRunStore = new JsonPersistentTestRunStore(); + var testRunCoordinator = Sys.ActorOf(Props.Create()); + var nodeIndexes = Enumerable.Range(1, 4).ToArray(); + + var beginSpec = new BeginNewSpec(NodeMessageHelpers.BuildNodeTests(nodeIndexes)); + + //begin a new spec + testRunCoordinator.Tell(beginSpec); + + // create some messages for each node, the test runner, and some result messages + // just like a real MultiNodeSpec + var allMessages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 300); + var runnerMessages = NodeMessageHelpers.GenerateTestRunnerMessageSequence(20); + var successMessages = NodeMessageHelpers.GenerateResultMessage(nodeIndexes, true); + var messageFragments = NodeMessageHelpers.GenerateMessageFragmentSequence(nodeIndexes, 100); + allMessages.UnionWith(runnerMessages); + allMessages.UnionWith(successMessages); + allMessages.UnionWith(messageFragments); + + foreach (var message in allMessages) + testRunCoordinator.Tell(message); + + //end the spec + testRunCoordinator.Tell(new EndTestRun(), TestActor); + var testRunData = ExpectMsg(); + + //save the test run + var file = Path.GetTempFileName(); + testRunStore.SaveTestRun(file, testRunData).Should().BeTrue("Should have been able to save test run"); + + //retrieve the test run from file + var retrievedFile = testRunStore.FetchTestRun(file); + Assert.NotNull(retrievedFile); + Assert.True(testRunData.Equals(retrievedFile)); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/ResultSummaryTests.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/ResultSummaryTests.cs new file mode 100644 index 00000000000..57655dcbeca --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/ResultSummaryTests.cs @@ -0,0 +1,84 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Collections.Generic; +using Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models; +using FluentAssertions; +using Xunit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal +{ + public class ResultSummaryTests + { + public static IEnumerable ResultsSummaryOutcomeData + { + get + { + yield return new object[] + { + new UnitTest[] { }, + TestOutcome.NotExecuted + }; + + yield return new object[] + { + new[] + { + new UnitTest("", "", Identifier.Empty, "") + { + Results = + { + new UnitTestResult(Identifier.Empty, Identifier.Empty, Identifier.Empty, "", "") + { + Outcome = TestOutcome.Passed + } + } + }, + }, + TestOutcome.Passed + }; + + yield return new object[] + { + new[] + { + new UnitTest("", "", Identifier.Empty, "") + { + Results = + { + new UnitTestResult(Identifier.Empty, Identifier.Empty, Identifier.Empty, "", "") + { + Outcome = TestOutcome.Passed + } + } + }, + new UnitTest("", "", Identifier.Empty, "") + { + Results = + { + new UnitTestResult(Identifier.Empty, Identifier.Empty, Identifier.Empty, "", "") + { + Outcome = TestOutcome.Failed + } + } + }, + }, + TestOutcome.Failed + }; + } + } + + [Theory] + [MemberData(nameof(ResultsSummaryOutcomeData))] + public void ResultsSummaryOutcome(UnitTest[] tests, TestOutcome outcome) + { + var summary = new ResultSummary(tests, new Output()); + + summary.Outcome.Should().Be(outcome); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/SpecRunCoordinatorSpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/SpecRunCoordinatorSpec.cs new file mode 100644 index 00000000000..d1ff63779e1 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/SpecRunCoordinatorSpec.cs @@ -0,0 +1,160 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Xunit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal +{ + public class SpecRunCoordinatorSpec : TestKit.Xunit2.TestKit + { + [Fact] + public void SpecRunCoordinator_should_log_TestRunner_messages() + { + var nodeIndexes = Enumerable.Range(1, 3).ToArray(); + var test = NodeMessageHelpers.BuildNodeTests(nodeIndexes); + + var specRunCoordinator = Sys.ActorOf(Props.Create(() => + new SpecRunCoordinator(test.TypeName, test.MethodName, test.Nodes))); + + var runnerMessages = NodeMessageHelpers.GenerateTestRunnerMessageSequence(100); + foreach (var multiNodeMessage in runnerMessages) + { + specRunCoordinator.Tell(multiNodeMessage); + } + + //End the test + specRunCoordinator.Tell(new EndSpec(), TestActor); + var factData = ExpectMsg(); + + Assert.True(factData.RunnerMessages.Any()); + Assert.True(runnerMessages.SetEquals(factData.RunnerMessages)); + } + + [Fact] + public void SpecRunCoordinator_should_route_messages_correctly_to_child_NodeDataActors() + { + var nodeIndexes = Enumerable.Range(1, 3).ToArray(); + var test = NodeMessageHelpers.BuildNodeTests(nodeIndexes); + + var specRunCoordinator = Sys.ActorOf(Props.Create(() => + new SpecRunCoordinator(test.TypeName, test.MethodName, test.Nodes))); + + var messages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 100); + foreach (var multiNodeMessage in messages) + { + specRunCoordinator.Tell(multiNodeMessage); + } + + //End the test + specRunCoordinator.Tell(new EndSpec(), TestActor); + var factData = ExpectMsg(); + + // Combine the messages from each individual NodeData back into a unioned set. + // Should match what we sent (messages.) + var combinedTimeline = new SortedSet(); + foreach(var nodeData in factData.NodeFacts) + combinedTimeline.UnionWith(nodeData.Value.EventStream); + + Assert.Equal(nodeIndexes.Length, factData.NodeFacts.Count); + Assert.True(messages.SetEquals(combinedTimeline)); + } + + [Fact] + public void SpecRunCoordinator_should_mark_spec_as_passed_if_all_nodes_pass() + { + var nodeIndexes = Enumerable.Range(1, 3).ToArray(); + var test = NodeMessageHelpers.BuildNodeTests(nodeIndexes); + + var specRunCoordinator = Sys.ActorOf(Props.Create(() => + new SpecRunCoordinator(test.TypeName, test.MethodName, test.Nodes))); + + var messages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 100); + + //Add some result (PASS) messages + messages.UnionWith(NodeMessageHelpers.GenerateResultMessage(nodeIndexes, true)); + + foreach (var multiNodeMessage in messages) + { + specRunCoordinator.Tell(multiNodeMessage); + } + + //End the test + specRunCoordinator.Tell(new EndSpec(), TestActor); + var factData = ExpectMsg(); + + Assert.Equal(nodeIndexes.Length, factData.NodeFacts.Count); + Assert.True(factData.NodeFacts.All(x => x.Value.Passed.Value), "each individual node should have marked their test as passed"); + Assert.True(factData.Passed.Value, "SpecCoordinator should have marked spec as passed"); + } + + [Fact] + public void SpecRunCoordinator_should_mark_spec_as_failed_if_all_nodes_fail() + { + var nodeIndexes = Enumerable.Range(1, 3).ToArray(); + var test = NodeMessageHelpers.BuildNodeTests(nodeIndexes); + + var specRunCoordinator = Sys.ActorOf(Props.Create(() => + new SpecRunCoordinator(test.TypeName, test.MethodName, test.Nodes))); + + var messages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 100); + + //Add some result (FAIL) messages + messages.UnionWith(NodeMessageHelpers.GenerateResultMessage(nodeIndexes, false)); + + foreach (var multiNodeMessage in messages) + { + specRunCoordinator.Tell(multiNodeMessage); + } + + //End the test + specRunCoordinator.Tell(new EndSpec(), TestActor); + var factData = ExpectMsg(); + + Assert.Equal(nodeIndexes.Length, factData.NodeFacts.Count); + Assert.True(factData.NodeFacts.All(x => !x.Value.Passed.Value), "each individual node should have marked their test as failed"); + Assert.False(factData.Passed.Value, "SpecCoordinator should have marked spec as failed"); + } + + [Fact] + public void SpecRunCoordinator_should_mark_spec_as_failed_if_one_node_fails() + { + var nodeIndexes = Enumerable.Range(1, 3).ToArray(); + var test = NodeMessageHelpers.BuildNodeTests(nodeIndexes); + + var specRunCoordinator = Sys.ActorOf(Props.Create(() => + new SpecRunCoordinator(test.TypeName, test.MethodName, test.Nodes))); + + var messages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 100); + + //Add some result (1 FAIL, 2 PASS) messages + var front = nodeIndexes.First(); + var nodesMinusFront = nodeIndexes.Where(x => x != front); + messages.UnionWith(NodeMessageHelpers.GenerateResultMessage(nodesMinusFront, true)); //PASS messages + messages.UnionWith(NodeMessageHelpers.GenerateResultMessage(front, false)); //one FAIL message + + foreach (var multiNodeMessage in messages) + { + specRunCoordinator.Tell(multiNodeMessage); + } + + //End the test + specRunCoordinator.Tell(new EndSpec(), TestActor); + var factData = ExpectMsg(); + + Assert.Equal(nodeIndexes.Length, factData.NodeFacts.Count); + Assert.True(factData.NodeFacts.Count(x => !x.Value.Passed.Value) == 1, "one node should have marked their test as failed"); + Assert.True(factData.NodeFacts.Count(x => x.Value.Passed.Value) == nodeIndexes.Length - 1, "rest of nodes should have marked their test as passed"); + Assert.False(factData.Passed.Value, "SpecCoordinator should have marked spec as failed"); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/TestRunCoordinatorSpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/TestRunCoordinatorSpec.cs new file mode 100644 index 00000000000..2bed19a372b --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/TestRunCoordinatorSpec.cs @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal +{ + public class TestRunCoordinatorSpec : TestKit.Xunit2.TestKit + { + public TestRunCoordinatorSpec(ITestOutputHelper output) : base((ActorSystem)null, output) + {} + + [Fact] + public void TestRunCoordinator_should_start_and_route_messages_to_SpecRunCoordinator() + { + var testRunCoordinator = Sys.ActorOf(Props.Create()); + var nodeIndexes = Enumerable.Range(1, 4).ToArray(); + + var beginSpec = new BeginNewSpec(NodeMessageHelpers.BuildNodeTests(nodeIndexes)); + + //begin a new spec + testRunCoordinator.Tell(beginSpec); + + // create some messages for each node, the test runner, and some result messages + // just like a real MultiNodeSpec + var allMessages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 300); + var runnerMessages = NodeMessageHelpers.GenerateTestRunnerMessageSequence(20); + allMessages.UnionWith(runnerMessages); + + foreach(var message in allMessages) + testRunCoordinator.Tell(message); + + //end the spec + testRunCoordinator.Tell(new EndTestRun(), TestActor); + var testRunData = ExpectMsg(); + + Assert.Single(testRunData.Specs); + + var specMessages = new SortedSet(); + foreach (var spec in testRunData.Specs) + { + specMessages.UnionWith(spec.RunnerMessages); + foreach(var fact in spec.NodeFacts) + specMessages.UnionWith(fact.Value.EventStream); + } + + Assert.True(allMessages.SetEquals(specMessages)); + + } + + [Fact] + public void TestRunCoordinator_should_publish_FactData_to_Subscribers_when_Specs_complete() + { + var testRunCoordinator = Sys.ActorOf(Props.Create()); + var nodeIndexes = Enumerable.Range(1, 4).ToArray(); + + var beginSpec = new BeginNewSpec(NodeMessageHelpers.BuildNodeTests(nodeIndexes)); + + var probe = CreateTestProbe(Sys); + //register the TestActor as a subscriber for FactData announcements + testRunCoordinator.Tell(new TestRunCoordinator.SubscribeFactCompletionMessages(probe.Ref)); + + //begin a new spec + testRunCoordinator.Tell(beginSpec); + + // create some messages for each node, the test runner, and some result messages + // just like a real MultiNodeSpec + var allMessages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 300); + var runnerMessages = NodeMessageHelpers.GenerateTestRunnerMessageSequence(20); + var passMessages = NodeMessageHelpers.GenerateResultMessage(nodeIndexes, true); + allMessages.UnionWith(runnerMessages); + allMessages.UnionWith(passMessages); + + foreach (var message in allMessages) + testRunCoordinator.Tell(message); + + //end the spec + testRunCoordinator.Tell(new EndSpec()); + + var factData = probe.ExpectMsg(); + Assert.True(factData.Passed.Value, "Spec should have passed"); + Assert.True(factData.NodeFacts.All(x => x.Value.Passed.Value), "All individual nodes should have reported test pass"); + } + + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/TestRunShutdownSpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/TestRunShutdownSpec.cs new file mode 100644 index 00000000000..1080ad3866b --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/TestRunShutdownSpec.cs @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Xunit; + +namespace Akka.MultiNode.TestAdapter.Tests.Internal +{ + /// + /// Used to validate that we can get final reporting on shutdown + /// + public class TestRunShutdownSpec : TestKit.Xunit2.TestKit + { + [Fact] + public void TestCoordinatorEnabledMessageSink_should_receive_TestRunTree_when_EndTestRun_is_received() + { + var consoleMessageSink = Sys.ActorOf(Props.Create(() => new ConsoleMessageSinkActor(true))); + var nodeIndexes = Enumerable.Range(1, 4).ToArray(); + + var beginSpec = new BeginNewSpec(NodeMessageHelpers.BuildNodeTests(nodeIndexes)); + consoleMessageSink.Tell(beginSpec); + + // create some messages for each node, the test runner, and some result messages + // just like a real MultiNodeSpec + var allMessages = NodeMessageHelpers.GenerateMessageSequence(nodeIndexes, 300); + var runnerMessages = NodeMessageHelpers.GenerateTestRunnerMessageSequence(20); + var passMessages = NodeMessageHelpers.GenerateResultMessage(nodeIndexes, true); + allMessages.UnionWith(runnerMessages); + allMessages.UnionWith(passMessages); + + foreach (var message in allMessages) + consoleMessageSink.Tell(message); + + //end the spec + consoleMessageSink.Tell(new EndSpec()); + + //end the test run... + var sinkReadyToTerminate = + consoleMessageSink.AskAndWait(new EndTestRun(), + TimeSpan.FromSeconds(10)); + Assert.NotNull(sinkReadyToTerminate); + + } + } + + public static class AskExtensions + { + public static TAnswer AskAndWait(this ICanTell self, object message, TimeSpan timeout) + { + var task = self.Ask(message, timeout); + task.Wait(); + return task.Result; + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/Utils/ContinuousEnumerator.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/Utils/ContinuousEnumerator.cs new file mode 100644 index 00000000000..7c09b187c0e --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/Internal/Utils/ContinuousEnumerator.cs @@ -0,0 +1,86 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + + using System.Collections; + using System.Collections.Generic; + + namespace Akka.MultiNode.TestAdapter.Tests.Internal.Utils +{ + /// + /// Implements a circular around an existing . + /// + /// This allows for continuous read-only iteration over a set. + /// + /// The type of objects to enumerate + /// + /// Duplicates Akka.Utils.ContinuousEnumerator class from Akka.NET https://github.com/akkadotnet/akka.net + /// + internal sealed class ContinuousEnumerator : IEnumerator + { + private readonly IEnumerator _internalEnumerator; + + /// + /// Initializes a new instance of the class. + /// + /// The raw iterator from some object + public ContinuousEnumerator(IEnumerator internalEnumerator) + { + _internalEnumerator = internalEnumerator; + } + + /// + public void Dispose() + { + _internalEnumerator.Dispose(); + } + + /// + public bool MoveNext() + { + if (!_internalEnumerator.MoveNext()) + { + _internalEnumerator.Reset(); + return _internalEnumerator.MoveNext(); + } + return true; + } + + /// + public void Reset() + { + _internalEnumerator.Reset(); + } + + /// + public T Current { get { return _internalEnumerator.Current; } } + + object IEnumerator.Current + { + get { return Current; } + } + } + + /// + /// Extension method class for adding support to any + /// instance within Akka.NET + /// + internal static class ContinuousEnumeratorExtensions + { + /// + /// Provides a instance for . + /// + /// Internally, it just wraps 's internal iterator with circular iteration behavior. + /// + /// TBD + /// TBD + public static ContinuousEnumerator GetContinuousEnumerator(this IEnumerable collection) + { + return new ContinuousEnumerator(collection.GetEnumerator()); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/MultiNodeTestExecutorSpec.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/MultiNodeTestExecutorSpec.cs new file mode 100644 index 00000000000..a666e7785c2 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/MultiNodeTestExecutorSpec.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Akka.MultiNode.TestAdapter.Internal; +using Akka.MultiNode.TestAdapter.SampleTests; +using Akka.MultiNode.TestAdapter.SampleTests.Metadata; +using Akka.MultiNode.TestAdapter.Tests.Helpers; +using Akka.Remote.TestKit; +using FluentAssertions; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using Xunit; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter.Tests +{ + [Collection(TestCollections.MultiNode)] + public class MultiNodeTestExecutorSpec + { + private readonly string _sampleTestsAssemblyPath; + + public MultiNodeTestExecutorSpec() + { + // AssertionOptions.FormattingOptions.MaxLines = 1000; + + _sampleTestsAssemblyPath = Path.GetFullPath(SampleTestsMetadata.AssemblyFileName); + File.Exists(_sampleTestsAssemblyPath).Should().BeTrue($"Assemblies with samples should exist at {_sampleTestsAssemblyPath}"); + CommandLine.Initialize(new []{"-Dmultinode.test-runner=\"multinode\""}); + } + + [Fact] + public void Should_run_tests_and_report_results() + { + using (var controller = new XunitFrontController(AppDomainSupport.IfAvailable, _sampleTestsAssemblyPath)) + { + using (var discovery = new Discovery(_sampleTestsAssemblyPath)) + { + controller.Find(false, discovery, TestFrameworkOptions.ForDiscovery()); + discovery.Finished.WaitOne(); + discovery.Errors.Count.Should().Be(0); + + using (var sink = new TestSink()) + { + controller.RunTests(discovery.TestCases, sink, TestFrameworkOptions.ForExecution( + new TestAssemblyConfiguration { ParallelizeTestCollections = false })); + sink.Finished.WaitOne(TimeSpan.FromMinutes(10)).Should().BeTrue("Test execution timed out"); + + sink.TestResults.Count.Should().Be(5); + + Should_report_passes(sink); + Should_report_failures(sink); + Should_report_failures_for_one_node(sink); + Should_report_skipped_specs(sink); + Should_ignore_specs_with_bad_config(sink); + } + } + } + } + + private void Should_report_passes(TestSink sink) + { + var passed = sink.TestResults.FirstOrDefault(r => r.TestCase.DisplayName + .Contains(nameof(SampleMultiNodeSpec))); + passed.Should().NotBeNull(); + passed.Passed.Should().BeTrue("Should report failed spec result"); + passed.RunSummary.Total.Should().Be(2); + passed.RunSummary.Failed.Should().Be(0); + passed.RunSummary.Skipped.Should().Be(0); + } + + private void Should_report_failures(TestSink sink) + { + var failed = sink.TestResults.FirstOrDefault(r => r.TestCase.DisplayName + .Contains($".{nameof(FailedMultiNodeSpec)}")); + failed.Should().NotBeNull(); + failed.Failed.Should().BeTrue("Should report failed spec result"); + failed.RunSummary.Total.Should().Be(2); + failed.RunSummary.Failed.Should().Be(2); + } + + private void Should_report_failures_for_one_node(TestSink sink) + { + var oneFailed = sink.TestResults.FirstOrDefault(r => r.TestCase.DisplayName + .Contains(nameof(OneNodeFailedMultiNodeSpec))); + + oneFailed.Should().NotBeNull(); + oneFailed.Failed.Should().BeTrue(); + oneFailed.RunSummary.Total.Should().Be(2); + oneFailed.RunSummary.Failed.Should().Be(1, "Should report failed spec result when only one node failed"); + oneFailed.RunSummary.Skipped.Should().Be(0, "Should still contain not-failed results"); + } + + private void Should_report_skipped_specs(TestSink sink) + { + var skipped = sink.TestResults.FirstOrDefault(r => r.TestCase.DisplayName + .Contains(nameof(SkippedMultiNodeSpec))); + + skipped.Should().NotBeNull(); + skipped.RunSummary.Total.Should().Be(2); + skipped.RunSummary.Skipped.Should().Be(2, "When skipped, all nodes should be skipped"); + skipped.Skipped.Should().BeTrue("Should report skipped spec result"); + } + + private void Should_ignore_specs_with_bad_config(TestSink sink) + { + var badConfig = sink.TestResults.FirstOrDefault(r => r.TestCase.DisplayName + .Contains(nameof(BadConfigMultiNodeSpec))); + + badConfig.Should().NotBeNull(); + badConfig.RunSummary.Total.Should().Be(1); + badConfig.RunSummary.Skipped.Should().Be(1, "Should skip specs with bad configuration - because can not build configuration"); + + ((MultiNodeTestCase)badConfig.TestCase).Nodes[0].DisplayName.Should().Be("ERRORED"); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter.Tests/TestSink.cs b/src/core/Akka.MultiNode.TestAdapter.Tests/TestSink.cs new file mode 100644 index 00000000000..3cea005df98 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter.Tests/TestSink.cs @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; +using LongLivedMarshalByRefObject = Xunit.Sdk.LongLivedMarshalByRefObject; +using TestCaseStarting = Xunit.Sdk.TestCaseStarting; + +namespace Akka.MultiNode.TestAdapter.Tests +{ + internal class TestSink : LongLivedMarshalByRefObject, IMessageSink, IDisposable + { + public ManualResetEvent Finished { get; }= new ManualResetEvent(false); + + public List TestResults { get; } = new List(); + + public bool OnMessage(IMessageSinkMessage message) + { + switch (message) + { + case TestCaseStarting start: + TestResults.Add(new TestResult(start.TestCase)); + return true; + + case ITestPassed testPassed: + { + var result = TestResults.First(t => ReferenceEquals(t.TestCase, testPassed.TestCase)); + result.RunSummary.Total++; + return true; + } + + case ITestFailed testFailed: + { + var result = TestResults.First(t => ReferenceEquals(t.TestCase, testFailed.TestCase)); + result.RunSummary.Total++; + result.RunSummary.Failed++; + return true; + } + + case ITestSkipped testSkipped: + { + var result = TestResults.First(t => ReferenceEquals(t.TestCase, testSkipped.TestCase)); + result.RunSummary.Total++; + result.RunSummary.Skipped++; + return true; + } + + case ITestAssemblyFinished _: + Finished.Set(); + return true; + + default: + return true; + } + } + + /// + public void Dispose() + { + Finished.Dispose(); + } + } + + internal class TestResult + { + public TestResult(ITestCase testCase) + { + TestCase = testCase; + } + + public ITestCase TestCase { get; } + public RunSummary RunSummary { get; } = new RunSummary(); + + public bool Passed => RunSummary.Failed == 0 && RunSummary.Skipped == 0; + public bool Failed => RunSummary.Failed > 0; + public bool Skipped => RunSummary.Failed == 0 && RunSummary.Skipped == RunSummary.Total; + public bool NotRun => RunSummary.Total == 0; + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Akka.MultiNode.TestAdapter.csproj b/src/core/Akka.MultiNode.TestAdapter/Akka.MultiNode.TestAdapter.csproj new file mode 100644 index 00000000000..190930e49c7 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Akka.MultiNode.TestAdapter.csproj @@ -0,0 +1,60 @@ + + + Visual Studio 2017 15.9+ Test Explorer runner for the Akka.NET MultiNode tests + $(NetStandardLibVersion) + true + true + + + + $(DefineConstants);RELEASE + + + + + + + + + + + + + + + + + TextTemplatingFilePreprocessor + VisualizerRuntimeTemplate.cs + + + + + + True + True + VisualizerRuntimeTemplate.tt + + + + + $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage + + + + + + <_ReferenceCopyLocalPaths Include="@(ReferenceCopyLocalPaths->WithMetadataValue('ReferenceSourceTarget', 'ProjectReference')->WithMetadataValue('PrivateAssets', 'All'))" /> + + + + + + + + + + + + + diff --git a/src/core/Akka.MultiNode.TestAdapter/Akka.MultiNode.TestAdapter.nuspec.template b/src/core/Akka.MultiNode.TestAdapter/Akka.MultiNode.TestAdapter.nuspec.template new file mode 100644 index 00000000000..49545c7a863 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Akka.MultiNode.TestAdapter.nuspec.template @@ -0,0 +1,20 @@ + + + + Akka.MultiNode.TestAdapter + @version@ + Akka.MultiNode.TestAdapter + Akka.NET Team + Akka.NET Team + false + https://github.com/akkadotnet/akka.net/blob/master/LICENSE + https://github.com/akkadotnet/akka.net + http://getakka.net/images/AkkaNetLogo.Normal.png + Akka.NET Multi-node Test Adapter; used for executing tests written with Akka.Remote.TestKit + Copyright � 2013-2023 .NET Foundation + akka actors actor model Akka concurrency + + + + + \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Configuration/Json.cs b/src/core/Akka.MultiNode.TestAdapter/Configuration/Json.cs new file mode 100644 index 00000000000..dafff7fdad4 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Configuration/Json.cs @@ -0,0 +1,880 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace Akka.MultiNode.TestAdapter.Configuration +{ + class JsonArray : JsonValue + { + readonly JsonValue[] _array; + + public JsonArray(JsonValue[] array, int line, int column) + : base(line, column) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + _array = array; + } + + public int Length + { + get { return _array.Length; } + } + + public JsonValue this[int index] + { + get { return _array[index]; } + } + } + + class JsonBoolean : JsonValue + { + public JsonBoolean(JsonToken token) + : base(token.Line, token.Column) + { + if (token.Type == JsonTokenType.True) + { + Value = true; + } + else if (token.Type == JsonTokenType.False) + { + Value = false; + } + else + { + throw new ArgumentException("Token value should be either True or False.", nameof(token)); + } + } + + public bool Value { get; private set; } + + public static implicit operator bool (JsonBoolean jsonBoolean) + { + return jsonBoolean.Value; + } + } + + class JsonBuffer + { + public const string ValueNull = "null"; + public const string ValueTrue = "true"; + public const string ValueFalse = "false"; + + StringBuilder _buffer = new StringBuilder(); + StringBuilder _codePointBuffer = new StringBuilder(4); + readonly TextReader _reader; + JsonToken _token; + int _line; + int _column; + + public JsonBuffer(TextReader reader) + { + _reader = reader; + _line = 1; + } + + public JsonToken Read() + { + int first; + while (true) + { + first = ReadNextChar(); + + if (first == -1) + { + _token.Type = JsonTokenType.EOF; + return _token; + } + else if (!IsWhitespace(first)) + { + break; + } + } + + _token.Value = ((char)first).ToString(); + _token.Line = _line; + _token.Column = _column; + + if (first == '{') + { + _token.Type = JsonTokenType.LeftCurlyBracket; + } + else if (first == '}') + { + _token.Type = JsonTokenType.RightCurlyBracket; + } + else if (first == '[') + { + _token.Type = JsonTokenType.LeftSquareBracket; + } + else if (first == ']') + { + _token.Type = JsonTokenType.RightSquareBracket; + } + else if (first == ':') + { + _token.Type = JsonTokenType.Colon; + } + else if (first == ',') + { + _token.Type = JsonTokenType.Comma; + } + else if (first == '"') + { + _token.Type = JsonTokenType.String; + _token.Value = ReadString(); + } + else if (first == 't') + { + ReadLiteral(ValueTrue); + _token.Type = JsonTokenType.True; + } + else if (first == 'f') + { + ReadLiteral(ValueFalse); + _token.Type = JsonTokenType.False; + } + else if (first == 'n') + { + ReadLiteral(ValueNull); + _token.Type = JsonTokenType.Null; + } + else if ((first >= '0' && first <= '9') || first == '-') + { + _token.Type = JsonTokenType.Number; + _token.Value = ReadNumber(first); + } + else + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_IllegalCharacter(first), + _token); + } + + // JsonToken is a value type + return _token; + } + + int ReadNextChar() + { + while (true) + { + var value = _reader.Read(); + _column++; + switch (value) + { + case -1: + // This is the end of file + return -1; + case '\n': + // This is a new line. Let the next loop read the first character of the following line. + // Set position ahead of next line + _column = 0; + _line++; + + continue; + case '\r': + break; + default: + // Returns the normal value + return value; + } + } + } + + string ReadNumber(int firstRead) + { +#if NET35 + _buffer = new StringBuilder(); +#else + _buffer.Clear(); +#endif + _buffer.Append((char)firstRead); + + while (true) + { + var next = _reader.Peek(); + + if ((next >= '0' && next <= '9') || + next == '.' || + next == 'e' || + next == 'E') + { + _buffer.Append((char)ReadNextChar()); + } + else + { + break; + } + } + + return _buffer.ToString(); + } + + void ReadLiteral(string literal) + { + for (int i = 1; i < literal.Length; ++i) + { + var next = _reader.Peek(); + if (next != literal[i]) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_UnrecognizedLiteral(literal), + _line, _column); + } + else + { + ReadNextChar(); + } + } + + var tail = _reader.Peek(); + if (tail != '}' && + tail != ']' && + tail != ',' && + tail != '\n' && + tail != -1 && + !IsWhitespace(tail)) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_IllegalTrailingCharacterAfterLiteral(tail, literal), + _line, _column); + } + } + + string ReadString() + { +#if NET35 + _buffer = new StringBuilder(); +#else + _buffer.Clear(); +#endif + var escaped = false; + + while (true) + { + var next = ReadNextChar(); + + if (next == -1 || next == '\n') + { + throw new JsonDeserializerException( + JsonDeserializerResource.JSON_OpenString, + _line, _column); + } + else if (escaped) + { + if ((next == '"') || (next == '\\') || (next == '/')) + { + _buffer.Append((char)next); + } + else if (next == 'b') + { + // '\b' backspace + _buffer.Append('\b'); + } + else if (next == 'f') + { + // '\f' form feed + _buffer.Append('\f'); + } + else if (next == 'n') + { + // '\n' line feed + _buffer.Append('\n'); + } + else if (next == 'r') + { + // '\r' carriage return + _buffer.Append('\r'); + } + else if (next == 't') + { + // '\t' tab + _buffer.Append('\t'); + } + else if (next == 'u') + { + // '\uXXXX' unicode + var unicodeLine = _line; + var unicodeColumn = _column; + +#if NET35 + _codePointBuffer = new StringBuilder(4); +#else + _codePointBuffer.Clear(); +#endif + for (int i = 0; i < 4; ++i) + { + next = ReadNextChar(); + if (next == -1) + { + throw new JsonDeserializerException( + JsonDeserializerResource.JSON_InvalidEnd, + unicodeLine, + unicodeColumn); + } + else + { + _codePointBuffer[i] = (char)next; + } + } + + try + { + var unicodeValue = int.Parse(_codePointBuffer.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + _buffer.Append((char)unicodeValue); + } + catch (FormatException ex) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidUnicode(_codePointBuffer.ToString()), + ex, + unicodeLine, + unicodeColumn); + } + } + else + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxNotExpected("character escape", "\\" + next), + _line, + _column); + } + + escaped = false; + } + else if (next == '\\') + { + escaped = true; + } + else if (next == '"') + { + break; + } + else + { + _buffer.Append((char)next); + } + } + + return _buffer.ToString(); + } + + static bool IsWhitespace(int value) + { + return value == ' ' || value == '\t' || value == '\r'; + } + } + + static class JsonDeserializer + { + public static JsonValue Deserialize(TextReader reader) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + var buffer = new JsonBuffer(reader); + + var result = DeserializeInternal(buffer.Read(), buffer); + + // There are still unprocessed char. The parsing is not finished. Error happened. + var nextToken = buffer.Read(); + if (nextToken.Type != JsonTokenType.EOF) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_UnfinishedJSON(nextToken.Value), + nextToken); + } + + return result; + } + + static JsonValue DeserializeInternal(JsonToken next, JsonBuffer buffer) + { + if (next.Type == JsonTokenType.EOF) + { + return null; + } + + if (next.Type == JsonTokenType.LeftSquareBracket) + { + return DeserializeArray(next, buffer); + } + + if (next.Type == JsonTokenType.LeftCurlyBracket) + { + return DeserializeObject(next, buffer); + } + + if (next.Type == JsonTokenType.String) + { + return new JsonString(next.Value, next.Line, next.Column); + } + + if (next.Type == JsonTokenType.True || next.Type == JsonTokenType.False) + { + return new JsonBoolean(next); + } + + if (next.Type == JsonTokenType.Null) + { + return new JsonNull(next.Line, next.Column); + } + + if (next.Type == JsonTokenType.Number) + { + return new JsonNumber(next); + } + + throw new JsonDeserializerException(JsonDeserializerResource.Format_InvalidTokenExpectation( + next.Value, "'{', '[', true, false, null, JSON string, JSON number, or the end of the file"), + next); + } + + static JsonArray DeserializeArray(JsonToken head, JsonBuffer buffer) + { + var list = new List(); + while (true) + { + var next = buffer.Read(); + if (next.Type == JsonTokenType.RightSquareBracket) + { + break; + } + + list.Add(DeserializeInternal(next, buffer)); + + next = buffer.Read(); + if (next.Type == JsonTokenType.EOF) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxExpectation("JSON array", ']', ','), + next); + } + else if (next.Type == JsonTokenType.RightSquareBracket) + { + break; + } + else if (next.Type != JsonTokenType.Comma) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxExpectation("JSON array", ','), + next); + } + } + + return new JsonArray(list.ToArray(), head.Line, head.Column); + } + + static JsonObject DeserializeObject(JsonToken head, JsonBuffer buffer) + { + var dictionary = new Dictionary(); + + // Loop through each JSON entry in the input object + while (true) + { + var next = buffer.Read(); + if (next.Type == JsonTokenType.EOF) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxExpectation("JSON object", '}'), + next); + } + + if (next.Type == JsonTokenType.Colon) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxNotExpected("JSON object", ':'), + next); + } + else if (next.Type == JsonTokenType.RightCurlyBracket) + { + break; + } + else + { + if (next.Type != JsonTokenType.String) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxExpectation("JSON object member name", "JSON string"), + next); + } + + var memberName = next.Value; + if (dictionary.ContainsKey(memberName)) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_DuplicateObjectMemberName(memberName), + next); + } + + next = buffer.Read(); + if (next.Type != JsonTokenType.Colon) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxExpectation("JSON object", ':'), + next); + } + + dictionary[memberName] = DeserializeInternal(buffer.Read(), buffer); + + next = buffer.Read(); + if (next.Type == JsonTokenType.RightCurlyBracket) + { + break; + } + else if (next.Type != JsonTokenType.Comma) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidSyntaxExpectation("JSON object", ',', '}'), + next); + } + } + } + + return new JsonObject(dictionary, head.Line, head.Column); + } + } + + class JsonDeserializerException : Exception + { + public JsonDeserializerException(string message, Exception innerException, int line, int column) + : base(message, innerException) + { + Line = line; + Column = column; + } + + public JsonDeserializerException(string message, int line, int column) + : base(message) + { + Line = line; + Column = column; + } + + public JsonDeserializerException(string message, JsonToken nextToken) + : base(message) + { + Line = nextToken.Line; + Column = nextToken.Column; + } + + public int Line { get; } + + public int Column { get; } + } + + static class JsonDeserializerResource + { + internal static string Format_IllegalCharacter(int value) + { + return $"Illegal character '{(char)value}' (Unicode hexadecimal {value:X4})."; + } + + internal static string Format_IllegalTrailingCharacterAfterLiteral(int value, string literal) + { + return $"Illegal character '{(char)value}' (Unicode hexadecimal {value:X4}) after the literal name '{literal}'."; + } + + internal static string Format_UnrecognizedLiteral(string literal) + { + return $"Invalid JSON literal. Expected literal '{literal}'."; + } + + internal static string Format_DuplicateObjectMemberName(string memberName) + { + return Format_InvalidSyntax("JSON object", $"Duplicate member name '{memberName}'"); + } + + internal static string Format_InvalidFloatNumberFormat(string raw) + { + return $"Invalid float number format: {raw}"; + } + + internal static string Format_FloatNumberOverflow(string raw) + { + return $"Float number overflow: {raw}"; + } + + internal static string Format_InvalidSyntax(string syntaxName, string issue) + { + return $"Invalid {syntaxName} syntax. {issue}."; + } + + internal static string Format_InvalidSyntaxNotExpected(string syntaxName, char unexpected) + { + return $"Invalid {syntaxName} syntax. Unexpected '{unexpected}'."; + } + + internal static string Format_InvalidSyntaxNotExpected(string syntaxName, string unexpected) + { + return $"Invalid {syntaxName} syntax. Unexpected {unexpected}."; + } + + internal static string Format_InvalidSyntaxExpectation(string syntaxName, char expectation) + { + return $"Invalid {syntaxName} syntax. Expected '{expectation}'."; + } + + internal static string Format_InvalidSyntaxExpectation(string syntaxName, string expectation) + { + return $"Invalid {syntaxName} syntax. Expected {expectation}."; + } + + internal static string Format_InvalidSyntaxExpectation(string syntaxName, char expectation1, char expectation2) + { + return $"Invalid {syntaxName} syntax. Expected '{expectation1}' or '{expectation2}'."; + } + + internal static string Format_InvalidTokenExpectation(string tokenValue, string expectation) + { + return $"Unexpected token '{tokenValue}'. Expected {expectation}."; + } + + internal static string Format_InvalidUnicode(string unicode) + { + return $"Invalid Unicode [{unicode}]"; + } + + internal static string Format_UnfinishedJSON(string nextTokenValue) + { + return $"Invalid JSON end. Unprocessed token {nextTokenValue}."; + } + + internal static string JSON_OpenString + { + get { return Format_InvalidSyntaxExpectation("JSON string", '\"'); } + } + + internal static string JSON_InvalidEnd + { + get { return "Invalid JSON. Unexpected end of file."; } + } + } + + class JsonNull : JsonValue + { + public JsonNull(int line, int column) + : base(line, column) + { + } + } + + class JsonNumber : JsonValue + { + readonly string _raw; + readonly double _double; + + public JsonNumber(JsonToken token) + : base(token.Line, token.Column) + { + try + { + _raw = token.Value; + _double = double.Parse(_raw, NumberStyles.Float); + } + catch (FormatException ex) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_InvalidFloatNumberFormat(_raw), + ex, + token.Line, + token.Column); + } + catch (OverflowException ex) + { + throw new JsonDeserializerException( + JsonDeserializerResource.Format_FloatNumberOverflow(_raw), + ex, + token.Line, + token.Column); + } + } + + public double Double + { + get { return _double; } + } + + public string Raw + { + get { return _raw; } + } + } + + class JsonObject : JsonValue + { + readonly IDictionary _data; + + public JsonObject(IDictionary data, int line, int column) + : base(line, column) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + + _data = data; + } + + public ICollection Keys + { + get { return _data.Keys; } + } + + public JsonValue Value(string key) + { + JsonValue result; + if (!_data.TryGetValue(key, out result)) + { + result = null; + } + + return result; + } + + public JsonObject ValueAsJsonObject(string key) + { + return Value(key) as JsonObject; + } + + public JsonString ValueAsString(string key) + { + return Value(key) as JsonString; + } + + public int ValueAsInt(string key) + { + var number = Value(key) as JsonNumber; + if (number == null) + { + throw new FormatException(); + } + return Convert.ToInt32(number.Raw); + } + + public bool ValueAsBoolean(string key, bool defaultValue = false) + { + var boolVal = Value(key) as JsonBoolean; + if (boolVal != null) + { + return boolVal.Value; + } + + return defaultValue; + } + + public bool? ValueAsNullableBoolean(string key) + { + var boolVal = Value(key) as JsonBoolean; + if (boolVal != null) + { + return boolVal.Value; + } + + return null; + } + + public string[] ValueAsStringArray(string key) + { + var list = Value(key) as JsonArray; + if (list == null) + { + return null; + } + + var result = new string[list.Length]; + + for (int i = 0; i < list.Length; ++i) + { + var jsonString = list[i] as JsonString; + result[i] = jsonString?.ToString(); + } + + return result; + } + } + + class JsonString : JsonValue + { + readonly string _value; + + public JsonString(string value, int line, int column) + : base(line, column) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _value = value; + } + + public string Value + { + get { return _value; } + } + + public override string ToString() + { + return _value; + } + + public static implicit operator string (JsonString instance) + { + if (instance == null) + { + return null; + } + else + { + return instance.Value; + } + } + } + + struct JsonToken + { + public JsonTokenType Type; + public string Value; + public int Line; + public int Column; + } + + enum JsonTokenType + { + LeftCurlyBracket, // [ + LeftSquareBracket, // { + RightCurlyBracket, // ] + RightSquareBracket, // } + Colon, // : + Comma, // , + Null, + True, + False, + Number, + String, + EOF + } + + class JsonValue + { + public JsonValue(int line, int column) + { + Line = line; + Column = column; + } + + public int Line { get; } + + public int Column { get; } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Configuration/MultiNodeTestRunnerOptions.cs b/src/core/Akka.MultiNode.TestAdapter/Configuration/MultiNodeTestRunnerOptions.cs new file mode 100644 index 00000000000..8369a4fcb1b --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Configuration/MultiNodeTestRunnerOptions.cs @@ -0,0 +1,66 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; +using Akka.Configuration; + +#nullable enable +namespace Akka.MultiNode.TestAdapter.Configuration +{ + /// + /// MultiNodeTestRunnerOptions + /// + public class MultiNodeTestRunnerOptions + { + /// + /// Default options + /// + public static readonly MultiNodeTestRunnerOptions Default = new MultiNodeTestRunnerOptions(); + + /// + /// File output directory + /// + public string OutputDirectory { get; set; } = "TestResults"; + + /// + /// Subdirectory to store failed specs logs + /// + public string FailedSpecsDirectory { get; set; } = "FAILED_SPECS_LOGS"; + + /// + /// MNTR controller listener address + /// + public string ListenAddress + { + get => ListenIpAddress.ToString(); + set + { + if (!IPAddress.TryParse(value, out var address)) + { + var addresses = Dns.GetHostAddresses(value); + address = + addresses.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork) ?? + addresses.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetworkV6) ?? + throw new ConfigurationException($"Invalid ListenAddress [{value}]. ListenAddress must be IPv4, IPv6, or a host name"); + } + + ListenIpAddress = address; + } + } + + public IPAddress ListenIpAddress { get; private set; } = IPAddress.Parse("127.0.0.1"); + + /// + /// MNTR controller listener port. Set 0 to use random available port + /// + public int ListenPort { get; set; } + + /// + /// If set, performs output directory cleanup before running tests + /// + public bool AppendLogOutput { get; set; } = true; + + public string? Platform { get; set; } + + public bool UseBuiltInTrxReporter { get; set; } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Configuration/OptionsReader.cs b/src/core/Akka.MultiNode.TestAdapter/Configuration/OptionsReader.cs new file mode 100644 index 00000000000..72d60daef0d --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Configuration/OptionsReader.cs @@ -0,0 +1,95 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.IO; +using Newtonsoft.Json; + +namespace Akka.MultiNode.TestAdapter.Configuration +{ + public static class OptionsReader + { + public static MultiNodeTestRunnerOptions Load(string assemblyFileName) + { + var assemblyName = Path.GetFileNameWithoutExtension(assemblyFileName); + var directoryName = Path.GetDirectoryName(assemblyFileName) ?? ""; + return LoadFile(Path.Combine(directoryName, $"{assemblyName}.xunit.multinode.runner.json")) + ?? LoadFile(Path.Combine(directoryName, "xunit.multinode.runner.json")) + ?? MultiNodeTestRunnerOptions.Default; + } + + private static MultiNodeTestRunnerOptions LoadFile(string configFileName) + { + try + { + if(!File.Exists(configFileName)) + { + return null; + } + using var stream = File.OpenRead(configFileName); + return Load(stream); + } + catch + { } + return null; + } + + private static MultiNodeTestRunnerOptions Load(Stream configStream) + { + var result = new MultiNodeTestRunnerOptions(); + try + { + using (var reader = new StreamReader(configStream)) + { + var config = (JsonObject) JsonDeserializer.Deserialize(reader); + foreach (var propertyName in config.Keys) + { + var propertyValue = config.Value(propertyName); + + switch (propertyValue) + { + case JsonBoolean booleanValue: + if (string.Equals(propertyName, Configuration.AppendLogOutput, StringComparison.OrdinalIgnoreCase)) + result.AppendLogOutput = booleanValue.Value; + if (string.Equals(propertyName, Configuration.UseBuiltInTrxReporter, StringComparison.OrdinalIgnoreCase)) + result.UseBuiltInTrxReporter = booleanValue.Value; + break; + + case JsonString stringValue: + if (string.Equals(propertyName, Configuration.OutputDirectory, StringComparison.OrdinalIgnoreCase)) + result.OutputDirectory = stringValue.Value; + if (string.Equals(propertyName, Configuration.FailedSpecsDirectory, StringComparison.OrdinalIgnoreCase)) + result.FailedSpecsDirectory = stringValue.Value; + if(string.Equals(propertyName, Configuration.ListenAddress, StringComparison.OrdinalIgnoreCase)) + result.ListenAddress = stringValue.Value; + break; + + case JsonNumber numberValue when string.Equals(propertyName, Configuration.ListenPort, StringComparison.OrdinalIgnoreCase): + int.TryParse(numberValue.Raw, out var port); + if(port != 0) + result.ListenPort = port; + break; + } + } + } + } + catch { } + + return result; + } + + static class Configuration + { + public const string OutputDirectory = "outputDirectory"; + public const string FailedSpecsDirectory = "failedSpecsDirectory"; + public const string ListenAddress = "listenAddress"; + public const string ListenPort = "listenPort"; + public const string AppendLogOutput = "appendLogOutput"; + public const string UseBuiltInTrxReporter = "useBuiltInTrxReporter"; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Constants.cs b/src/core/Akka.MultiNode.TestAdapter/Constants.cs new file mode 100644 index 00000000000..2dd8fb93b86 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Constants.cs @@ -0,0 +1,17 @@ +using System; + +namespace Akka.MultiNode.TestAdapter +{ + /// + /// ExecutorMetadata + /// + public static class Constants + { + /// + /// Executor URI used by this test adapter + /// + public const string ExecutorUriString = "executor://MultiNodeExecutor"; + + public static readonly Uri ExecutorUri = new Uri(ExecutorUriString); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Helpers/RuntimeConfigGenerator.cs b/src/core/Akka.MultiNode.TestAdapter/Helpers/RuntimeConfigGenerator.cs new file mode 100644 index 00000000000..3231c0684a4 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Helpers/RuntimeConfigGenerator.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; +using System.Runtime.Versioning; +using System.Text.RegularExpressions; + +namespace Akka.MultiNode.TestAdapter.Helpers +{ + /// + /// RuntimeConfigGenerator + /// + internal static class RuntimeConfigGenerator + { + /// + /// Generates .NET Core runtimeconfig.json content for current target framework and runtime + /// + public static string GetRuntimeConfigContent(Assembly assembly) + { + var version = Environment.Version.ToString(); // Something like 3.1.1 + var framework = assembly.GetCustomAttribute(); + var frameworkName = framework?.FrameworkName ?? ".NETCoreApp,Version=v3.0"; // Something like .NETCoreApp,Version=v3.0 + var majorFrameworkVersion = Regex.Match(frameworkName, @"\d\.\d").Value; + + return $@" +{{ + ""runtimeOptions"": {{ + ""tfm"": ""netcoreapp{majorFrameworkVersion}"", + ""framework"": {{ + ""name"": ""Microsoft.NETCore.App"", + ""version"": ""{version}"" + }} + }} +}}"; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Helpers/SocketUtil.cs b/src/core/Akka.MultiNode.TestAdapter/Helpers/SocketUtil.cs new file mode 100644 index 00000000000..a246473794e --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Helpers/SocketUtil.cs @@ -0,0 +1,43 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Linq; +using System.Net; +using System.Net.Sockets; +using Akka.Configuration; + +namespace Akka.MultiNode.TestAdapter.Helpers +{ + internal static class SocketUtil + { + public static IPEndPoint TemporaryTcpAddress(string hostName) + { + if (!IPAddress.TryParse(hostName, out var address)) + { + // If hostName isn't an IP, its probably a dns address, try to resolve it. + var addresses = Dns.GetHostAddresses(hostName); + address = + addresses.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork) ?? + addresses.FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetworkV6) ?? + throw new ConfigurationException( + $"Failed to look up IPv4 or IPv6 address for host name [{hostName}]"); + } + + return TemporaryTcpAddress(address); + } + + public static IPEndPoint TemporaryTcpAddress(IPAddress address) + { + using (var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)) + { + var endpoint = new IPEndPoint(address, 0); + socket.Bind(endpoint); + return (IPEndPoint) socket.LocalEndPoint; + } + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/CollectionPerSessionTestCollectionFactory.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/CollectionPerSessionTestCollectionFactory.cs new file mode 100644 index 00000000000..2af87fd7084 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/CollectionPerSessionTestCollectionFactory.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + internal class CollectionPerSessionTestCollectionFactory : IXunitTestCollectionFactory + { + private readonly Dictionary _collectionCache = + new Dictionary(); + + public ITestCollection Get(ITypeInfo testClass) + { + if (_collectionCache.TryGetValue(testClass.Assembly, out var collection)) + return collection; + + collection = new TestCollection( + new TestAssembly(testClass.Assembly), + null, + $"MultiNode test collection for {Path.GetFileName(testClass.Assembly.AssemblyPath)}"); + _collectionCache[testClass.Assembly] = collection; + return collection; + } + + public string DisplayName => "collection-per-session"; + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/CompilerErrorCollection.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/CompilerErrorCollection.cs new file mode 100644 index 00000000000..f50cf89b363 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/CompilerErrorCollection.cs @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + public class CompilerErrorCollection : List + { + } + + public class CompilerError + { + public string ErrorText { get; set; } + public bool IsWarning { get; set; } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Discovery.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Discovery.cs new file mode 100644 index 00000000000..b4ad7760ace --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Discovery.cs @@ -0,0 +1,71 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Xunit.Abstractions; +using Xunit.Sdk; +using LongLivedMarshalByRefObject = Xunit.LongLivedMarshalByRefObject; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + internal class Discovery : LongLivedMarshalByRefObject, IMessageSink, IDisposable + { + // There can be multiple fact attributes in a single class, but our convention + // limits them to 1 fact attribute per test class + public List TestCases { get; } + public List Errors { get; } = new List(); + public bool WasSuccessful => Errors.Count == 0; + + private readonly string _assemblyPath; + + /// + /// Initializes a new instance of the class. + /// + public Discovery(string assemblyPath) + { + _assemblyPath = assemblyPath; + TestCases = new List(); + Finished = new ManualResetEvent(false); + } + + public ManualResetEvent Finished { get; } + + public virtual bool OnMessage(IMessageSinkMessage message) + { + switch (message) + { + case ITestCaseDiscoveryMessage discovery: + var testClass = discovery.TestClass.Class; + if (testClass.IsAbstract) + break; + + foreach (var c in discovery.TestCases.Where(t => t is MultiNodeTestCase).Cast()) + { + TestCases.Add(c); + } + break; + case IDiscoveryCompleteMessage _: + Finished.Set(); + break; + case ErrorMessage err: + Errors.Add(err); + break; + } + + return true; + } + + /// + public void Dispose() + { + Finished.Dispose(); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Exceptions.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Exceptions.cs new file mode 100644 index 00000000000..3d9b9b65c9b --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Exceptions.cs @@ -0,0 +1,60 @@ +using System; +using Akka.Remote.TestKit; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + public class TestConfigurationException: Exception + { + public TestConfigurationException(Type type) + : base( + $"[{type}] or one of its base classes must specify constructor, " + + $"which first parameter is a subclass of {typeof(MultiNodeConfig)}") + { } + + public TestConfigurationException(Type type, Exception innerException) + : base( + $"[{type}] or one of its base classes must specify constructor, " + + $"which first parameter is a subclass of {typeof(MultiNodeConfig)}", + innerException) + { } + } + + public class TestConfigurationConstructorException: Exception + { + public TestConfigurationConstructorException(Type type) + : base($"[{type}] constructor, which is a subclass of {typeof(MultiNodeConfig)}, throws an exception") + { } + + public TestConfigurationConstructorException(Type type, Exception innerException) + : base( + $"[{type}] constructor, which is a subclass of {typeof(MultiNodeConfig)}, throws an exception", + innerException) + { } + } + + public class TestBaseTypeException: Exception + { + public TestBaseTypeException() + : base($"MultiNode.TestRunner spec should inherit from {typeof(MultiNodeSpec).FullName}") + { } + } + + internal class TestFailedException : Exception + { + private readonly string _stackTrace; + + public TestFailedException(string type, string message, string stacktrace):base($"Original exception: [{type}: {message}]") + { + _stackTrace = stacktrace; + } + + public TestFailedException(string type, string message, string stacktrace, Exception innerException) + : base($"Original exception: [{type}: {message}]", innerException) + { + _stackTrace = stacktrace; + } + + public override string StackTrace => _stackTrace ?? base.StackTrace; + } + +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/ExitCodeContainer.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/ExitCodeContainer.cs new file mode 100644 index 00000000000..ba3d7880ffc --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/ExitCodeContainer.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.MultiNode.TestAdapter.Internal.Sinks; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + /// + /// Global state for hanging onto the exit code used by the process. + /// + /// The sets this value once during shutdown. + /// + public static class ExitCodeContainer + { + public static int ExitCode = 0; + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Extensions/DateTimeExtension.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Extensions/DateTimeExtension.cs new file mode 100644 index 00000000000..6a2ab75e7cc --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Extensions/DateTimeExtension.cs @@ -0,0 +1,19 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; + +namespace Akka.MultiNode.TestAdapter.Internal.Extensions +{ + internal static class DateTimeExtension + { + public static string ToShortTimeString(this DateTime dateTime) + { + return dateTime.ToString("d"); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Extensions/TypeExtension.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Extensions/TypeExtension.cs new file mode 100644 index 00000000000..1c94fb723f2 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Extensions/TypeExtension.cs @@ -0,0 +1,20 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Reflection; + +namespace Akka.MultiNode.TestAdapter.Internal.Extensions +{ + internal static class TypeExtension + { + public static MethodInfo GetMethod(this Type type, string method, params Type[] parameters) + { + return type.GetRuntimeMethod(method, parameters); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestAssemblyRunner.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestAssemblyRunner.cs new file mode 100644 index 00000000000..69353ccbc4f --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestAssemblyRunner.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + internal class MultiNodeTestAssemblyRunner : XunitTestAssemblyRunner + { + public MultiNodeTestAssemblyRunner( + ITestAssembly testAssembly, + IEnumerable testCases, + IMessageSink diagnosticMessageSink, + IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) + : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions) + { + } + + protected override async Task RunTestCollectionsAsync(IMessageBus messageBus, CancellationTokenSource cancellationTokenSource) + { + var summary = new RunSummary(); + + foreach (var (testCollection, testCases) in OrderTestCollections()) + { + summary.Aggregate(await RunTestCollectionAsync(messageBus, testCollection, testCases, cancellationTokenSource)); + if (cancellationTokenSource.IsCancellationRequested) + break; + } + + return summary; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestCase.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestCase.cs new file mode 100644 index 00000000000..98679a6f394 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestCase.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Akka.Remote.TestKit; +using Xunit.Abstractions; +using Xunit.Sdk; +using TestMethodDisplay = Xunit.Sdk.TestMethodDisplay; +using TestMethodDisplayOptions = Xunit.Sdk.TestMethodDisplayOptions; + +#nullable enable +namespace Akka.MultiNode.TestAdapter.Internal +{ + public class MultiNodeTestCase : XunitTestCase + { + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public MultiNodeTestCase() { } + + public MultiNodeTestCase( + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod, + object[]? testMethodArguments = null) + : base( + diagnosticMessageSink, + defaultMethodDisplay, + defaultMethodDisplayOptions, + testMethod, + testMethodArguments) + { } + + public virtual string? AssemblyPath { get; protected set; } + public virtual string TypeName => TestMethod.TestClass.Class.Name; + public virtual string MethodName => TestMethod.Method.Name; + + protected List? InternalNodes; + + /// Spec did not inherit from + /// Invalid configuration class + public List Nodes + { + get + { + EnsureInitialized(); + return InternalNodes ?? new List(); + } + } + + private string? _skipReason; + public new string? SkipReason + { + get => _skipReason ?? base.SkipReason; + set => _skipReason = value; + } + + public bool InExecutionMode { get; set; } + + protected override void Initialize() + { + base.Initialize(); + try + { + AssemblyPath = Path.GetFullPath(TestMethod.TestClass.Class.Assembly.AssemblyPath); + InternalNodes = LoadDetails(); + } + catch (Exception e) + { + SkipReason = e.ToString(); + InitializationException = e; + DisplayName = $"{BaseDisplayName}(???)"; + } + } + + internal void Load() + { + EnsureInitialized(); + } + + public override Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + if (!InExecutionMode) + { + return new MultiNodeTestCaseRunner(this, DisplayName, SkipReason, messageBus, diagnosticMessageSink, + aggregator, cancellationTokenSource).RunAsync(); + } + return new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource).RunAsync(); + } + + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + data.AddValue(nameof(AssemblyPath), AssemblyPath); + data.AddValue(nameof(_skipReason), _skipReason); + } + + public override void Deserialize(IXunitSerializationInfo data) + { + base.Deserialize(data); + AssemblyPath = data.GetValue(nameof(AssemblyPath)); + _skipReason = data.GetValue(nameof(_skipReason)); + } + + /// Spec did not inherit from + /// Invalid configuration class + protected virtual List LoadDetails() + { + var specType = TestMethod.TestClass.Class.Assembly.GetType(TypeName).ToRuntimeType(); + if (!typeof(MultiNodeSpec).IsAssignableFrom(specType)) + { + throw new TestBaseTypeException(); + } + + try + { + var roles = RoleNames(specType); + return roles.Select((r, i) => new NodeTest(this, i + 1, r.Name)).ToList(); + } + catch (Exception e) + { + SkipReason = e.ToString(); + return new List + { + new ErrorTest(this) + }; + } + } + + private IEnumerable RoleNames(Type specType) + { + var ctorWithConfig = FindConfigConstructor(specType); + try + { + var configType = ctorWithConfig.GetParameters().First().ParameterType; + var args = ConfigConstructorParamValues(configType); + var configInstance = (MultiNodeConfig) Activator.CreateInstance(configType, args); + return configInstance.Roles; + } + catch (Exception e) + { + throw new TestConfigurationConstructorException(specType, e); + } + } + + internal static ConstructorInfo FindConfigConstructor(Type configUser) + { + var baseConfigType = typeof(MultiNodeConfig); + var current = configUser; + while (current != null) + { + var ctorWithConfig = current + .GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(c => null != c.GetParameters().FirstOrDefault(p => p.ParameterType.GetTypeInfo().IsSubclassOf(baseConfigType))); + + current = current.GetTypeInfo().BaseType; + if (ctorWithConfig != null) return ctorWithConfig; + } + + throw new TestConfigurationException(configUser); + } + + private object?[] ConfigConstructorParamValues(Type configType) + { + var ctors = configType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); + var empty = ctors.FirstOrDefault(c => !c.GetParameters().Any()); + + return empty != null + ? Array.Empty() + : ctors.First().GetParameters().Select(p => p.ParameterType.GetTypeInfo().IsValueType ? Activator.CreateInstance(p.ParameterType) : null).ToArray(); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestCaseRunner.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestCaseRunner.cs new file mode 100644 index 00000000000..89f1ad9afbb --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestCaseRunner.cs @@ -0,0 +1,278 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Configuration; +using Akka.IO; +using Akka.MultiNode.TestAdapter.Internal.Persistence; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Akka.MultiNode.TestAdapter.Internal.TrxReporter; +using Akka.MultiNode.TestAdapter.Configuration; +using Akka.MultiNode.TestAdapter.Helpers; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + /// + /// Entry point for the MultiNodeTestRunner + /// + internal class MultiNodeTestCaseRunner : TestCaseRunner + { + // Fixed TCP buffer size + public const int TcpBufferSize = 10240; + + private ActorSystem TestRunSystem { get; set; } + private IActorRef SinkCoordinator { get; set; } + private MultiNodeTestRunnerOptions Options { get; } + + /// + /// Gets or sets the display name of the test case + /// + private string DisplayName { get; } + + /// + /// Gets or sets the skip reason for the test, if set. + /// + private string SkipReason { get; } + + /// + /// Gets or sets the runtime type for the test class that the test method belongs to. + /// + private Type TestClass { get; } + + /// + /// Gets of sets the runtime method for the test method that the test case belongs to. + /// + private MethodInfo TestMethod { get; } + + private readonly Xunit.Abstractions.IMessageSink _diagnosticSink; + + public MultiNodeTestCaseRunner( + MultiNodeTestCase testCase, + string displayName, + string skipReason, + IMessageBus messageBus, + Xunit.Abstractions.IMessageSink diagnosticSink, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + : base(testCase, messageBus, aggregator, cancellationTokenSource) + { + _diagnosticSink = diagnosticSink; + DisplayName = displayName; + SkipReason = skipReason; + + TestClass = TestCase.TestMethod.TestClass.Class.ToRuntimeType(); + TestMethod = TestCase.Method.ToRuntimeMethod(); + + var assembly = TestClass.Assembly; + var attr = assembly.GetCustomAttribute(); + var frameworkParts = attr.FrameworkName.Split(','); + var versionParts = frameworkParts[1].Split('='); + var platformName = (frameworkParts[0].Replace(".", "") + versionParts[1].Replace("v", "").Replace(".", "_")).ToLowerInvariant(); + Options = OptionsReader.Load(testCase.AssemblyPath); + Options.Platform = platformName; + + if (Options.ListenPort == 0) + Options.ListenPort = SocketUtil.TemporaryTcpAddress(Options.ListenIpAddress).Port; + } + + protected override async Task RunTestAsync() + { + // Shortcut the spec if it is skipped + if (!string.IsNullOrEmpty(SkipReason)) + { + foreach (var test in TestCase.Nodes) + { + MessageBus.QueueMessage(new TestStarting(test)); + MessageBus.QueueMessage(new TestSkipped(test, SkipReason)); + } + + return new RunSummary + { + Total = TestCase.Nodes.Count, + Skipped = TestCase.Nodes.Count + }; + } + + // Shortcut the spec if it already failed + if (Aggregator.HasExceptions) + { + var exception = Aggregator.ToException(); + foreach (var test in TestCase.Nodes) + { + MessageBus.QueueMessage(new TestStarting(test)); + MessageBus.QueueMessage(new TestFailed(test, 0, "Test failed before being executed", exception)); + } + + return new RunSummary + { + Total = TestCase.Nodes.Count, + Failed = TestCase.Nodes.Count + }; + } + + // Run the actual spec + var config = ConfigurationFactory.ParseString($@" +akka.io.tcp {{ + buffer-pool = ""akka.io.tcp.disabled-buffer-pool"" + disabled-buffer-pool.buffer-size = {TcpBufferSize} +}} +"); + TestRunSystem = ActorSystem.Create("TestRunnerLogging", config); + + var sinks = new List + { + new DiagnosticMessageSink(_diagnosticSink) + }; + if(Options.UseBuiltInTrxReporter) + sinks.Add(new TrxMessageSink(DisplayName, Options)); + + SinkCoordinator = TestRunSystem.ActorOf(Props.Create(() + => new SinkCoordinator(sinks)), "sinkCoordinator"); + + await SinkCoordinator.Ask(Sinks.SinkCoordinator.Ready.Instance); + + var tcpLogger = TestRunSystem.ActorOf(Props.Create(() => new TcpLoggingServer(SinkCoordinator)), "TcpLogger"); + var listenEndpoint = new IPEndPoint(IPAddress.Parse(Options.ListenAddress), Options.ListenPort); + TestRunSystem.Tcp().Tell(new Tcp.Bind(tcpLogger, listenEndpoint), sender: tcpLogger); + + StartNewSpec(); + PublishRunnerMessage($"Starting test {TestCase.DisplayName}"); + + var timelineCollector = TestRunSystem.ActorOf(Props.Create(() => new TimelineLogCollectorActor(Options.AppendLogOutput))); + + var tasks = new List>(); + var serverPort = SocketUtil.TemporaryTcpAddress("localhost").Port; + foreach (var nodeTest in TestCase.Nodes) + { + //Loop through each test, work out number of nodes to run on and kick off process + var args = new [] + { + $@"-Dmultinode.test-class=""{nodeTest.TestCase.TypeName}""", + $@"-Dmultinode.test-method=""{nodeTest.TestCase.MethodName}""", + $@"-Dmultinode.max-nodes={TestCase.Nodes.Count}", + $@"-Dmultinode.server-host=""{"localhost"}""", + $@"-Dmultinode.server-port={serverPort}", + $@"-Dmultinode.host=""{"localhost"}""", + $@"-Dmultinode.index={nodeTest.Node - 1}", + $@"-Dmultinode.role=""{nodeTest.Role}""", + $@"-Dmultinode.listen-address={Options.ListenAddress}", + $@"-Dmultinode.listen-port={Options.ListenPort}", + $@"-Dmultinode.test-assembly=""{TestCase.AssemblyPath}""" + }; + + // Start process for node + var runner = new MultiNodeTestRunner( + nodeTest, MessageBus, args, SkipReason, Aggregator, SinkCoordinator, + timelineCollector, Options, CancellationTokenSource); + + tasks.Add(runner.RunAsync()); + } + + var summary = new RunSummary(); + // Wait for all nodes to finish and collect results + while (tasks.Count > 0) + { + // TODO: might be a bug source if await throws + var finished = await Task.WhenAny(tasks); + tasks.Remove(finished); + summary.Aggregate(finished.Result); + } + + try + { + // Limit TCP logger unbind to 10 seconds, abort the test if failed. + await tcpLogger.Ask( + new TcpLoggingServer.StopListener(), + TimeSpan.FromSeconds(10)); + } + catch + { + CancellationTokenSource.Cancel(); + } + + // Save timelined logs to file system + await DumpAggregatedSpecLogs(summary, timelineCollector); + + await FinishSpec(timelineCollector); + + SinkCoordinator.Tell(new SinkCoordinator.CloseAllSinks()); + + // Block until all Sinks have been terminated. + var cts2 = new CancellationTokenSource(); + try + { + // Limit test ActorSystem shutdown to 5 seconds, abort the test if failed + var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5), cts2.Token); + var shutdownTask = TestRunSystem.WhenTerminated; + var task = await Task.WhenAny(timeoutTask, shutdownTask); + if(task != timeoutTask) + cts2.Cancel(); + else + CancellationTokenSource.Cancel(); + } + finally + { + cts2.Dispose(); + } + + return summary; + } + + private async Task DumpAggregatedSpecLogs(RunSummary summary, IActorRef timelineCollector) + { + var dumpFolder = Path.GetFullPath(Path.Combine(Options.OutputDirectory, TestCase.DisplayName)); + var dumpPath = Path.Combine(dumpFolder, "aggregated.txt"); + + Directory.CreateDirectory(dumpFolder); + if (!Options.AppendLogOutput && File.Exists(dumpPath)) + File.Delete(dumpPath); + + var logLines = await timelineCollector.Ask(new TimelineLogCollectorActor.GetLog()); + + // Dump aggregated timeline to file for this test + File.AppendAllLines(dumpPath, logLines); + + if (summary.Failed > 0) + { + var failedSpecFolder = Path.GetFullPath(Path.Combine(Options.OutputDirectory, Options.FailedSpecsDirectory)); + var failedSpecPath = Path.Combine(failedSpecFolder, $"{TestCase.DisplayName}.txt"); + + Directory.CreateDirectory(failedSpecFolder); + if(!Options.AppendLogOutput && File.Exists(failedSpecPath)) + File.Delete(failedSpecPath); + + File.AppendAllLines(failedSpecPath, logLines); + } + } + + private void StartNewSpec() + { + SinkCoordinator.Tell(TestCase); + } + + private async Task FinishSpec(IActorRef timelineCollector) + { + var log = await timelineCollector.Ask(new TimelineLogCollectorActor.GetSpecLog(), TimeSpan.FromMinutes(1)); + SinkCoordinator.Tell(new EndSpec(TestCase, log)); + } + + private void PublishRunnerMessage(string message) + { + SinkCoordinator.Tell(new SinkCoordinator.RunnerMessage(message)); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestFrameworkExecutor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestFrameworkExecutor.cs new file mode 100644 index 00000000000..9083e232e9d --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestFrameworkExecutor.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Reflection; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + internal class MultiNodeTestFrameworkExecutor : XunitTestFrameworkExecutor + { + public MultiNodeTestFrameworkExecutor( + AssemblyName assemblyName, + ISourceInformationProvider sourceInformationProvider, + IMessageSink diagnosticMessageSink) + : base(assemblyName, sourceInformationProvider, diagnosticMessageSink) + { + } + + protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, + ITestFrameworkExecutionOptions executionOptions) + { + using (var assemblyRunner = new MultiNodeTestAssemblyRunner(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions)) + await assemblyRunner.RunAsync(); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestRunner.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestRunner.cs new file mode 100644 index 00000000000..5e6b0fad871 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/MultiNodeTestRunner.cs @@ -0,0 +1,253 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Configuration; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Akka.MultiNode.TestAdapter.NodeRunner; +using Xunit.Sdk; +using TestFailed = Xunit.Sdk.TestFailed; +using TestFinished = Xunit.Sdk.TestFinished; +using TestPassed = Xunit.Sdk.TestPassed; +using TestResultMessage = Xunit.Sdk.TestResultMessage; +using TestSkipped = Xunit.Sdk.TestSkipped; +using TestStarting = Xunit.Sdk.TestStarting; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + internal class MultiNodeTestRunner + { + public MultiNodeTestRunner( + NodeTest test, + IMessageBus messageBus, + string[] remoteArguments, + string skipReason, + ExceptionAggregator aggregator, + IActorRef sinkCoordinator, + IActorRef timelineCollector, + MultiNodeTestRunnerOptions options, + CancellationTokenSource cancellationTokenSource) + { + _test = test; + _messageBus = messageBus; + _remoteArguments = remoteArguments; + _aggregator = aggregator; + _sinkCoordinator = sinkCoordinator; + _timelineCollector = timelineCollector; + _options = options; + _cancellationTokenSource = cancellationTokenSource; + _skipReason = skipReason; + } + + private readonly MultiNodeTestRunnerOptions _options; + private readonly IActorRef _sinkCoordinator; + private readonly IActorRef _timelineCollector; + + private readonly string _skipReason; + + /// + /// Gets or sets the exception aggregator used to run code and collect exceptions. + /// + private readonly ExceptionAggregator _aggregator; + + /// + /// Gets or sets the task cancellation token source, used to cancel the test run. + /// + private readonly CancellationTokenSource _cancellationTokenSource; + + /// + /// Gets or sets the constructor arguments used to construct the test class. + /// + private readonly string[] _remoteArguments; + + /// + /// Gets or sets the display name of the invoked test. + /// + private string DisplayName => _test.DisplayName; + + /// + /// Gets or sets the message bus to report run status to. + /// + private readonly IMessageBus _messageBus; + + /// + /// Gets or sets the test to be run. + /// + private readonly NodeTest _test; + + /// + /// Gets the test case to be run. + /// + private MultiNodeTestCase TestCase => _test.TestCase; + + private readonly StringBuilder _outputBuilder = new StringBuilder(); + private string Output => _outputBuilder.ToString(); + + private readonly List _exceptionType = new List(); + private readonly List _exceptionMessage = new List(); + private readonly List _exceptionStacktrace = new List(); + + /// + /// Runs the test. + /// + /// Returns summary information about the test that was run. + public async Task RunAsync() + { + var summary = new RunSummary { Total = 1 }; + + _messageBus.QueueMessage(new TestStarting(_test)); + var aggregator = new ExceptionAggregator(_aggregator); + var returnCode = -1; + + if (!aggregator.HasExceptions) + { + await aggregator.RunAsync(async () => + { + var stopwatch = Stopwatch.StartNew(); + try + { + returnCode = await RunNode(); + } + finally + { + stopwatch.Stop(); + summary.Time = (decimal)stopwatch.Elapsed.TotalSeconds; + } + }); + } + + TestResultMessage testResult; + var exception = aggregator.ToException(); + if (exception == null) + { + switch (returnCode) + { + case 0: + testResult = new TestPassed(_test, summary.Time, Output); + break; + default: + summary.Failed++; + + testResult = new TestFailed( + test: _test, + executionTime: summary.Time, + output: Output, + exceptionTypes: _exceptionType.ToArray(), + messages: _exceptionMessage.ToArray(), + stackTraces: _exceptionStacktrace.ToArray(), + exceptionParentIndices: Enumerable.Range(0, _exceptionType.Count).ToArray()); + break; + } + } + else + { + testResult = new TestFailed(_test, summary.Time, Output, exception); + summary.Failed++; + } + + _messageBus.QueueMessage(testResult); + var specFolder = Directory.CreateDirectory(Path.Combine(_options.OutputDirectory, TestCase.DisplayName)); + var logFilePath = Path.GetFullPath(Path.Combine(specFolder.FullName, $"node{_test.Node}__{_test.Role}__{_options.Platform}.txt")); + bool dumpSuccess; + do + { + try + { + if(!_options.AppendLogOutput && File.Exists(logFilePath)) + File.Delete(logFilePath); + + File.AppendAllText(logFilePath, Output); + dumpSuccess = true; + } + catch + { + dumpSuccess = false; + } + } while (!dumpSuccess); + + _messageBus.QueueMessage(new TestFinished(_test, summary.Time, Output)); + + return summary; + } + + private void ExtractExceptionData(string data) + { + if (data.Contains("[FAIL-EXCEPTION]")) + { + var index = data.IndexOf("[FAIL-EXCEPTION] Type: ", StringComparison.OrdinalIgnoreCase); + if(index != -1) + { + _exceptionType.Add(data.Substring(index + 23)); + return; + } + + index = data.IndexOf("[FAIL-EXCEPTION] Message: ", StringComparison.OrdinalIgnoreCase); + if(index != -1) + { + _exceptionMessage.Add(data.Substring(index + 26)); + return; + } + + index = data.IndexOf("[FAIL-EXCEPTION] StackTrace: ", StringComparison.OrdinalIgnoreCase); + if(index != -1) + { + _exceptionStacktrace.Add(data.Substring(index + 29)); + } + } + } + + private async Task RunNode() + { + var nodeInfo = new TimelineLogCollectorActor.NodeInfo(_test.Node, _test.Role, _options.Platform, TestCase.DisplayName); + + void OutputHandler(object sender, DataReceivedEventArgs eventArgs) + { + if (eventArgs?.Data != null) + { + var data = eventArgs.Data; + _outputBuilder.AppendLine(data); + _messageBus.QueueMessage(new TestOutput(_test, data + Environment.NewLine)); + _timelineCollector.Tell(new TimelineLogCollectorActor.LogMessage(nodeInfo, data)); + + ExtractExceptionData(data); + } + } + + var exitCode = -1; + var (process, task) = RemoteHost.RemoteHost.RunProcessAsync(new Executor().Execute, _remoteArguments, opt => + { + opt.OnExit = p => + { + exitCode = p.ExitCode; + if (p.ExitCode == 0) + { + _sinkCoordinator.Tell(new NodeCompletedSpecWithSuccess(_test.Node, _test.Role, _test.DisplayName + " passed.")); + } + else + { + _sinkCoordinator.Tell(new NodeCompletedSpecWithFail(_test.Node, _test.Role, _test.DisplayName + " passed.")); + } + }; + opt.OutputDataReceived = OutputHandler; + opt.ErrorDataReceived = OutputHandler; + }, _cancellationTokenSource.Token); + + _sinkCoordinator.Tell(new SinkCoordinator.RunnerMessage($"Started node {_test.Node} : {_test.Role} on pid {process.Id}")); + + await task; + return exitCode; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/NodeTest.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/NodeTest.cs new file mode 100644 index 00000000000..feb61fd5d26 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/NodeTest.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using Xunit.Abstractions; +using Xunit.Sdk; +using LongLivedMarshalByRefObject = Xunit.LongLivedMarshalByRefObject; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + public class NodeTest : LongLivedMarshalByRefObject, ITest + { + public NodeTest(MultiNodeTestCase testCase, int node, string role) + { + TestCase = testCase; + Node = node; + Role = role; + } + + public int Node { get; } + public string Role { get; } + public virtual string DisplayName => $"Node {Node} [{Role}]"; + public MultiNodeTestCase TestCase { get; } + ITestCase ITest.TestCase => TestCase; + public string Name => $"{TestCase.DisplayName}_node{Node}[{Role}]"; + } + + internal class ErrorTest : NodeTest + { + public ErrorTest(MultiNodeTestCase testCase) : base(testCase, 0, "") + { + } + + public override string DisplayName => "ERRORED"; + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/EnumerableExtensions.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/EnumerableExtensions.cs new file mode 100644 index 00000000000..1742d117606 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/EnumerableExtensions.cs @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + internal static class EnumerableExtensions + { + public static IEnumerable Concat(this IEnumerable source, T item) + { + foreach (var cur in source) + { + yield return cur; + } + yield return item; + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/FileNameGenerator.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/FileNameGenerator.cs new file mode 100644 index 00000000000..ee33ccfed5e --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/FileNameGenerator.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.IO; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + internal static class FileNameGenerator + { + public static string GenerateFileName(string assemblyName, string platform, string fileExtension) + { + return GenerateFileName(assemblyName, platform, fileExtension, DateTime.UtcNow); + } + + public static string GenerateFileName(string assemblyName, string platform, string fileExtension, DateTime utcNow) + { + return $"{assemblyName.Replace(".dll", "")}-{platform}{fileExtension}"; + } + + public static string GenerateFileName(string folderPath, string assemblyName, string platform, string fileExtension, DateTime utcNow) + { + if(string.IsNullOrEmpty(folderPath)) + return GenerateFileName(assemblyName, platform, fileExtension, utcNow); + var assemblyNameOnly = Path.GetFileName(assemblyName); + return Path.Combine(folderPath, GenerateFileName(assemblyNameOnly, platform, fileExtension, utcNow)); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/IPersistentTestRunStore.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/IPersistentTestRunStore.cs new file mode 100644 index 00000000000..f9562603a5e --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/IPersistentTestRunStore.cs @@ -0,0 +1,21 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + /// + /// Persistent store for saving instances + /// from disk. + /// + internal interface IPersistentTestRunStore + { + bool SaveTestRun(string filePath, TestRunTree data); + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/IRetrievableTestRunStore.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/IRetrievableTestRunStore.cs new file mode 100644 index 00000000000..77b456b462e --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/IRetrievableTestRunStore.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + /// + /// Persistent store for retrieving instances + /// from disk. + /// + internal interface IRetrievableTestRunStore :IPersistentTestRunStore + { + bool TestRunExists(string filePath); + + TestRunTree FetchTestRun(string filePath); + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/JsonPersistentTestRunStore.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/JsonPersistentTestRunStore.cs new file mode 100644 index 00000000000..89a62c49b9e --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/JsonPersistentTestRunStore.cs @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + /// + /// JavaScript Object Notation (JSON) implementation of the + /// + internal class JsonPersistentTestRunStore : IRetrievableTestRunStore + { + //Internal version of the contract resolver + private class AkkaContractResolver : DefaultContractResolver + { + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var prop = base.CreateProperty(member, memberSerialization); + + if (!prop.Writable) + { + var property = member as PropertyInfo; + if (property != null) + { + var hasPrivateSetter = property.GetSetMethod(true) != null; + prop.Writable = hasPrivateSetter; + } + } + + return prop; + } + } + + public static JsonSerializerSettings Settings = new JsonSerializerSettings + { + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + DefaultValueHandling = DefaultValueHandling.Ignore, + MissingMemberHandling = MissingMemberHandling.Ignore, + ObjectCreationHandling = ObjectCreationHandling.Replace, + //important: if reuse, the serializer will overwrite properties in default references, e.g. Props.DefaultDeploy or Props.noArgs + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + TypeNameHandling = TypeNameHandling.All, + ContractResolver = new AkkaContractResolver() + { + //SerializeCompilerGeneratedMembers = true, + //IgnoreSerializableAttribute = true, + //IgnoreSerializableInterface = true, + } + }; + + public bool SaveTestRun(string filePath, TestRunTree data) + { + if (data == null) throw new ArgumentNullException("data"); + if (string.IsNullOrEmpty(filePath)) throw new ArgumentException("filePath must not be null or empty"); + + +// ReSharper disable once AssignNullToNotNullAttribute //already made this null check with Guard + var finalPath = Path.GetFullPath(filePath); + var serializedObj = JsonConvert.SerializeObject(data, Formatting.Indented, Settings); + +// ReSharper disable once AssignNullToNotNullAttribute + File.WriteAllText(finalPath, serializedObj, Encoding.UTF8); + + return true; + } + + public bool TestRunExists(string filePath) + { + return !string.IsNullOrEmpty(filePath) && File.Exists(Path.GetFullPath(filePath)); + } + + public TestRunTree FetchTestRun(string filePath) + { + if (string.IsNullOrEmpty(filePath)) throw new ArgumentException("filePath must not be null or empty"); + // ReSharper disable once AssignNullToNotNullAttribute //already made this null check with Guard + var finalPath = Path.GetFullPath(filePath); + var fileText = File.ReadAllText(finalPath, Encoding.UTF8); + if (string.IsNullOrEmpty(fileText)) return null; + return JsonConvert.DeserializeObject(fileText, Settings); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/TimelineItem.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/TimelineItem.cs new file mode 100644 index 00000000000..f1792b60d31 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/TimelineItem.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + internal class TimelineItem + { + private const string EventFormat = "{{ className:'{0}', content:'{1}', start:'{2}', group:{3}, title:'{4}' }}"; + + public TimelineItem(string cssClass, string content, string title, DateTime dateTime, int groupId) + { + Classname = cssClass; + Content = content; + Start = dateTime; + GroupId = groupId; + Title = title; + } + + public string Classname { get; private set; } + + public string Content { get; private set; } + + public string Title { get; private set; } + + public DateTime Start { get; private set; } + + public int GroupId { get; private set; } + + public string ToJavascriptString() + { + return string.Format(EventFormat, Classname, Content, Start.ToString("o"), GroupId, Title); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/TimelineItemFactory.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/TimelineItemFactory.cs new file mode 100644 index 00000000000..85ee1ffe728 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/TimelineItemFactory.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + internal static class TimelineItemFactory + { + private static readonly string[] CssClasses = + { + "vis-item-one", + "vis-item-two", + "vis-item-three", + "vis-item-four", + "vis-item-five", + "vis-item-six", + "vis-item-seven", + "vis-item-eight", + "vis-item-nine", + "vis-item-ten", + "vis-item-eleven", + "vis-item-twelve", + "vis-item-thirteen", + "vis-item-fourteen", + "vis-item-fifteen" + }; + + private static readonly string passedTestContent = @"
"; + + public static TimelineItem CreateSpecMessage(string prefix, string title, int groupId, long startTimeStamp) + { + var content = title.Replace(prefix, string.Empty); + return new TimelineItem("timeline-message", content, title, new DateTime(startTimeStamp), groupId); + } + + public static TimelineItem CreateNodeFact(string prefix, string title, int groupId, long startTimeStamp) + { + var content = title.Replace(prefix, string.Empty); + if (title.EndsWith("PASS") || title.EndsWith("passed.")) + { + content = passedTestContent; + } + return new TimelineItem(CssClasses[startTimeStamp%15], content, title, new DateTime(startTimeStamp), groupId); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerPersistentTestRunStore.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerPersistentTestRunStore.cs new file mode 100644 index 00000000000..093b11cc492 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerPersistentTestRunStore.cs @@ -0,0 +1,28 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.IO; +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + /// + /// Stores test run as a html page. + /// + internal class VisualizerPersistentTestRunStore : IPersistentTestRunStore + { + public bool SaveTestRun(string filePath, TestRunTree data) + { + var template = new VisualizerRuntimeTemplate { Tree = data }; + var content = template.TransformText(); + var fullPath = Path.GetFullPath(filePath); + File.WriteAllText(fullPath, content); + + return true; + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.Tree.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.Tree.cs new file mode 100644 index 00000000000..fe3067f02d1 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.Tree.cs @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + partial class VisualizerRuntimeTemplate + { + private TestRunTree _tree; + public string Prefix { get; private set; } + + public TestRunTree Tree + { + get { return _tree; } + set + { + _tree = value; + Prefix = LongestCommonPrefix( + value.Specs + .Select(s => s.FactName) + .ToArray()); + } + } + + public string BuildSpecificationId(FactData spec) + { + return spec.FactName.Replace(".", "_"); + } + + public string BuildTimelineItem(FactData spec) + { + var messages = spec.RunnerMessages + .Select(m => TimelineItemFactory.CreateSpecMessage(Prefix, m.Message, m.NodeIndex, m.TimeStamp)); + + var facts = + spec.NodeFacts.SelectMany( + nodeFact => + nodeFact.Value.EventStream.Select( + nodeMessage => + TimelineItemFactory.CreateNodeFact( + Prefix, + nodeMessage.Message, + nodeMessage.NodeIndex, + nodeMessage.TimeStamp))); + + var itemStrings = messages.Concat(facts) + .Select(i => i.ToJavascriptString()); + + return string.Join(",\r\n", itemStrings); + } + + public string BuildGroupItems(FactData spec) + { + var groups = spec.NodeFacts + .Select( + nf => + string.Format("{{ id:{0}, content:'Node {0}:{1}' }}", nf.Value.NodeIndex, nf.Value.NodeRole)) + .Concat(@"{ id:-1, content:'Misc' }"); + + return string.Join(",\r\n", groups); + } + + public string BuildOptions(FactData spec) + { + var events = + spec.NodeFacts.SelectMany( + nodeFact => + nodeFact.Value.EventStream + .Select( + nodeMessage => + nodeMessage.TimeStamp)) + .ToList(); + + var startEventTimeParameter = "null"; + var endEventTimeParameter = "null"; + + if (events.Count > 0) + { + var firstEventTimeStamp = events.Aggregate( + (aggregate, nextValue) => + aggregate > nextValue + ? nextValue + : aggregate); + + var lastEventTimeStamp = events.Aggregate( + (aggregate, nextValue) => + aggregate < nextValue + ? nextValue + : aggregate); + + + var startEventTime = new DateTime(firstEventTimeStamp); + var endDisplayTime = new DateTime(lastEventTimeStamp); + + // TODO: Find a better way of calculating additional time from message length + // The last message is the 3 second wait. Which is about half the delta from start to end in length. + var startEndDelta = (endDisplayTime - startEventTime).Ticks / 2; + endDisplayTime = endDisplayTime.AddTicks(startEndDelta); + + startEventTimeParameter = string.Format("'{0}'", startEventTime.ToString("o")); + endEventTimeParameter = string.Format("'{0}'", endDisplayTime.ToString("o")); + } + + + return string.Format( + "{{ start:{0}, end:{1}, align:'left', clickToUse:true }}", + startEventTimeParameter, + endEventTimeParameter); + } + + private static string LongestCommonPrefix(IReadOnlyList strings) + { + if (strings == null || strings.Count == 0) + { + return string.Empty; + } + + var commonPrefix = strings[0]; + + for (var i = 1; i < strings.Count; i++) + { + var j = 0; + for (; j < commonPrefix.Length && j < strings[i].Length; j++) + { + if (commonPrefix[j] != strings[i][j]) + { + commonPrefix = commonPrefix.Substring(0, j); + break; + } + } + + if (j == strings[i].Length) + { + commonPrefix = strings[i]; + } + } + + return commonPrefix; + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.cs new file mode 100644 index 00000000000..8e3f6e92d96 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.cs @@ -0,0 +1,373 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version: 16.0.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +namespace Akka.MultiNode.TestAdapter.Internal.Persistence +{ + using System.Linq; + using System.Text; + using System.Collections.Generic; + using System; + + /// + /// Class to produce the template output + /// + + #line 1 "D:\git\akkadotnet\Akka.MultiNodeTestRunner\src\Akka.MultiNode.TestAdapter\Internal\Persistence\VisualizerRuntimeTemplate.tt" + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "16.0.0.0")] + public partial class VisualizerRuntimeTemplate : VisualizerRuntimeTemplateBase + { +#line hidden + /// + /// Create the template output + /// + public virtual string TransformText() + { + this.Write("\r\n\r\n\r\n\t\r\n\t\r\n\t\r\n\t"); + + #line 42 "D:\git\akkadotnet\Akka.MultiNodeTestRunner\src\Akka.MultiNode.TestAdapter\Internal\Persistence\VisualizerRuntimeTemplate.tt" + Write(Prefix); + + #line default + #line hidden + this.Write("\r\n\r\n\r\n
\r\n\tHelp / Instructions\r\n\t

Click on a timeline to activate. Click off the timeline or press ESC to deactivate

\r\n\t

Scroll up to zoom into an active timeline. Scroll down to zoom out of an active timeline

\r\n\t

Click and hold to move an active timeline.

\r\n
\r\n"); + + #line 51 "D:\git\akkadotnet\Akka.MultiNodeTestRunner\src\Akka.MultiNode.TestAdapter\Internal\Persistence\VisualizerRuntimeTemplate.tt" + foreach (var spec in Tree.Specs) { + + #line default + #line hidden + this.Write("
\r\n

"); + + #line 53 "D:\git\akkadotnet\Akka.MultiNodeTestRunner\src\Akka.MultiNode.TestAdapter\Internal\Persistence\VisualizerRuntimeTemplate.tt" + Write(spec.FactName.Replace(Prefix, "")); + + #line default + #line hidden + this.Write("

\r\n\r\n\r\n\r\n
\r\n"); + + #line 72 "D:\git\akkadotnet\Akka.MultiNodeTestRunner\src\Akka.MultiNode.TestAdapter\Internal\Persistence\VisualizerRuntimeTemplate.tt" + } + + #line default + #line hidden + this.Write("\r\n"); + return this.GenerationEnvironment.ToString(); + } + } + + #line default + #line hidden + #region Base class + /// + /// Base class for this transformation + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "16.0.0.0")] + public class VisualizerRuntimeTemplateBase + { + #region Fields + private global::System.Text.StringBuilder generationEnvironmentField; + private global::System.CodeDom.Compiler.CompilerErrorCollection errorsField; + private global::System.Collections.Generic.List indentLengthsField; + private string currentIndentField = ""; + private bool endsWithNewline; + private global::System.Collections.Generic.IDictionary sessionField; + #endregion + #region Properties + /// + /// The string builder that generation-time code is using to assemble generated output + /// + protected System.Text.StringBuilder GenerationEnvironment + { + get + { + if ((this.generationEnvironmentField == null)) + { + this.generationEnvironmentField = new global::System.Text.StringBuilder(); + } + return this.generationEnvironmentField; + } + set + { + this.generationEnvironmentField = value; + } + } + /// + /// The error collection for the generation process + /// + public System.CodeDom.Compiler.CompilerErrorCollection Errors + { + get + { + if ((this.errorsField == null)) + { + this.errorsField = new global::System.CodeDom.Compiler.CompilerErrorCollection(); + } + return this.errorsField; + } + } + /// + /// A list of the lengths of each indent that was added with PushIndent + /// + private System.Collections.Generic.List indentLengths + { + get + { + if ((this.indentLengthsField == null)) + { + this.indentLengthsField = new global::System.Collections.Generic.List(); + } + return this.indentLengthsField; + } + } + /// + /// Gets the current indent we use when adding lines to the output + /// + public string CurrentIndent + { + get + { + return this.currentIndentField; + } + } + /// + /// Current transformation session + /// + public virtual global::System.Collections.Generic.IDictionary Session + { + get + { + return this.sessionField; + } + set + { + this.sessionField = value; + } + } + #endregion + #region Transform-time helpers + /// + /// Write text directly into the generated output + /// + public void Write(string textToAppend) + { + if (string.IsNullOrEmpty(textToAppend)) + { + return; + } + // If we're starting off, or if the previous text ended with a newline, + // we have to append the current indent first. + if (((this.GenerationEnvironment.Length == 0) + || this.endsWithNewline)) + { + this.GenerationEnvironment.Append(this.currentIndentField); + this.endsWithNewline = false; + } + // Check if the current text ends with a newline + if (textToAppend.EndsWith(global::System.Environment.NewLine, global::System.StringComparison.CurrentCulture)) + { + this.endsWithNewline = true; + } + // This is an optimization. If the current indent is "", then we don't have to do any + // of the more complex stuff further down. + if ((this.currentIndentField.Length == 0)) + { + this.GenerationEnvironment.Append(textToAppend); + return; + } + // Everywhere there is a newline in the text, add an indent after it + textToAppend = textToAppend.Replace(global::System.Environment.NewLine, (global::System.Environment.NewLine + this.currentIndentField)); + // If the text ends with a newline, then we should strip off the indent added at the very end + // because the appropriate indent will be added when the next time Write() is called + if (this.endsWithNewline) + { + this.GenerationEnvironment.Append(textToAppend, 0, (textToAppend.Length - this.currentIndentField.Length)); + } + else + { + this.GenerationEnvironment.Append(textToAppend); + } + } + /// + /// Write text directly into the generated output + /// + public void WriteLine(string textToAppend) + { + this.Write(textToAppend); + this.GenerationEnvironment.AppendLine(); + this.endsWithNewline = true; + } + /// + /// Write formatted text directly into the generated output + /// + public void Write(string format, params object[] args) + { + this.Write(string.Format(global::System.Globalization.CultureInfo.CurrentCulture, format, args)); + } + /// + /// Write formatted text directly into the generated output + /// + public void WriteLine(string format, params object[] args) + { + this.WriteLine(string.Format(global::System.Globalization.CultureInfo.CurrentCulture, format, args)); + } + /// + /// Raise an error + /// + public void Error(string message) + { + System.CodeDom.Compiler.CompilerError error = new global::System.CodeDom.Compiler.CompilerError(); + error.ErrorText = message; + this.Errors.Add(error); + } + /// + /// Raise a warning + /// + public void Warning(string message) + { + System.CodeDom.Compiler.CompilerError error = new global::System.CodeDom.Compiler.CompilerError(); + error.ErrorText = message; + error.IsWarning = true; + this.Errors.Add(error); + } + /// + /// Increase the indent + /// + public void PushIndent(string indent) + { + if ((indent == null)) + { + throw new global::System.ArgumentNullException("indent"); + } + this.currentIndentField = (this.currentIndentField + indent); + this.indentLengths.Add(indent.Length); + } + /// + /// Remove the last indent that was added with PushIndent + /// + public string PopIndent() + { + string returnValue = ""; + if ((this.indentLengths.Count > 0)) + { + int indentLength = this.indentLengths[(this.indentLengths.Count - 1)]; + this.indentLengths.RemoveAt((this.indentLengths.Count - 1)); + if ((indentLength > 0)) + { + returnValue = this.currentIndentField.Substring((this.currentIndentField.Length - indentLength)); + this.currentIndentField = this.currentIndentField.Remove((this.currentIndentField.Length - indentLength)); + } + } + return returnValue; + } + /// + /// Remove any indentation + /// + public void ClearIndent() + { + this.indentLengths.Clear(); + this.currentIndentField = ""; + } + #endregion + #region ToString Helpers + /// + /// Utility class to produce culture-oriented representation of an object as a string. + /// + public class ToStringInstanceHelper + { + private System.IFormatProvider formatProviderField = global::System.Globalization.CultureInfo.InvariantCulture; + /// + /// Gets or sets format provider to be used by ToStringWithCulture method. + /// + public System.IFormatProvider FormatProvider + { + get + { + return this.formatProviderField ; + } + set + { + if ((value != null)) + { + this.formatProviderField = value; + } + } + } + /// + /// This is called from the compile/run appdomain to convert objects within an expression block to a string + /// + public string ToStringWithCulture(object objectToConvert) + { + if ((objectToConvert == null)) + { + throw new global::System.ArgumentNullException("objectToConvert"); + } + System.Type t = objectToConvert.GetType(); + System.Reflection.MethodInfo method = t.GetMethod("ToString", new System.Type[] { + typeof(System.IFormatProvider)}); + if ((method == null)) + { + return objectToConvert.ToString(); + } + else + { + return ((string)(method.Invoke(objectToConvert, new object[] { + this.formatProviderField }))); + } + } + } + private ToStringInstanceHelper toStringHelperField = new ToStringInstanceHelper(); + /// + /// Helper to produce culture-oriented representation of an object as a string + /// + public ToStringInstanceHelper ToStringHelper + { + get + { + return this.toStringHelperField; + } + } + #endregion + } + #endregion +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.tt b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.tt new file mode 100644 index 00000000000..e8fa5e9539f --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Persistence/VisualizerRuntimeTemplate.tt @@ -0,0 +1,74 @@ +<#@ template language="C#" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> + + + + + + + <# Write(Prefix); #> + + +
+ Help / Instructions +

Click on a timeline to activate. Click off the timeline or press ESC to deactivate

+

Scroll up to zoom into an active timeline. Scroll down to zoom out of an active timeline

+

Click and hold to move an active timeline.

+
+<# foreach (var spec in Tree.Specs) { #> +
+

<# Write(spec.FactName.Replace(Prefix, "")); #>

+
+ + +
+<# } #> + + \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/MultiNodeMessage.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/MultiNodeMessage.cs new file mode 100644 index 00000000000..41e7f5ba011 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/MultiNodeMessage.cs @@ -0,0 +1,298 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Event; + +namespace Akka.MultiNode.TestAdapter.Internal.Reporting +{ + /// + /// Message from an individual node + /// + public abstract class MultiNodeMessage : IComparable, IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The time that the message occurred + /// The contents of the log message + /// The index of the node where the message occurred + /// The role of the node where the message occurred + protected MultiNodeMessage(long timeStamp, string message, int nodeIndex, string nodeRole) + { + NodeIndex = nodeIndex; + NodeRole = nodeRole; + Message = message; + TimeStamp = timeStamp; + } + + + /// + /// The absolute time this message occurred represented as + /// + public long TimeStamp { get; private set; } + + /// + /// The contents of the log message. + /// + public string Message { get; private set; } + + /// + /// The index of the node in question. + /// + public int NodeIndex { get; private set; } + + /// + /// The Role of the node in question. + /// + public string NodeRole { get; private set; } + + #region Comparisons + + /// + public virtual int CompareTo(MultiNodeMessage other) + { + var tc = TimeStamp.CompareTo(other.TimeStamp); + if(tc != 0) return tc; + var m = string.Compare(Message, other.Message, StringComparison.Ordinal); + if (m != 0) return m; + var ni = NodeIndex.CompareTo(other.NodeIndex); + if (ni != 0) return ni; + var nr = string.Compare(NodeRole, other.NodeRole, StringComparison.Ordinal); + if (nr != 0) return nr; + return 0; + } + + #endregion + + #region Equality + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = 13; + hashCode = (hashCode * 397) ^ TimeStamp.GetHashCode(); + hashCode = (hashCode * 397) ^ NodeIndex; + hashCode = (hashCode * 397) ^ Message.GetHashCode(); + hashCode = (hashCode * 397) ^ NodeRole.GetHashCode(); + return hashCode; + } + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + var msg = obj as MultiNodeMessage; + return msg != null && Equals(msg); + } + + /// + public virtual bool Equals(MultiNodeMessage other) + { + return other != null && + NodeIndex == other.NodeIndex && + string.Equals(NodeRole, other.NodeRole, StringComparison.Ordinal) && + TimeStamp == other.TimeStamp && + string.Equals(Message, other.Message); + + } + + #endregion + } + + /// + /// Message used to contain the PASS / FAIL results for a specific test + /// + public class MultiNodeResultMessage : MultiNodeMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The time that the message occurred + /// The contents of the log message + /// The index of the node where the message occurred + /// The role of the node where the message occurred + /// The flag used to determine if the test passed. true if successful; otherwise false. + public MultiNodeResultMessage(long timeStamp, string message, int nodeIndex, string nodeRole, bool passed) + : base(timeStamp, message, nodeIndex, nodeRole) + { + Passed = passed; + } + + /// + /// Flag to determine whether or not this passed its test or not. + /// + public bool Passed { get; private set; } + + #region Equality + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ Passed.GetHashCode(); + return hashCode; + } + } + + /// + public override bool Equals(MultiNodeMessage other) + { + var otherResultMessage = other as MultiNodeResultMessage; + return otherResultMessage != null && + base.Equals(other) && + Passed == otherResultMessage.Passed; + } + + #endregion + } + + /// + /// Messages emitted directly by the test runner itself for an individual spec + /// + internal class MultiNodeTestRunnerMessage : MultiNodeMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The time that the message occurred + /// The contents of the log message + /// The path to the remote node where the message occurred + /// The log level of the message + public MultiNodeTestRunnerMessage(long timeStamp, string message, string actorPath, LogLevel logLevel) + : base(timeStamp, message, -1, string.Empty) + { + ActorPath = actorPath; + LogLevel = logLevel; + } + + /// + /// The path of the actor on the remote node who generated this message. + /// + /// CAN BE NULL. + /// + public string ActorPath { get; private set; } + + /// + /// The log level for this message. + /// + public LogLevel LogLevel { get; private set; } + + #region Equality + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)LogLevel; + hashCode = (hashCode * 397) ^ ActorPath.GetHashCode(); + return hashCode; + } + } + + /// + public override bool Equals(MultiNodeMessage other) + { + var otherLogMessage = other as MultiNodeTestRunnerMessage; + return otherLogMessage != null && + base.Equals(other) && + LogLevel == otherLogMessage.LogLevel && + string.Equals(ActorPath, otherLogMessage.ActorPath); + } + + #endregion + } + + /// + /// Used in cases where a log message was broken up across multiple lines and this fragment has to be appended + /// to a previous message in the timeline + /// + internal class MultiNodeLogMessageFragment : MultiNodeMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The time that the message occurred + /// The contents of the log message + /// The index of the node where the message occurred + /// The role of the node where the message occurred + public MultiNodeLogMessageFragment(long timeStamp, string message, int nodeIndex, string nodeRole) + : base(timeStamp, message, nodeIndex, nodeRole) + { + } + } + + /// + /// Message from a node containing log information + /// + internal class MultiNodeLogMessage : MultiNodeMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The time that the message occurred + /// The contents of the log message. + /// The index of the node where the message occurred + /// The role of the node where the message occurred + /// The path to the remote node where the message occurred + /// The log level of the message + public MultiNodeLogMessage(long timeStamp, string message, int nodeIndex, string nodeRole, string actorPath, LogLevel logLevel) + : base(timeStamp, message, nodeIndex, nodeRole) + { + ActorPath = actorPath; + LogLevel = logLevel; + } + + /// + /// The path of the actor on the remote node who generated this message. + /// + /// CAN BE NULL. + /// + public string ActorPath { get; private set; } + + /// + /// The log level for this message. + /// + public LogLevel LogLevel { get; private set; } + + #region Equality + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)LogLevel; + hashCode = (hashCode * 397) ^ ActorPath.GetHashCode(); + return hashCode; + } + } + + /// + public override bool Equals(MultiNodeMessage other) + { + var otherLogMessage = other as MultiNodeLogMessage; + return otherLogMessage != null && + base.Equals(other) && + LogLevel == otherLogMessage.LogLevel && + string.Equals(ActorPath, otherLogMessage.ActorPath); + } + + #endregion + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/NodeDataActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/NodeDataActor.cs new file mode 100644 index 00000000000..85e6a227c6c --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/NodeDataActor.cs @@ -0,0 +1,64 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Sinks; + +namespace Akka.MultiNode.TestAdapter.Internal.Reporting +{ + /// + /// Actor responsible for processing test messages for an individual node within a multi-node test + /// + internal class NodeDataActor : ReceiveActor + { + /// + /// Data that will be processed and aggregated for an individual node + /// + protected NodeData NodeData; + + /// + /// The ID of this node in the 0-N index of all nodes for this test. + /// + protected readonly int NodeIndex; + + /// + /// The Role of this node. + /// + protected readonly string NodeRole; + + public NodeDataActor(int nodeIndex, string nodeRole) + { + NodeIndex = nodeIndex; + NodeRole = nodeRole; + NodeData = new NodeData(nodeIndex, nodeRole); + SetReceive(); + } + + #region Message-handling + + private void SetReceive() + { + Receive(message => NodeData.Put(message)); + + + Receive(spec => + { + + + //Send NodeData to parent for aggregation purposes + Sender.Tell(NodeData.Copy()); + + //Begin shutdown + Context.Self.GracefulStop(TimeSpan.FromSeconds(1)); + }); + } + + #endregion + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/SpecRunCoordinator.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/SpecRunCoordinator.cs new file mode 100644 index 00000000000..0e4fb1e2f4c --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/SpecRunCoordinator.cs @@ -0,0 +1,130 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Sinks; + +namespace Akka.MultiNode.TestAdapter.Internal.Reporting +{ + /// + /// Actor responsible for organizing the results of an individual spec + /// + internal class SpecRunCoordinator : ReceiveActor + { + public SpecRunCoordinator(string className, string methodName, IList nodes) + { + Nodes = nodes; + MethodName = methodName; + ClassName = className; + FactData = new FactData(string.Format("{0}.{1}", className, methodName)); + _nodeActors = new Dictionary(); + SetReceive(); + } + + public string ClassName { get; private set; } + + public string MethodName { get; private set; } + + public IList Nodes { get; private set; } + + /// + /// All of the data for this individual spec + /// + protected FactData FactData; + + /// + /// Internal dictionary used to route messages to their discrete nodes + /// + private readonly Dictionary _nodeActors; + + #region Actor Lifecycle + + protected override void PreStart() + { + //create all of the NodeFactActor instances + foreach (var node in Nodes) + { + var index = node.Node; + var role = node.Role; + _nodeActors.Add(index, Context.ActorOf(Props.Create(() => new NodeDataActor(index, role)))); + } + } + + #endregion + + #region Message-handling + + private void SetReceive() + { + Receive(message => + { + FactData.Put(message); + }); + Receive(message => RouteToNodeActor(message)); + Receive(spec => HandleEndSpec(spec)); + Receive(datum => HandleNodeDatum(datum)); + } + + /// + /// Send a to the correct based on the + /// property. + /// + private void RouteToNodeActor(MultiNodeMessage message) + { + var actor = _nodeActors[message.NodeIndex]; + actor.Tell(message); + } + + /// + /// Wait for all child instances to finish processing + /// and report their results + /// + /// An awaitable task, since this operation uses the pattern + private void HandleEndSpec(EndSpec endSpec) + { + var futures = new Task[Nodes.Count]; + + var i = 0; + foreach (var node in _nodeActors) + { + futures[i] = node.Value.Ask(endSpec, TimeSpan.FromSeconds(1)); + i++; + } + + var sender = Context.Sender; + + //wait for all Ask operations to complete and pipe the result back to ourselves, including the ref for the original sender + Task.WhenAll(futures) + .PipeTo(Self, sender); + } + + /// + /// When the result of a finally gets finished... + /// + /// An envelope with all of the messages we processed from earlier + private void HandleNodeDatum(NodeData[] nodeDatum) + { + FactData.AddNodes(nodeDatum); + + //mark this test as complete + FactData.Complete(); + + //Send our FactData back to the sender + Sender.Tell(FactData.Copy()); + + //Shut ourselves down + Self.GracefulStop(TimeSpan.FromSeconds(1)); + } + + #endregion + + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TeamCityLoggerActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TeamCityLoggerActor.cs new file mode 100644 index 00000000000..42564a075d0 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TeamCityLoggerActor.cs @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; + +namespace Akka.MultiNode.TestAdapter.Internal.Reporting +{ + internal class TeamCityLoggerActor : ReceiveActor + { + private readonly bool _unMuted = false; + public TeamCityLoggerActor(bool unMuted) + { + _unMuted = unMuted; + + ReceiveAny(o => + { + if (_unMuted) + { + Console.WriteLine(o.ToString()); + } + }); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TestRunCoordinator.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TestRunCoordinator.cs new file mode 100644 index 00000000000..460fbb81b8d --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TestRunCoordinator.cs @@ -0,0 +1,183 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Sinks; + +namespace Akka.MultiNode.TestAdapter.Internal.Reporting +{ + /// + /// Actor responsible for organizing all of the data for each test run + /// + internal class TestRunCoordinator : ReceiveActor + { + #region Internal message classes + + /// + /// Message used to request the current value. + /// + public class RequestTestRunState { } + + /// + /// Signals that we need to publish all messages to the + /// + public class SubscribeFactCompletionMessages + { + public SubscribeFactCompletionMessages(IActorRef subscriber) + { + Subscriber = subscriber; + } + + public IActorRef Subscriber { get; private set; } + } + + /// + /// Signals that no longer wants to receive messages + /// + public class UnsubscribeFactCompletionMessages + { + public UnsubscribeFactCompletionMessages(IActorRef subscriber) + { + Subscriber = subscriber; + } + + + public IActorRef Subscriber { get; private set; } + } + + #endregion + + /// + /// Default constructor which uses as the time for . + /// + public TestRunCoordinator() : this(DateTime.UtcNow) { } + + public TestRunCoordinator(DateTime testRunStarted) + { + TestRunStarted = testRunStarted; + TestRunData = new TestRunTree(testRunStarted.Ticks); + Subscribers = new List(); + SetReceive(); + } + + #region Internal fields and Properties + + protected readonly DateTime TestRunStarted; + + protected IActorRef _currentSpecRunActor; + + /// + /// Automatically set when is sent to this actor. + /// + protected DateTime? TestRunCompleted { get; private set; } + + /// + /// The amount of time elapsed for this test run + /// + protected TimeSpan TestRunElapsed + { + get + { + return TestRunStarted - (TestRunCompleted.HasValue ? TestRunCompleted.Value : DateTime.UtcNow); + } + } + + /// + /// Contains the entire tree of information needed to process results of a full test run. + /// + protected TestRunTree TestRunData; + + /// + /// All of the subscribers who wish to receive notifications + /// + protected List Subscribers; + + #endregion + + #region Message-handling + + private void SetReceive() + { + Receive(message => + { + if (_currentSpecRunActor == null) return; + _currentSpecRunActor.Forward(message); + }); + Receive(ReceiveBeginSpecRun); + ReceiveAsync(spec => _currentSpecRunActor != null ? ReceiveEndSpecRun(spec) : Task.CompletedTask); + Receive(state => Sender.Tell(TestRunData.Copy(TestRunPassed(TestRunData)))); + Receive(AddSubscriber); + Receive(RemoveSubscriber); + ReceiveAsync(async run => + { + //clean up the current spec, if it hasn't been done already + if (_currentSpecRunActor != null) + { + await ReceiveEndSpecRun(new EndSpec()); + } + + //Mark the test run as finished + TestRunData.Complete(); + + //Deliver the final copy of the TestRunData + Sender.Tell(TestRunData.Copy()); + + //shutdown + Context.Stop(Self); + }); + } + + private void RemoveSubscriber(UnsubscribeFactCompletionMessages unsubscribe) + { + Subscribers.Remove(unsubscribe.Subscriber); + } + + private void AddSubscriber(SubscribeFactCompletionMessages subscription) + { + Subscribers.Add(subscription.Subscriber); + } + + private void ReceiveBeginSpecRun(BeginNewSpec spec) + { + if (_currentSpecRunActor != null) throw new InvalidOperationException("EndSpec has not been called for previous run yet. Cannot begin next run."); + + //Create the new spec run actor + _currentSpecRunActor = + Context.ActorOf( + Props.Create(() => new SpecRunCoordinator(spec.ClassName, spec.MethodName, spec.Nodes))); + } + + private async Task ReceiveEndSpecRun(EndSpec spec) + { + //Should receive a FactData in return + var factData = await _currentSpecRunActor.Ask(spec, TimeSpan.FromSeconds(2)); + + TestRunData.AddSpec(factData); + + //Publish the FactData back to any subscribers who wanted it + foreach (var subscriber in Subscribers) + { + subscriber.Tell(factData); + } + + //Ready to begin the next spec + _currentSpecRunActor = null; + } + + private static bool TestRunPassed(TestRunTree tree) + { + return tree.Specs.All(x => x.Passed.HasValue && x.Passed.Value); + } + + #endregion + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TestRunTree.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TestRunTree.cs new file mode 100644 index 00000000000..e507921a695 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Reporting/TestRunTree.cs @@ -0,0 +1,426 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Akka.MultiNode.TestAdapter.Internal.Reporting +{ + /// + /// The top of the tree - represents an entire test run. + /// + public class TestRunTree : IEquatable + { + private readonly List _specs; + + public TestRunTree(long startTime) : this(startTime, new List()) + { + } + + public TestRunTree(long startTime, List specs) : this(startTime, specs, null, null) + { + + } + + [JsonConstructor] + public TestRunTree(long startTime, List specs, long? endTime, bool? passed) + { + StartTime = startTime; + _specs = specs; + EndTime = endTime; + Passed = passed; + } + + /// + /// The absolute time tests began for this individual node + /// + public long StartTime { get; private set; } + + /// + /// The absolute time tests ended for this individual node + /// + public long? EndTime { get; set; } + + /// + /// Whether or not this test has acquired a result yet + /// + public bool? Passed { get; set; } + + public IEnumerable Specs { get { return _specs; } } + + public TimeSpan Elapsed + { + get + { + return ((EndTime.HasValue ? new DateTime(EndTime.Value) : DateTime.UtcNow) - new DateTime(StartTime)); + } + } + + public void AddSpec(FactData spec) + { + _specs.Add(spec); + } + + public void Complete() + { + var passes = _specs.Select(x => x.Passed); + if (passes.All(x => x.HasValue)) + { + Passed = passes.All(x => x.Value); + EndTime = DateTime.UtcNow.Ticks; + } + } + + /// + /// Returns a deep copy of the current tree. + /// + public TestRunTree Copy(bool? passed = null) + { + var specs = new FactData[_specs.Count]; + _specs.CopyTo(specs); + + return new TestRunTree(StartTime, specs.ToList(), EndTime, passed ?? Passed); + } + + #region Equality + + /// + public bool Equals(TestRunTree other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return + _specs.SequenceEqual(other._specs) + && StartTime == other.StartTime + && EndTime == other.EndTime + && Passed.Equals(other.Passed); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((TestRunTree) obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = (_specs != null ? _specs.GetHashCode() : 0); + hashCode = (hashCode*397) ^ StartTime.GetHashCode(); + hashCode = (hashCode*397) ^ EndTime.GetHashCode(); + hashCode = (hashCode*397) ^ Passed.GetHashCode(); + return hashCode; + } + } + + #endregion + } + + /// + /// A collection of data about a particular test + /// + public class FactData : IEquatable + { + private readonly Dictionary _nodes; + + /// + /// Messages sent by the test runner for this spec, rather than any individual nodes + /// + private readonly SortedSet _testRunnerTimeLine; + + public FactData(string factName) + : this(factName, DateTime.UtcNow.Ticks, new Dictionary(), new List()) { } + + public FactData(string factName, long startTime, Dictionary nodes, IEnumerable testRunnerTimeLine) + : this(factName, startTime, nodes, testRunnerTimeLine, null, null) + { + + } + + public FactData(string factName, long startTime, Dictionary nodes, IEnumerable testRunnerTimeLine, long? endTime, bool? passed) + { + _nodes = nodes; + _testRunnerTimeLine = new SortedSet(testRunnerTimeLine ?? new List()); + StartTime = startTime; + FactName = factName; + EndTime = endTime; + Passed = passed; + } + + [JsonConstructor] + public FactData(string factName, long startTime, IEnumerable nodes, IEnumerable testRunnerTimeLine, long? endTime, bool? passed) + : this(factName, startTime, + nodes == null ? new Dictionary() : + nodes.ToDictionary(key => key.NodeIndex, data => data), + testRunnerTimeLine, endTime, passed) + { + } + + + public string FactName { get; private set; } + + public IEnumerable RunnerMessages { get { return _testRunnerTimeLine; } } + + public Dictionary NodeFacts { get { return _nodes; } } + + /// + /// The absolute time tests began for this individual node + /// + public long StartTime { get; private set; } + + /// + /// The absolute time tests ended for this individual node + /// + public long? EndTime { get; set; } + + /// + /// Whether or not this test has acquired a result yet + /// + public bool? Passed { get; set; } + + public TimeSpan Elapsed + { + get + { + return ((EndTime.HasValue ? new DateTime(EndTime.Value) : DateTime.UtcNow) - new DateTime(StartTime)); + } + } + + public void Complete() + { + var passes = _nodes.Select(x => x.Value.Passed); + if (passes.All(x => x.HasValue)) + { + Passed = passes.All(x => x.Value); + EndTime = DateTime.UtcNow.Ticks; + } + } + + public void AddNodes(NodeData[] nodeDatum) + { + foreach(var node in nodeDatum) + AddNode(node); + } + + public void AddNode(NodeData nodeData) + { + _nodes[nodeData.NodeIndex] = nodeData; + } + + public void Put(MultiNodeMessage message) + { + _testRunnerTimeLine.Add(message); + } + + /// + /// Creates a deep copy of the current object. + /// + public FactData Copy() + { + var nodeData = new Dictionary(_nodes.Count); + foreach (var node in _nodes) + { + //make a copy of the NodeData too + nodeData[node.Key] = node.Value.Copy(); + } + + var messageTimeline = new MultiNodeMessage[_testRunnerTimeLine.Count]; + _testRunnerTimeLine.CopyTo(messageTimeline); + + return new FactData(FactName, StartTime, nodeData, messageTimeline, EndTime, Passed); + } + + #region Equality + + /// + public bool Equals(FactData other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return _nodes.SequenceEqual(other._nodes) + && _testRunnerTimeLine.SetEquals(other._testRunnerTimeLine) + && string.Equals(FactName, other.FactName) + && StartTime == other.StartTime + && EndTime == other.EndTime + && Passed.Equals(other.Passed); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((FactData) obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (FactName.GetHashCode() * 397) ^ StartTime.GetHashCode(); + } + } + + #endregion + } + + /// + /// A collection of data about the status of a particular node + /// + public class NodeData : IEquatable + { + private readonly SortedSet _eventTimeLine; + + public NodeData(int nodeIndex, string nodeRole) : this(nodeIndex, nodeRole, DateTime.UtcNow.Ticks, new List()) { } + + public NodeData(int nodeIndex, string nodeRole, long startTime, IEnumerable eventTimeLine) + : this(nodeIndex, nodeRole, startTime, eventTimeLine, null, null) + { + + } + + /// + /// Copy constructor + /// + [JsonConstructor] + public NodeData(int nodeIndex, string nodeRole, long startTime, IEnumerable eventTimeLine, long? endTime, + bool? passed) + { + NodeIndex = nodeIndex; + NodeRole = nodeRole; + StartTime = startTime; + _eventTimeLine = new SortedSet(eventTimeLine ?? new List()); + EndTime = endTime; + Passed = passed; + } + + /// + /// The position of this node in the 0...N index of all nodes in the set. + /// + public int NodeIndex { get; private set; } + + /// + /// The Role of this node. + /// + public string NodeRole { get; private set; } + + /// + /// The absolute time tests began for this individual node + /// + public long StartTime { get; private set; } + + /// + /// The absolute time tests ended for this individual node + /// + public long? EndTime { get; set; } + + /// + /// Whether or not this test has acquired a result yet + /// + public bool? Passed { get; set; } + + /// + /// Filter all of the result messages to the top + /// + public SortedSet ResultMessages + { + get + { + return new SortedSet(_eventTimeLine.Where(x => x.GetType() == typeof(MultiNodeResultMessage)).Cast()); + } + } + + public TimeSpan Elapsed + { + get + { + return ((EndTime.HasValue ? new DateTime(EndTime.Value) : DateTime.UtcNow) - new DateTime(StartTime)); + } + } + + /// + /// All of the events that occurred for this node - time sequenced. + /// + public IEnumerable EventStream + { + get { return _eventTimeLine; } + } + + /// + /// Pushes a new message onto the for this node. + /// + public void Put(MultiNodeMessage message) + { + //Check for passed messages + if (!Passed.HasValue && message is MultiNodeResultMessage) + { + var resultMessage = message as MultiNodeResultMessage; + Passed = resultMessage.Passed; + EndTime = DateTime.UtcNow.Ticks; + } + + _eventTimeLine.Add(message); + } + + /// + /// Creates a deep copy of the current object + /// + public NodeData Copy() + { + var events = new MultiNodeMessage[_eventTimeLine.Count]; + _eventTimeLine.CopyTo(events); + + return new NodeData(NodeIndex, NodeRole, StartTime, events, EndTime, Passed); + } + + #region Equality + + /// + public bool Equals(NodeData other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return _eventTimeLine.SetEquals(other._eventTimeLine) + && NodeIndex == other.NodeIndex + && String.Equals(NodeRole, other.NodeRole, StringComparison.Ordinal) + && StartTime == other.StartTime + && EndTime == other.EndTime + && Passed.Equals(other.Passed); + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((NodeData) obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (NodeIndex * 397) ^ (NodeRole.GetHashCode() * 397) ^ StartTime.GetHashCode(); + } + } + + #endregion + + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/ConsoleMessageSinkActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/ConsoleMessageSinkActor.cs new file mode 100644 index 00000000000..36bb711dcfd --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/ConsoleMessageSinkActor.cs @@ -0,0 +1,215 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using Akka.Actor; +using Akka.Event; +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// implementation that logs all of its output directly to the . + /// + /// Has no persistence capabilities. Can optionally use a to provide total "end of test" reporting. + /// + internal class ConsoleMessageSinkActor : TestCoordinatorEnabledMessageSink + { + public ConsoleMessageSinkActor(bool useTestCoordinator) : base(useTestCoordinator) + { + } + + #region Message handling + + protected override void AdditionalReceives() + { + Receive(data => ReceiveFactData(data)); + } + + protected override void ReceiveFactData(FactData data) + { + PrintSpecRunResults(data); + } + + private void PrintSpecRunResults(FactData data) + { + WriteSpecMessage(string.Format("Results for {0}", data.FactName)); + WriteSpecMessage(string.Format("Start time: {0}", new DateTime(data.StartTime, DateTimeKind.Utc))); + foreach (var node in data.NodeFacts) + { + WriteSpecMessage(string.Format(" --> Node {0}:{1} : {2} [{3} elapsed]", node.Value.NodeIndex, node.Value.NodeRole, + node.Value.Passed.GetValueOrDefault(false) ? "PASS" : "FAIL", node.Value.Elapsed)); + } + WriteSpecMessage(string.Format("End time: {0}", + new DateTime(data.EndTime.GetValueOrDefault(DateTime.UtcNow.Ticks), DateTimeKind.Utc))); + WriteSpecMessage(string.Format("FINAL RESULT: {0} after {1}.", + data.Passed.GetValueOrDefault(false) ? "PASS" : "FAIL", data.Elapsed)); + + //If we had a failure + if (data.Passed.GetValueOrDefault(false) == false) + { + WriteSpecMessage("Failure messages by Node"); + foreach (var node in data.NodeFacts) + { + if (node.Value.Passed.GetValueOrDefault(false) == false) + { + WriteSpecMessage(string.Format("<----------- BEGIN NODE {0}:{1} ----------->", node.Key, node.Value.NodeRole)); + foreach (var resultMessage in node.Value.ResultMessages) + { + WriteSpecMessage(String.Format(" --> {0}", resultMessage.Message)); + } + if (node.Value.ResultMessages == null || node.Value.ResultMessages.Count == 0) + WriteSpecMessage("[received no messages - SILENT FAILURE]."); + WriteSpecMessage(string.Format("<----------- END NODE {0}:{1} ----------->", node.Key, node.Value.NodeRole)); + } + } + } + } + + protected override void HandleNodeSpecFail(NodeCompletedSpecWithFail nodeFail) + { + WriteSpecFail(nodeFail.NodeIndex, nodeFail.NodeRole, nodeFail.Message); + + base.HandleNodeSpecFail(nodeFail); + } + + protected override void HandleTestRunEnd(EndTestRun endTestRun) + { + WriteSpecMessage("Test run complete."); + + base.HandleTestRunEnd(endTestRun); + } + + protected override void HandleTestRunTree(TestRunTree tree) + { + var passedSpecs = tree.Specs.Count(x => x.Passed.GetValueOrDefault(false)); + WriteSpecMessage(string.Format("Test run completed in [{0}] with {1}/{2} specs passed.", tree.Elapsed, passedSpecs, tree.Specs.Count())); + foreach (var factData in tree.Specs) + { + PrintSpecRunResults(factData); + } + } + + protected override void HandleNewSpec(BeginNewSpec newSpec) + { + WriteSpecMessage(string.Format("Beginning spec {0}.{1} on {2} nodes", newSpec.ClassName, newSpec.MethodName, newSpec.Nodes.Count)); + + base.HandleNewSpec(newSpec); + } + + protected override void HandleEndSpec(EndSpec endSpec) + { + WriteSpecMessage("Spec completed."); + + base.HandleEndSpec(endSpec); + } + + protected override void HandleNodeMessageFragment(LogMessageFragmentForNode logMessage) + { + WriteNodeMessage(logMessage); + + base.HandleNodeMessageFragment(logMessage); + } + + protected override void HandleRunnerMessage(LogMessageForTestRunner node) + { + WriteRunnerMessage(node); + + base.HandleRunnerMessage(node); + } + + protected override void HandleNodeSpecPass(NodeCompletedSpecWithSuccess nodeSuccess) + { + WriteSpecPass(nodeSuccess.NodeIndex, nodeSuccess.NodeRole, nodeSuccess.Message); + + base.HandleNodeSpecPass(nodeSuccess); + } + + #endregion + + #region Console output methods + + /// + /// Used to print a spec status message (spec starting, finishing, failed, etc...) + /// + private void WriteSpecMessage(string message) + { + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine("[RUNNER][{0}]: {1}", DateTime.UtcNow.ToShortTimeString(), message); + Console.ResetColor(); + } + + private void WriteSpecPass(int nodeIndex, string nodeRole, string message) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("[NODE{0}:{1}][{2}]: SPEC PASSED: {3}", nodeIndex, nodeRole, DateTime.UtcNow.ToShortTimeString(), message); + Console.ResetColor(); + } + + private void WriteSpecFail(int nodeIndex, string nodeRole, string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("[NODE{0}:{1}][{2}]: SPEC FAILED: {3}", nodeIndex, nodeRole, DateTime.UtcNow.ToShortTimeString(), message); + Console.ResetColor(); + } + + private void WriteRunnerMessage(LogMessageForTestRunner nodeMessage) + { + Console.ForegroundColor = ColorForLogLevel(nodeMessage.Level); + Console.WriteLine(nodeMessage.ToString()); + Console.ResetColor(); + } + + private void WriteNodeMessage(LogMessageFragmentForNode nodeMessage) + { + Console.WriteLine(nodeMessage.ToString()); + } + + private static ConsoleColor ColorForLogLevel(LogLevel level) + { + var color = ConsoleColor.DarkGray; + switch (level) + { + case LogLevel.DebugLevel: + color = ConsoleColor.Gray; + break; + case LogLevel.InfoLevel: + color = ConsoleColor.White; + break; + case LogLevel.WarningLevel: + color = ConsoleColor.Yellow; + break; + case LogLevel.ErrorLevel: + color = ConsoleColor.Red; + break; + } + + return color; + } + + #endregion + } + + /// + /// implementation that writes directly to the console. + /// + internal class ConsoleMessageSink : MessageSink + { + public ConsoleMessageSink() + : base(Props.Create(() => new ConsoleMessageSinkActor(true))) + { + } + + protected override void HandleUnknownMessageType(string message) + { + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine("Unknown message: {0}", message); + Console.ResetColor(); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/DiagnosticMessageSinkActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/DiagnosticMessageSinkActor.cs new file mode 100644 index 00000000000..4e58f1f1567 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/DiagnosticMessageSinkActor.cs @@ -0,0 +1,208 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Linq; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using DiagnosticMessage = Xunit.DiagnosticMessage; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// implementation that logs all of its output directly to the . + /// + /// Has no persistence capabilities. Can optionally use a to provide total "end of test" reporting. + /// + internal class DiagnosticMessageSinkActor : TestCoordinatorEnabledMessageSink + { + private readonly Xunit.Abstractions.IMessageSink _diagnosticSink; + + public DiagnosticMessageSinkActor( + Xunit.Abstractions.IMessageSink diagnosticSink, + bool useTestCoordinator) : base(useTestCoordinator) + { + _diagnosticSink = diagnosticSink; + } + + #region Message handling + + protected override void AdditionalReceives() + { + Receive(data => ReceiveFactData(data)); + } + + protected override void ReceiveFactData(FactData data) + { + PrintSpecRunResults(data); + } + + private void PrintSpecRunResults(FactData data) + { + WriteSpecMessage($"Results for {data.FactName}"); + WriteSpecMessage($"Start time: {new DateTime(data.StartTime, DateTimeKind.Utc)}"); + foreach (var node in data.NodeFacts) + { + WriteSpecMessage( + $" --> Node {node.Value.NodeIndex}:{node.Value.NodeRole} : {(node.Value.Passed.GetValueOrDefault(false) ? "PASS" : "FAIL")} [{node.Value.Elapsed} elapsed]"); + } + WriteSpecMessage( + $"End time: {new DateTime(data.EndTime.GetValueOrDefault(DateTime.UtcNow.Ticks), DateTimeKind.Utc)}"); + WriteSpecMessage( + $"FINAL RESULT: {(data.Passed.GetValueOrDefault(false) ? "PASS" : "FAIL")} after {data.Elapsed}."); + + //If we had a failure + if (data.Passed.GetValueOrDefault(false) == false) + { + WriteSpecMessage("Failure messages by Node"); + foreach (var node in data.NodeFacts) + { + if (node.Value.Passed.GetValueOrDefault(false) == false) + { + WriteSpecMessage($"<----------- BEGIN NODE {node.Key}:{node.Value.NodeRole} ----------->"); + foreach (var resultMessage in node.Value.ResultMessages) + { + WriteSpecMessage($" --> {resultMessage.Message}"); + } + if (node.Value.ResultMessages == null || node.Value.ResultMessages.Count == 0) + WriteSpecMessage("[received no messages - SILENT FAILURE]."); + WriteSpecMessage($"<----------- END NODE {node.Key}:{node.Value.NodeRole} ----------->"); + } + } + } + } + + protected override void HandleNodeSpecFail(NodeCompletedSpecWithFail nodeFail) + { + WriteSpecFail(nodeFail.NodeIndex, nodeFail.NodeRole, nodeFail.Message); + + base.HandleNodeSpecFail(nodeFail); + } + + protected override void HandleTestRunEnd(EndTestRun endTestRun) + { + WriteSpecMessage("Test run complete."); + + base.HandleTestRunEnd(endTestRun); + } + + protected override void HandleTestRunTree(TestRunTree tree) + { + var passedSpecs = tree.Specs.Count(x => x.Passed.GetValueOrDefault(false)); + WriteSpecMessage( + $"Test run completed in [{tree.Elapsed}] with {passedSpecs}/{tree.Specs.Count()} specs passed."); + foreach (var factData in tree.Specs) + { + PrintSpecRunResults(factData); + } + } + + protected override void HandleNewSpec(BeginNewSpec newSpec) + { + WriteSpecMessage($"Beginning spec {newSpec.ClassName}.{newSpec.MethodName} on {newSpec.Nodes.Count} nodes"); + + base.HandleNewSpec(newSpec); + } + + protected override void HandleEndSpec(EndSpec endSpec) + { + WriteSpecMessage("Spec completed."); + + base.HandleEndSpec(endSpec); + } + + protected override void HandleNodeMessageFragment(LogMessageFragmentForNode logMessage) + { + WriteNodeMessage(logMessage); + + base.HandleNodeMessageFragment(logMessage); + } + + protected override void HandleRunnerMessage(LogMessageForTestRunner node) + { + WriteRunnerMessage(node); + + base.HandleRunnerMessage(node); + } + + protected override void HandleNodeSpecPass(NodeCompletedSpecWithSuccess nodeSuccess) + { + WriteSpecPass(nodeSuccess.NodeIndex, nodeSuccess.NodeRole, nodeSuccess.Message); + + base.HandleNodeSpecPass(nodeSuccess); + } + + #endregion + + #region Diagnostic output methods + + /// + /// Used to print a spec status message (spec starting, finishing, failed, etc...) + /// + private void WriteSpecMessage(string message) + { + _diagnosticSink?.OnMessage(new DiagnosticMessage( + "[RUNNER][{0}]: {1}", DateTime.UtcNow.ToShortTimeString(), + message)); + } + + private void WriteSpecPass(int nodeIndex, string nodeRole, string message) + { + _diagnosticSink?.OnMessage(new DiagnosticMessage( + "[NODE #{0}({1})][{2}]: SPEC PASSED: {3}", + nodeIndex, + nodeRole, + DateTime.UtcNow.ToShortTimeString(), + message)); + } + + private void WriteSpecFail(int nodeIndex, string nodeRole, string message) + { + _diagnosticSink?.OnMessage(new DiagnosticMessage( + "[NODE{0}:{1}][{2}]: SPEC FAILED: {3}", + nodeIndex, + nodeRole, + DateTime.UtcNow.ToShortTimeString(), + message + )); + } + + private void WriteRunnerMessage(LogMessageForTestRunner nodeMessage) + { + _diagnosticSink?.OnMessage(new DiagnosticMessage(nodeMessage.ToString())); + } + + private void WriteNodeMessage(LogMessageFragmentForNode nodeMessage) + { + _diagnosticSink?.OnMessage(new DiagnosticMessage(nodeMessage.ToString())); + } + #endregion + } + + /// + /// implementation that writes directly to the console. + /// + internal class DiagnosticMessageSink : MessageSink + { + private readonly Xunit.Abstractions.IMessageSink _diagnosticSink; + + public DiagnosticMessageSink( + Xunit.Abstractions.IMessageSink diagnosticSink) + : base(Props.Create(() => new DiagnosticMessageSinkActor(diagnosticSink, true))) + { + _diagnosticSink = diagnosticSink; + } + + protected override void HandleUnknownMessageType(string message) + { + _diagnosticSink?.OnMessage(new DiagnosticMessage( + "[RUNNER][{0}]: Unknown message: {1}", + DateTime.UtcNow.ToShortTimeString(), + message)); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/FileSystemMessageSinkActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/FileSystemMessageSinkActor.cs new file mode 100644 index 00000000000..edfebd3a3cb --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/FileSystemMessageSinkActor.cs @@ -0,0 +1,96 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.IO; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Persistence; +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// A file system implementation + /// + internal class FileSystemMessageSink : MessageSink + { + public FileSystemMessageSink(string assemblyName, string platform) + : this( + Props.Create( + () => + new FileSystemMessageSinkActor(new JsonPersistentTestRunStore(), + FileNameGenerator.GenerateFileName(assemblyName, platform, ".json"), + true, + true))) + { + + } + + public FileSystemMessageSink(Props messageSinkActorProps) : base(messageSinkActorProps) + { + } + + protected override void HandleUnknownMessageType(string message) + { + //do nothing + } + } + + /// + /// responsible for writing to the file system. + /// + internal class FileSystemMessageSinkActor : TestCoordinatorEnabledMessageSink + { + protected IPersistentTestRunStore FileStore; + protected string FileName; + private readonly bool _reportStatus; + + public FileSystemMessageSinkActor(IPersistentTestRunStore store, string fileName, bool reportStatus, bool useTestCoordinator) + : base(useTestCoordinator) + { + FileStore = store; + FileName = fileName; + _reportStatus = reportStatus; + } + + protected override void AdditionalReceives() + { + Receive(data => ReceiveFactData(data)); + } + + protected override void HandleTestRunTree(TestRunTree tree) + { + var filePath = Path.GetFullPath(FileName); + + // Create output dir if not exists + var dir = new DirectoryInfo(Path.GetDirectoryName(filePath)); + if (!dir.Exists) + dir.Create(); + + if (_reportStatus) + Console.WriteLine("Writing test state to: {0}", filePath); + try + { + FileStore.SaveTestRun(FileName, tree); + } + catch (Exception ex) //avoid throwing exception back to parent - just continue + { + if (_reportStatus) + Console.WriteLine("Failed to write test state to {0}. Cause: {1}", filePath, ex); + } + if (_reportStatus) + Console.WriteLine("Finished."); + } + + protected override void ReceiveFactData(FactData data) + { + //Ask the TestRunCoordinator to give us the latest state + Sender.Tell(new TestRunCoordinator.RequestTestRunState()); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/IMessageSink.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/IMessageSink.cs new file mode 100644 index 00000000000..bb9c7309c11 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/IMessageSink.cs @@ -0,0 +1,92 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// Interface used to define destinations for MultiNodeTest messages + /// + internal interface IMessageSink + { + + #region Flow Control + + /// + /// Make this ready for business. + /// + /// Typically called at the beginning of a test run. + /// + Task Open(ActorSystem context); + + /// + /// Flag that determines if has been successfully called or not. + /// + bool IsOpen { get; } + + /// + /// Flag that determines if has been successfully called or not. + /// + bool IsClosed { get; } + + /// + /// Shut down the instance. + /// + /// Typically called at the end of a test run. + /// + /// During instances of when a test run has been successfully started, this method + /// will wait up to 10 seconds for any instances included as part of this + /// to shutdown, via the method. + /// + Task Close(ActorSystem context); + + #endregion + + #region Message Handling + + /// + /// Report that the test runner is moving onto the next test in the testsuite. + /// + /// + void BeginTest(MultiNodeTestCase testCase); + + /// + /// Report that the test runner is terminating the current test in the suite. + /// + void EndTest(MultiNodeTestCase testCase, SpecLog specLog); + + /// + /// Report that an individual node has passed its test. + /// + /// The Id of the node in the 0-N index. + /// The Role of the node. + /// A string message included with the notification. + void Success(int nodeIndex, string nodeRole, string message); + + /// + /// Report a log message from the MultiNodeTestRunner itself. + /// + /// A string message included with the notification. + /// The source of a log message. + /// The of this message. + void LogRunnerMessage(string message, string logSource, LogLevel level); + + /// + /// Offer a raw message to the message sink. will attempt to parse it + /// and turn it into one of the below parsing calls. + /// + /// A raw log message + void Offer(string messageStr); + + #endregion + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/MessageSink.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/MessageSink.cs new file mode 100644 index 00000000000..35bc644a974 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/MessageSink.cs @@ -0,0 +1,290 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// Abstract base class for all implementations. Includes some methods + /// for parsing log messages into structured formats. + /// + internal abstract class MessageSink : IMessageSink + { + /// + /// ActorRef for the actor who coordinates all of reporting for each test run + /// + protected IActorRef MessageSinkActorRef; + + protected readonly Props MessageSinkActorProps; + + protected MessageSink(Props messageSinkActorProps) + { + MessageSinkActorProps = messageSinkActorProps; + } + + #region Flow Control + + public async Task Open(ActorSystem context) + { + //Do nothing + if(IsClosed || IsOpen) return; + + IsOpen = true; + + //Start the TestCoordinatorActor + MessageSinkActorRef = context.ActorOf(MessageSinkActorProps); + await MessageSinkActorRef.Ask(SinkCoordinator.Ready.Instance); + } + + public bool IsOpen { get; private set; } + public bool IsClosed { get; private set; } + + internal void RequestExitCode(IActorRef sender) + { + MessageSinkActorRef.Tell(new SinkCoordinator.RequestExitCode(), sender); + } + + public async Task Close(ActorSystem context) + { + //Test run has already been closed or hasn't started + if (!IsOpen || IsClosed) return false; + + IsOpen = false; + IsClosed = true; + + //Signal that the test run has ended + return await MessageSinkActorRef.Ask(new EndTestRun()) + .ContinueWith(tr => MessageSinkActorRef.GracefulStop(TimeSpan.FromSeconds(2)), + TaskContinuationOptions.ExecuteSynchronously).Unwrap(); + } + + #endregion + + #region Static methods and constants + + /// + /// Constant used on calls where no message is proceeded by the caller. + /// + public const string NoMessage = "[no message given.]"; + + public enum MultiNodeTestRunnerMessageType + { + RunnerLogMessage, + NodeLogFragment, //for messages that had line breaks (such as stack traces) + NodeLogMessage, + NodePassMessage, + NodeFailMessage, + NodeFailureException, + Unknown + }; + + private const string NodePassStatusRegexString = + @"\[(\w){4}(?[0-9]{1,2})(?:\w+)?\]\[(?(PASS|FAIL))\]{1}\s(?.*)"; + protected static readonly Regex NodePassStatusRegex = new Regex(NodePassStatusRegexString); + + private const string NodePassed = "PASS"; + + private const string NodeFailed = "FAIL"; + + private const string NodeFailureReasonRegexString = + @"\[(\w){4}(?[0-9]{1,2})(?:\w+)?\]\[(?(FAIL-EXCEPTION))\]{1}\s(?.*)"; + protected static readonly Regex NodeFailureReasonRegex = new Regex(NodeFailureReasonRegexString); + + /* + * Regular expressions - go big or go home. [Aaronontheweb] + */ + private const string RunnerLogMessageRegexString = @"\[(?[\w]*)\]\[(?[\d\/ :.Z+-]*)\]\[(?[\w|\s]*)\]\[(?[\[\w:\/\(\)\]\.\$%\+#\^@)-]*)\]\s(?.*)"; + protected static readonly Regex RunnerLogMessageRegex = new Regex(RunnerLogMessageRegexString, RegexOptions.Compiled); + + private const string NodeLogFragmentRegexString = @"\[\w{4}(?[0-9]{1,4})[:]?(?:\w+)?\](?.*)"; + protected static readonly Regex NodeLogFragmentRegex = new Regex(NodeLogFragmentRegexString); + + public static MultiNodeTestRunnerMessageType DetermineMessageType(string messageStr) + { + var matchRunnerLog = RunnerLogMessageRegex.Match(messageStr); + if (matchRunnerLog.Success) return MultiNodeTestRunnerMessageType.RunnerLogMessage; + + var matchFailureReason = NodeFailureReasonRegex.Match(messageStr); + if(matchFailureReason.Success) return MultiNodeTestRunnerMessageType.NodeFailureException; + + var matchStatus = NodePassStatusRegex.Match(messageStr); + if (matchStatus.Success) + { + return matchStatus.Groups["status"].Value.Equals(NodePassed) ? MultiNodeTestRunnerMessageType.NodePassMessage : MultiNodeTestRunnerMessageType.NodeFailMessage; + } + + var nodeLogFragmentStatus = NodeLogFragmentRegex.Match(messageStr); + if(nodeLogFragmentStatus.Success) return MultiNodeTestRunnerMessageType.NodeLogFragment; + + return MultiNodeTestRunnerMessageType.Unknown; + } + + public static bool TryParseLogMessage(string messageStr, out LogMessageFragmentForNode logMessage) + { + var matchLog = NodeLogFragmentRegex.Match(messageStr); + if (!matchLog.Success) + { + logMessage = null; + return false; + } + + var message = matchLog.Groups["message"].Value; + var nodeIndex = Int32.Parse(matchLog.Groups["node"].Value); + var nodeRoleGroup = matchLog.Groups["role"]; + var nodeRole = nodeRoleGroup.Success ? nodeRoleGroup.Value : string.Empty; + logMessage = new LogMessageFragmentForNode(nodeIndex, nodeRole, message, DateTime.UtcNow); + + return true; + } + + public static bool TryParseLogMessage(string messageStr, out LogMessageForTestRunner logMessage) + { + var matchLog = RunnerLogMessageRegex.Match(messageStr); + if (!matchLog.Success) + { + logMessage = null; + return false; + } + + LogLevel logLevel; + Enum.TryParse(matchLog.Groups["level"].Value, true, out logLevel); + + var logSource = matchLog.Groups["logsource"].Value; + var message = matchLog.Groups["message"].Value; + logMessage = new LogMessageForTestRunner(message, logLevel, DateTime.UtcNow, logSource); + + return true; + } + + public static bool TryParseSuccessMessage(string messageStr, out NodeCompletedSpecWithSuccess message) + { + var matchStatus = NodePassStatusRegex.Match(messageStr); + message = null; + if (!matchStatus.Success) return false; + if (!matchStatus.Groups["status"].Value.Equals(NodePassed)) return false; + + var nodeIndex = Int32.Parse(matchStatus.Groups["node"].Value); + var passMessage = matchStatus.Groups["test"].Value + " " + matchStatus.Groups["status"].Value; + var nodeRoleGroup = matchStatus.Groups["role"]; + var nodeRole = nodeRoleGroup.Success ? nodeRoleGroup.Value.Substring(1) : String.Empty; + message = new NodeCompletedSpecWithSuccess(nodeIndex, nodeRole, passMessage); + + return true; + } + + public static bool TryParseFailureMessage(string messageStr, out NodeCompletedSpecWithFail message) + { + var matchStatus = NodePassStatusRegex.Match(messageStr); + message = null; + if (!matchStatus.Success) return false; + if (!matchStatus.Groups["status"].Value.Equals(NodeFailed)) return false; + + var nodeIndex = Int32.Parse(matchStatus.Groups["node"].Value); + var passMessage = matchStatus.Groups["test"].Value + " " + matchStatus.Groups["status"].Value; + var nodeRoleGroup = matchStatus.Groups["role"]; + var nodeRole = nodeRoleGroup.Success ? nodeRoleGroup.Value.Substring(1) : String.Empty; + message = new NodeCompletedSpecWithFail(nodeIndex, nodeRole, passMessage); + + return true; + } + + public static bool TryParseFailureExceptionMessage(string messageStr, out NodeCompletedSpecWithFail message) + { + var matchStatus = NodeFailureReasonRegex.Match(messageStr); + message = null; + if (!matchStatus.Success) return false; + + var nodeIndex = Int32.Parse(matchStatus.Groups["node"].Value); + var failureMessage = matchStatus.Groups["message"].Value; + var nodeRoleGroup = matchStatus.Groups["role"]; + var nodeRole = nodeRoleGroup.Success ? nodeRoleGroup.Value.Substring(1) : String.Empty; + message = new NodeCompletedSpecWithFail(nodeIndex, nodeRole, failureMessage); + + return true; + } + + #endregion + + #region Message Handling + + public void BeginTest(MultiNodeTestCase testCase) + { + //begin the next spec + MessageSinkActorRef.Tell(new BeginNewSpec(testCase)); + } + + public void EndTest(MultiNodeTestCase testCase, SpecLog log) + { + //end the current spec + MessageSinkActorRef.Tell(new EndSpec(testCase, log)); + } + + public void Success(int nodeIndex, string nodeRole, string message) + { + MessageSinkActorRef.Tell(new NodeCompletedSpecWithSuccess(nodeIndex, nodeRole, message ?? NoMessage)); + } + + public void LogRunnerMessage(string message, string logSource, LogLevel level) + { + MessageSinkActorRef.Tell(new LogMessageForTestRunner(message, level, DateTime.UtcNow, logSource)); + } + + public void Offer(string messageStr) + { + switch (DetermineMessageType(messageStr)) + { + case MultiNodeTestRunnerMessageType.Unknown: + HandleUnknownMessageType(messageStr); + return; + + case MultiNodeTestRunnerMessageType.RunnerLogMessage: + if (!TryParseLogMessage(messageStr, out LogMessageForTestRunner runnerLog)) + throw new InvalidOperationException("could not parse test runner log message: " + messageStr); + MessageSinkActorRef.Tell(runnerLog); + return; + + case MultiNodeTestRunnerMessageType.NodePassMessage: + if (!TryParseSuccessMessage(messageStr, out var nodePass)) + throw new InvalidOperationException("could not parse node spec pass message: " + messageStr); + MessageSinkActorRef.Tell(nodePass); + return; + + case MultiNodeTestRunnerMessageType.NodeFailMessage: + if (!TryParseFailureMessage(messageStr, out var nodeFail)) + throw new InvalidOperationException("could not parse node spec fail message: " + messageStr); + MessageSinkActorRef.Tell(nodeFail); + return; + + case MultiNodeTestRunnerMessageType.NodeFailureException: + if (!TryParseFailureExceptionMessage(messageStr, out var nodeFailEx)) + throw new InvalidOperationException("could not parse node spec failure + EXCEPTION message: " + messageStr); + MessageSinkActorRef.Tell(nodeFailEx); + return; + + case MultiNodeTestRunnerMessageType.NodeLogFragment: + case MultiNodeTestRunnerMessageType.NodeLogMessage: + if (!TryParseLogMessage(messageStr, out LogMessageFragmentForNode fragmentLog)) + throw new InvalidOperationException("could not parse test runner log message: " + messageStr); + MessageSinkActorRef.Tell(fragmentLog); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + protected abstract void HandleUnknownMessageType(string message); + + #endregion + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/MessageSinkActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/MessageSinkActor.cs new file mode 100644 index 00000000000..91d9e2aef3a --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/MessageSinkActor.cs @@ -0,0 +1,109 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// Actor responsible for directing the flow of all messages for each test run. + /// + internal abstract class MessageSinkActor : ReceiveActor + { + #region Message classes + + /// + /// Used to signal that the underlying + /// must collect and report its final test run results. + /// + /// Shut down process is ready to begin. + /// + public class BeginSinkTerminate + { + public BeginSinkTerminate(TestRunTree testRun, IActorRef subscriber) + { + Subscriber = subscriber; + TestRun = testRun; + } + + public TestRunTree TestRun { get; private set; } + public IActorRef Subscriber { get; private set; } + } + + /// + /// Signals to that the is ready to be + /// shut down. + /// + public class SinkCanBeTerminated { } + + #endregion + + protected MessageSinkActor() + { + SetReceive(); + } + + /// + /// Use the template method pattern here to force child actors to fill in + /// all handlers for these classes + /// + private void SetReceive() + { + Receive(HandleReady); + Receive(HandleNewSpec); + Receive(HandleEndSpec); + Receive(HandleNodeMessageFragment); + Receive(HandleRunnerMessage); + Receive(HandleNodeSpecPass); + Receive(HandleNodeSpecFail); + Receive(HandleTestRunEnd); + Receive(HandleTestRunTree); + Receive(HandleSinkTerminate); + AdditionalReceives(); + } + + #region Abstract message-handling methods + + /// + /// Used to hook additional methods into the + /// + protected abstract void AdditionalReceives(); + + private void HandleReady(SinkCoordinator.Ready ready) => Sender.Tell(ready); + + protected abstract void HandleNewSpec(BeginNewSpec newSpec); + + protected abstract void HandleEndSpec(EndSpec endSpec); + + /// + /// Used for truncated messages (happens when there's a line break during standard I/O redirection from child nodes) + /// + protected abstract void HandleNodeMessageFragment(LogMessageFragmentForNode logMessageFragment); + + protected abstract void HandleRunnerMessage(LogMessageForTestRunner node); + + protected abstract void HandleNodeSpecPass(NodeCompletedSpecWithSuccess nodeSuccess); + + protected abstract void HandleNodeSpecFail(NodeCompletedSpecWithFail nodeFail); + + protected virtual void HandleTestRunEnd(EndTestRun endTestRun) + { + Self.Tell(new BeginSinkTerminate(null, Sender)); + } + + protected virtual void HandleSinkTerminate(BeginSinkTerminate terminate) + { + terminate.Subscriber.Tell(new SinkCanBeTerminated()); + } + + protected abstract void HandleTestRunTree(TestRunTree tree); + + #endregion + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/Messages.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/Messages.cs new file mode 100644 index 00000000000..32e49f557d5 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/Messages.cs @@ -0,0 +1,160 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.Event; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + #region Message types + + /// + /// Message type for signaling that a new spec is ready to be run + /// + internal class BeginNewSpec + { + public BeginNewSpec(MultiNodeTestCase testCase) + { + TestCase = testCase; + } + + public MultiNodeTestCase TestCase { get; } + + public string ClassName => TestCase.TypeName; + + public string MethodName => TestCase.MethodName; + + public IList Nodes => TestCase.Nodes; + } + + /// + /// Message type for indicating that the current spec has ended. + /// + internal class EndSpec + { + public EndSpec() + { } + + public EndSpec(MultiNodeTestCase testCase, SpecLog log) + { + TestCase = testCase; + Log = log; + } + + public MultiNodeTestCase TestCase { get; } + + public string ClassName => TestCase?.TypeName; + public string MethodName => TestCase?.MethodName; + public SpecLog Log { get; } + } + + /// + /// Message type for signaling that a node has completed a spec successfully + /// + public class NodeCompletedSpecWithSuccess + { + public NodeCompletedSpecWithSuccess(int nodeIndex, string nodeRole, string message) + { + Message = message; + NodeIndex = nodeIndex; + NodeRole = nodeRole; + } + + public int NodeIndex { get; private set; } + + public string NodeRole { get; private set; } + + public string Message { get; private set; } + } + + /// + /// Message type for signaling that a node has completed a spec unsuccessfully + /// + public class NodeCompletedSpecWithFail + { + public NodeCompletedSpecWithFail(int nodeIndex, string nodeRole, string message) + { + Message = message; + NodeIndex = nodeIndex; + NodeRole = nodeRole; + } + + public int NodeIndex { get; private set; } + + public string NodeRole { get; private set; } + + public string Message { get; private set; } + } + + /// + /// Truncated message - cut off from it's parent due to line break in I/O redirection + /// + public class LogMessageFragmentForNode + { + public LogMessageFragmentForNode(int nodeIndex, string nodeRole, string message, DateTime when) + { + NodeIndex = nodeIndex; + NodeRole = nodeRole; + Message = message; + When = when; + } + + public int NodeIndex { get; private set; } + public string NodeRole { get; private set; } + + public DateTime When { get; private set; } + + public string Message { get; private set; } + + public override string ToString() + { + return string.Format("[NODE{1}:{2}][{0}]: {3}", When, NodeIndex, NodeRole, Message); + } + } + + /// + /// Message for an individual node participating in a spec + /// + public class LogMessageForTestRunner + { + public LogMessageForTestRunner(string message, LogLevel level, DateTime when, string logSource) + { + LogSource = logSource; + When = when; + Level = level; + Message = message; + } + + public DateTime When { get; private set; } + + public string Message { get; private set; } + + public string LogSource { get; private set; } + + public LogLevel Level { get; private set; } + + public override string ToString() + { + return string.Format("[RUNNER][{0}][{1}][{2}]: {3}", When, + Level.ToString().Replace("Level", "").ToUpperInvariant(), LogSource, + Message); + } + } + + + /// + /// Message used to signal the end of the test run. + /// + public class EndTestRun + { + + } + + #endregion +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/SinkCoordinator.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/SinkCoordinator.cs new file mode 100644 index 00000000000..b1d4135a569 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/SinkCoordinator.cs @@ -0,0 +1,232 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Event; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// Top-level actor responsible for managing all instances. + /// + internal class SinkCoordinator : ReceiveActor + { + #region Message classes + + public sealed class Ready + { + public static readonly Ready Instance = new(); + private Ready() { } + } + + /// + /// Used to signal that we need to enable a given instance + /// + internal class EnableSinks + { + public static readonly EnableSinks Instance = new(); + private EnableSinks() { } + } + + /// + /// Test run is complete. Shut down all sinks. + /// + /// NOTE: Sending this message also means that the will be shut down. + /// + public class CloseAllSinks { } + + /// + /// Confirms that a has been closed + /// + public class SinkClosed { } + + /// + /// Case class for distinguishing runner messages + /// + public class RunnerMessage + { + public RunnerMessage(string message) + { + Message = message; + } + + public string Message { get; private set; } + } + + /// + /// Message that the will pass onto a + /// + public class RequestExitCode { } + + /// + /// Response sent to + /// + public class RecommendedExitCode + { + public RecommendedExitCode(int code) + { + Code = code; + } + + public int Code { get; private set; } + } + + #endregion + + private bool _ready; + + protected readonly List DefaultSinks; + protected readonly List Sinks = new List(); + + protected int TotalReceiveClosedConfirmations = 0; + protected int ReceivedSinkCloseConfirmations = 0; + + protected ILoggingAdapter Log { get; } + + /// + /// Leave the console message sink enabled by default + /// + public SinkCoordinator() + : this(new[] { new ConsoleMessageSink() }) + { + Log = Context.GetLogger(); + } + + public SinkCoordinator(IEnumerable defaultSinks) + { + DefaultSinks = defaultSinks.ToList(); + InitializeReceives(); + } + + #region Actor lifecycle + + protected override void PreStart() + { + Self.Tell(EnableSinks.Instance); + } + + #endregion + + #region Message-handling + + private void InitializeReceives() + { + ReceiveAsync(async _ => + { + foreach (var sink in DefaultSinks) + { + Sinks.Add(sink); + await sink.Open(Context.System); + } + + _ready = true; + }); + + Receive(r => + { + if (!_ready) + // If we're not yet ready, requeue the message + Self.Tell(r, Sender); + else + Sender.Tell(r); + }); + + Receive(closed => + { + ReceivedSinkCloseConfirmations++; + + //Shut down the ActorSystem if all confirmations have been received + if (ReceivedSinkCloseConfirmations >= TotalReceiveClosedConfirmations) + Context.System.Terminate(); + }); + + Receive(code => + { + ExitCodeContainer.ExitCode = code.Code; + }); + + Receive(sinks => + { + //Ignore duplicate CloseAllSinks calls + if (TotalReceiveClosedConfirmations > 0) return; + + TotalReceiveClosedConfirmations = Sinks.Count; + ReceivedSinkCloseConfirmations = 0; + + foreach (var sink in Sinks) + { + sink.RequestExitCode(Self); + sink.Close(Context.System) + .ContinueWith(r => new SinkClosed(), + TaskContinuationOptions.ExecuteSynchronously) + .PipeTo(Self); + } + }); + Receive(PublishToChildren); + Receive(PublishToChildren); + Receive(BeginSpec); + Receive(spec => EndSpec(spec.TestCase, spec.Log)); + Receive(PublishToChildren); + } + + private void PublishToChildren(NodeCompletedSpecWithSuccess message) + { + foreach(var sink in Sinks) + sink.Success(message.NodeIndex, message.NodeRole, message.Message); + } + + + private void EndSpec(MultiNodeTestCase testCase, SpecLog specLog) + { + foreach (var sink in Sinks) + sink.EndTest(testCase, specLog); + } + + private void BeginSpec(MultiNodeTestCase testCase) + { + foreach (var sink in Sinks) + sink.BeginTest(testCase); + } + + private void PublishToChildren(RunnerMessage message) + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + foreach (var sink in Sinks) + { + sink.LogRunnerMessage(message.Message, assembly.GetName().Name, LogLevel.InfoLevel); + } + } + + /// + /// Publish a message to all instances. + /// + private void PublishToChildren(string message) + { + foreach (var sink in Sinks) + { + try + { + sink.Offer(message); + } + catch (Exception e) + { + // This message might never make it to console, due to the way dotnet test is being set, + // but at least this catch would not cause the SinkCoordinator to die mid test because of an exception + Log.Error(e, "Sink {0} failed to process message {1}: {2}", sink.GetType(), message, e.Message); + } + } + } + + #endregion + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/Spec.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/Spec.cs new file mode 100644 index 00000000000..c783a31ff20 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/Spec.cs @@ -0,0 +1,84 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Text; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// Message class used for reporting a test pass. + /// + /// + /// The Akka.MultiNode.Shared.MessageSink depends on the format string + /// that this class produces, so do not remove or refactor it. + /// + /// + internal class SpecPass + { + public SpecPass(int nodeIndex, string nodeRole, string testDisplayName) + { + TestDisplayName = testDisplayName; + NodeIndex = nodeIndex; + NodeRole = nodeRole; + } + + public int NodeIndex { get; private set; } + public string NodeRole { get; private set; } + + public string TestDisplayName { get; private set; } + + public override string ToString() + { + return string.Format("[Node{0}:{1}][PASS] {2}", NodeIndex, NodeRole, TestDisplayName); + } + } + + /// + /// Message class used for reporting a test fail. + /// + /// + /// The Akka.MultiNode.Shared.MessageSink depends on the format string + /// that this class produces, so do not remove or refactor it. + /// + /// + internal class SpecFail : SpecPass + { + public SpecFail(int nodeIndex, string nodeRole, string testDisplayName) : base(nodeIndex, nodeRole, testDisplayName) + { + FailureMessages = new List(); + FailureStackTraces = new List(); + FailureExceptionTypes = new List(); + } + + public IList FailureMessages { get; private set; } + public IList FailureStackTraces { get; private set; } + public IList FailureExceptionTypes { get; private set; } + + public override string ToString() + { + var sb = new StringBuilder(); + sb.AppendLine(string.Format("[Node{0}:{1}][FAIL] {2}", NodeIndex, NodeRole, TestDisplayName)); + foreach (var exception in FailureExceptionTypes) + { + sb.AppendFormat("[Node{0}:{1}][FAIL-EXCEPTION] Type: {2}", NodeIndex, NodeRole, exception); + sb.AppendLine(); + } + foreach (var exception in FailureMessages) + { + sb.AppendFormat("--> [Node{0}:{1}][FAIL-EXCEPTION] Message: {2}", NodeIndex, NodeRole, exception); + sb.AppendLine(); + } + foreach (var exception in FailureStackTraces) + { + sb.AppendFormat("--> [Node{0}:{1}][FAIL-EXCEPTION] StackTrace: {2}", NodeIndex, NodeRole, exception); + sb.AppendLine(); + } + return sb.ToString(); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/SpecLog.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/SpecLog.cs new file mode 100644 index 00000000000..d692952d853 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/SpecLog.cs @@ -0,0 +1,32 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2019 Lightbend Inc. +// // Copyright (C) 2013-2019 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// SpecLog + /// + internal class SpecLog + { + /// + /// Aggregated timeline logs for all notes in spec + /// + public List AggregatedTimelineLog { get; set; } + /// + /// Timelines per each node + /// + public List<(int NodeIndex, string NodeRole, List Log)> NodeLogs { get; set; } + + public static SpecLog Empty => new SpecLog() + { + AggregatedTimelineLog = new List(), + NodeLogs = new List<(int NodeIndex, string NodeRole, List Log)>() + }; + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TeamCityMessageSinkActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TeamCityMessageSinkActor.cs new file mode 100644 index 00000000000..4066d6cd4a4 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TeamCityMessageSinkActor.cs @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using JetBrains.TeamCity.ServiceMessages.Write.Special; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + internal class TeamCityMessageSinkActor : TestCoordinatorEnabledMessageSink + { + private readonly ITeamCityWriter _teamCityWriter; + private readonly ITeamCityTestsSubWriter _teamCityTestSuiteWriter; + + private ITeamCityTestsSubWriter _teamCityFlowWriter; + private ITeamCityTestWriter _teamCityTestWriter; + + public TeamCityMessageSinkActor(Action writer, string suiteName, + bool useTestCoordinator) : base(useTestCoordinator) + { + _teamCityWriter = new TeamCityServiceMessages().CreateWriter(writer); + _teamCityTestSuiteWriter = _teamCityWriter.OpenTestSuite(suiteName); + } + + protected override void AdditionalReceives() + { + } + + protected override void HandleTestRunTree(TestRunTree tree) + { + } + + protected override void ReceiveFactData(FactData data) + { + } + + protected override void HandleNewSpec(BeginNewSpec beginNewSpec) + { + _teamCityFlowWriter = _teamCityTestSuiteWriter.OpenFlow(); + _teamCityTestWriter = _teamCityFlowWriter.OpenTest($"{beginNewSpec.ClassName}.{beginNewSpec.MethodName}"); + + base.HandleNewSpec(beginNewSpec); + } + + protected override void HandleRunnerMessage(LogMessageForTestRunner node) + { + _teamCityTestWriter?.WriteStdOutput(node.Message); + + base.HandleRunnerMessage(node); + } + + protected override void HandleNodeMessageFragment(LogMessageFragmentForNode logMessage) + { + _teamCityTestWriter?.WriteStdOutput(logMessage.Message); + + base.HandleNodeMessageFragment(logMessage); + } + + protected override void HandleNodeSpecPass(NodeCompletedSpecWithSuccess nodeSuccess) + { + Console.ForegroundColor = ConsoleColor.Green; + _teamCityTestWriter?.WriteStdOutput( + $"[NODE{nodeSuccess.NodeIndex}:{nodeSuccess.NodeRole}][{DateTime.UtcNow.ToShortTimeString()}]: SPEC PASSED: {nodeSuccess.Message}"); + Console.ResetColor(); + + base.HandleNodeSpecPass(nodeSuccess); + } + + protected override void HandleNodeSpecFail(NodeCompletedSpecWithFail nodeFail) + { + Console.ForegroundColor = ConsoleColor.Red; + _teamCityTestWriter?.WriteFailed( + $"[NODE{nodeFail.NodeIndex}:{nodeFail.NodeRole}][{DateTime.UtcNow.ToShortTimeString()}]: SPEC FAILED: {nodeFail.Message}", ""); + Console.ResetColor(); + + base.HandleNodeSpecFail(nodeFail); + } + + protected override void HandleEndSpec(EndSpec endSpec) + { + _teamCityTestWriter?.Dispose(); + _teamCityFlowWriter?.Dispose(); + + base.HandleEndSpec(endSpec); + } + } + + /// + /// implementation that writes directly to the console. + /// + internal class TeamCityMessageSink : MessageSink + { + public TeamCityMessageSink(Action writer, string suiteName) + : base(Props.Create(() => new TeamCityMessageSinkActor(writer, suiteName, true))) + { + } + + protected override void HandleUnknownMessageType(string message) + { + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine("Unknown message: {0}", message); + Console.ResetColor(); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TestCoordinatorEnabledMessageSink.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TestCoordinatorEnabledMessageSink.cs new file mode 100644 index 00000000000..7804494209f --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TestCoordinatorEnabledMessageSink.cs @@ -0,0 +1,138 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Reporting; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + /// + /// A implementation that is capable of using a for + /// test run summaries and other purposes. + /// + internal abstract class TestCoordinatorEnabledMessageSink : MessageSinkActor + { + protected IActorRef TestCoordinatorActorRef; + protected bool UseTestCoordinator; + + protected TestCoordinatorEnabledMessageSink(bool useTestCoordinator) + { + UseTestCoordinator = useTestCoordinator; + Receive(code => + { + if (UseTestCoordinator) + { + var sender = Sender; + TestCoordinatorActorRef.Ask(new TestRunCoordinator.RequestTestRunState()) + .ContinueWith(task => + { + return new SinkCoordinator.RecommendedExitCode(task.Result.Passed.GetValueOrDefault(false) + ? 0 + : 1); + }, TaskContinuationOptions.ExecuteSynchronously) + .PipeTo(sender, Self); + } + }); + } + + protected override void PreStart() + { + //Fire up a TestRunCoordinator instance and subscribe to FactData messages when they arrive + if (UseTestCoordinator) + { + TestCoordinatorActorRef = Context.ActorOf(); + TestCoordinatorActorRef.Tell(new TestRunCoordinator.SubscribeFactCompletionMessages(Self)); + } + } + + protected abstract void ReceiveFactData(FactData data); + + protected override void HandleNewSpec(BeginNewSpec newSpec) + { + if (UseTestCoordinator) + { + TestCoordinatorActorRef.Tell(newSpec); + } + } + + protected override void HandleEndSpec(EndSpec endSpec) + { + if (UseTestCoordinator) + { + TestCoordinatorActorRef.Tell(endSpec); + } + } + + protected override void HandleNodeMessageFragment(LogMessageFragmentForNode logMessage) + { + if (UseTestCoordinator) + { + var nodeMessage = new MultiNodeLogMessageFragment(logMessage.When.Ticks, logMessage.Message, + logMessage.NodeIndex, logMessage.NodeRole); + + TestCoordinatorActorRef.Tell(nodeMessage); + } + } + + protected override void HandleRunnerMessage(LogMessageForTestRunner node) + { + if (UseTestCoordinator) + { + var runnerMessage = new MultiNodeTestRunnerMessage(node.When.Ticks, node.Message, node.LogSource, + node.Level); + + TestCoordinatorActorRef.Tell(runnerMessage); + } + } + + protected override void HandleNodeSpecPass(NodeCompletedSpecWithSuccess nodeSuccess) + { + if (UseTestCoordinator) + { + var nodeMessage = new MultiNodeResultMessage(DateTime.UtcNow.Ticks, nodeSuccess.Message, + nodeSuccess.NodeIndex, nodeSuccess.NodeRole, true); + + TestCoordinatorActorRef.Tell(nodeMessage); + } + } + + protected override void HandleNodeSpecFail(NodeCompletedSpecWithFail nodeFail) + { + if (UseTestCoordinator) + { + var nodeMessage = new MultiNodeResultMessage(DateTime.UtcNow.Ticks, nodeFail.Message, + nodeFail.NodeIndex, nodeFail.NodeRole, false); + + TestCoordinatorActorRef.Tell(nodeMessage); + } + } + + protected override void HandleTestRunEnd(EndTestRun endTestRun) + { + if (UseTestCoordinator) + { + var sender = Sender; + TestCoordinatorActorRef.Ask(endTestRun) + .ContinueWith(tr => + { + var testRunTree = tr.Result; + return new BeginSinkTerminate(testRunTree, sender); + }, TaskContinuationOptions.ExecuteSynchronously) + .PipeTo(Self); + } + } + + protected override void HandleSinkTerminate(BeginSinkTerminate terminate) + { + HandleTestRunTree(terminate.TestRun); + base.HandleSinkTerminate(terminate); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TimelineLogCollectorActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TimelineLogCollectorActor.cs new file mode 100644 index 00000000000..230c0b7c5f4 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/Sinks/TimelineLogCollectorActor.cs @@ -0,0 +1,233 @@ +// //----------------------------------------------------------------------- +// // +// // Copyright (C) 2009-2019 Lightbend Inc. +// // Copyright (C) 2013-2019 .NET Foundation +// // +// //----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Akka.Actor; +using Akka.Event; + +namespace Akka.MultiNode.TestAdapter.Internal.Sinks +{ + internal class TimelineLogCollectorActor : ReceiveActor + { + private readonly bool _appendLogOutput; + private readonly SortedList> _timeline = new SortedList>(); + + public TimelineLogCollectorActor(bool appendLogOutput) + { + _appendLogOutput = appendLogOutput; + + Receive(msg => + { + var parsedInfo = new LogMessageInfo(msg); + if (_timeline.ContainsKey(parsedInfo.When)) + _timeline[parsedInfo.When].Add(parsedInfo); + else + _timeline.Add(parsedInfo.When, new HashSet() { parsedInfo }); + }); + + Receive(_ => Sender.Tell(_timeline.Values.ToList())); + + Receive(_ => + { + var log = new SpecLog() + { + AggregatedTimelineLog = _timeline.Select(pairs => pairs.Value).SelectMany(msg => msg).Select(m => m.ToString()).ToList(), + NodeLogs = _timeline.Select(pairs => pairs.Value).SelectMany(msg => msg).GroupBy(msg => msg.Node).Select(nodeMessages => + { + var node = nodeMessages.Key; + return (NodeIndex: node.Index, NodeRole: node.Role, Logs: nodeMessages.Select(m => m.ToString()).ToList()); + }).ToList() + }; + + Sender.Tell(log); + }); + + Receive(_ => + { + Sender.Tell(_timeline.Select(pairs => pairs.Value).SelectMany(msg => msg).Select(m => m.ToString()).ToArray()); + }); + + Receive(dump => + { + // Verify that directory exists + var dir = new DirectoryInfo(Path.GetDirectoryName(dump.FilePath)); + if (!dir.Exists) + dir.Create(); + + var lines = + _timeline.Select(pairs => pairs.Value).SelectMany(msg => msg).Select(m => m.ToString()).ToArray(); + bool dumpSuccess; + do + { + try + { + if(!_appendLogOutput && File.Exists(dump.FilePath)) + File.Delete(dump.FilePath); + + File.AppendAllLines(dump.FilePath, lines); + dumpSuccess = true; + } + catch + { + dumpSuccess = false; + } + } while (!dumpSuccess); + + Sender.Tell(Done.Instance); + }); + + Receive(_ => + { + var logsPerTest = _timeline + .Select(pairs => pairs.Value) + .SelectMany(msg => msg) + .GroupBy(m => m.Node.TestName); + + var sb = new StringBuilder(); + foreach (var testLogs in logsPerTest) + { + Console.WriteLine($"Detailed logs for {testLogs.Key}\n"); + foreach (var log in testLogs) + { + var logLine = log.ToString(); + Console.WriteLine(logLine); + sb.AppendLine(logLine); + } + Console.WriteLine($"\nEnd logs for {testLogs.Key}\n"); + } + + Sender.Tell(sb.ToString()); + }); + } + + public class LogMessageInfo + { + public NodeInfo Node { get; } + public string OriginalMessage { get; } + public DateTime When { get; } + public LogLevel LogLevel { get; } + public string Message { get; } + + public LogMessageInfo(LogMessage msg) + { + OriginalMessage = msg.Message; + Node = msg.Node; + When = DateTime.UtcNow; + LogLevel = LogLevel.InfoLevel; // In case if we could not find log level, assume that it is Info + Message = OriginalMessage; + + var pieces = Regex.Matches(msg.Message, @"\[([^\]]+)\]"); + foreach (Match piece in pieces) + { + Message = Message.Replace(piece.Value, ""); + + if (DateTime.TryParse(piece.Value, CultureInfo.CurrentCulture, DateTimeStyles.None, out var when)) + When = when; + + if (TryParseLogLevel(piece.Value, out var logLevel)) + LogLevel = logLevel; + } + } + + public override string ToString() + { + return $"[Node #{Node.Index}({Node.Role})]{OriginalMessage}"; + } + + private bool TryParseLogLevel(string str, out LogLevel logLevel) + { + var enumValues = Enum.GetValues(typeof(LogLevel)).Cast().ToList(); + foreach (var logLevelInfo in Enum.GetNames(typeof(LogLevel)).Select((name, i) => (Name: name, Index: i))) + { + if (string.Equals(str + "Level", logLevelInfo.Name, StringComparison.OrdinalIgnoreCase)) + { + logLevel = enumValues[logLevelInfo.Index]; + return true; + } + } + + logLevel = default(LogLevel); + return false; + } + } + + public class NodeInfo : IEquatable + { + public NodeInfo(int index, string role, string platform, string testName) + { + Index = index; + Role = role; + Platform = platform; + TestName = testName; + } + + public int Index { get; } + public string Role { get; } + public string Platform { get; } + public string TestName { get; set; } + + /// + public bool Equals(NodeInfo other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Index == other.Index; + } + + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((NodeInfo)obj); + } + + /// + public override int GetHashCode() + { + return Index; + } + } + + public class LogMessage + { + public LogMessage(NodeInfo node, string message) + { + Node = node; + Message = message; + } + + public NodeInfo Node { get; } + public string Message { get; } + } + + public class SendMeAll { } + + public class PrintToConsole { } + + public class GetSpecLog { } + + public class GetLog { } + + public class DumpToFile + { + public DumpToFile(string filePath) + { + FilePath = filePath; + } + + public string FilePath { get; } + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TcpLoggingServer.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TcpLoggingServer.cs new file mode 100644 index 00000000000..aea4f150e01 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TcpLoggingServer.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Immutable; +using System.Net; +using Akka.Actor; +using Akka.Event; +using Akka.IO; +using Akka.Util; +using Akka.Util.Internal; + +namespace Akka.MultiNode.TestAdapter.Internal +{ + internal class TcpLoggingServer : ReceiveActor + { + private readonly ILoggingAdapter _log = Context.GetLogger(); + private IActorRef _tcpManager = Nobody.Instance; + private IActorRef _abortSender; + + private Option _boundPort; + private IImmutableSet _boundPortSubscribers = ImmutableHashSet.Empty; + + private string _buffer = string.Empty; + + public TcpLoggingServer(IActorRef sinkCoordinator) + { + Receive(bound => + { + // When bound, save port and notify requestors if any + _boundPort = ((IPEndPoint)bound.LocalAddress).Port; + _boundPortSubscribers.ForEach(s => s.Tell(_boundPort.Value)); + + _tcpManager = Sender; + }); + + Receive(_ => + { + // If bound port is not received yet, just save subscriber and send respose later + if (_boundPort.HasValue) + Sender.Tell(_boundPort.Value); + else + _boundPortSubscribers = _boundPortSubscribers.Add(Sender); + }); + + Receive(connected => + { + _log.Info($"Node connected on {Sender}"); + Sender.Tell(new Tcp.Register(Self)); + }); + + Receive( + closed => _log.Info($"Node disconnected on {Sender}{Environment.NewLine}")); + + Receive(received => + { + // It should be unlikely that a single stack trace be bigger than 10 Kib, + // but we should buffer this anyway, just in case. + // + // An edge case would be when a message is __exactly__ 10240 bytes in size, + // but that should be extremely unlikely + if (received.Data.Count >= MultiNodeTestCaseRunner.TcpBufferSize) + { + _buffer += received.Data; + return; + } + + sinkCoordinator.Tell(_buffer + received.Data); + _buffer = string.Empty; + }); + + Receive(_ => + { + _abortSender = Sender; + _tcpManager.Tell(Tcp.Unbind.Instance); + }); + Receive(_ => _abortSender.Tell(new ListenerStopped())); + } + + public class StopListener { } + public class ListenerStopped { } + + public class GetBoundPort + { + private GetBoundPort() { } + public static readonly GetBoundPort Instance = new GetBoundPort(); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ErrorInfo.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ErrorInfo.cs new file mode 100644 index 00000000000..db28177667d --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ErrorInfo.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + public class ErrorInfo : ITestEntity + { + public string Message { get; set; } + public string StackTrace { get; set; } + + public XElement Serialize() + { + return Elem("ErrorInfo", + Elem("Message", Text(Message ?? "")), + Elem("StackTrace", Text(StackTrace ?? "")) + ); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ITestEntity.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ITestEntity.cs new file mode 100644 index 00000000000..e241c1a28ac --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ITestEntity.cs @@ -0,0 +1,16 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Xml.Linq; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal interface ITestEntity + { + XElement Serialize(); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Identifier.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Identifier.cs new file mode 100644 index 00000000000..c754a3606cf --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Identifier.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + public struct Identifier + { + public Identifier(Guid value) + { + Value = value; + } + + public static readonly Identifier Empty = Create(Guid.Empty); + + public Guid Value { get; } + + public override string ToString() => Value.ToString("D"); + + public static Identifier Create() => new Identifier(Guid.NewGuid()); + + public static Identifier Create(Guid value) => new Identifier(value); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Output.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Output.cs new file mode 100644 index 00000000000..5c3d0d9d99b --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Output.cs @@ -0,0 +1,42 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + public class Output : ITestEntity + { + public List StdOut { get; } = new List(); + public List StdErr { get; } = new List(); + public List DebugTrace { get; } = new List(); + public ErrorInfo ErrorInfo { get; set; } + public List TextMessages { get; } = new List(); + + public XElement Serialize() + { + XElement TextElem(string element, List lines) => + lines.Count > 0 + ? Elem(element, Text(string.Join(System.Environment.NewLine, lines))) + : null; + + return Elem("Output", + TextElem("StdOut", StdOut), + TextElem("StdErr", StdErr), + TextElem("DebugTrace", DebugTrace), + ErrorInfo, + ElemList( + "TextMessages", + TextMessages.Select(x => Elem("Message", Text(x))) + ) + ); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ResultSummary.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ResultSummary.cs new file mode 100644 index 00000000000..7236049a6e7 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/ResultSummary.cs @@ -0,0 +1,101 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal class ResultSummary : ITestEntity + { + public ResultSummary(IEnumerable unitTests, Output output) + { + Output = output; + + var stats = unitTests + .SelectMany(x => x.Results) + .GroupBy(x => x.Outcome) + .ToDictionary(k => k.Key, v => v.Count()); + + Total = stats.Values.Sum(); + Executed = Total; + + int GetStats(TestOutcome outcome) + { + return stats.TryGetValue(outcome, out var value) ? value : 0; + } + + Passed = GetStats(TestOutcome.Passed); + Failed = GetStats(TestOutcome.Failed); + Error = GetStats(TestOutcome.Error); + Timeout = GetStats(TestOutcome.Timeout); + Aborted = GetStats(TestOutcome.Aborted); + Inconclusive = GetStats(TestOutcome.Inconclusive); + PassedButRunAborted = GetStats(TestOutcome.PassedButRunAborted); + NotRunnable = GetStats(TestOutcome.NotRunnable); + NotExecuted = GetStats(TestOutcome.NotExecuted); + Disconnected = GetStats(TestOutcome.Disconnected); + Warning = GetStats(TestOutcome.Warning); + Completed = GetStats(TestOutcome.Completed); + InProgress = GetStats(TestOutcome.InProgress); + Pending = GetStats(TestOutcome.Pending); + + Outcome = Total == 0 + ? TestOutcome.NotExecuted + : Passed == Total + ? TestOutcome.Passed + : TestOutcome.Failed; + } + + public TestOutcome Outcome { get; } + + public int Total { get; } + public int Executed { get; } + public int Passed { get; } + public int Failed { get; } + public int Error { get; } + public int Timeout { get; } + public int Aborted { get; } + public int Inconclusive { get; } + public int PassedButRunAborted { get; } + public int NotRunnable { get; } + public int NotExecuted { get; } + public int Disconnected { get; } + public int Warning { get; } + public int Completed { get; } + public int InProgress { get; } + public int Pending { get; } + + public Output Output { get; } + + public XElement Serialize() => Elem("ResultSummary", + Attr("outcome", Enum.GetName(typeof(TestOutcome), Outcome)), + Elem("Counters", + Attr("total", Total), + Attr("executed", Executed), + Attr("passed", Passed), + Attr("failed", Failed), + Attr("error", Error), + Attr("timeout", Timeout), + Attr("aborted", Aborted), + Attr("inconclusive", Inconclusive), + Attr("passedButRunAborted", PassedButRunAborted), + Attr("notRunnable", NotRunnable), + Attr("notExecuted", NotExecuted), + Attr("disconnected", Disconnected), + Attr("warning", Warning), + Attr("completed", Completed), + Attr("inProgress", InProgress), + Attr("pending", Pending) + ), + Output + ); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestEntry.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestEntry.cs new file mode 100644 index 00000000000..fa493ae6ca5 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestEntry.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal class TestEntry : ITestEntity + { + public TestEntry(Identifier testId, Identifier executionId, Identifier testListId) + { + TestId = testId; + ExecutionId = executionId; + TestListId = testListId; + } + + public Identifier TestId { get; } + public Identifier ExecutionId { get; } + public Identifier TestListId { get; } + + public XElement Serialize() => Elem("TestEntry", + Attr("testId", TestId), + Attr("executionId", ExecutionId), + Attr("testListId", TestListId) + ); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestList.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestList.cs new file mode 100644 index 00000000000..4c4ed0671ef --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestList.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal class TestList : ITestEntity + { + public TestList(string name) + { + Name = name; + } + + public Identifier Id { get; } = Identifier.Create(); + public string Name { get; } + + public XElement Serialize() => Elem("TestList", + Attr("id", Id), + Attr("name", Name) + ); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestOutcome.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestOutcome.cs new file mode 100644 index 00000000000..7195523af42 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestOutcome.cs @@ -0,0 +1,87 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + public enum TestOutcome + { + /// + /// There was a system error while we were trying to execute a test. + /// + Error, + + /// + /// Test was executed, but there were issues. + /// Issues may involve exceptions or failed assertions. + /// + Failed, + + /// + /// The test timed out + /// + Timeout, + + /// + /// Test was aborted. + /// This was not caused by a user gesture, but rather by a framework decision. + /// + Aborted, + + /// + /// Test has completed, but we can't say if it passed or failed. + /// May be used for aborted tests... + /// + Inconclusive, + + /// + /// Test was executed w/o any issues, but run was aborted. + /// + PassedButRunAborted, + + /// + /// Test had it chance for been executed but was not, as ITestElement.IsRunnable == false. + /// + NotRunnable, + + /// + /// Test was not executed. + /// This was caused by a user gesture - e.g. user hit stop button. + /// + NotExecuted, + + /// + /// Test run was disconnected before it finished running. + /// + Disconnected, + + /// + /// To be used by Run level results. + /// This is not a failure. + /// + Warning, + + /// + /// Test was executed w/o any issues. + /// + Passed, + + /// + /// Test has completed, but there is no qualitative measure of completeness. + /// + Completed, + + /// + /// Test is currently executing. + /// + InProgress, + + /// + /// Test is in the execution queue, was not started yet. + /// + Pending + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestRun.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestRun.cs new file mode 100644 index 00000000000..f9f6b6049e3 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestRun.cs @@ -0,0 +1,67 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal class TestRun : ITestEntity + { + public TestRun(string name) + { + Name = name; + } + + public Identifier Id { get; } = Identifier.Create(); + public string Name { get; } + public string RunUser { get; set; } + public Times Times { get; } = new Times(); + public TestList TestList { get; } = new TestList("Results Not in a List"); + public IList UnitTests { get; } = new List(); + public Output Output { get; set; } + + public void Log(string item) + { + if (Output == null) + { + Output = new Output(); + } + + Output.DebugTrace.Add(item); + Output.StdOut.Add(item); + } + + public UnitTest AddUnitTest(string className, string name, string storage) + { + var unitTest = new UnitTest(className, name, TestList.Id, storage); + UnitTests.Add(unitTest); + + return unitTest; + } + + public XElement Serialize() + { + return Elem("TestRun", + Attr("id", Id), + Attr("name", Name), + Attr("runUser", RunUser), + Times, + // TestSettings + ElemList("Results", UnitTests.SelectMany(x => x.Results)), + ElemList("TestDefinitions", UnitTests), + ElemList("TestEntries", UnitTests.Select(x => new TestEntry(x.Id, x.ExecutionId, x.TestListId))), + Elem("TestLists", + TestList + ), + new ResultSummary(UnitTests, Output) + ); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestSettings.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestSettings.cs new file mode 100644 index 00000000000..ea3edf87896 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/TestSettings.cs @@ -0,0 +1,28 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal class TestSettings : ITestEntity + { + public TestSettings(string name) + { + Name = name; + } + + public Identifier Id { get; } = Identifier.Create(); + public string Name { get; } + + public XElement Serialize() => Elem("TestSettings", + Attr("id", Id), + Attr("name", Name) + ); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Times.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Times.cs new file mode 100644 index 00000000000..d041b5db012 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/Times.cs @@ -0,0 +1,38 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal class Times : ITestEntity + { + public Times() + { + var now = DateTime.UtcNow; + + Creation = now; + Queuing = now; + Start = now; + Finish = now; + } + + public DateTime Creation { get; set; } + public DateTime Queuing { get; set; } + public DateTime Start { get; set; } + public DateTime Finish { get; set; } + + public XElement Serialize() => Elem("Times", + Attr("creation", Creation.ToString("O")), + Attr("queuing", Queuing.ToString("O")), + Attr("start", Start.ToString("O")), + Attr("finish", Finish.ToString("O")) + ); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/UnitTest.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/UnitTest.cs new file mode 100644 index 00000000000..23623a00b76 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/UnitTest.cs @@ -0,0 +1,57 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + public class UnitTest : ITestEntity + { + public UnitTest(string className, string name, Identifier testListId, string storage) + { + ClassName = className; + Name = name; + TestListId = testListId; + Storage = storage; + } + + public Identifier Id { get; } = Identifier.Create(); + public Identifier TestListId { get; } + public Identifier ExecutionId { get; } = Identifier.Create(); + + public string Name { get; } + public string Storage { get; } + public string AdapterTypeName => "executor://xunit/MulitNodeTestRunner"; + public string ClassName { get; } + + public List Results = new List(); + + public UnitTestResult AddResult(string name, string computerName) + { + var result = new UnitTestResult(Id, ExecutionId, TestListId, name, computerName); + Results.Add(result); + return result; + } + + public XElement Serialize() => Elem("UnitTest", + Attr("id", Id), + Attr("name", Name), + Attr("storage", Storage), + Elem("Execution", + Attr("id", ExecutionId) + ), + Elem("TestMethod", + Attr("codeBase", Storage), + Attr("adapterTypeName", AdapterTypeName), + Attr("className", ClassName), + Attr("name", Name) + ) + ); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/UnitTestResult.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/UnitTestResult.cs new file mode 100644 index 00000000000..6ca1ef530bc --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/UnitTestResult.cs @@ -0,0 +1,75 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using static Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models.XmlHelper; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + public class UnitTestResult : ITestEntity + { + public UnitTestResult(Identifier testId, Identifier executionId, Identifier testListId, string testName, string computerName) + { + TestId = testId; + ExecutionId = executionId; + RelativeResultsDirectory = executionId; + TestName = testName; + ComputerName = computerName; + TestListId = testListId; + + var now = DateTime.UtcNow; + StartTime = now; + EndTime = now; + } + + public static readonly Identifier TEST_TYPE = Identifier.Create(new Guid("fc0e28d9-ef63-4031-b8b7-1b8cd96208d4")); + + public Identifier TestId { get; } + public Identifier ExecutionId { get; } + public string TestName { get; } + + public string ComputerName { get; } + public TimeSpan Duration => EndTime - StartTime; + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public Identifier TestType => TEST_TYPE; + public TestOutcome Outcome { get; set; } = TestOutcome.NotExecuted; + public Identifier TestListId { get; } + public Identifier RelativeResultsDirectory { get; } + public List InnerResults { get; } = new List(); + + public Output Output { get; set; } + + public UnitTestResult AddChildResult(string name) + { + var result = new UnitTestResult(TestId, ExecutionId, TestListId, name, ComputerName); + InnerResults.Add(result); + return result; + } + + public XElement Serialize() + { + return Elem("UnitTestResult", + Attr("executionId", ExecutionId), + Attr("testId", TestId), + Attr("testName", TestName), + Attr("computerName", ComputerName), + Attr("duration", Duration.ToString("c")), + Attr("startTime", StartTime.ToString("O")), + Attr("endTime", EndTime.ToString("O")), + Attr("testType", TestType), + Attr("outcome", Enum.GetName(typeof(TestOutcome), Outcome)), + Attr("testListId", TestListId), + Attr("relativeResultsDirectory", RelativeResultsDirectory), + Output, + ElemList("InnerResults", InnerResults) + ); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/XmlHelper.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/XmlHelper.cs new file mode 100644 index 00000000000..9ee6e04e5cf --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/Models/XmlHelper.cs @@ -0,0 +1,67 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models +{ + internal static class XmlHelper + { + public static readonly XNamespace NS = @"http://microsoft.com/schemas/VisualStudio/TeamTest/2010"; + + private static XElement CreateElement(string name, IEnumerable content) + { + var items = new List(); + foreach (var item in content ?? Enumerable.Empty()) + { + if (item == null) + { + continue; + } + + switch (item) + { + case ITestEntity te: + items.Add(te.Serialize()); + break; + + default: + items.Add(item); + break; + } + } + + return new XElement(NS + name, items.ToArray()); + } + + public static XElement Elem(string name, params object[] content) => CreateElement(name, content); + + public static XElement Elem(string name, IEnumerable content) => CreateElement(name, content); + + public static XAttribute Attr(string name, object value) => value == null ? null : new XAttribute(name, value); + + public static XText Text(string value) => string.IsNullOrWhiteSpace(value) ? null : new XText(value); + + private static XElement CreateElementList(string containerElement, IEnumerable items) + { + // ReSharper disable once PossibleMultipleEnumeration + if (items == null || !items.Any()) + { + return null; + } + + // ReSharper disable once PossibleMultipleEnumeration + return CreateElement(containerElement, items.Cast()); + } + + public static XElement ElemList(string containerElement, IEnumerable items) => CreateElementList(containerElement, items); + + public static XElement ElemList(string containerElement, IEnumerable items) => CreateElementList(containerElement, items); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/SpecEvent.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/SpecEvent.cs new file mode 100644 index 00000000000..5dafaa3fc6c --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/SpecEvent.cs @@ -0,0 +1,29 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter +{ + internal class SpecEvent + { + public SpecEvent(DateTime time, T value) + { + Time = time; + Value = value; + } + + public DateTime Time { get; } + public T Value { get; } + + public void Deconstruct(out DateTime time, out T value) + { + time = Time; + value = Value; + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/SpecSession.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/SpecSession.cs new file mode 100644 index 00000000000..0a527fe2361 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/SpecSession.cs @@ -0,0 +1,32 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using Akka.MultiNode.TestAdapter.Internal.Sinks; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter +{ + internal class SpecSession + { + private readonly List> _successes = new List>(); + private readonly List> _fails = new List>(); + private readonly List> _messages = new List>(); + + public SpecEvent Begin { get; private set; } + public IReadOnlyList> Successes => _successes; + public IReadOnlyList> Fails => _fails; + public IReadOnlyList> Messages => _messages; + public SpecEvent End { get; private set; } + + public void OnBegin(BeginNewSpec value) => Begin = new SpecEvent(DateTime.UtcNow, value); + public void OnSuccess(NodeCompletedSpecWithSuccess value) => _successes.Add(new SpecEvent(DateTime.UtcNow, value)); + public void OnFailure(NodeCompletedSpecWithFail value) => _fails.Add(new SpecEvent(DateTime.UtcNow, value)); + public void OnMessage(LogMessageFragmentForNode value) => _messages.Add(new SpecEvent(DateTime.UtcNow, value)); + public void OnEnd(EndSpec value) => End = new SpecEvent(DateTime.UtcNow, value); + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/TrxMessageSink.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/TrxMessageSink.cs new file mode 100644 index 00000000000..3b22e168c7a --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/TrxMessageSink.cs @@ -0,0 +1,30 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Configuration; +using Akka.MultiNode.TestAdapter.Internal.Sinks; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter +{ + internal class TrxMessageSink : MessageSink + { + public TrxMessageSink(string suiteName, MultiNodeTestRunnerOptions options) + : base(Props.Create(() => + new TrxSinkActor(suiteName, Environment.UserName, Environment.MachineName, true, options))) + { + } + + protected override void HandleUnknownMessageType(string message) + { + Console.ForegroundColor = ConsoleColor.DarkYellow; + Console.WriteLine("Unknown message: {0}", message); + Console.ResetColor(); + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/TrxSinkActor.cs b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/TrxSinkActor.cs new file mode 100644 index 00000000000..ecd942457fa --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Internal/TrxReporter/TrxSinkActor.cs @@ -0,0 +1,239 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Akka.MultiNode.TestAdapter.Configuration; +using Akka.MultiNode.TestAdapter.Internal.Reporting; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Akka.MultiNode.TestAdapter.Internal.TrxReporter.Models; + +namespace Akka.MultiNode.TestAdapter.Internal.TrxReporter +{ + internal class TrxSinkActor : TestCoordinatorEnabledMessageSink + { + public TrxSinkActor( + string suiteName, + string userName, + string computerName, + bool useTestCoordinator, + MultiNodeTestRunnerOptions options) + : base(useTestCoordinator) + { + _computerName = computerName; + _testRun = new TestRun(suiteName) + { + RunUser = userName + }; + _options = options; + } + + private readonly string _computerName; + private readonly TestRun _testRun; + private SpecSession _session; + private MultiNodeTestRunnerOptions _options; + + protected override void AdditionalReceives() + { + + } + + protected override void HandleTestRunTree(TestRunTree tree) + { + } + + protected override void ReceiveFactData(FactData data) + { + } + + protected override void HandleEndSpec(EndSpec endSpec) + { + base.HandleEndSpec(endSpec); + + _session.OnEnd(endSpec); + ReportSpec(_session, _testRun, _computerName, endSpec.Log); + + _session = null; + } + + protected override void HandleNewSpec(BeginNewSpec newSpec) + { + base.HandleNewSpec(newSpec); + + _session = new SpecSession(); + _session.OnBegin(newSpec); + } + + protected override void HandleNodeMessageFragment(LogMessageFragmentForNode logMessage) + { + base.HandleNodeMessageFragment(logMessage); + + _session.OnMessage(logMessage); + } + + protected override void HandleNodeSpecFail(NodeCompletedSpecWithFail nodeFail) + { + base.HandleNodeSpecFail(nodeFail); + + _session.OnFailure(nodeFail); + } + + protected override void HandleNodeSpecPass(NodeCompletedSpecWithSuccess nodeSuccess) + { + base.HandleNodeSpecPass(nodeSuccess); + + _session.OnSuccess(nodeSuccess); + } + + protected override void HandleRunnerMessage(LogMessageForTestRunner node) + { + base.HandleRunnerMessage(node); + + _testRun.Log($"{node.When:G} [{node.Level}] {node.LogSource} : {node.Message}"); + } + + protected override void HandleTestRunEnd(EndTestRun endTestRun) + { + base.HandleTestRunEnd(endTestRun); + + var doc = new XDocument( + _testRun.Serialize() + ); + + doc.Save(Path.Combine(Path.GetFullPath(_options.OutputDirectory), $@"mntr-{DateTime.UtcNow:yyyy'-'MM'-'dd'T'HH'-'mm'-'ss'-'fffffffK}.trx")); + } + + private static void ReportSpec(SpecSession session, TestRun testRun, string computerName, SpecLog log) + { + var begin = session.Begin.Value; + var beginTime = session.Begin.Time; + + var test = testRun.AddUnitTest(begin.ClassName, begin.MethodName, $"{begin.ClassName}.{begin.MethodName}"); + var specResult = test.AddResult(begin.MethodName, computerName); + + var nodeResults = new Dictionary(); + + ReportNodes(begin, specResult, beginTime, nodeResults); + ReportSuccess(session, nodeResults); + ReportFailure(session, nodeResults, log); + + specResult.Outcome = GetCombinedTestOutcome(nodeResults.Values); + specResult.StartTime = beginTime; + specResult.EndTime = session.End.Time; + + ReportTestMessages(session, nodeResults, specResult, log); + } + + private static void ReportFailure(SpecSession session, Dictionary nodeResults, SpecLog log) + { + foreach (var (time, message) in session.Fails) + { + var result = nodeResults[message.NodeIndex]; + result.Outcome = TestOutcome.Failed; + result.EndTime = time; + + result.Output = new Output(); + result.Output.StdErr.Add(message.Message); + + var nodeLog = log.NodeLogs.Find(n => n.NodeIndex == message.NodeIndex); + if (nodeLog.Log != null) + result.Output.StdErr.AddRange(nodeLog.Log); + + result.Output.DebugTrace.Add(message.Message); + result.Output.ErrorInfo = new ErrorInfo() { Message = message.Message }; + } + } + + private static void ReportSuccess(SpecSession session, Dictionary nodeResults) + { + foreach (var (time, message) in session.Successes) + { + var result = nodeResults[message.NodeIndex]; + result.Outcome = TestOutcome.Passed; + result.EndTime = time; + result.Output = new Output(); + + result.Output.StdOut.Add(message.Message); + result.Output.DebugTrace.Add(message.Message); + } + } + + private static void ReportNodes( + BeginNewSpec begin, + UnitTestResult specResult, + DateTime beginTime, + Dictionary nodeResults) + { + var skipReason = begin.TestCase.SkipReason; + foreach (var node in begin.Nodes) + { + var result = specResult.AddChildResult(node.Role); + if (!string.IsNullOrWhiteSpace(skipReason)) + { + result.Outcome = TestOutcome.NotExecuted; + result.StartTime = beginTime; + result.EndTime = beginTime; + } + else + { + result.Outcome = TestOutcome.InProgress; + result.StartTime = beginTime; + } + + nodeResults.Add(node.Node, result); + } + } + + private static void ReportTestMessages(SpecSession session, Dictionary nodeResults, UnitTestResult specResult, SpecLog log) + { + foreach (var (_, message) in session.Messages) + { + var textMessage = $"[{message.When:G}] {message.Message}"; + + Output output; + if (nodeResults.TryGetValue(message.NodeIndex, out var result)) + { + if (result.Output == null) + { + result.Output = new Output(); + } + + output = result.Output; + } + else + { + if (specResult.Output == null) + { + specResult.Output = new Output(); + } + + output = specResult.Output; + } + + output.StdOut.Add(textMessage); + output.DebugTrace.Add(textMessage); + } + + specResult.Output = specResult.Output ?? new Output(); + specResult.Output.StdErr.AddRange(log.AggregatedTimelineLog); + } + + private static TestOutcome GetCombinedTestOutcome(IEnumerable results) + { + var outcomes = results.Select(x => x.Outcome).Distinct().ToArray(); + + return outcomes.Length == 1 + ? outcomes[0] + : outcomes.Any(x => x == TestOutcome.Failed) + ? TestOutcome.Failed + : TestOutcome.Inconclusive; + } + } +} diff --git a/src/core/Akka.MultiNode.TestAdapter/MultiNodeFactAttribute.cs b/src/core/Akka.MultiNode.TestAdapter/MultiNodeFactAttribute.cs new file mode 100644 index 00000000000..6d8a185a326 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/MultiNodeFactAttribute.cs @@ -0,0 +1,19 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +// ----------------------------------------------------------------------- + +using System; +using Xunit; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter +{ + [XunitTestCaseDiscoverer("Akka.MultiNode.TestAdapter.MultiNodeFactDiscoverer", "Akka.MultiNode.TestAdapter")] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class MultiNodeFactAttribute : FactAttribute + { + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/MultiNodeFactDiscoverer.cs b/src/core/Akka.MultiNode.TestAdapter/MultiNodeFactDiscoverer.cs new file mode 100644 index 00000000000..0c62046f3a2 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/MultiNodeFactDiscoverer.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; +using Akka.MultiNode.TestAdapter.Internal; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter +{ + public class MultiNodeFactDiscoverer : IXunitTestCaseDiscoverer + { + /// + /// Initializes a new instance of the class. + /// + /// The message sink used to send diagnostic messages + public MultiNodeFactDiscoverer(IMessageSink diagnosticMessageSink) + => DiagnosticMessageSink = diagnosticMessageSink; + + /// + /// Gets the message sink used to report messages. + /// + protected IMessageSink DiagnosticMessageSink { get; } + + /// + /// Creates a single for the given test method. + /// + /// The discovery options to be used. + /// The test method. + /// The attribute that decorates the test method. + /// + protected virtual IXunitTestCase CreateTestCase( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + return new MultiNodeTestCase( + DiagnosticMessageSink, + discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), + testMethod); + } + + /// + /// Discover test cases from a test method. By default, if the method is generic, or + /// it contains arguments, returns a single ; + /// otherwise, it returns the result of calling . + /// + /// The discovery options to be used. + /// The test method the test cases belong to. + /// The fact attribute attached to the test method. + /// Returns zero or more test cases represented by the test method. + public virtual IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + var testCase = !testMethod.Method.GetParameters().Any() + ? !testMethod.Method.IsGenericMethodDefinition + ? CreateTestCase(discoveryOptions, testMethod, factAttribute) + : new ExecutionErrorTestCase( + DiagnosticMessageSink, + discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), + testMethod, "[MultiNodeFact] methods are not allowed to be generic.") + : new ExecutionErrorTestCase( + DiagnosticMessageSink, + discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), + testMethod, + "[MultiNodeFact] methods are not allowed to have parameters."); + + if (!(testCase is MultiNodeTestCase test)) + yield break; + + test.Load(); + yield return test.InitializationException == null + ? (IXunitTestCase) test + : new ExecutionErrorTestCase( + DiagnosticMessageSink, + discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), + testMethod, + test.InitializationException.ToString()); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/MultiNodeTestFramework.cs b/src/core/Akka.MultiNode.TestAdapter/MultiNodeTestFramework.cs new file mode 100644 index 00000000000..cdf0c7e3ac1 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/MultiNodeTestFramework.cs @@ -0,0 +1,29 @@ +using System; +using System.Reflection; +using Akka.MultiNode.TestAdapter.Internal; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Akka.MultiNode.TestAdapter +{ + public class MultiNodeTestFramework : TestFramework + { + public MultiNodeTestFramework(IMessageSink diagnosticMessageSink) : base(diagnosticMessageSink) + { + } + + protected override ITestFrameworkDiscoverer CreateDiscoverer(IAssemblyInfo assemblyInfo) + { + return new XunitTestFrameworkDiscoverer( + assemblyInfo: assemblyInfo, + sourceProvider: SourceInformationProvider, + diagnosticMessageSink: DiagnosticMessageSink, + collectionFactory: new CollectionPerSessionTestCollectionFactory()); + } + + protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) + { + return new MultiNodeTestFrameworkExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/NodeRunner/Executor.cs b/src/core/Akka.MultiNode.TestAdapter/NodeRunner/Executor.cs new file mode 100644 index 00000000000..93beb2a0bde --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/NodeRunner/Executor.cs @@ -0,0 +1,211 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.IO; +using Akka.MultiNode.TestAdapter.Internal; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Akka.Remote.TestKit; +using Xunit; +using Dns = System.Net.Dns; + +namespace Akka.MultiNode.TestAdapter.NodeRunner +{ + internal class Executor + { + /// + /// If it takes longer than this value for the to get back to us + /// about a particular test passing or failing, throw loudly. + /// + public async Task Execute(string[] args) + { + var maxProcessWaitTimeout = TimeSpan.FromMinutes(5); + IActorRef logger = null; + + try + { + CommandLine.Initialize(args); + + var nodeIndex = CommandLine.GetInt32("multinode.index"); + var nodeRole = CommandLine.GetProperty("multinode.role"); + var assemblyFileName = CommandLine.GetProperty("multinode.test-assembly"); + var typeName = CommandLine.GetProperty("multinode.test-class"); + var testName = CommandLine.GetProperty("multinode.test-method"); + var displayName = testName; + + var listenAddress = IPAddress.Parse(CommandLine.GetProperty("multinode.listen-address")); + var listenPort = CommandLine.GetInt32("multinode.listen-port"); + var listenEndpoint = new IPEndPoint(listenAddress, listenPort); + + try + { + var system = ActorSystem.Create("NodeTestRunner-" + nodeIndex); + logger = system.ActorOf(); + system.Tcp().Tell(new Tcp.Connect(listenEndpoint), logger); + + using (var controller = new XunitFrontController(AppDomainSupport.IfAvailable, assemblyFileName)) + { + /* need to pass in just the assembly name to Discovery, not the full path + * i.e. "Akka.Cluster.Tests.MultiNode.dll" + * not "bin/Release/Akka.Cluster.Tests.MultiNode.dll" - this will cause + * the Discovery class to actually not find any individual specs to run + */ + var assemblyName = Path.GetFileName(assemblyFileName); + Console.WriteLine("Running specs for {0} [{1}] ", assemblyName, assemblyFileName); + + using (var discovery = new Discovery(assemblyName)) + { + using (var sink = new ExecutorSink(nodeIndex, nodeRole, logger)) + { + controller.Find(true, discovery, TestFrameworkOptions.ForDiscovery()); + discovery.Finished.WaitOne(); + var tests = discovery.TestCases + .Where(t => t.TestMethod.Method.Name == testName && t.TestMethod.TestClass.Class.Name == typeName).ToList(); + Assert.Single(tests); + tests[0].InExecutionMode = true; + + controller.RunTests(tests, sink, TestFrameworkOptions.ForExecution()); + + var timedOut = false; + if (!sink.Finished.WaitOne(maxProcessWaitTimeout)) //timed out + { + var line = + $"Timed out while waiting for test to complete after {maxProcessWaitTimeout} ms"; + logger.Tell(line); + Console.WriteLine(line); + timedOut = true; + } + + await FlushLogMessages(); + + var shutdown = CoordinatedShutdown.Get(system); + await shutdown.Run(CoordinatedShutdown.ActorSystemTerminateReason.Instance); + + return sink.Passed && !timedOut ? 0 : 1; + } + } + } + } + catch (AggregateException ex) + { + var specFail = new SpecFail(nodeIndex, nodeRole, displayName); + specFail.FailureExceptionTypes.Add(ex.GetType().ToString()); + specFail.FailureMessages.Add(ex.Message); + specFail.FailureStackTraces.Add(ex.StackTrace); + foreach (var innerEx in ex.Flatten().InnerExceptions) + { + specFail.FailureExceptionTypes.Add(innerEx.GetType().ToString()); + specFail.FailureMessages.Add(innerEx.Message); + specFail.FailureStackTraces.Add(innerEx.StackTrace); + } + + logger?.Tell(specFail.ToString()); + Console.WriteLine(specFail); + + //make sure message is send over the wire + await FlushLogMessages(); + return 1; + } + catch (Exception ex) + { + var specFail = new SpecFail(nodeIndex, nodeRole, displayName); + specFail.FailureExceptionTypes.Add(ex.GetType().ToString()); + specFail.FailureMessages.Add(ex.Message); + specFail.FailureStackTraces.Add(ex.StackTrace); + var innerEx = ex.InnerException; + while (innerEx != null) + { + specFail.FailureExceptionTypes.Add(innerEx.GetType().ToString()); + specFail.FailureMessages.Add(innerEx.Message); + specFail.FailureStackTraces.Add(innerEx.StackTrace); + innerEx = innerEx.InnerException; + } + + logger?.Tell(specFail.ToString()); + Console.WriteLine(specFail); + + //make sure message is send over the wire + await FlushLogMessages(); + return 1; + } + } + catch (Exception ex) + { + Console.WriteLine($"Unexpected FATAL exception: {ex}"); + return 1; + } + + async Task FlushLogMessages() + { + try + { + if(logger != null) + await logger.GracefulStop(TimeSpan.FromSeconds(2)); + } + catch + { + Console.WriteLine("Exception thrown while waiting for TCP transport to flush - not all messages may have been logged."); + } + } + } + } + + class RunnerTcpClient : ReceiveActor, IWithUnboundedStash + { + private IActorRef _connection; + + public RunnerTcpClient() + { + Become(WaitingForConnection); + } + + /// + protected override void PostStop() + { + // Close connection property to avoid exception logged at TcpConnection actor once this actor is terminated + try + { + _connection.Ask(Tcp.Close.Instance, TimeSpan.FromSeconds(1)).Wait(); + } + catch { /* well... at least we have tried */ } + + base.PostStop(); + } + + private void WaitingForConnection() + { + Receive(connected => + { + Sender.Tell(new Tcp.Register(Self)); + _connection = Sender; + Become(Connected(Sender)); + }); + Receive(_ => Stash.Stash()); + } + + private Receive Connected(IActorRef connection) + { + Stash.UnstashAll(); + + return message => + { + var bytes = ByteString.FromString(message.ToString()); + connection.Tell(Tcp.Write.Create(bytes)); + + return true; + }; + } + + public IStash Stash { get; set; } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/NodeRunner/ExecutorSink.cs b/src/core/Akka.MultiNode.TestAdapter/NodeRunner/ExecutorSink.cs new file mode 100644 index 00000000000..87da659eb60 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/NodeRunner/ExecutorSink.cs @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2019 Lightbend Inc. +// Copyright (C) 2013-2019 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Threading; +using Akka.Actor; +using Akka.MultiNode.TestAdapter.Internal.Sinks; +using Xunit; +using Xunit.Abstractions; +using IMessageSink = Xunit.Abstractions.IMessageSink; +using LongLivedMarshalByRefObject = Xunit.Sdk.LongLivedMarshalByRefObject; + +namespace Akka.MultiNode.TestAdapter.NodeRunner +{ + internal class ExecutorSink : LongLivedMarshalByRefObject, IMessageSink, IDisposable + { + public bool Passed { get; private set; } + public ManualResetEvent Finished { get; private set; } + private readonly int _nodeIndex; + private readonly string _nodeRole; + + private readonly IActorRef _logger; + + public ExecutorSink(int nodeIndex, string nodeRole, IActorRef logger) + { + _nodeIndex = nodeIndex; + _nodeRole = nodeRole; + Finished = new ManualResetEvent(false); + _logger = logger; + } + + public bool OnMessage(IMessageSinkMessage message) + { + if (message is ITestResultMessage resultMessage) + { + _logger.Tell(resultMessage.Output); + Console.WriteLine(resultMessage.Output); + } + + if (message is ITestPassed testPassed) + { + //the MultiNodeTestRunner uses 1-based indexing, which is why we have to add 1 to the index. + var specPass = new SpecPass(_nodeIndex + 1, _nodeRole, testPassed.TestCase.DisplayName); + _logger.Tell(specPass.ToString()); + Console.WriteLine(specPass.ToString()); //so the message also shows up in the individual per-node build log + Passed = true; + return true; + } + + if (message is ITestFailed testFailed) + { + //the MultiNodeTestRunner uses 1-based indexing, which is why we have to add 1 to the index. + var specFail = new SpecFail(_nodeIndex + 1, _nodeRole, testFailed.TestCase.DisplayName); + foreach (var failedMessage in testFailed.Messages) specFail.FailureMessages.Add(failedMessage); + foreach (var stackTrace in testFailed.StackTraces) specFail.FailureStackTraces.Add(stackTrace); + foreach(var exceptionType in testFailed.ExceptionTypes) specFail.FailureExceptionTypes.Add(exceptionType); + _logger.Tell(specFail.ToString()); + Console.WriteLine(specFail.ToString()); + return true; + } + + if (message is ErrorMessage errorMessage) + { + var specFail = new SpecFail(_nodeIndex + 1, _nodeRole, "ERRORED"); + foreach (var failedMessage in errorMessage.Messages) specFail.FailureMessages.Add(failedMessage); + foreach (var stackTrace in errorMessage.StackTraces) specFail.FailureStackTraces.Add(stackTrace); + foreach (var exceptionType in errorMessage.ExceptionTypes) specFail.FailureExceptionTypes.Add(exceptionType); + _logger.Tell(specFail.ToString()); + Console.WriteLine(specFail.ToString()); + } + + if (message is ITestAssemblyFinished) + { + Finished.Set(); + } + + return true; + } + + /// + public void Dispose() + { + Finished.Dispose(); + } + } +} + diff --git a/src/core/Akka.MultiNode.TestAdapter/Properties/Friends.cs b/src/core/Akka.MultiNode.TestAdapter/Properties/Friends.cs new file mode 100644 index 00000000000..48a01d661f2 --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/Properties/Friends.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Akka.MultiNode.TestAdapter.Tests")] \ No newline at end of file diff --git a/src/core/Akka.MultiNode.TestAdapter/xunit.multinode.runner.json b/src/core/Akka.MultiNode.TestAdapter/xunit.multinode.runner.json new file mode 100644 index 00000000000..6a7d64af5bc --- /dev/null +++ b/src/core/Akka.MultiNode.TestAdapter/xunit.multinode.runner.json @@ -0,0 +1,8 @@ +{ + "outputDirectory": "TestResults", + "failedSpecsDirectory": "FAILED_SPECS_LOGS", + "listenAddress": "127.0.0.1", + "listenPort": 0, + "clearOutputDirectory": false, + "useBuiltInTrxReporter": false +} \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit.Tests/TestConductorSpec.cs b/src/core/Akka.Remote.TestKit.Tests/TestConductorSpec.cs new file mode 100644 index 00000000000..c056b8a9ef8 --- /dev/null +++ b/src/core/Akka.Remote.TestKit.Tests/TestConductorSpec.cs @@ -0,0 +1,26 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2026 .NET Foundation +// +// ----------------------------------------------------------------------- + +using FluentAssertions; +using FluentAssertions.Extensions; +using Xunit; + +namespace Akka.Remote.TestKit.Tests; + +public class TestConductorSpec: Akka.TestKit.Xunit.TestKit +{ + public TestConductorSpec(ITestOutputHelper output): base("akka.actor.provider = remote", nameof(TestConductorSpec), output) + { + } + + [Fact(DisplayName = "TestConductor must initialize with not error")] + public void InitializationTest() + { + var conductor = TestConductor.Get(Sys); + conductor.Settings.BarrierTimeout.Should().Be(30.Seconds()); + } +} \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit.Xunit2/Akka.Remote.TestKit.Xunit2.csproj b/src/core/Akka.Remote.TestKit.Xunit2/Akka.Remote.TestKit.Xunit2.csproj index 8ec0eb11b33..125b1f8f074 100644 --- a/src/core/Akka.Remote.TestKit.Xunit2/Akka.Remote.TestKit.Xunit2.csproj +++ b/src/core/Akka.Remote.TestKit.Xunit2/Akka.Remote.TestKit.Xunit2.csproj @@ -8,7 +8,6 @@ - @@ -48,12 +47,18 @@ Extension.cs + + Internals\TestConductorConfigFactory.cs + MsgDecoder.cs MsgEncoder.cs + + MultiNodeSpec.cs + Player.cs @@ -67,5 +72,11 @@ RemoteConnection.cs + + + + Internals\Reference.conf + + diff --git a/src/core/Akka.Remote.TestKit.Xunit2/GlobalUsings.cs b/src/core/Akka.Remote.TestKit.Xunit2/GlobalUsings.cs new file mode 100644 index 00000000000..f6ecf62a58f --- /dev/null +++ b/src/core/Akka.Remote.TestKit.Xunit2/GlobalUsings.cs @@ -0,0 +1 @@ +global using Akka.TestKit.Xunit2; \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit.Xunit2/Internals/Reference.conf b/src/core/Akka.Remote.TestKit.Xunit2/Internals/Reference.conf deleted file mode 100644 index f30e631f7ef..00000000000 --- a/src/core/Akka.Remote.TestKit.Xunit2/Internals/Reference.conf +++ /dev/null @@ -1,65 +0,0 @@ -############################################# -# Akka Remote Testing Reference Config File # -############################################# - -# This is the reference config file that contains all the default settings. -# Make your edits/overrides in your application.conf. - -akka { - testconductor { - - # Timeout for joining a barrier: this is the maximum time any participants - # waits for everybody else to join a named barrier. - barrier-timeout = 30s - - # Timeout for interrogation of TestConductor’s Controller actor - query-timeout = 30s - - # Threshold for packet size in time unit above which the failure injector will - # split the packet and deliver in smaller portions; do not give value smaller - # than HashedWheelTimer resolution (would not make sense) - packet-split-threshold = 100ms - - # amount of time for the ClientFSM to wait for the connection to the conductor - # to be successful - connect-timeout = 20s - - # Number of connect attempts to be made to the conductor controller - client-reconnects = 30 - - # minimum time interval which is to be inserted between reconnect attempts - reconnect-backoff = 1s - - helios { - # (I&O) Used to configure the number of I/O worker threads on server sockets - server-socket-worker-pool { - # Min number of threads to cap factor-based number to - pool-size-min = 2 - - # The pool size factor is used to determine thread pool size - # using the following formula: ceil(available processors * factor). - # Resulting size is then bounded by the pool-size-min and - # pool-size-max values. - pool-size-factor = 1.0 - - # Max number of threads to cap factor-based number to - pool-size-max = 2 - } - - # (I&O) Used to configure the number of I/O worker threads on client sockets - client-socket-worker-pool { - # Min number of threads to cap factor-based number to - pool-size-min = 2 - - # The pool size factor is used to determine thread pool size - # using the following formula: ceil(available processors * factor). - # Resulting size is then bounded by the pool-size-min and - # pool-size-max values. - pool-size-factor = 1.0 - - # Max number of threads to cap factor-based number to - pool-size-max = 2 - } - } - } -} \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit.Xunit2/Internals/TestConductorConfigFactory.cs b/src/core/Akka.Remote.TestKit.Xunit2/Internals/TestConductorConfigFactory.cs deleted file mode 100644 index b3c98384336..00000000000 --- a/src/core/Akka.Remote.TestKit.Xunit2/Internals/TestConductorConfigFactory.cs +++ /dev/null @@ -1,54 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2022 Lightbend Inc. -// Copyright (C) 2013-2025 .NET Foundation -// -//----------------------------------------------------------------------- - -using System.Diagnostics; -using System.IO; -using System.Reflection; -using Akka.Configuration; - -namespace Akka.Remote.TestKit.Internals -{ - /// - /// This class contains methods used to retrieve Multi-Node TestKit configuration options from this assembly's resources - /// and injects them in relevant tests. - /// - /// Note! Part of internal API. Breaking changes may occur without notice. Use at own risk. - /// - internal static class TestConductorConfigFactory - { - /// - /// Retrieves the default Multi-Node TestKit options that Akka.NET uses when no configuration has been defined. - /// - /// The configuration that contains default values for all Multi-Node TestKit options. - public static Config Default() - { - return FromResource("Akka.Remote.TestKit.Internals.Reference.conf"); - } - - /// - /// Retrieves a configuration defined in a resource of the current executing assembly. - /// - /// The name of the resource that contains the configuration. - /// The configuration defined in the current executing assembly. - internal static Config FromResource(string resourceName) - { - var assembly = typeof(TestConductorConfigFactory).Assembly; - - using (var stream = assembly.GetManifestResourceStream(resourceName)) - { - Debug.Assert(stream != null, "stream != null"); - using (var reader = new StreamReader(stream)) - { - var result = reader.ReadToEnd(); - - return ConfigurationFactory.ParseString(result); - } - } - } - } -} - diff --git a/src/core/Akka.Remote.TestKit.Xunit2/MultiNodeSpec.cs b/src/core/Akka.Remote.TestKit.Xunit2/MultiNodeSpec.cs deleted file mode 100644 index fe4adebc3b5..00000000000 --- a/src/core/Akka.Remote.TestKit.Xunit2/MultiNodeSpec.cs +++ /dev/null @@ -1,759 +0,0 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2022 Lightbend Inc. -// Copyright (C) 2013-2025 .NET Foundation -// -//----------------------------------------------------------------------- - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Akka.Actor; -using Akka.Actor.Setup; -using Akka.Configuration; -using Akka.Configuration.Hocon; -using Akka.Event; -using Akka.TestKit; -using Akka.TestKit.Xunit2; -using Akka.Util.Internal; - -namespace Akka.Remote.TestKit; - -/// -/// Configure the role names and participants of the test, including configuration settings -/// -public abstract class MultiNodeConfig -{ - // allows us to avoid NullReferenceExceptions if we make this empty rather than null - // so that way if a MultiNodeConfig doesn't explicitly set CommonConfig to some value - // it will remain safe by defaut - private Config _commonConf = ConfigurationFactory.Empty; - - private ImmutableDictionary _nodeConf = ImmutableDictionary.Create(); - private ImmutableList _roles = ImmutableList.Create(); - private ImmutableDictionary> _deployments = ImmutableDictionary.Create>(); - private ImmutableList _allDeploy = ImmutableList.Create(); - private bool _testTransport = false; - - /// - /// Register a common base config for all test participants, if so desired. - /// - public Config CommonConfig - { - set { _commonConf = value; } - } - - /// - /// Register a config override for a specific participant. - /// - public void NodeConfig(IEnumerable roles, IEnumerable configs) - { - var c = configs.Aggregate((a, b) => a.WithFallback(b)); - _nodeConf = _nodeConf.AddRange(roles.Select(r => new KeyValuePair(r, c))); - } - - /// - /// Include for verbose debug logging - /// - /// when `true` debug Config is returned, otherwise config with info logging - public Config DebugConfig(bool on) - { - if (on) - return ConfigurationFactory.ParseString(@" - akka.loglevel = DEBUG - akka.remote { - log-received-messages = on - log-sent-messages = on - } - akka.actor.debug { - receive = on - fsm = on - } - akka.remote.log-remote-lifecycle-events = on - akka.log-dead-letters = on - "); - return ConfigurationFactory.Empty; - } - - public RoleName Role(string name) - { - if (_roles.Exists(r => r.Name == name)) throw new ArgumentException("non-unique role name " + name); - var roleName = new RoleName(name); - _roles = _roles.Add(roleName); - return roleName; - } - - public void DeployOn(RoleName role, string deployment) - { - _deployments.TryGetValue(role, out var roleDeployments); - _deployments = _deployments.SetItem(role, - roleDeployments == null ? ImmutableList.Create(deployment) : roleDeployments.Add(deployment)); - } - - public void DeployOnAll(string deployment) - { - _allDeploy = _allDeploy.Add(deployment); - } - - /// - /// To be able to use `blackhole`, `passThrough`, and `throttle` you must - /// activate the failure injector and throttler transport adapters by - /// specifying `testTransport(on = true)` in your MultiNodeConfig. - /// - public bool TestTransport - { - set { _testTransport = value; } - } - - private readonly Lazy _myself; - - protected MultiNodeConfig() - { - var roleName = CommandLine.GetPropertyOrDefault("multinode.role", null); - - if (string.IsNullOrEmpty(roleName)) - { - _myself = new Lazy(() => - { - if (MultiNodeSpec.SelfIndex > _roles.Count) throw new ArgumentException("not enough roles declared for this test"); - return _roles[MultiNodeSpec.SelfIndex]; - }); - } - else - { - _myself = new Lazy(() => - { - var myself = _roles.FirstOrDefault(r => r.Name.Equals(roleName, StringComparison.OrdinalIgnoreCase)); - if (myself is null) throw new ArgumentException($"cannot find {roleName} among configured roles"); - return myself; - }); - } - } - - public RoleName Myself => _myself.Value; - - internal Config Config - { - get - { - var transportConfig = _testTransport ? - ConfigurationFactory.ParseString("akka.remote.dot-netty.tcp.applied-adapters = [trttl, gremlin]") - : ConfigurationFactory.Empty; - - var builder = ImmutableList.CreateBuilder(); - if (_nodeConf.TryGetValue(Myself, out var nodeConfig)) - builder.Add(nodeConfig); - builder.Add(_commonConf); - builder.Add(transportConfig); - builder.Add(MultiNodeSpec.NodeConfig); - builder.Add(MultiNodeSpec.BaseConfig); - - return builder.ToImmutable().Aggregate((a, b) => a.WithFallback(b)); - } - } - - internal ImmutableList Deployments(RoleName node) - { - _deployments.TryGetValue(node, out var deployments); - return deployments == null ? _allDeploy : deployments.AddRange(_allDeploy); - } - - public ImmutableList Roles => _roles; -} - -//TODO: Applicable? -/// -/// Note: To be able to run tests with everything ignored or excluded by tags -/// you must not use `testconductor`, or helper methods that use `testconductor`, -/// from the constructor of your test class. Otherwise the controller node might -/// be shutdown before other nodes have completed and you will see errors like: -/// `AskTimeoutException: sending to terminated ref breaks promises`. Using lazy -/// val is fine. -/// -public abstract class MultiNodeSpec : TestKitBase, IMultiNodeSpecCallbacks, IDisposable -{ - //TODO: Sort out references to Java classes in - - /// - /// Marker used to indicate that has not been set yet. - /// - private const int MaxNodesUnset = -1; - private static int _maxNodes = MaxNodesUnset; - - /// - /// Number of nodes node taking part in this test. - /// -Dmultinode.max-nodes=4 - /// - public static int MaxNodes - { - get - { - if (_maxNodes == MaxNodesUnset) - { - _maxNodes = CommandLine.GetInt32("multinode.max-nodes"); - } - - if (_maxNodes <= 0) throw new InvalidOperationException("multinode.max-nodes must be greater than 0"); - return _maxNodes; - } - } - - private static string _multiNodeHost; - - /// - /// Name (or IP address; must be resolvable) - /// of the host this node is running on - /// - /// -Dmultinode.host=host.example.com - /// - /// InetAddress.getLocalHost.getHostAddress is used if empty or "localhost" - /// is defined as system property "multinode.host". - /// - public static string SelfName - { - get - { - if (string.IsNullOrEmpty(_multiNodeHost)) - { - _multiNodeHost = CommandLine.GetProperty("multinode.host"); - } - - //Run this assertion every time. Consistency is more important than performance. - if (string.IsNullOrEmpty(_multiNodeHost)) throw new InvalidOperationException("multinode.host must not be empty"); - return _multiNodeHost; - } - } - - /// - /// Marker used to indicate what the "not been set" value of is. - /// - private const int SelfPortUnsetValue = -1; - private static int _selfPort = SelfPortUnsetValue; - - - /// - /// Port number of this node. Defaults to 0 which means a random port. - /// - /// -Dmultinode.port=0 - /// - public static int SelfPort - { - get - { - if (_selfPort == SelfPortUnsetValue) //unset - { - var selfPortStr = CommandLine.GetProperty("multinode.port"); - _selfPort = string.IsNullOrEmpty(selfPortStr) ? 0 : Int32.Parse(selfPortStr); - } - - if (!(_selfPort >= 0 && _selfPort < 65535)) throw new InvalidOperationException("multinode.port is out of bounds: " + _selfPort); - return _selfPort; - } - } - - private static string _serverName; - /// - /// Name (or IP address; must be resolvable using InetAddress.getByName) - /// of the host that the server node is running on. - /// - /// -Dmultinode.server-host=server.example.com - /// - public static string ServerName - { - get - { - if (string.IsNullOrEmpty(_serverName)) - { - _serverName = CommandLine.GetProperty("multinode.server-host"); - } - if (string.IsNullOrEmpty(_serverName)) throw new InvalidOperationException("multinode.server-host must not be empty"); - return _serverName; - } - } - - /// - /// Marker used to indicate what the "not been set" value of is. - /// - private const int ServerPortUnsetValue = -1; - - /// - /// Default value for - /// - private const int ServerPortDefault = 47110; - - private static int _serverPort = ServerPortUnsetValue; - - /// - /// Port number of the node that's running the server system. Defaults to 4711. - /// - /// -Dmultinode.server-port=4711 - /// - public static int ServerPort - { - get - { - if (_serverPort == ServerPortUnsetValue) - { - var serverPortStr = CommandLine.GetProperty("multinode.server-port"); - _serverPort = string.IsNullOrEmpty(serverPortStr) ? ServerPortDefault : Int32.Parse(serverPortStr); - } - - if (!(_serverPort > 0 && _serverPort < 65535)) throw new InvalidOperationException("multinode.server-port is out of bounds: " + _serverPort); - return _serverPort; - } - } - - /// - /// Marker value used to indicate that has not been set yet. - /// - private const int SelfIndexUnset = -1; - - private static int _selfIndex = SelfIndexUnset; - - /// - /// Index of this node in the roles sequence. The TestConductor - /// is started in "controller" mode on selfIndex 0, i.e. there you can inject - /// failures and shutdown other nodes etc. - /// - public static int SelfIndex - { - get - { - if (_selfIndex == SelfIndexUnset) - { - _selfIndex = CommandLine.GetInt32("multinode.index"); - } - - if (!(_selfIndex >= 0 && _selfIndex < MaxNodes)) throw new InvalidOperationException("multinode.index is out of bounds: " + _selfIndex); - return _selfIndex; - } - } - - public static Config NodeConfig - { - get - { - const string config = @" - akka.actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote"" - akka.remote.dot-netty.tcp.hostname = ""{0}"" - akka.remote.dot-netty.tcp.port = {1}"; - - return ConfigurationFactory.ParseString(String.Format(config, SelfName, SelfPort)); - } - } - - public static Config BaseConfig - { - get - { - return ConfigurationFactory.ParseString( - @"akka { - loglevel = ""WARNING"" - stdout-loglevel = ""WARNING"" - coordinated-shutdown.terminate-actor-system = off - coordinated-shutdown.run-by-actor-system-terminate = off - coordinated-shutdown.run-by-clr-shutdown-hook = off - log-dead-letters = off - log-dead-letters-during-shutdown = on - actor { - default-dispatcher { - executor = ""fork-join-executor"" - fork-join-executor { - parallelism-min = 8 - parallelism-factor = 2.0 - parallelism-max = 8 - } - } - } - cluster.downing-provider-class = """" #disable SBR by default - }").WithFallback(TestKitBase.DefaultConfig); - } - } - - public RoleName Myself { get; } - - private readonly ILoggingAdapter _log; - private bool _isDisposed; //Automatically initialized to false; - private readonly Func> _deployments; - private readonly ImmutableDictionary _replacements; - private readonly Address _myAddress; - - protected MultiNodeSpec(MultiNodeConfig config, Type type) : - this(config.Myself, ActorSystem.Create(type.Name, config.Config), config.Roles, config.Deployments) - { - } - - protected MultiNodeSpec( - RoleName myself, - ActorSystem system, - ImmutableList roles, - Func> deployments) - : this(myself, system, null, roles, deployments) - { - } - - protected MultiNodeSpec( - RoleName myself, - ActorSystemSetup setup, - ImmutableList roles, - Func> deployments) - : this(myself, null, setup, roles, deployments) - { - } - - private MultiNodeSpec( - RoleName myself, - ActorSystem system, - ActorSystemSetup setup, - ImmutableList roles, - Func> deployments) - : base(new XunitAssertions(), system, setup, null, null) - { - Myself = myself; - _log = Logging.GetLogger(Sys, this); - Roles = roles; - _deployments = deployments; - - var node = new IPEndPoint(Dns.GetHostAddresses(ServerName)[0], ServerPort); - _controllerAddr = node; - - AttachConductor(new TestConductor(Sys)); - - _replacements = Roles.ToImmutableDictionary(r => r, r => new Replacement("@" + r.Name + "@", r, this)); - - InjectDeployments(Sys, myself); - - _myAddress = Sys.AsInstanceOf().Provider.DefaultAddress; - - Log.Info("Role [{0}] started with address [{1}]", myself.Name, _myAddress); - MultiNodeSpecBeforeAll(); - } - - public void MultiNodeSpecBeforeAll() - { - AtStartup(); - } - - public void MultiNodeSpecAfterAll() - { - // wait for all nodes to remove themselves before we shut the conductor down - if (SelfIndex == 0) - { - TestConductor.RemoveNode(Myself); - Within(TestConductor.Settings.BarrierTimeout, () => - AwaitCondition(() => TestConductor.GetNodes().Result.All(n => n.Equals(Myself)))); - - } - Shutdown(Sys); - AfterTermination(); - } - - protected virtual TimeSpan ShutdownTimeout { get { return TimeSpan.FromSeconds(5); } } - - /// - /// Override this and return `true` to assert that the - /// shutdown of the `ActorSystem` was done properly. - /// - protected virtual bool VerifySystemShutdown { get { return false; } } - - //Test Class Interface - - /// - /// Override this method to do something when the whole test is starting up. - /// - protected virtual void AtStartup() - { - } - - /// - /// Override this method to do something when the whole test is terminating. - /// - protected virtual void AfterTermination() - { - } - - /// - /// All registered roles - /// - public ImmutableList Roles { get; } - - /// - /// MUST BE DEFINED BY USER. - /// - /// Defines the number of participants required for starting the test. This - /// might not be equals to the number of nodes available to the test. - /// - public int InitialParticipants - { - get - { - var initialParticipants = InitialParticipantsValueFactory; - if (initialParticipants <= 0) throw new InvalidOperationException("InitialParticipantsValueFactory must be populated early on, and it must be greater zero"); - if (initialParticipants > MaxNodes) throw new InvalidOperationException("not enough nodes to run this test"); - return initialParticipants; - } - - } - - /// - /// Must be defined by user. Creates the values used by - /// - protected abstract int InitialParticipantsValueFactory { get; } - - protected TestConductor TestConductor; - - /// - /// Execute the given block of code only on the given nodes (names according - /// to the `roleMap`). - /// - public void RunOn(Action thunk, params RoleName[] nodes) - { - if (IsNode(nodes)) thunk(); - } - - /// - /// Execute the given block of code only on the given nodes (names according - /// to the `roleMap`). - /// - public async Task RunOnAsync(Func thunkAsync, params RoleName[] nodes) - { - if (IsNode(nodes)) await thunkAsync(); - } - - /// - /// Verify that the running node matches one of the given nodes - /// - public bool IsNode(params RoleName[] nodes) - { - return nodes.Contains(Myself); - } - - /// - /// Enter the named barriers in the order given. Use the remaining duration from - /// the innermost enclosing `within` block or the default `BarrierTimeout` - /// - public void EnterBarrier(params string[] name) - { - TestConductor.Enter(RemainingOr(TestConductor.Settings.BarrierTimeout), Myself, name.ToImmutableList()); - } - - /// - /// Async version of EnterBarrier. Enter the named barriers in the order given. - /// Use the remaining duration from the innermost enclosing `within` block or the default `BarrierTimeout` - /// - public Task EnterBarrierAsync(params string[] name) - { - return EnterBarrierAsync(CancellationToken.None, name); - } - - /// - /// Async version of EnterBarrier with cancellation support. Enter the named barriers in the order given. - /// Use the remaining duration from the innermost enclosing `within` block or the default `BarrierTimeout` - /// - public Task EnterBarrierAsync(CancellationToken cancellationToken, params string[] name) - { - return TestConductor.EnterAsync(RemainingOr(TestConductor.Settings.BarrierTimeout), Myself, name.ToImmutableList(), cancellationToken); - } - - /// - /// Query the controller for the transport address of the given node (by role name) and - /// return that as an ActorPath for easy composition: - /// - /// var serviceA = Sys.ActorSelection(Node(new RoleName("master")) / "user" / "serviceA"); - /// - public ActorPath Node(RoleName role) - { - return NodeAsync(role).GetAwaiter().GetResult(); - } - - /// - /// Async version of Node. Query the controller for the transport address of the given node (by role name) and - /// return that as an ActorPath for easy composition. - /// - public async Task NodeAsync(RoleName role, CancellationToken cancellationToken = default) - { - var address = await TestConductor.GetAddressForAsync(role, cancellationToken); - return new RootActorPath(address); - } - - public void MuteDeadLetters(ActorSystem system = null, params Type[] messageClasses) - { - if (system == null) system = Sys; - if (!system.Log.IsDebugEnabled) - { - if (messageClasses.Any()) - foreach (var @class in messageClasses) EventFilter.DeadLetter(@class).Mute(); - else EventFilter.DeadLetter(typeof(object)).Mute(); - } - } - - /* - * Implementation (i.e. wait for start etc.) - */ - - private readonly IPEndPoint _controllerAddr; - - protected void AttachConductor(TestConductor tc) - { - AttachConductorAsync(tc, CancellationToken.None).GetAwaiter().GetResult(); - } - - protected async Task AttachConductorAsync(TestConductor tc, CancellationToken cancellationToken = default) - { - using var cts = cancellationToken is { CanBeCanceled: true } - ? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken) - : new CancellationTokenSource(); - cts.CancelAfter(tc.Settings.BarrierTimeout); - try - { - if (SelfIndex == 0) - await tc.StartControllerAsync(InitialParticipants, Myself, _controllerAddr, cts.Token); - else - await tc.StartClientAsync(Myself, _controllerAddr, cts.Token); - } - catch (Exception e) - { - throw new Exception("failure while attaching new conductor", e); - } - TestConductor = tc; - } - - // now add deployments, if so desired - - private sealed class Replacement - { - public string Tag { get; } - public RoleName Role { get; } - private readonly Lazy _addr; - public string Addr { get { return _addr.Value; } } - - public Replacement(string tag, RoleName role, MultiNodeSpec spec) - { - Tag = tag; - Role = role; - _addr = new Lazy(() => spec.Node(role).Address.ToString()); - } - } - - protected void InjectDeployments(ActorSystem system, RoleName role) - { - var deployer = system.AsInstanceOf().Provider.Deployer; - foreach (var str in _deployments(role)) - { - var deployString = _replacements.Values.Aggregate(str, (@base, r) => - { - var indexOf = @base.IndexOf(r.Tag, StringComparison.Ordinal); - if (indexOf == -1) return @base; - string replaceWith; - try - { - replaceWith = r.Addr; - } - catch (Exception e) - { - // might happen if all test cases are ignored (excluded) and - // controller node is finished/exited before r.addr is run - // on the other nodes - var unresolved = "akka://unresolved-replacement-" + r.Role.Name; - Log.Warning(unresolved + " due to: {0}", e.ToString()); - replaceWith = unresolved; - } - return @base.Replace(r.Tag, replaceWith); - }); - foreach (var pair in ConfigurationFactory.ParseString(deployString).AsEnumerable()) - { - if (pair.Value.IsObject()) - { - var deploy = deployer.ParseConfig(pair.Key, new Config(new HoconRoot(pair.Value))); - deployer.SetDeploy(deploy); - } - else - { - throw new ArgumentException($"key {pair.Key} must map to deployment section, not simple value {pair.Value}"); - } - } - } - } - - protected ActorSystem StartNewSystem() - { - return StartNewSystemAsync(CancellationToken.None).GetAwaiter().GetResult(); - } - - protected async Task StartNewSystemAsync(CancellationToken cancellationToken = default) - { - var sb = - new StringBuilder("akka.remote.dot-netty.tcp{").AppendLine() - .AppendFormat("port={0}", _myAddress.Port) - .AppendLine() - .AppendFormat(@"hostname=""{0}""", _myAddress.Host) - .AppendLine("}"); - var config = - ConfigurationFactory - .ParseString(sb.ToString()) - .WithFallback(Sys.Settings.Config); - - var system = ActorSystem.Create(Sys.Name, config); - InjectDeployments(system, Myself); - await AttachConductorAsync(new TestConductor(system), cancellationToken); - return system; - } - - public void Dispose() - { - Dispose(true); - //Take this object off the finalization queue and prevent finalization code for this object - //from executing a second time. - GC.SuppressFinalize(this); - } - - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// if set to true the method has been called directly or indirectly by a - /// user's code. Managed and unmanaged resources will be disposed.
- /// if set to false the method has been called by the runtime from inside the finalizer and only - /// unmanaged resources can be disposed. - protected void Dispose(bool disposing) - { - // If disposing equals false, the method has been called by the - // runtime from inside the finalizer and you should not reference - // other objects. Only unmanaged resources can be disposed. - - //Make sure Dispose does not get called more than once, by checking the disposed field - if (!_isDisposed) - { - if (disposing) - { - Console.WriteLine("---------------DISPOSING--------------------"); - MultiNodeSpecAfterAll(); - } - } - _isDisposed = true; - } -} - -//TODO: Improve docs -/// -/// Use this to hook into your test framework lifecycle -/// -public interface IMultiNodeSpecCallbacks -{ - /// - /// Call this before the start of the test run. NOT before every test case. - /// - void MultiNodeSpecBeforeAll(); - - /// - /// Call this after the all test cases have run. NOT after every test case. - /// - void MultiNodeSpecAfterAll(); -} \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit.Xunit2/Properties/FriendsTo.cs b/src/core/Akka.Remote.TestKit.Xunit2/Properties/FriendsTo.cs new file mode 100644 index 00000000000..3b2888a9566 --- /dev/null +++ b/src/core/Akka.Remote.TestKit.Xunit2/Properties/FriendsTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Akka.Remote.TestKit.Xunit2.Tests")] \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit/GlobalUsings.cs b/src/core/Akka.Remote.TestKit/GlobalUsings.cs new file mode 100644 index 00000000000..c065fa154bb --- /dev/null +++ b/src/core/Akka.Remote.TestKit/GlobalUsings.cs @@ -0,0 +1 @@ +global using Akka.TestKit.Xunit; \ No newline at end of file diff --git a/src/core/Akka.Remote.TestKit/Internals/TestConductorConfigFactory.cs b/src/core/Akka.Remote.TestKit/Internals/TestConductorConfigFactory.cs index b3c98384336..97a23380ad3 100644 --- a/src/core/Akka.Remote.TestKit/Internals/TestConductorConfigFactory.cs +++ b/src/core/Akka.Remote.TestKit/Internals/TestConductorConfigFactory.cs @@ -26,7 +26,7 @@ internal static class TestConductorConfigFactory /// The configuration that contains default values for all Multi-Node TestKit options. public static Config Default() { - return FromResource("Akka.Remote.TestKit.Internals.Reference.conf"); + return FromResource($"{typeof(TestConductorConfigFactory).Assembly.GetName().Name}.Internals.Reference.conf"); } /// diff --git a/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs b/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs index 651a29a0303..b5509dba786 100644 --- a/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs +++ b/src/core/Akka.Remote.TestKit/MultiNodeSpec.cs @@ -23,7 +23,6 @@ using Akka.Configuration.Hocon; using Akka.Event; using Akka.TestKit; -using Akka.TestKit.Xunit; using Akka.Util.Internal; namespace Akka.Remote.TestKit; diff --git a/src/core/Akka.Remote.TestKit/Properties/AssemblyInfo.cs b/src/core/Akka.Remote.TestKit/Properties/AssemblyInfo.cs index 4da60bcd828..304da93ed52 100644 --- a/src/core/Akka.Remote.TestKit/Properties/AssemblyInfo.cs +++ b/src/core/Akka.Remote.TestKit/Properties/AssemblyInfo.cs @@ -9,16 +9,5 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("de375180-0f6f-40c5-9dd4-3a27e2559d5d")] [assembly: InternalsVisibleTo("Akka.Remote.TestKit.Tests")] diff --git a/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj b/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj index b1f069af045..a64c7c23af4 100644 --- a/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj +++ b/src/core/Akka.Remote.Tests.MultiNode/Akka.Remote.Tests.MultiNode.csproj @@ -5,6 +5,7 @@ + @@ -15,7 +16,6 @@ - diff --git a/src/core/Akka.Remote/Properties/AssemblyInfo.cs b/src/core/Akka.Remote/Properties/AssemblyInfo.cs index 76dc2931a35..b23650e11f3 100644 --- a/src/core/Akka.Remote/Properties/AssemblyInfo.cs +++ b/src/core/Akka.Remote/Properties/AssemblyInfo.cs @@ -24,6 +24,7 @@ [assembly: Guid("78986bdb-73f7-4532-8e03-1c9ccbe8148e")] [assembly: InternalsVisibleTo("Akka.Remote.TestKit.Tests")] [assembly: InternalsVisibleTo("Akka.Remote.TestKit")] +[assembly: InternalsVisibleTo("Akka.Remote.TestKit.Xunit2.Tests")] [assembly: InternalsVisibleTo("Akka.Remote.TestKit.Xunit2")] [assembly: InternalsVisibleTo("Akka.Remote.Tests")] [assembly: InternalsVisibleTo("Akka.Remote.Tests.MultiNode")] diff --git a/src/core/Akka/Properties/AssemblyInfo.cs b/src/core/Akka/Properties/AssemblyInfo.cs index a9e2dfef0dc..b0db0007208 100644 --- a/src/core/Akka/Properties/AssemblyInfo.cs +++ b/src/core/Akka/Properties/AssemblyInfo.cs @@ -35,6 +35,7 @@ [assembly: InternalsVisibleTo("Akka.Remote.Tests")] [assembly: InternalsVisibleTo("Akka.Remote.Tests.MultiNode")] [assembly: InternalsVisibleTo("Akka.Remote.TestKit.Tests")] +[assembly: InternalsVisibleTo("Akka.Remote.TestKit.Xunit2.Tests")] [assembly: InternalsVisibleTo("Akka.Cluster")] [assembly: InternalsVisibleTo("Akka.Cluster.Sharding")] [assembly: InternalsVisibleTo("Akka.Cluster.Sharding.Tests")] diff --git a/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/Akka.Remote.TestKit.Xunit2.Tests.csproj b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/Akka.Remote.TestKit.Xunit2.Tests.csproj new file mode 100644 index 00000000000..00eb1d79528 --- /dev/null +++ b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/Akka.Remote.TestKit.Xunit2.Tests.csproj @@ -0,0 +1,19 @@ + + + $(NetFrameworkTestVersion);$(NetTestVersion) + false + Local only test project to test that Akka.Remote.TestKit.Xunit2 package works. Not to be run in CI/CD. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/BarrierSpec.cs b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/BarrierSpec.cs new file mode 100644 index 00000000000..937a8f1bdbf --- /dev/null +++ b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/BarrierSpec.cs @@ -0,0 +1,390 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Immutable; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.TestKit; +using Xunit; + +namespace Akka.Remote.TestKit.Tests +{ + public class BarrierSpec : AkkaSpec + { + private sealed class Failed + { + private readonly IActorRef _ref; + private readonly Exception _exception; + + public Failed(IActorRef @ref, Exception exception) + { + _ref = @ref; + _exception = exception; + } + + public IActorRef Ref + { + get { return _ref; } + } + + public Exception Exception + { + get { return _exception; } + } + + private bool Equals(Failed other) + { + return Equals(_ref, other._ref) && _exception.GetType() == other._exception.GetType(); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is Failed failed && Equals(failed); + } + + public override int GetHashCode() + { + unchecked + { + return ((_ref != null ? _ref.GetHashCode() : 0)*397) ^ + (_exception != null ? _exception.GetHashCode() : 0); + } + } + + public static bool operator ==(Failed left, Failed right) + { + return Equals(left, right); + } + + public static bool operator !=(Failed left, Failed right) + { + return !Equals(left, right); + } + } + + private const string Config = @" + akka.testconductor.barrier-timeout = 5s + akka.actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote"" + akka.actor.debug.fsm = on + akka.actor.debug.lifecycle = on + "; + + public BarrierSpec() : base(Config) + { + } + + private readonly RoleName A = new("a"); + private readonly RoleName B = new("b"); + private readonly RoleName C = new("c"); + + [Fact] + public void A_BarrierCoordinator_must_register_clients_and_remove_them() + { + var b = GetBarrier(); + b.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), Sys.DeadLetters), TestActor); + b.Tell(new BarrierCoordinator.RemoveClient(B)); + b.Tell(new BarrierCoordinator.RemoveClient(A)); + //EventFilter(1, () => b.Tell(new BarrierCoordinator.RemoveClient(A), TestActor)); //appears to be a bug in the testfilter + b.Tell(new BarrierCoordinator.RemoveClient(A)); + ExpectMsg(new Failed(b, + new BarrierCoordinator.BarrierEmptyException( + new BarrierCoordinator.Data(ImmutableHashSet.Create(), "", null, null), + "cannot remove RoleName(a): no client to remove"))); + } + + [Fact] + public void A_BarrierCoordinator_must_register_clients_and_disconnect_them() + { + var b = GetBarrier(); + b.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), Sys.DeadLetters)); + b.Tell(new Controller.ClientDisconnected(B)); + ExpectNoMsg(TimeSpan.FromSeconds(1)); + b.Tell(new Controller.ClientDisconnected(A)); + ExpectNoMsg(TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task A_BarrierCoordinator_must_fail_entering_barrier_when_nobody_registered() + { + var b = GetBarrier(); + b.Tell(new EnterBarrier("bar1", null, new RoleName("b")), TestActor); + await ExpectMsgAsync(new ToClient(new BarrierResult("bar1", false)), TimeSpan.FromSeconds(300)); + } + + [Fact] + public async Task A_BarrierCoordinator_must_enter_barrier() + { + var barrier = GetBarrier(); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + barrier.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref)); + barrier.Tell(new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref)); + a.Send(barrier, new EnterBarrier("bar2", null, new RoleName("a"))); + NoMsg(a, b); + await WithinAsync(TimeSpan.FromSeconds(2), async () => + { + b.Send(barrier, new EnterBarrier("bar2", null, new RoleName("b"))); + await a.ExpectMsgAsync(new ToClient(new BarrierResult("bar2", true))); + await b.ExpectMsgAsync(new ToClient(new BarrierResult("bar2", true))); + }); + } + + [Fact] + public async Task A_BarrierCoordinator_must_enter_barrier_with_joining_node() + { + var barrier = GetBarrier(); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + var c = CreateTestProbe(); + barrier.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref)); + barrier.Tell(new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref)); + a.Send(barrier, new EnterBarrier("bar3", null, new RoleName("a"))); + barrier.Tell(new Controller.NodeInfo(C, Address.Parse("akka://sys"), c.Ref)); + b.Send(barrier, new EnterBarrier("bar3", null, new RoleName("b"))); + NoMsg(a, b, c); + await WithinAsync(TimeSpan.FromSeconds(2), async () => + { + c.Send(barrier, new EnterBarrier("bar3", null, new RoleName("c"))); + await a.ExpectMsgAsync(new ToClient(new BarrierResult("bar3", true))); + await b.ExpectMsgAsync(new ToClient(new BarrierResult("bar3", true))); + await c.ExpectMsgAsync(new ToClient(new BarrierResult("bar3", true))); + }); + } + + [Fact] + public async Task A_BarrierCoordinator_must_enter_barrier_with_leaving_node() + { + var barrier = GetBarrier(); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + var c = CreateTestProbe(); + barrier.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref)); + barrier.Tell(new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref)); + barrier.Tell(new Controller.NodeInfo(C, Address.Parse("akka://sys"), c.Ref)); + a.Send(barrier, new EnterBarrier("bar4", null, new RoleName("a"))); + b.Send(barrier, new EnterBarrier("bar4", null, new RoleName("b"))); + barrier.Tell(new BarrierCoordinator.RemoveClient(A)); + barrier.Tell(new Controller.ClientDisconnected(A)); + NoMsg(a, b, c); + await WithinAsync(TimeSpan.FromSeconds(2), async () => + { + barrier.Tell(new BarrierCoordinator.RemoveClient(C)); + await b.ExpectMsgAsync(new ToClient(new BarrierResult("bar4", true))); + }); + barrier.Tell(new Controller.ClientDisconnected(C)); + await ExpectNoMsgAsync(TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task A_BarrierCoordinator_must_enter_leave_barrier_when_last_arrived_is_removed() + { + var barrier = GetBarrier(); + var roleName = new RoleName("normal"); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + barrier.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref)); + barrier.Tell(new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref)); + a.Send(barrier, new EnterBarrier("bar5", null, roleName)); + barrier.Tell(new BarrierCoordinator.RemoveClient(A)); + b.Send(barrier, new EnterBarrier("foo", null, roleName)); + await b.ExpectMsgAsync(new ToClient(new BarrierResult("foo", true))); + } + + [Fact] + public async Task A_BarrierCoordinator_must_fail_barrier_with_disconnecting_node() + { + var barrier = GetBarrier(); + var roleName = new RoleName("normal"); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + var nodeA = new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref); + barrier.Tell(nodeA); + barrier.Tell(new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref)); + a.Send(barrier, new EnterBarrier("bar6", null, roleName)); + //TODO: EventFilter? + barrier.Tell(new Controller.ClientDisconnected(B)); + var msg = await ExpectMsgAsync(); + Assert.Equal(new BarrierCoordinator.ClientLostException( + new BarrierCoordinator.Data( + ImmutableHashSet.Create(nodeA), + "bar6", + ImmutableHashSet.Create(a.Ref), + ((BarrierCoordinator.ClientLostException) msg.Exception).BarrierData.Deadline) + , B), msg.Exception); + } + + [Fact] + public async Task A_BarrierCoordinator_must_fail_barrier_when_disconnecting_node_who_already_arrived() + { + var barrier = GetBarrier(); + var roleNameA = new RoleName("a"); + var roleNameB = new RoleName("b"); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + var c = CreateTestProbe(); + var nodeA = new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref); + var nodeC = new Controller.NodeInfo(C, Address.Parse("akka://sys"), c.Ref); + barrier.Tell(nodeA); + barrier.Tell(new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref)); + barrier.Tell(nodeC); + a.Send(barrier, new EnterBarrier("bar7", null, roleNameA)); + b.Send(barrier, new EnterBarrier("bar7", null, roleNameB)); + //TODO: Event filter? + barrier.Tell(new Controller.ClientDisconnected(B)); + var msg = await ExpectMsgAsync(); + Assert.Equal(new BarrierCoordinator.ClientLostException( + new BarrierCoordinator.Data( + ImmutableHashSet.Create(nodeA, nodeC), + "bar7", + ImmutableHashSet.Create(a.Ref), + ((BarrierCoordinator.ClientLostException)msg.Exception).BarrierData.Deadline) + , B), msg.Exception); + + } + + [Fact] + public async Task A_BarrierCoordinator_must_fail_when_entering_wrong_barrier() + { + var barrier = GetBarrier(); + var roleName = new RoleName("failer"); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + var nodeA = (new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref)); + barrier.Tell(nodeA); + var nodeB = (new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref)); + barrier.Tell(nodeB); + a.Send(barrier, new EnterBarrier("bar8", null, roleName)); + //TODO: Event filter + b.Send(barrier, new EnterBarrier("foo", null, roleName)); + var msg = await ExpectMsgAsync(); + Assert.Equal(new BarrierCoordinator.WrongBarrierException( + "foo", + b.Ref, + roleName, + new BarrierCoordinator.Data( + ImmutableHashSet.Create(nodeA, nodeB), + "bar8", + ImmutableHashSet.Create(a.Ref), + ((BarrierCoordinator.WrongBarrierException)msg.Exception).BarrierData.Deadline) + ), msg.Exception); + } + + [Fact] + public async Task A_BarrierCoordinator_must_fail_barrier_after_first_failure() + { + var barrier = GetBarrier(); + var a = CreateTestProbe(); + var roleName = new RoleName("failer"); + //TODO: EventFilter + barrier.Tell(new BarrierCoordinator.RemoveClient(A)); + var msg = await ExpectMsgAsync(); + Assert.Equal(new BarrierCoordinator.BarrierEmptyException( + new BarrierCoordinator.Data( + ImmutableHashSet.Create(), + "", + ImmutableHashSet.Create(), + ((BarrierCoordinator.BarrierEmptyException)msg.Exception).BarrierData.Deadline) + , "cannot remove RoleName(a): no client to remove"), msg.Exception); + barrier.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref)); + a.Send(barrier, new EnterBarrier("bar9", null, roleName)); + a.ExpectMsg(new ToClient(new BarrierResult("bar9", false))); + } + + [Fact] + public async Task A_BarrierCoordinator_must_fail_after_barrier_timeout() + { + var barrier = GetBarrier(); + var roleName = new RoleName("failer"); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + var nodeA = new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref); + var nodeB = new Controller.NodeInfo(B, Address.Parse("akka://sys"), b.Ref); + barrier.Tell(nodeA); + barrier.Tell(nodeB); + a.Send(barrier, new EnterBarrier("bar10", null, roleName)); + await EventFilter.Exception().ExpectOneAsync(async () => + { + var msg = await ExpectMsgAsync(TimeSpan.FromSeconds(7)); + Assert.Equal(new BarrierCoordinator.BarrierTimeoutException( + new BarrierCoordinator.Data( + ImmutableHashSet.Create(nodeA, nodeB), + "bar10", + ImmutableHashSet.Create(a.Ref), + ((BarrierCoordinator.BarrierTimeoutException)msg.Exception).BarrierData.Deadline) + ), msg.Exception); + }); + } + + [Fact] + public void A_BarrierCoordinator_must_fail_if_a_node_registers_twice() + { + var barrier = GetBarrier(); + var a = CreateTestProbe(); + var b = CreateTestProbe(); + var nodeA = new Controller.NodeInfo(A, Address.Parse("akka://sys"), a.Ref); + var nodeB = new Controller.NodeInfo(A, Address.Parse("akka://sys"), b.Ref); + barrier.Tell(nodeA); + //TODO: Event filter + barrier.Tell(nodeB); + var msg = ExpectMsg(); + Assert.Equal(new BarrierCoordinator.DuplicateNodeException( + new BarrierCoordinator.Data( + ImmutableHashSet.Create(nodeA), + "", + ImmutableHashSet.Create(), + ((BarrierCoordinator.DuplicateNodeException)msg.Exception).BarrierData.Deadline) + , nodeB), msg.Exception); + } + + //TODO: Controller tests. + + private IActorRef GetBarrier() + { + var actor = + Sys.ActorOf( + new Props(typeof (BarrierCoordinatorSupervisor), new object[] {TestActor}).WithDeploy(Deploy.Local)); + actor.Tell("", TestActor); + return ExpectMsg(); + } + + private class BarrierCoordinatorSupervisor : UntypedActor + { + private readonly IActorRef _testActor; + private readonly IActorRef _barrier; + + public BarrierCoordinatorSupervisor(IActorRef testActor) + { + _testActor = testActor; + _barrier = Context.ActorOf(Props.Create()); + } + + protected override void OnReceive(object message) + { + Sender.Tell(_barrier); + } + + protected override SupervisorStrategy SupervisorStrategy() + { + return new OneForOneStrategy(e => + { + _testActor.Tell(new Failed(_barrier, e)); + return Directive.Restart; + }); + } + } + + private void NoMsg(params TestProbe[] probes) + { + ExpectNoMsg(TimeSpan.FromSeconds(1)); + foreach (var probe in probes) Assert.False(probe.HasMessages); + } + } +} + diff --git a/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/ControllerSpec.cs b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/ControllerSpec.cs new file mode 100644 index 00000000000..faaaef84684 --- /dev/null +++ b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/ControllerSpec.cs @@ -0,0 +1,53 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Net; +using Akka.Actor; +using Akka.TestKit; +using Xunit; + +namespace Akka.Remote.TestKit.Tests +{ + public class ControllerSpec : AkkaSpec + { + private const string Config = @" + akka.testconductor.barrier-timeout = 5s + akka.actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote"" + akka.actor.debug.fsm = on + akka.actor.debug.lifecycle = on + "; + + public ControllerSpec() + : base(Config) + { + } + + private readonly RoleName A = new("a"); + private readonly RoleName B = new("b"); + + [Fact] + public void Controller_must_publish_its_nodes() + { + var c = Sys.ActorOf(Props.Create(() => new Controller(1, new IPEndPoint(IPAddress.Loopback, 0)))); + c.Tell(new Controller.NodeInfo(A, Address.Parse("akka://sys"), TestActor)); + ExpectMsg>(); + c.Tell(new Controller.NodeInfo(B, Address.Parse("akka://sys"), TestActor)); + ExpectMsg>(); + c.Tell(Controller.GetNodes.Instance); + ExpectMsg>(names => XAssert.Equivalent(names, new[] {A, B})); + AwaitAssert(() => + { + Watch(c); + c.Tell(PoisonPill.Instance); + ExpectMsg(); + }, TimeSpan.FromSeconds(20)); + } + } +} + diff --git a/src/core/Akka.Remote.TestKit.Xunit2/Properties/AssemblyInfo.cs b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/Properties/AssemblyInfo.cs similarity index 67% rename from src/core/Akka.Remote.TestKit.Xunit2/Properties/AssemblyInfo.cs rename to src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/Properties/AssemblyInfo.cs index 4da60bcd828..18c00e8f4eb 100644 --- a/src/core/Akka.Remote.TestKit.Xunit2/Properties/AssemblyInfo.cs +++ b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/Properties/AssemblyInfo.cs @@ -6,12 +6,12 @@ //----------------------------------------------------------------------- using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. +using Xunit; // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from @@ -19,6 +19,17 @@ [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("de375180-0f6f-40c5-9dd4-3a27e2559d5d")] -[assembly: InternalsVisibleTo("Akka.Remote.TestKit.Tests")] +[assembly: Guid("6750d264-42e4-4004-bbc2-9caca7228c60")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: CollectionBehavior(DisableTestParallelization = true)] diff --git a/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/RemoteConnectionSpecs.cs b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/RemoteConnectionSpecs.cs new file mode 100644 index 00000000000..ae1a49aea00 --- /dev/null +++ b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/RemoteConnectionSpecs.cs @@ -0,0 +1,179 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2025 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using FluentAssertions; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.TestKit; +using Akka.Util.Internal; +using DotNetty.Transport.Channels; +using Xunit; + +namespace Akka.Remote.TestKit.Tests +{ + public class RemoteConnectionSpecs : AkkaSpec + { + private const string Config = @" + akka.testconductor.barrier-timeout = 5s + akka.loglevel = DEBUG + akka.actor.provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote"" + akka.actor.debug.fsm = on + akka.actor.debug.lifecycle = on + "; + + public RemoteConnectionSpecs() : base(Config) + { + + } + + [Fact(Skip = "Consistently fails on buildserver - appears to be some binding issue on Azure DevOps")] + public void RemoteConnection_should_send_and_decode_messages() + { + var serverProbe = CreateTestProbe("server"); + var clientProbe = CreateTestProbe("client"); + + var serverAddress = IPAddress.Parse("127.0.0.1"); + var serverEndpoint = new IPEndPoint(serverAddress, 0); + + IChannel server = null; + IChannel client = null; + + try + { + var t1 = RemoteConnection.CreateConnection(Role.Server, serverEndpoint, 3, + new TestConductorHandler(serverProbe.Ref)); + t1.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + server = t1.Result; // task will already be complete or cancelled + + var reachableEndpoint = (IPEndPoint)server.LocalAddress; + + var t2 = RemoteConnection.CreateConnection(Role.Client, reachableEndpoint, 3, + new PlayerHandler(serverEndpoint, 2, TimeSpan.FromSeconds(1), 3, clientProbe.Ref, Log, Sys.Scheduler)); + t2.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); + client = t2.Result; // task will already be completed or cancelled + + serverProbe.ExpectMsg("active"); + var serverClientChannel = serverProbe.ExpectMsg(); + clientProbe.ExpectMsg(); + + var address = RARP.For(Sys).Provider.DefaultAddress; + + // have the client send a message to the server + client.WriteAndFlushAsync(new Hello("test", address)); + var hello = serverProbe.ExpectMsg(); + hello.Name.Should().Be("test"); + hello.Address.Should().Be(address); + + // have the server send a message back to the client + serverClientChannel.WriteAndFlushAsync(new Hello("test2", address)); + var hello2 = clientProbe.ExpectMsg(); + hello2.Name.Should().Be("test2"); + hello2.Address.Should().Be(address); + } + finally + { + server?.CloseAsync().Wait(TimeSpan.FromSeconds(2)); + client?.CloseAsync().Wait(TimeSpan.FromSeconds(2)); + } + + } + + [Fact(Skip = "This causes a deadlock sometimes")] + public async Task RemoteConnection_should_send_and_decode_Done_message() + { + var serverProbe = CreateTestProbe("server"); + var clientProbe = CreateTestProbe("client"); + + var serverAddress = IPAddress.Parse("127.0.0.1"); + var serverEndpoint = new IPEndPoint(serverAddress, 0); + + IChannel server = null; + IChannel client = null; + + try + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(10)); + var t1 = RemoteConnection.CreateConnection(Role.Server, serverEndpoint, 3, + new TestConductorHandler(serverProbe.Ref)); + await t1.WithCancellation(cts.Token); + server = t1.Result; // task will already be complete or cancelled + + var reachableEndpoint = (IPEndPoint)server.LocalAddress; + + var t2 = RemoteConnection.CreateConnection(Role.Client, reachableEndpoint, 3, + new PlayerHandler(serverEndpoint, 2, TimeSpan.FromSeconds(1), 3, clientProbe.Ref, Log, Sys.Scheduler)); + await t2.WithCancellation(cts.Token); + client = t2.Result; // task will already be completed or cancelled + + serverProbe.ExpectMsg("active"); + var serverClientChannel = serverProbe.ExpectMsg(); + clientProbe.ExpectMsg(); + + var address = RARP.For(Sys).Provider.DefaultAddress; + + // have the client send a message to the server + await client.WriteAndFlushAsync(Done.Instance); + var done = serverProbe.ExpectMsg(); + done.Should().BeOfType(); + + //have the server send a message back to the client + await serverClientChannel.WriteAndFlushAsync(Done.Instance); + var done2 = clientProbe.ExpectMsg(); + done2.Should().BeOfType(); + } + finally + { + server?.CloseAsync().Wait(TimeSpan.FromSeconds(2)); + client?.CloseAsync().Wait(TimeSpan.FromSeconds(2)); + } + + } + } + + public class TestConductorHandler : ChannelHandlerAdapter + { + private readonly IActorRef _testActorRef; + + public TestConductorHandler(IActorRef testActorRef) + { + _testActorRef = testActorRef; + } + + public override bool IsSharable => true; + + public override void ChannelActive(IChannelHandlerContext context) + { + _testActorRef.Tell("active"); + _testActorRef.Tell(context.Channel); + } + + public override void ChannelInactive(IChannelHandlerContext context) + { + _testActorRef.Tell("inactive"); + } + + public override void ChannelRead(IChannelHandlerContext context, object message) + { + if (message is INetworkOp) + { + _testActorRef.Tell(message); + } + else + { + //_log.Debug("client {0} sent garbage `{1}`, disconnecting", channel.RemoteAddress, message); + context.Channel.CloseAsync(); + } + } + } +} diff --git a/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/TestConductorSpec.cs b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/TestConductorSpec.cs new file mode 100644 index 00000000000..e16f256ce45 --- /dev/null +++ b/src/examples/Testing/Xunit2/Akka.Remote.TestKit.Xunit2.Tests/TestConductorSpec.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------- +// +// Copyright (C) 2009-2022 Lightbend Inc. +// Copyright (C) 2013-2026 .NET Foundation +// +// ----------------------------------------------------------------------- + +using FluentAssertions; +using FluentAssertions.Extensions; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Remote.TestKit.Tests; + +public class TestConductorSpec: Akka.TestKit.Xunit2.TestKit +{ + public TestConductorSpec(ITestOutputHelper output): base("akka.actor.provider = remote", nameof(TestConductorSpec), output) + { + } + + [Fact(DisplayName = "TestConductor must initialize with not error")] + public void InitializationTest() + { + var conductor = TestConductor.Get(Sys); + conductor.Settings.BarrierTimeout.Should().Be(30.Seconds()); + } +} \ No newline at end of file