From 0912912484c01d91d3f1dd089fcf37535304b4d3 Mon Sep 17 00:00:00 2001 From: Pantazis Deligiannis Date: Fri, 28 Feb 2020 16:01:21 -0800 Subject: [PATCH] Initial commit --- .gitattributes | 20 + .gitignore | 343 +++ Common/Key.snk | Bin 0 -> 596 bytes Common/build.props | 32 + Common/codeanalysis.ruleset | 148 ++ Common/dependencies.props | 8 + Common/key.props | 6 + Common/stylecop.json | 45 + Common/version.props | 7 + Coyote.sln | 119 + History.md | 2 + LICENSE | 23 + NuGet.config | 12 + README.md | 30 + SECURITY.md | 35 + Scripts/NuGet/Coyote.nuspec | 44 + Scripts/build.ps1 | 57 + Scripts/create-nuget-packages.ps1 | 41 + Scripts/powershell/common.psm1 | 34 + Scripts/publish-nuget-package.ps1 | 38 + Scripts/run-benchmarks.ps1 | 29 + Scripts/run-tests.ps1 | 38 + Scripts/upload-benchmark-results.ps1 | 33 + Source/Core/Core.csproj | 24 + Source/Core/IO/Debugging/Debug.cs | 115 + Source/Core/IO/Debugging/Error.cs | 59 + Source/Core/IO/Logging/ConsoleLogger.cs | 148 ++ Source/Core/IO/Logging/ILogger.cs | 73 + Source/Core/IO/Logging/InMemoryLogger.cs | 273 +++ Source/Core/IO/Logging/LogWriter.cs | 339 +++ Source/Core/IO/Logging/NulLogger.cs | 99 + Source/Core/IO/Logging/RuntimeLogWriter.cs | 756 ++++++ Source/Core/Machines/AsyncMachine.cs | 85 + Source/Core/Machines/DequeueStatus.cs | 31 + Source/Core/Machines/EnqueueStatus.cs | 36 + .../Core/Machines/EventQueues/EventQueue.cs | 316 +++ .../Core/Machines/EventQueues/IEventQueue.cs | 65 + Source/Core/Machines/Events/Default.cs | 27 + Source/Core/Machines/Events/GotoStateEvent.cs | 30 + Source/Core/Machines/Events/Halt.cs | 22 + Source/Core/Machines/Events/PushStateEvent.cs | 30 + Source/Core/Machines/Events/QuiescentEvent.cs | 28 + Source/Core/Machines/Events/WildcardEvent.cs | 22 + .../Core/Machines/Handlers/ActionBinding.cs | 24 + .../Core/Machines/Handlers/CachedDelegate.cs | 39 + Source/Core/Machines/Handlers/DeferAction.cs | 12 + .../Machines/Handlers/EventActionHandler.cs | 12 + .../Machines/Handlers/EventHandlerStatus.cs | 26 + .../Machines/Handlers/GotoStateTransition.cs | 45 + Source/Core/Machines/Handlers/IgnoreAction.cs | 12 + .../Machines/Handlers/PushStateTransition.cs | 27 + Source/Core/Machines/IMachineStateManager.cs | 96 + Source/Core/Machines/Machine.cs | 1748 ++++++++++++++ Source/Core/Machines/MachineFactory.cs | 41 + Source/Core/Machines/MachineId.cs | 166 ++ Source/Core/Machines/MachineState.cs | 600 +++++ Source/Core/Machines/MachineStateManager.cs | 155 ++ Source/Core/Machines/SendOptions.cs | 57 + Source/Core/Machines/SingleStateMachine.cs | 62 + Source/Core/Machines/StateGroup.cs | 12 + Source/Core/Machines/Timers/IMachineTimer.cs | 18 + Source/Core/Machines/Timers/MachineTimer.cs | 120 + .../Core/Machines/Timers/TimerElapsedEvent.cs | 25 + Source/Core/Machines/Timers/TimerInfo.cs | 91 + Source/Core/Properties/InternalsVisibleTo.cs | 64 + Source/Core/Runtime/Configuration.cs | 433 ++++ Source/Core/Runtime/CoyoteRuntime.cs | 888 +++++++ Source/Core/Runtime/Events/Event.cs | 15 + Source/Core/Runtime/Events/EventInfo.cs | 75 + Source/Core/Runtime/Events/EventOriginInfo.cs | 43 + .../Exceptions/AssertionFailureException.cs | 32 + .../Exceptions/ExecutionCanceledException.cs | 22 + .../MachineActionExceptionFilterException.cs | 32 + .../Exceptions/OnEventDroppedHandler.cs | 12 + .../Runtime/Exceptions/OnExceptionOutcome.cs | 26 + .../Runtime/Exceptions/OnFailureHandler.cs | 12 + .../Runtime/Exceptions/RuntimeException.cs | 48 + .../Exceptions/UnhandledEventException.cs | 36 + Source/Core/Runtime/IMachineRuntime.cs | 277 +++ Source/Core/Runtime/MachineRuntimeFactory.cs | 32 + Source/Core/Runtime/ProductionRuntime.cs | 910 ++++++++ .../Providers/AsyncLocalRuntimeProvider.cs | 45 + .../Core/Runtime/Providers/RuntimeProvider.cs | 36 + Source/Core/Runtime/TestAttributes.cs | 54 + .../Core/Specifications/Monitors/Monitor.cs | 846 +++++++ .../Specifications/Monitors/MonitorState.cs | 474 ++++ Source/Core/Specifications/Specification.cs | 77 + Source/Core/Threading/ControlledLock.cs | 125 + .../Tasks/AsyncControlledTaskMethodBuilder.cs | 294 +++ .../ConfiguredControlledTaskAwaitable.cs | 165 ++ Source/Core/Threading/Tasks/ControlledTask.cs | 637 +++++ .../Threading/Tasks/ControlledTaskAwaiter.cs | 121 + .../Threading/Tasks/ControlledTaskMachine.cs | 47 + .../Tasks/ControlledYieldAwaitable.cs | 73 + Source/Core/Threading/Tasks/TaskExtensions.cs | 24 + Source/Core/Utilities/ErrorReporter.cs | 77 + Source/Core/Utilities/NameResolver.cs | 60 + Source/Core/Utilities/Profiler.cs | 41 + .../Tooling/BaseCommandLineOptions.cs | 132 ++ .../Utilities/Tooling/SchedulingStrategy.cs | 81 + .../SharedCounter/ISharedCounter.cs | 49 + .../SharedCounter/MockSharedCounter.cs | 96 + .../SharedCounter/ProductionSharedCounter.cs | 63 + .../SharedCounter/SharedCounter.cs | 35 + .../SharedCounter/SharedCounterEvent.cs | 105 + .../SharedCounter/SharedCounterMachine.cs | 80 + .../SharedCounterResponseEvent.cs | 26 + .../SharedDictionary/ISharedDictionary.cs | 57 + .../SharedDictionary/MockSharedDictionary.cs | 125 + .../ProductionSharedDictionary.cs | 77 + .../SharedDictionary/SharedDictionary.cs | 57 + .../SharedDictionary/SharedDictionaryEvent.cs | 119 + .../SharedDictionaryMachine.cs | 134 ++ .../SharedDictionaryResponseEvent.cs | 26 + Source/SharedObjects/SharedObjects.csproj | 21 + .../SharedRegister/ISharedRegister.cs | 34 + .../SharedRegister/MockSharedRegister.cs | 66 + .../ProductionSharedRegister.cs | 79 + .../SharedRegister/SharedRegister.cs | 36 + .../SharedRegister/SharedRegisterEvent.cs | 78 + .../SharedRegister/SharedRegisterMachine.cs | 62 + .../SharedRegisterResponseEvent.cs | 26 + .../Engines/AbstractTestingEngine.cs | 669 ++++++ .../Engines/BugFindingEngine.cs | 449 ++++ .../TestingServices/Engines/ITestingEngine.cs | 44 + .../TestingServices/Engines/ReplayEngine.cs | 231 ++ .../Engines/TestingEngineFactory.cs | 87 + .../Exploration/ISchedulingStrategy.cs | 95 + .../Exploration/OperationScheduler.cs | 593 +++++ .../DefaultRandomNumberGenerator.cs | 74 + .../IRandomNumberGenerator.cs | 27 + .../Bounded/DelayBoundingStrategy.cs | 203 ++ .../ExhaustiveDelayBoundingStrategy.cs | 97 + .../Strategies/Bounded/PCTStrategy.cs | 349 +++ .../Bounded/RandomDelayBoundingStrategy.cs | 82 + .../Strategies/Exhaustive/DFSStrategy.cs | 469 ++++ .../IterativeDeepeningDFSStrategy.cs | 66 + .../Liveness/CycleDetectionStrategy.cs | 682 ++++++ .../Liveness/LivenessCheckingStrategy.cs | 134 ++ .../Liveness/TemperatureCheckingStrategy.cs | 69 + .../ProbabilisticRandomStrategy.cs | 91 + .../Probabilistic/RandomStrategy.cs | 169 ++ .../Strategies/Special/ComboStrategy.cs | 177 ++ .../Strategies/Special/InteractiveStrategy.cs | 463 ++++ .../Strategies/Special/ReplayStrategy.cs | 365 +++ .../SerializedMachineEventQueue.cs | 375 +++ .../Machines/MachineTestKit.cs | 253 ++ .../Machines/MachineTestingRuntime.cs | 785 +++++++ .../Machines/SerializedMachineStateManager.cs | 197 ++ .../Machines/Timers/MockMachineTimer.cs | 128 + .../Machines/Timers/TimerSetupEvent.cs | 42 + .../Operations/AsyncOperationStatus.cs | 46 + .../Operations/IAsyncOperation.cs | 27 + .../Operations/MachineOperation.cs | 230 ++ .../Properties/InternalsVisibleTo.cs | 58 + .../StateCaching/Fingerprint.cs | 44 + .../StateCaching/MonitorStatus.cs | 15 + Source/TestingServices/StateCaching/State.cs | 59 + .../StateCaching/StateCache.cs | 119 + .../Coverage/ActivityCoverageReporter.cs | 386 +++ .../Statistics/Coverage/CoverageInfo.cs | 115 + .../Statistics/Coverage/Transition.cs | 70 + .../TestingServices/Statistics/TestReport.cs | 295 +++ .../SystematicTestingRuntime.cs | 2075 +++++++++++++++++ Source/TestingServices/TestingServices.csproj | 24 + .../Threading/InterceptingTaskScheduler.cs | 82 + .../TestingServices/Threading/MachineLock.cs | 80 + .../Threading/Tasks/ActionMachine.cs | 71 + .../Threading/Tasks/DelayMachine.cs | 63 + .../Threading/Tasks/FuncMachine.cs | 209 ++ .../Threading/Tasks/MachineTask.cs | 319 +++ .../Threading/Tasks/MachineTaskType.cs | 23 + .../Threading/Tasks/TestExecutionMachine.cs | 93 + .../TestingServices/Tracing/Error/BugTrace.cs | 242 ++ .../Tracing/Error/BugTraceStep.cs | 139 ++ .../Tracing/Error/BugTraceStepType.cs | 37 + .../Tracing/Schedules/ScheduleStep.cs | 161 ++ .../Tracing/Schedules/ScheduleStepType.cs | 15 + .../Tracing/Schedules/ScheduleTrace.cs | 181 ++ Tests/Core.Tests/BaseTest.cs | 100 + Tests/Core.Tests/Core.Tests.csproj | 34 + .../EventQueues/EventQueueStressTest.cs | 141 ++ .../Core.Tests/EventQueues/EventQueueTest.cs | 465 ++++ .../EventQueues/MockMachineStateManager.cs | 140 ++ .../ExceptionPropagationTest.cs | 144 ++ .../ExceptionPropagation/OnExceptionTest.cs | 337 +++ .../Features/GetOperationGroupIdTest.cs | 105 + .../Core.Tests/Features/OnEventDroppedTest.cs | 249 ++ Tests/Core.Tests/Features/OnHaltTest.cs | 256 ++ .../Features/OperationGroupingTest.cs | 519 +++++ .../LogMessages/Common/CustomLogWriter.cs | 25 + .../LogMessages/Common/CustomLogger.cs | 86 + .../Core.Tests/LogMessages/Common/Machines.cs | 80 + .../LogMessages/CustomLogWriterTest.cs | 52 + .../LogMessages/CustomLoggerTest.cs | 84 + .../Machines/GotoStateTransitionTest.cs | 103 + Tests/Core.Tests/Machines/HandleEventTest.cs | 136 ++ Tests/Core.Tests/Machines/InvokeMethodTest.cs | 124 + .../Machines/ReceiveEventIntegrationTest.cs | 190 ++ .../Machines/ReceiveEventStressTest.cs | 258 ++ Tests/Core.Tests/Machines/ReceiveEventTest.cs | 181 ++ .../Machines/Timers/TimerStressTest.cs | 126 + Tests/Core.Tests/Machines/Timers/TimerTest.cs | 298 +++ .../MemoryLeak/NoMemoryLeakAfterHaltTest.cs | 115 + .../NoMemoryLeakInEventSendingTest.cs | 109 + .../CreateMachineIdFromNameTest.cs | 279 +++ .../RuntimeInterface/SendAndExecuteTest.cs | 446 ++++ .../Threading/Tasks/CompletedTaskTest.cs | 60 + .../Threading/Tasks/TaskAwaitTest.cs | 137 ++ .../Tasks/TaskConfigureAwaitFalseTest.cs | 137 ++ .../Tasks/TaskConfigureAwaitTrueTest.cs | 137 ++ .../Threading/Tasks/TaskExceptionTest.cs | 240 ++ .../Tasks/TaskRunConfigureAwaitFalseTest.cs | 172 ++ .../Tasks/TaskRunConfigureAwaitTrueTest.cs | 172 ++ .../Core.Tests/Threading/Tasks/TaskRunTest.cs | 172 ++ .../Threading/Tasks/TaskWhenAllTest.cs | 163 ++ .../Threading/Tasks/TaskWhenAnyTest.cs | 148 ++ Tests/Core.Tests/xunit.runner.json | 6 + Tests/SharedObjects.Tests/BaseTest.cs | 209 ++ .../ProductionSharedObjectsTest.cs | 122 + .../SharedCounter/MockSharedCounterTest.cs | 264 +++ .../ProductionSharedCounterTest.cs | 148 ++ .../MockSharedDictionaryTest.cs | 346 +++ .../ProductionSharedDictionaryTest.cs | 347 +++ .../SharedObjects.Tests.csproj | 28 + .../SharedRegister/MockSharedRegisterTest.cs | 210 ++ .../ProductionSharedRegisterTest.cs | 85 + Tests/TestingServices.Tests/BaseTest.cs | 293 +++ .../Coverage/ActivityCoverageTest.cs | 361 +++ .../EntryPoint/EntryPointEventSendingTest.cs | 54 + .../EntryPointMachineCreationTest.cs | 44 + .../EntryPointMachineExecutionTest.cs | 51 + .../EntryPoint/EntryPointRandomChoiceTest.cs | 37 + .../EntryPointThrowExceptionTest.cs | 47 + .../LogMessages/Common/CustomLogWriter.cs | 25 + .../LogMessages/Common/CustomLogger.cs | 86 + .../LogMessages/Common/Machines.cs | 57 + .../LogMessages/CustomLogWriterTest.cs | 70 + .../Machines/EventHandling/ActionsFailTest.cs | 364 +++ .../IgnoreEvent/IgnoreRaisedTest.cs | 88 + .../EventHandling/MaxEventInstancesTest.cs | 173 ++ .../ReceiveEvent/ReceiveEventFailTest.cs | 154 ++ .../ReceiveEvent/ReceiveEventTest.cs | 131 ++ .../Wildcard/WildCardEventTest.cs | 79 + .../Features/AmbiguousEventHandlerTest.cs | 90 + .../Machines/Features/CurrentStateTest.cs | 61 + .../Features/DuplicateEventHandlersTest.cs | 234 ++ .../Machines/Features/EventInheritanceTest.cs | 229 ++ .../Machines/Features/GroupStateTest.cs | 134 ++ .../Features/MachineStateInheritanceTest.cs | 696 ++++++ .../Machines/Features/MethodCallTest.cs | 51 + .../Machines/Features/NameofTest.cs | 138 ++ .../Machines/Features/PopTest.cs | 77 + .../Machines/Features/ReceiveTest.cs | 49 + .../Machines/GenericMachineTest.cs | 66 + .../Integration/BubbleSortAlgorithmTest.cs | 122 + .../Integration/ChainReplicationTest.cs | 1560 +++++++++++++ .../Machines/Integration/ChordTest.cs | 912 ++++++++ .../Integration/DiningPhilosophersTest.cs | 236 ++ .../Integration/OneMachineIntegrationTests.cs | 964 ++++++++ .../Integration/ProcessSchedulerTest.cs | 644 +++++ .../Machines/Integration/RaftTest.cs | 1245 ++++++++++ .../Integration/ReplicatingStorageTest.cs | 903 +++++++ .../Integration/SendInterleavingsTest.cs | 108 + .../Integration/TwoMachineIntegrationTests.cs | 475 ++++ .../Machines/SingleStateMachineTest.cs | 89 + .../Threading/UncontrolledMachineDelayTest.cs | 121 + .../Threading/UncontrolledMachineTaskTest.cs | 94 + .../Transitions/GotoStateExitFailTest.cs | 53 + .../Machines/Transitions/GotoStateFailTest.cs | 68 + .../GotoStateMultipleInActionFailTest.cs | 149 ++ .../Machines/Transitions/GotoStateTest.cs | 82 + .../Machines/Transitions/PushApiTest.cs | 178 ++ .../Machines/Transitions/PushStateTest.cs | 83 + .../Runtime/CompletenessTest.cs | 104 + .../Runtime/CreateMachineIdFromNameTest.cs | 304 +++ .../Runtime/CreateMachineWithIdTest.cs | 308 +++ .../Runtime/FairRandomTest.cs | 88 + .../Runtime/GetOperationGroupIdTest.cs | 118 + .../Runtime/MustHandleEventTest.cs | 172 ++ .../Runtime/OnEventDequeueOrHandledTest.cs | 586 +++++ .../Runtime/OnEventDroppedTest.cs | 194 ++ .../Runtime/OnEventUnhandledTest.cs | 158 ++ .../Runtime/OnExceptionTest.cs | 443 ++++ .../Runtime/OnHaltTest.cs | 210 ++ .../Runtime/OperationGroupingTest.cs | 615 +++++ .../Runtime/ReceivingExternalEventTest.cs | 64 + .../Runtime/SendAndExecuteTest.cs | 569 +++++ .../CycleDetection/CycleDetectionBasicTest.cs | 112 + .../CycleDetectionCounterTest.cs | 99 + .../CycleDetectionDefaultHandlerTest.cs | 110 + .../CycleDetectionRandomChoiceTest.cs | 135 ++ .../CycleDetectionRingOfNodesTest.cs | 151 ++ .../CycleDetection/FairNondet1Test.cs | 125 + .../Liveness/CycleDetection/Liveness2Test.cs | 113 + .../Liveness/CycleDetection/Liveness3Test.cs | 127 + .../Liveness/CycleDetection/Nondet1Test.cs | 126 + .../CycleDetection/WarmStateBugTest.cs | 112 + .../Specifications/Liveness/HotStateTest.cs | 191 ++ .../Specifications/Liveness/Liveness1Test.cs | 107 + .../Liveness/Liveness2BugFoundTest.cs | 113 + .../Liveness/Liveness2LoopMachineTest.cs | 126 + .../Liveness/UnfairExecutionTest.cs | 120 + .../Specifications/Liveness/WarmStateTest.cs | 103 + .../Monitors/GenericMonitorTest.cs | 77 + .../Monitors/IdempotentRegisterMonitorTest.cs | 80 + .../MachineMonitorIntegrationTests.cs | 139 ++ .../Monitors/MonitorStateInheritanceTest.cs | 454 ++++ .../Monitors/MonitorWildCardEventTest.cs | 106 + .../TestingServices.Tests.csproj | 28 + .../Threading/ControlledLockTest.cs | 129 + .../TaskBooleanNondeterminismTest.cs | 183 ++ .../TaskIntegerNondeterminismTest.cs | 183 ++ .../Specifications/TaskLivenessMonitorTest.cs | 235 ++ .../Specifications/TaskSafetyMonitorTest.cs | 144 ++ .../TaskConfigureAwaitFalseTest.cs | 281 +++ .../TaskConfigureAwaitTrueTest.cs | 281 +++ .../TaskRunConfigureAwaitFalseTest.cs | 421 ++++ .../TaskRunConfigureAwaitTrueTest.cs | 421 ++++ .../MixedControlledTaskAwaitTest.cs | 178 ++ .../MixedTypes/MixedMultipleTaskAwaitTest.cs | 198 ++ .../MixedTypes/MixedSkipTaskAwaitTest.cs | 198 ++ .../MixedUncontrolledTaskAwaitTest.cs | 338 +++ .../Tasks/MultipleJoin/TaskWaitAnyTest.cs | 212 ++ .../Tasks/MultipleJoin/TaskWhenAllTest.cs | 197 ++ .../Tasks/MultipleJoin/TaskWhenAnyTest.cs | 201 ++ .../Threading/Tasks/TaskAwaitTest.cs | 281 +++ .../Threading/Tasks/TaskDelayTest.cs | 74 + .../Threading/Tasks/TaskExceptionTest.cs | 296 +++ .../Threading/Tasks/TaskInterleavingsTest.cs | 183 ++ .../Threading/Tasks/TaskRunTest.cs | 421 ++++ .../Threading/Tasks/TaskYieldTest.cs | 156 ++ .../Timers/BasicTimerTest.cs | 296 +++ .../Timers/StartStopTimerTest.cs | 81 + .../Timers/TimerLivenessTest.cs | 75 + Tests/Tests.Common/BaseTest.cs | 17 + Tests/Tests.Common/TestConsoleLogger.cs | 157 ++ Tests/Tests.Common/TestOutputLogger.cs | 170 ++ Tests/Tests.Common/Tests.Common.csproj | 30 + .../CoyoteBenchmarkRunner.csproj | 23 + .../CoyoteBenchmarkRunner/Program.cs | 26 + .../MachineCreationThroughputBenchmark.cs | 84 + .../DequeueEventThroughputBenchmark.cs | 171 ++ .../ExchangeEventLatencyBenchmark.cs | 181 ++ .../Messaging/SendEventThroughputBenchmark.cs | 195 ++ .../CoverageReportMerger.csproj | 21 + Tools/Testing/CoverageReportMerger/Program.cs | 131 ++ Tools/Testing/Replayer/Program.cs | 41 + Tools/Testing/Replayer/Replayer.csproj | 21 + Tools/Testing/Replayer/ReplayingProcess.cs | 51 + .../Utilities/ReplayerCommandLineOptions.cs | 120 + Tools/Testing/Tester/App.config | 9 + .../CodeCoverageInstrumentation.cs | 286 +++ .../Tester/Interfaces/ITestingProcess.cs | 41 + .../Interfaces/ITestingProcessScheduler.cs | 42 + .../Tester/Monitoring/CodeCoverageMonitor.cs | 134 ++ Tools/Testing/Tester/Program.cs | 114 + .../Scheduling/TestingProcessScheduler.cs | 489 ++++ Tools/Testing/Tester/Tester.csproj | 29 + .../Tester/Testing/TestingPortfolio.cs | 42 + .../Testing/Tester/Testing/TestingProcess.cs | 317 +++ .../Tester/Testing/TestingProcessFactory.cs | 129 + .../Tester/Utilities/DependencyGraph.cs | 162 ++ .../Utilities/DependentAssemblyLoader.cs | 43 + Tools/Testing/Tester/Utilities/ExitCode.cs | 26 + Tools/Testing/Tester/Utilities/Reporter.cs | 107 + .../Utilities/TesterCommandLineOptions.cs | 446 ++++ Versioning.md | 55 + global.json | 5 + 369 files changed, 64545 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Common/Key.snk create mode 100644 Common/build.props create mode 100644 Common/codeanalysis.ruleset create mode 100644 Common/dependencies.props create mode 100644 Common/key.props create mode 100644 Common/stylecop.json create mode 100644 Common/version.props create mode 100644 Coyote.sln create mode 100644 History.md create mode 100644 LICENSE create mode 100644 NuGet.config create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 Scripts/NuGet/Coyote.nuspec create mode 100644 Scripts/build.ps1 create mode 100644 Scripts/create-nuget-packages.ps1 create mode 100644 Scripts/powershell/common.psm1 create mode 100644 Scripts/publish-nuget-package.ps1 create mode 100644 Scripts/run-benchmarks.ps1 create mode 100644 Scripts/run-tests.ps1 create mode 100644 Scripts/upload-benchmark-results.ps1 create mode 100644 Source/Core/Core.csproj create mode 100644 Source/Core/IO/Debugging/Debug.cs create mode 100644 Source/Core/IO/Debugging/Error.cs create mode 100644 Source/Core/IO/Logging/ConsoleLogger.cs create mode 100644 Source/Core/IO/Logging/ILogger.cs create mode 100644 Source/Core/IO/Logging/InMemoryLogger.cs create mode 100644 Source/Core/IO/Logging/LogWriter.cs create mode 100644 Source/Core/IO/Logging/NulLogger.cs create mode 100644 Source/Core/IO/Logging/RuntimeLogWriter.cs create mode 100644 Source/Core/Machines/AsyncMachine.cs create mode 100644 Source/Core/Machines/DequeueStatus.cs create mode 100644 Source/Core/Machines/EnqueueStatus.cs create mode 100644 Source/Core/Machines/EventQueues/EventQueue.cs create mode 100644 Source/Core/Machines/EventQueues/IEventQueue.cs create mode 100644 Source/Core/Machines/Events/Default.cs create mode 100644 Source/Core/Machines/Events/GotoStateEvent.cs create mode 100644 Source/Core/Machines/Events/Halt.cs create mode 100644 Source/Core/Machines/Events/PushStateEvent.cs create mode 100644 Source/Core/Machines/Events/QuiescentEvent.cs create mode 100644 Source/Core/Machines/Events/WildcardEvent.cs create mode 100644 Source/Core/Machines/Handlers/ActionBinding.cs create mode 100644 Source/Core/Machines/Handlers/CachedDelegate.cs create mode 100644 Source/Core/Machines/Handlers/DeferAction.cs create mode 100644 Source/Core/Machines/Handlers/EventActionHandler.cs create mode 100644 Source/Core/Machines/Handlers/EventHandlerStatus.cs create mode 100644 Source/Core/Machines/Handlers/GotoStateTransition.cs create mode 100644 Source/Core/Machines/Handlers/IgnoreAction.cs create mode 100644 Source/Core/Machines/Handlers/PushStateTransition.cs create mode 100644 Source/Core/Machines/IMachineStateManager.cs create mode 100644 Source/Core/Machines/Machine.cs create mode 100644 Source/Core/Machines/MachineFactory.cs create mode 100644 Source/Core/Machines/MachineId.cs create mode 100644 Source/Core/Machines/MachineState.cs create mode 100644 Source/Core/Machines/MachineStateManager.cs create mode 100644 Source/Core/Machines/SendOptions.cs create mode 100644 Source/Core/Machines/SingleStateMachine.cs create mode 100644 Source/Core/Machines/StateGroup.cs create mode 100644 Source/Core/Machines/Timers/IMachineTimer.cs create mode 100644 Source/Core/Machines/Timers/MachineTimer.cs create mode 100644 Source/Core/Machines/Timers/TimerElapsedEvent.cs create mode 100644 Source/Core/Machines/Timers/TimerInfo.cs create mode 100644 Source/Core/Properties/InternalsVisibleTo.cs create mode 100644 Source/Core/Runtime/Configuration.cs create mode 100644 Source/Core/Runtime/CoyoteRuntime.cs create mode 100644 Source/Core/Runtime/Events/Event.cs create mode 100644 Source/Core/Runtime/Events/EventInfo.cs create mode 100644 Source/Core/Runtime/Events/EventOriginInfo.cs create mode 100644 Source/Core/Runtime/Exceptions/AssertionFailureException.cs create mode 100644 Source/Core/Runtime/Exceptions/ExecutionCanceledException.cs create mode 100644 Source/Core/Runtime/Exceptions/MachineActionExceptionFilterException.cs create mode 100644 Source/Core/Runtime/Exceptions/OnEventDroppedHandler.cs create mode 100644 Source/Core/Runtime/Exceptions/OnExceptionOutcome.cs create mode 100644 Source/Core/Runtime/Exceptions/OnFailureHandler.cs create mode 100644 Source/Core/Runtime/Exceptions/RuntimeException.cs create mode 100644 Source/Core/Runtime/Exceptions/UnhandledEventException.cs create mode 100644 Source/Core/Runtime/IMachineRuntime.cs create mode 100644 Source/Core/Runtime/MachineRuntimeFactory.cs create mode 100644 Source/Core/Runtime/ProductionRuntime.cs create mode 100644 Source/Core/Runtime/Providers/AsyncLocalRuntimeProvider.cs create mode 100644 Source/Core/Runtime/Providers/RuntimeProvider.cs create mode 100644 Source/Core/Runtime/TestAttributes.cs create mode 100644 Source/Core/Specifications/Monitors/Monitor.cs create mode 100644 Source/Core/Specifications/Monitors/MonitorState.cs create mode 100644 Source/Core/Specifications/Specification.cs create mode 100644 Source/Core/Threading/ControlledLock.cs create mode 100644 Source/Core/Threading/Tasks/AsyncControlledTaskMethodBuilder.cs create mode 100644 Source/Core/Threading/Tasks/ConfiguredControlledTaskAwaitable.cs create mode 100644 Source/Core/Threading/Tasks/ControlledTask.cs create mode 100644 Source/Core/Threading/Tasks/ControlledTaskAwaiter.cs create mode 100644 Source/Core/Threading/Tasks/ControlledTaskMachine.cs create mode 100644 Source/Core/Threading/Tasks/ControlledYieldAwaitable.cs create mode 100644 Source/Core/Threading/Tasks/TaskExtensions.cs create mode 100644 Source/Core/Utilities/ErrorReporter.cs create mode 100644 Source/Core/Utilities/NameResolver.cs create mode 100644 Source/Core/Utilities/Profiler.cs create mode 100644 Source/Core/Utilities/Tooling/BaseCommandLineOptions.cs create mode 100644 Source/Core/Utilities/Tooling/SchedulingStrategy.cs create mode 100644 Source/SharedObjects/SharedCounter/ISharedCounter.cs create mode 100644 Source/SharedObjects/SharedCounter/MockSharedCounter.cs create mode 100644 Source/SharedObjects/SharedCounter/ProductionSharedCounter.cs create mode 100644 Source/SharedObjects/SharedCounter/SharedCounter.cs create mode 100644 Source/SharedObjects/SharedCounter/SharedCounterEvent.cs create mode 100644 Source/SharedObjects/SharedCounter/SharedCounterMachine.cs create mode 100644 Source/SharedObjects/SharedCounter/SharedCounterResponseEvent.cs create mode 100644 Source/SharedObjects/SharedDictionary/ISharedDictionary.cs create mode 100644 Source/SharedObjects/SharedDictionary/MockSharedDictionary.cs create mode 100644 Source/SharedObjects/SharedDictionary/ProductionSharedDictionary.cs create mode 100644 Source/SharedObjects/SharedDictionary/SharedDictionary.cs create mode 100644 Source/SharedObjects/SharedDictionary/SharedDictionaryEvent.cs create mode 100644 Source/SharedObjects/SharedDictionary/SharedDictionaryMachine.cs create mode 100644 Source/SharedObjects/SharedDictionary/SharedDictionaryResponseEvent.cs create mode 100644 Source/SharedObjects/SharedObjects.csproj create mode 100644 Source/SharedObjects/SharedRegister/ISharedRegister.cs create mode 100644 Source/SharedObjects/SharedRegister/MockSharedRegister.cs create mode 100644 Source/SharedObjects/SharedRegister/ProductionSharedRegister.cs create mode 100644 Source/SharedObjects/SharedRegister/SharedRegister.cs create mode 100644 Source/SharedObjects/SharedRegister/SharedRegisterEvent.cs create mode 100644 Source/SharedObjects/SharedRegister/SharedRegisterMachine.cs create mode 100644 Source/SharedObjects/SharedRegister/SharedRegisterResponseEvent.cs create mode 100644 Source/TestingServices/Engines/AbstractTestingEngine.cs create mode 100644 Source/TestingServices/Engines/BugFindingEngine.cs create mode 100644 Source/TestingServices/Engines/ITestingEngine.cs create mode 100644 Source/TestingServices/Engines/ReplayEngine.cs create mode 100644 Source/TestingServices/Engines/TestingEngineFactory.cs create mode 100644 Source/TestingServices/Exploration/ISchedulingStrategy.cs create mode 100644 Source/TestingServices/Exploration/OperationScheduler.cs create mode 100644 Source/TestingServices/Exploration/RandomNumberGenerators/DefaultRandomNumberGenerator.cs create mode 100644 Source/TestingServices/Exploration/RandomNumberGenerators/IRandomNumberGenerator.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Bounded/DelayBoundingStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Bounded/ExhaustiveDelayBoundingStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Bounded/PCTStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Bounded/RandomDelayBoundingStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Exhaustive/DFSStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Exhaustive/IterativeDeepeningDFSStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Liveness/CycleDetectionStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Liveness/LivenessCheckingStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Liveness/TemperatureCheckingStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Probabilistic/ProbabilisticRandomStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Probabilistic/RandomStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Special/ComboStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Special/InteractiveStrategy.cs create mode 100644 Source/TestingServices/Exploration/Strategies/Special/ReplayStrategy.cs create mode 100644 Source/TestingServices/Machines/EventQueues/SerializedMachineEventQueue.cs create mode 100644 Source/TestingServices/Machines/MachineTestKit.cs create mode 100644 Source/TestingServices/Machines/MachineTestingRuntime.cs create mode 100644 Source/TestingServices/Machines/SerializedMachineStateManager.cs create mode 100644 Source/TestingServices/Machines/Timers/MockMachineTimer.cs create mode 100644 Source/TestingServices/Machines/Timers/TimerSetupEvent.cs create mode 100644 Source/TestingServices/Operations/AsyncOperationStatus.cs create mode 100644 Source/TestingServices/Operations/IAsyncOperation.cs create mode 100644 Source/TestingServices/Operations/MachineOperation.cs create mode 100644 Source/TestingServices/Properties/InternalsVisibleTo.cs create mode 100644 Source/TestingServices/StateCaching/Fingerprint.cs create mode 100644 Source/TestingServices/StateCaching/MonitorStatus.cs create mode 100644 Source/TestingServices/StateCaching/State.cs create mode 100644 Source/TestingServices/StateCaching/StateCache.cs create mode 100644 Source/TestingServices/Statistics/Coverage/ActivityCoverageReporter.cs create mode 100644 Source/TestingServices/Statistics/Coverage/CoverageInfo.cs create mode 100644 Source/TestingServices/Statistics/Coverage/Transition.cs create mode 100644 Source/TestingServices/Statistics/TestReport.cs create mode 100644 Source/TestingServices/SystematicTestingRuntime.cs create mode 100644 Source/TestingServices/TestingServices.csproj create mode 100644 Source/TestingServices/Threading/InterceptingTaskScheduler.cs create mode 100644 Source/TestingServices/Threading/MachineLock.cs create mode 100644 Source/TestingServices/Threading/Tasks/ActionMachine.cs create mode 100644 Source/TestingServices/Threading/Tasks/DelayMachine.cs create mode 100644 Source/TestingServices/Threading/Tasks/FuncMachine.cs create mode 100644 Source/TestingServices/Threading/Tasks/MachineTask.cs create mode 100644 Source/TestingServices/Threading/Tasks/MachineTaskType.cs create mode 100644 Source/TestingServices/Threading/Tasks/TestExecutionMachine.cs create mode 100644 Source/TestingServices/Tracing/Error/BugTrace.cs create mode 100644 Source/TestingServices/Tracing/Error/BugTraceStep.cs create mode 100644 Source/TestingServices/Tracing/Error/BugTraceStepType.cs create mode 100644 Source/TestingServices/Tracing/Schedules/ScheduleStep.cs create mode 100644 Source/TestingServices/Tracing/Schedules/ScheduleStepType.cs create mode 100644 Source/TestingServices/Tracing/Schedules/ScheduleTrace.cs create mode 100644 Tests/Core.Tests/BaseTest.cs create mode 100644 Tests/Core.Tests/Core.Tests.csproj create mode 100644 Tests/Core.Tests/EventQueues/EventQueueStressTest.cs create mode 100644 Tests/Core.Tests/EventQueues/EventQueueTest.cs create mode 100644 Tests/Core.Tests/EventQueues/MockMachineStateManager.cs create mode 100644 Tests/Core.Tests/ExceptionPropagation/ExceptionPropagationTest.cs create mode 100644 Tests/Core.Tests/ExceptionPropagation/OnExceptionTest.cs create mode 100644 Tests/Core.Tests/Features/GetOperationGroupIdTest.cs create mode 100644 Tests/Core.Tests/Features/OnEventDroppedTest.cs create mode 100644 Tests/Core.Tests/Features/OnHaltTest.cs create mode 100644 Tests/Core.Tests/Features/OperationGroupingTest.cs create mode 100644 Tests/Core.Tests/LogMessages/Common/CustomLogWriter.cs create mode 100644 Tests/Core.Tests/LogMessages/Common/CustomLogger.cs create mode 100644 Tests/Core.Tests/LogMessages/Common/Machines.cs create mode 100644 Tests/Core.Tests/LogMessages/CustomLogWriterTest.cs create mode 100644 Tests/Core.Tests/LogMessages/CustomLoggerTest.cs create mode 100644 Tests/Core.Tests/Machines/GotoStateTransitionTest.cs create mode 100644 Tests/Core.Tests/Machines/HandleEventTest.cs create mode 100644 Tests/Core.Tests/Machines/InvokeMethodTest.cs create mode 100644 Tests/Core.Tests/Machines/ReceiveEventIntegrationTest.cs create mode 100644 Tests/Core.Tests/Machines/ReceiveEventStressTest.cs create mode 100644 Tests/Core.Tests/Machines/ReceiveEventTest.cs create mode 100644 Tests/Core.Tests/Machines/Timers/TimerStressTest.cs create mode 100644 Tests/Core.Tests/Machines/Timers/TimerTest.cs create mode 100644 Tests/Core.Tests/MemoryLeak/NoMemoryLeakAfterHaltTest.cs create mode 100644 Tests/Core.Tests/MemoryLeak/NoMemoryLeakInEventSendingTest.cs create mode 100644 Tests/Core.Tests/RuntimeInterface/CreateMachineIdFromNameTest.cs create mode 100644 Tests/Core.Tests/RuntimeInterface/SendAndExecuteTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/CompletedTaskTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskAwaitTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitFalseTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitTrueTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskExceptionTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitFalseTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitTrueTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskRunTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskWhenAllTest.cs create mode 100644 Tests/Core.Tests/Threading/Tasks/TaskWhenAnyTest.cs create mode 100644 Tests/Core.Tests/xunit.runner.json create mode 100644 Tests/SharedObjects.Tests/BaseTest.cs create mode 100644 Tests/SharedObjects.Tests/ProductionSharedObjectsTest.cs create mode 100644 Tests/SharedObjects.Tests/SharedCounter/MockSharedCounterTest.cs create mode 100644 Tests/SharedObjects.Tests/SharedCounter/ProductionSharedCounterTest.cs create mode 100644 Tests/SharedObjects.Tests/SharedDictionary/MockSharedDictionaryTest.cs create mode 100644 Tests/SharedObjects.Tests/SharedDictionary/ProductionSharedDictionaryTest.cs create mode 100644 Tests/SharedObjects.Tests/SharedObjects.Tests.csproj create mode 100644 Tests/SharedObjects.Tests/SharedRegister/MockSharedRegisterTest.cs create mode 100644 Tests/SharedObjects.Tests/SharedRegister/ProductionSharedRegisterTest.cs create mode 100644 Tests/TestingServices.Tests/BaseTest.cs create mode 100644 Tests/TestingServices.Tests/Coverage/ActivityCoverageTest.cs create mode 100644 Tests/TestingServices.Tests/EntryPoint/EntryPointEventSendingTest.cs create mode 100644 Tests/TestingServices.Tests/EntryPoint/EntryPointMachineCreationTest.cs create mode 100644 Tests/TestingServices.Tests/EntryPoint/EntryPointMachineExecutionTest.cs create mode 100644 Tests/TestingServices.Tests/EntryPoint/EntryPointRandomChoiceTest.cs create mode 100644 Tests/TestingServices.Tests/EntryPoint/EntryPointThrowExceptionTest.cs create mode 100644 Tests/TestingServices.Tests/LogMessages/Common/CustomLogWriter.cs create mode 100644 Tests/TestingServices.Tests/LogMessages/Common/CustomLogger.cs create mode 100644 Tests/TestingServices.Tests/LogMessages/Common/Machines.cs create mode 100644 Tests/TestingServices.Tests/LogMessages/CustomLogWriterTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/EventHandling/ActionsFailTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/EventHandling/IgnoreEvent/IgnoreRaisedTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/EventHandling/MaxEventInstancesTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventFailTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/EventHandling/Wildcard/WildCardEventTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/AmbiguousEventHandlerTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/CurrentStateTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/DuplicateEventHandlersTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/EventInheritanceTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/GroupStateTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/MachineStateInheritanceTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/MethodCallTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/NameofTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/PopTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Features/ReceiveTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/GenericMachineTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/BubbleSortAlgorithmTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/ChainReplicationTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/ChordTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/DiningPhilosophersTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/OneMachineIntegrationTests.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/ProcessSchedulerTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/RaftTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/ReplicatingStorageTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/SendInterleavingsTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Integration/TwoMachineIntegrationTests.cs create mode 100644 Tests/TestingServices.Tests/Machines/SingleStateMachineTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineDelayTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineTaskTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Transitions/GotoStateExitFailTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Transitions/GotoStateFailTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Transitions/GotoStateMultipleInActionFailTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Transitions/GotoStateTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Transitions/PushApiTest.cs create mode 100644 Tests/TestingServices.Tests/Machines/Transitions/PushStateTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/CompletenessTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/CreateMachineIdFromNameTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/CreateMachineWithIdTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/FairRandomTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/GetOperationGroupIdTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/MustHandleEventTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/OnEventDequeueOrHandledTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/OnEventDroppedTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/OnEventUnhandledTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/OnExceptionTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/OnHaltTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/OperationGroupingTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/ReceivingExternalEventTest.cs create mode 100644 Tests/TestingServices.Tests/Runtime/SendAndExecuteTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionBasicTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionCounterTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionDefaultHandlerTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRandomChoiceTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRingOfNodesTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/FairNondet1Test.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness2Test.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness3Test.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Nondet1Test.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/WarmStateBugTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/HotStateTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/Liveness1Test.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/Liveness2BugFoundTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/Liveness2LoopMachineTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/UnfairExecutionTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Liveness/WarmStateTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Monitors/GenericMonitorTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Monitors/IdempotentRegisterMonitorTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Monitors/MachineMonitorIntegrationTests.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Monitors/MonitorStateInheritanceTest.cs create mode 100644 Tests/TestingServices.Tests/Specifications/Monitors/MonitorWildCardEventTest.cs create mode 100644 Tests/TestingServices.Tests/TestingServices.Tests.csproj create mode 100644 Tests/TestingServices.Tests/Threading/ControlledLockTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskBooleanNondeterminismTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskIntegerNondeterminismTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Specifications/TaskLivenessMonitorTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Specifications/TaskSafetyMonitorTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitFalseTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitTrueTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitFalseTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitTrueTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedControlledTaskAwaitTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedMultipleTaskAwaitTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedSkipTaskAwaitTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedUncontrolledTaskAwaitTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWaitAnyTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAllTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAnyTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/TaskAwaitTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/TaskDelayTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/TaskExceptionTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/TaskInterleavingsTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/TaskRunTest.cs create mode 100644 Tests/TestingServices.Tests/Threading/Tasks/TaskYieldTest.cs create mode 100644 Tests/TestingServices.Tests/Timers/BasicTimerTest.cs create mode 100644 Tests/TestingServices.Tests/Timers/StartStopTimerTest.cs create mode 100644 Tests/TestingServices.Tests/Timers/TimerLivenessTest.cs create mode 100644 Tests/Tests.Common/BaseTest.cs create mode 100644 Tests/Tests.Common/TestConsoleLogger.cs create mode 100644 Tests/Tests.Common/TestOutputLogger.cs create mode 100644 Tests/Tests.Common/Tests.Common.csproj create mode 100644 Tools/Benchmarking/CoyoteBenchmarkRunner/CoyoteBenchmarkRunner.csproj create mode 100644 Tools/Benchmarking/CoyoteBenchmarkRunner/Program.cs create mode 100644 Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Creation/MachineCreationThroughputBenchmark.cs create mode 100644 Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/DequeueEventThroughputBenchmark.cs create mode 100644 Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/ExchangeEventLatencyBenchmark.cs create mode 100644 Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/SendEventThroughputBenchmark.cs create mode 100644 Tools/Testing/CoverageReportMerger/CoverageReportMerger.csproj create mode 100644 Tools/Testing/CoverageReportMerger/Program.cs create mode 100644 Tools/Testing/Replayer/Program.cs create mode 100644 Tools/Testing/Replayer/Replayer.csproj create mode 100644 Tools/Testing/Replayer/ReplayingProcess.cs create mode 100644 Tools/Testing/Replayer/Utilities/ReplayerCommandLineOptions.cs create mode 100644 Tools/Testing/Tester/App.config create mode 100644 Tools/Testing/Tester/Instrumentation/CodeCoverageInstrumentation.cs create mode 100644 Tools/Testing/Tester/Interfaces/ITestingProcess.cs create mode 100644 Tools/Testing/Tester/Interfaces/ITestingProcessScheduler.cs create mode 100644 Tools/Testing/Tester/Monitoring/CodeCoverageMonitor.cs create mode 100644 Tools/Testing/Tester/Program.cs create mode 100644 Tools/Testing/Tester/Scheduling/TestingProcessScheduler.cs create mode 100644 Tools/Testing/Tester/Tester.csproj create mode 100644 Tools/Testing/Tester/Testing/TestingPortfolio.cs create mode 100644 Tools/Testing/Tester/Testing/TestingProcess.cs create mode 100644 Tools/Testing/Tester/Testing/TestingProcessFactory.cs create mode 100644 Tools/Testing/Tester/Utilities/DependencyGraph.cs create mode 100644 Tools/Testing/Tester/Utilities/DependentAssemblyLoader.cs create mode 100644 Tools/Testing/Tester/Utilities/ExitCode.cs create mode 100644 Tools/Testing/Tester/Utilities/Reporter.cs create mode 100644 Tools/Testing/Tester/Utilities/TesterCommandLineOptions.cs create mode 100644 Versioning.md create mode 100644 global.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..ce10785b6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs text diff=csharp + +*.csproj text +*.sln eol=crlf + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..508b76b4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,343 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Bb]uild/ +[Cc]odegen/ +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Bb]inaries/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio cache/options directory +.vs/ +**/.vscode/** + +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets +nuget.exe + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Jekyll +_site +.sass-cache +.jekyll-cache/ +.jekyll-metadata +Gemfile.lock diff --git a/Common/Key.snk b/Common/Key.snk new file mode 100644 index 0000000000000000000000000000000000000000..4c929381c73853475c91e0080cde08d30bd01427 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098kmlA=L6=_rbitzxfUdr*=Z>v{@V!07B zW<6}n@Us)pH>fb84SMm;0;Xggao4<`)_Mrj`Vt`^q-hn6qAF!7^iYLyteY0bpj|6F zZEysvcD{wf$~{%~u7W($hoXT3L-M_A<@CCH0c?QUl|ymmk+BSyu#=B+9C1lI$$y^B zj)qnHc~q!5LSOY(hqRbj5SLqvlZ8Zn_UiwO5y>L8uAmK@k#G4TC!$l>nO{322ve9H zvGye?!vDsYtn<<{xmq4l86Po-0Zr5i__ z_T;?a@1v5aiI05Y^2A-a+ZVJppjg1Qea5tq&xTe0+%W1F>!xyj*Rso8H;GLIg`AI1 zbK=`z=EG^?!ycn$0Cd+cU;BbvxPkGQ;a#^Q#FYKKS!qvIOV_G^(FVt)b_+U0s9dlc zO{;QTHdQm!Y|Zf}F_H}WH%qBPW&>yB=UH!^(n`#mlqDt{6mV41B97#HkBv$xsV@c> zwLIi*FZIq@8n`y?=4x8>D)>0vsR6LzOoKtL&`0Jzq#58sS8 z6_b119F!Y?^Bh+fBaxRhLBdfv5}9ASJ&5 literal 0 HcmV?d00001 diff --git a/Common/build.props b/Common/build.props new file mode 100644 index 000000000..fe26e3153 --- /dev/null +++ b/Common/build.props @@ -0,0 +1,32 @@ + + + + + Coyote + Microsoft Corporation + Copyright © Microsoft Corporation. All rights reserved. + https://github.com/microsoft/coyote + git + $(MSBuildThisFileDirectory) + $(CodeAnalysisRuleSetLocation)\codeanalysis.ruleset + + + true + full + false + DEBUG;TRACE + true + + + pdbonly + true + TRACE + + + + + + + + + \ No newline at end of file diff --git a/Common/codeanalysis.ruleset b/Common/codeanalysis.ruleset new file mode 100644 index 000000000..e2c9a4e1a --- /dev/null +++ b/Common/codeanalysis.ruleset @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Common/dependencies.props b/Common/dependencies.props new file mode 100644 index 000000000..535156dfb --- /dev/null +++ b/Common/dependencies.props @@ -0,0 +1,8 @@ + + + 7.3 + 4.3.0 + $(BundledNETStandardPackageVersion) + 2.2.0 + + \ No newline at end of file diff --git a/Common/key.props b/Common/key.props new file mode 100644 index 000000000..75b670be0 --- /dev/null +++ b/Common/key.props @@ -0,0 +1,6 @@ + + + $(MSBuildThisFileDirectory)Key.snk + true + + \ No newline at end of file diff --git a/Common/stylecop.json b/Common/stylecop.json new file mode 100644 index 000000000..cf31aa7fd --- /dev/null +++ b/Common/stylecop.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Microsoft Corporation", + "copyrightText": "Copyright (c) {companyName}.\nLicensed under the MIT License.", + "xmlHeader": false, + "documentInterfaces": true, + "documentExposedElements": true, + "documentInternalElements": true, + "documentPrivateElements": false, + "documentPrivateFields": false, + "documentationCulture": "en-US" + }, + "indentation": { + "indentationSize": 4, + "tabSize": 4, + "useTabs": false + }, + "layoutRules": { + "allowConsecutiveUsings": false, + "newlineAtEndOfFile": "require" + }, + "maintainabilityRules": { + "topLevelTypes": [ + "class", + "struct", + "interface", + "enum", + "delegate" + ] + }, + "namingRules": { + "allowCommonHungarianPrefixes": true + }, + "orderingRules": { + "systemUsingDirectivesFirst": true, + "usingDirectivesPlacement": "outsideNamespace", + "blankLinesBetweenUsingGroups": "require" + }, + "readabilityRules": { + "allowBuiltInTypeAliases": false + } + } +} diff --git a/Common/version.props b/Common/version.props new file mode 100644 index 000000000..e0c2d97c6 --- /dev/null +++ b/Common/version.props @@ -0,0 +1,7 @@ + + + + 1.0.0 + rc + + diff --git a/Coyote.sln b/Coyote.sln new file mode 100644 index 000000000..24f2180c0 --- /dev/null +++ b/Coyote.sln @@ -0,0 +1,119 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28803.156 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{458F6344-4ADE-475F-8A31-4DF3D01CF364}" + ProjectSection(SolutionItems) = preProject + Common\build.props = Common\build.props + Common\dependencies.props = Common\dependencies.props + Common\key.props = Common\key.props + Common\stylecop.json = Common\stylecop.json + Common\version.props = Common\version.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{83369B7E-5C21-4D49-A14C-E8A6A4892807}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{2012300C-6E5D-47A0-9D57-B3F0A73AA1D4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Testing", "Testing", "{DF864130-1926-4B4E-92AF-B5D5C3AB152D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Source\Core\Core.csproj", "{E75DB9C9-7842-4AE4-A29D-624F6B49F607}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingServices", "Source\TestingServices\TestingServices.csproj", "{C28B3E1F-A955-49A1-8CAE-2B0374F9444A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoverageReportMerger", "Tools\Testing\CoverageReportMerger\CoverageReportMerger.csproj", "{AB7728CE-164F-4D09-82DB-3CE6389D897C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Replayer", "Tools\Testing\Replayer\Replayer.csproj", "{82BA322D-C3E2-493E-8BEE-BB48414C34DF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tester", "Tools\Testing\Tester\Tester.csproj", "{E6C9AF6D-091C-4DAF-BCB6-BFFB0CF12843}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Tests", "Tests\Core.Tests\Core.Tests.csproj", "{911F1779-3558-4590-836C-C75112D65FD8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestingServices.Tests", "Tests\TestingServices.Tests\TestingServices.Tests.csproj", "{DABC68C1-79D3-4324-A750-7CF72E0A0ACF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{9BC0914F-3068-4148-B778-4CA27D828D39}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedObjects", "Source\SharedObjects\SharedObjects.csproj", "{B1A04084-448F-4765-9068-C8C7D5F0C6C6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedObjects.Tests", "Tests\SharedObjects.Tests\SharedObjects.Tests.csproj", "{4240DE34-A775-4B3E-A67A-37E50AC9850B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.Common", "Tests\Tests.Common\Tests.Common.csproj", "{61FC86A6-AF87-4007-B184-AF860A57AB9E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarking", "Benchmarking", "{80164985-D9AE-495B-9D01-BA86AE137739}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoyoteBenchmarkRunner", "Tools\Benchmarking\CoyoteBenchmarkRunner\CoyoteBenchmarkRunner.csproj", "{B27EFAA7-9166-4CC6-94AF-214A69DC5794}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E75DB9C9-7842-4AE4-A29D-624F6B49F607}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E75DB9C9-7842-4AE4-A29D-624F6B49F607}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E75DB9C9-7842-4AE4-A29D-624F6B49F607}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E75DB9C9-7842-4AE4-A29D-624F6B49F607}.Release|Any CPU.Build.0 = Release|Any CPU + {C28B3E1F-A955-49A1-8CAE-2B0374F9444A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C28B3E1F-A955-49A1-8CAE-2B0374F9444A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C28B3E1F-A955-49A1-8CAE-2B0374F9444A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C28B3E1F-A955-49A1-8CAE-2B0374F9444A}.Release|Any CPU.Build.0 = Release|Any CPU + {AB7728CE-164F-4D09-82DB-3CE6389D897C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB7728CE-164F-4D09-82DB-3CE6389D897C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB7728CE-164F-4D09-82DB-3CE6389D897C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB7728CE-164F-4D09-82DB-3CE6389D897C}.Release|Any CPU.Build.0 = Release|Any CPU + {82BA322D-C3E2-493E-8BEE-BB48414C34DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82BA322D-C3E2-493E-8BEE-BB48414C34DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82BA322D-C3E2-493E-8BEE-BB48414C34DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82BA322D-C3E2-493E-8BEE-BB48414C34DF}.Release|Any CPU.Build.0 = Release|Any CPU + {E6C9AF6D-091C-4DAF-BCB6-BFFB0CF12843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6C9AF6D-091C-4DAF-BCB6-BFFB0CF12843}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6C9AF6D-091C-4DAF-BCB6-BFFB0CF12843}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6C9AF6D-091C-4DAF-BCB6-BFFB0CF12843}.Release|Any CPU.Build.0 = Release|Any CPU + {911F1779-3558-4590-836C-C75112D65FD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {911F1779-3558-4590-836C-C75112D65FD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {911F1779-3558-4590-836C-C75112D65FD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {911F1779-3558-4590-836C-C75112D65FD8}.Release|Any CPU.Build.0 = Release|Any CPU + {DABC68C1-79D3-4324-A750-7CF72E0A0ACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DABC68C1-79D3-4324-A750-7CF72E0A0ACF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DABC68C1-79D3-4324-A750-7CF72E0A0ACF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DABC68C1-79D3-4324-A750-7CF72E0A0ACF}.Release|Any CPU.Build.0 = Release|Any CPU + {B1A04084-448F-4765-9068-C8C7D5F0C6C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1A04084-448F-4765-9068-C8C7D5F0C6C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1A04084-448F-4765-9068-C8C7D5F0C6C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1A04084-448F-4765-9068-C8C7D5F0C6C6}.Release|Any CPU.Build.0 = Release|Any CPU + {4240DE34-A775-4B3E-A67A-37E50AC9850B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4240DE34-A775-4B3E-A67A-37E50AC9850B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4240DE34-A775-4B3E-A67A-37E50AC9850B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4240DE34-A775-4B3E-A67A-37E50AC9850B}.Release|Any CPU.Build.0 = Release|Any CPU + {61FC86A6-AF87-4007-B184-AF860A57AB9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61FC86A6-AF87-4007-B184-AF860A57AB9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61FC86A6-AF87-4007-B184-AF860A57AB9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61FC86A6-AF87-4007-B184-AF860A57AB9E}.Release|Any CPU.Build.0 = Release|Any CPU + {B27EFAA7-9166-4CC6-94AF-214A69DC5794}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B27EFAA7-9166-4CC6-94AF-214A69DC5794}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B27EFAA7-9166-4CC6-94AF-214A69DC5794}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B27EFAA7-9166-4CC6-94AF-214A69DC5794}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF864130-1926-4B4E-92AF-B5D5C3AB152D} = {9BC0914F-3068-4148-B778-4CA27D828D39} + {E75DB9C9-7842-4AE4-A29D-624F6B49F607} = {83369B7E-5C21-4D49-A14C-E8A6A4892807} + {C28B3E1F-A955-49A1-8CAE-2B0374F9444A} = {83369B7E-5C21-4D49-A14C-E8A6A4892807} + {AB7728CE-164F-4D09-82DB-3CE6389D897C} = {DF864130-1926-4B4E-92AF-B5D5C3AB152D} + {82BA322D-C3E2-493E-8BEE-BB48414C34DF} = {DF864130-1926-4B4E-92AF-B5D5C3AB152D} + {E6C9AF6D-091C-4DAF-BCB6-BFFB0CF12843} = {DF864130-1926-4B4E-92AF-B5D5C3AB152D} + {911F1779-3558-4590-836C-C75112D65FD8} = {2012300C-6E5D-47A0-9D57-B3F0A73AA1D4} + {DABC68C1-79D3-4324-A750-7CF72E0A0ACF} = {2012300C-6E5D-47A0-9D57-B3F0A73AA1D4} + {B1A04084-448F-4765-9068-C8C7D5F0C6C6} = {83369B7E-5C21-4D49-A14C-E8A6A4892807} + {4240DE34-A775-4B3E-A67A-37E50AC9850B} = {2012300C-6E5D-47A0-9D57-B3F0A73AA1D4} + {61FC86A6-AF87-4007-B184-AF860A57AB9E} = {2012300C-6E5D-47A0-9D57-B3F0A73AA1D4} + {80164985-D9AE-495B-9D01-BA86AE137739} = {9BC0914F-3068-4148-B778-4CA27D828D39} + {B27EFAA7-9166-4CC6-94AF-214A69DC5794} = {80164985-D9AE-495B-9D01-BA86AE137739} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B9407046-CB24-4B07-8031-2749696EC7D8} + EndGlobalSection +EndGlobal diff --git a/History.md b/History.md new file mode 100644 index 000000000..55b41ef30 --- /dev/null +++ b/History.md @@ -0,0 +1,2 @@ +## v1.0.0* +- The initial release of the Coyote framework and test tools. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..5ada2157e --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) Microsoft Corporation. + +Coyote + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 000000000..65526f2c5 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..76ef2db16 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +[![NuGet](https://img.shields.io/nuget/v/Microsoft.Coyote.svg)](https://www.nuget.org/packages/Microsoft.Coyote/) +[![Build status](https://dev.azure.com/foundry99/Coyote/_apis/build/status/Coyote-Windows-CI)](https://dev.azure.com/foundry99/Coyote/_build/latest?definitionId=49) + +Coyote is a programming framework for building reliable asynchronous software. +Coyote ensures design and code remain in sync, dramatically simplifying the +addition of new features. +Coyote comes with with a systematic testing engine that allows finding and +deterministically reproducing hard-to-find safety and liveness bugs. + +Coyote is used by several teams in [Azure](https://azure.microsoft.com/) to design, +implement and systematically test production distributed systems and services. +In the words of an Azure service architect: +> Coyote found several issues early in the dev process, this sort of issues that would +> usually bleed through into production and become very expensive to fix later. + +Coyote is made with :heart: by Microsoft Research and is the evolution of the +[P# project](https://github.com/p-org/PSharp). + +# Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repositories using our CLA. + +# Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..7fb127cf0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [many more](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [definition](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** Instead, please report them to the Microsoft Security Response Center at [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://technet.microsoft.com/en-us/security/dn606155). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + \ No newline at end of file diff --git a/Scripts/NuGet/Coyote.nuspec b/Scripts/NuGet/Coyote.nuspec new file mode 100644 index 000000000..fb7006674 --- /dev/null +++ b/Scripts/NuGet/Coyote.nuspec @@ -0,0 +1,44 @@ + + + + Microsoft.Coyote + $version$ + Microsoft + Coyote is a framework for building reliable asynchronous software. + https://microsoft.github.io/coyote/ + + MIT + false + © Microsoft Corporation. All rights reserved. + asynchrony reliability actors state-machines tasks specifications testing + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Scripts/build.ps1 b/Scripts/build.ps1 new file mode 100644 index 000000000..0ded3262e --- /dev/null +++ b/Scripts/build.ps1 @@ -0,0 +1,57 @@ +param( + [string]$dotnet="dotnet", + [ValidateSet("Debug","Release")] + [string]$configuration="Release" +) + +Import-Module $PSScriptRoot\powershell\common.psm1 + +# check that dotnet sdk is installed... +Function FindInPath() { + param ([string]$name) + $ENV:PATH.split(';') | ForEach-Object { + If (Test-Path -Path $_\$name) { + return $_ + } + } + return $null +} + +$json = Get-Content '$PSScriptRoot\..\global.json' | Out-String | ConvertFrom-Json +$pattern = $json.sdk.version.Trim("0") + "*" + +$dotnet=$dotnet.Replace(".exe","") +$versions = $null +$dotnetpath=FindInPath "$dotnet.exe" +if ($dotnetpath -is [array]){ + $dotnetpath = $dotnetpath[0] +} +$sdkpath = Join-Path -Path $dotnetpath -ChildPath "sdk" +if (-not ("" -eq $dotnetpath)) +{ + $versions = Get-ChildItem "$sdkpath" -directory | Where-Object {$_ -like $pattern} +} + +if ($null -eq $versions) +{ + Write-Comment -text "The global.json file is pointing to version: $pattern but no matching version was found in $sdkpath." -color "yellow" + Write-Comment -text "Please install dotnet sdk version $pattern from https://dotnet.microsoft.com/download/dotnet-core." -color "yellow" + exit 1 +} +else +{ + if ($versions -is [array]){ + $versions = $versions[0] + } + Write-Comment -text "Using dotnet sdk version $versions at: $sdkpath" -color yellow +} + + +Write-Comment -prefix "." -text "Building Coyote" -color "yellow" +Write-Comment -prefix "..." -text "Configuration: $configuration" -color "white" +$solution = $PSScriptRoot + "\..\Coyote.sln" +$command = "build -c $configuration $solution" +$error_msg = "Failed to build Coyote" +Invoke-ToolCommand -tool $dotnet -command $command -error_msg $error_msg + +Write-Comment -prefix "." -text "Successfully built Coyote" -color "green" diff --git a/Scripts/create-nuget-packages.ps1 b/Scripts/create-nuget-packages.ps1 new file mode 100644 index 000000000..978a2304e --- /dev/null +++ b/Scripts/create-nuget-packages.ps1 @@ -0,0 +1,41 @@ +param( + [string]$nuget="$PSScriptRoot\NuGet\nuget.exe" +) + +Import-Module $PSScriptRoot\powershell\common.psm1 + +$nuget_exe_dir = "$PSScriptRoot\NuGet" +$nuget_exe_url = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" + +Write-Comment -prefix "." -text "Creating the Coyote NuGet packages" -color "yellow" +if (-not (Test-Path $nuget)) { + Write-Comment -prefix "..." -text "Downloading latest 'nuget.exe'" -color "white" + Invoke-WebRequest "$nuget_exe_url" -OutFile "$nuget_exe_dir\nuget.exe" + if (-not (Test-Path $nuget)) { + Write-Error "Unable to download 'nuget.exe'. Please download '$nuget_exe_url' and place in '$nuget_exe_dir\' directory." + exit 1 + } + Write-Comment -prefix "..." -text "Installed 'nuget.exe' in '$nuget_exe_dir'" -color "white" +} + +if (Test-Path $PSScriptRoot\..\bin\nuget) { + Remove-Item $PSScriptRoot\..\bin\nuget\* +} + +# Extract the package version. +$version_file = "$PSScriptRoot\..\Common\version.props" +$version_node = Select-Xml -Path $version_file -XPath "/" | Select-Object -ExpandProperty Node +$version = $version_node.Project.PropertyGroup.VersionPrefix +$version_suffix = $version_node.Project.PropertyGroup.VersionSuffix + +# Setup the command line options for nuget pack. +$command_options = "-OutputDirectory $PSScriptRoot\..\bin\nuget -Version $version" +if ($version_suffix) { + $command_options = "$command_options -Suffix $version_suffix" +} + +$command = "pack $nuget_exe_dir\Coyote.nuspec $command_options" +$error_msg = "Failed to create the Coyote NuGet packages" +Invoke-ToolCommand -tool $nuget -command $command -error_msg $error_msg + +Write-Comment -prefix "." -text "Successfully created the Coyote NuGet packages" -color "green" diff --git a/Scripts/powershell/common.psm1 b/Scripts/powershell/common.psm1 new file mode 100644 index 000000000..8bac4d991 --- /dev/null +++ b/Scripts/powershell/common.psm1 @@ -0,0 +1,34 @@ +# Runs the specified .NET test using the specified framework. +function Invoke-DotnetTest([String]$dotnet, [String]$project, [String]$target, [string]$filter, [string]$framework, [string]$verbosity) { + Write-Comment -prefix "..." -text "Testing '$project' ($framework)" -color "white" + if (-not (Test-Path $target)) { + Write-Error "tests for '$project' ($framework) not found." + exit + } + + if (!($filter -eq "")) { + $command = "test $target --filter $filter -f $framework --no-build -v $verbosity" + } else { + $command = "test $target -f $framework --no-build -v $verbosity" + } + + $error_msg = "Failed to test '$project'" + Invoke-ToolCommand -tool $dotnet -command $command -error_msg $error_msg +} + +# Runs the specified tool command. +function Invoke-ToolCommand([String]$tool, [String]$command, [String]$error_msg) { + Invoke-Expression "$tool $command" + if (-not ($LASTEXITCODE -eq 0)) { + Write-Error $error_msg + exit $LASTEXITCODE + } +} + +function Write-Comment([String]$prefix, [String]$text, [String]$color) { + Write-Host "$prefix " -b "black" -nonewline; Write-Host $text -b "black" -f $color +} + +function Write-Error([String]$text) { + Write-Host "Error: $text" -b "black" -f "red" +} diff --git a/Scripts/publish-nuget-package.ps1 b/Scripts/publish-nuget-package.ps1 new file mode 100644 index 000000000..ae365355a --- /dev/null +++ b/Scripts/publish-nuget-package.ps1 @@ -0,0 +1,38 @@ +param( + [string]$api_key="" +) + +if ($api_key -eq ""){ + Write-Error "Please provide api-key for the nuget push command" + exit 1 +} + +Import-Module $PSScriptRoot\powershell\common.psm1 + +Write-Comment -prefix "." -text "Uploading the Coyote Nuget package to http://www.nuget.org" -color "yellow" + +$package_dir = "$PSScriptRoot\..\bin\nuget" + +$package = (Get-ChildItem -Path $package_dir\*.* -Filter *.nupkg) + +if ($null -eq $package) { + Write-Error "Found no nuget packages in $package_dir" + exit 1 +} + +if ($package -is [array]) { + Write-Error "Too many nuget packages in $package_dir" + exit 1 +} + +Write-Host "Uploading package: $package" + +$nuget_exe = "$PSScriptRoot\NuGet\NuGet.exe" + +if (-not (Test-Path $nuget_exe)) { + Write-Error "Unable to find the nuget.exe in ($nuget_exe), please run create-nuget-package.ps1 first." + exit 1 +} + +$command = "push $package $api_key" +Invoke-ToolCommand -tool $nuget_exe -command $command -error_msg $error_msg diff --git a/Scripts/run-benchmarks.ps1 b/Scripts/run-benchmarks.ps1 new file mode 100644 index 000000000..69344c2be --- /dev/null +++ b/Scripts/run-benchmarks.ps1 @@ -0,0 +1,29 @@ +param( + [boolean]$set_vso_result_var = $false +) + +Import-Module $PSScriptRoot\powershell\common.psm1 + +$current_dir = (Get-Item -Path ".\").FullName +$benchmarks_dir = "$PSScriptRoot\..\Tools\bin\net472" +$benchmark_runner = "CoyoteBenchmarkRunner.exe" +$artifacts_dir = "$current_dir\BenchmarkDotNet.Artifacts" +$timestamp = (Get-Date).ToString('yyyy_MM_dd_hh_mm_ss') +$results = "benchmark_results_$timestamp" + +Write-Comment -prefix "." -text "Running the Coyote performance benchmarks" -color "yellow" + +Invoke-Expression "$benchmarks_dir\$benchmark_runner" + +if (-not (Test-Path $artifacts_dir)) { + Write-Error "Unable to find the benchmark results ($artifacts_dir)." + exit +} + +Rename-Item -path $artifacts_dir -newName "$results" + +if ($set_vso_result_var) { + Write-Host "##vso[task.setvariable variable=vso_benchmark_results;]$current_dir\$results" +} + +Write-Comment -prefix "." -text "Done" -color "green" diff --git a/Scripts/run-tests.ps1 b/Scripts/run-tests.ps1 new file mode 100644 index 000000000..b3f7485cd --- /dev/null +++ b/Scripts/run-tests.ps1 @@ -0,0 +1,38 @@ +param( + [string]$dotnet="dotnet", + [ValidateSet("all","netcoreapp2.1","net46","net47")] + [string]$framework="all", + [ValidateSet("all","core","testing-services","shared-objects")] + [string]$test="all", + [string]$filter="", + [ValidateSet("quiet","minimal","normal","detailed","diagnostic")] + [string]$v="normal" +) + +Import-Module $PSScriptRoot\powershell\common.psm1 + +$frameworks = "netcoreapp2.1", "net46", "net47" + +$targets = [ordered]@{ + "core" = "Core.Tests" + "testing-services" = "TestingServices.Tests" + "shared-objects" = "SharedObjects.Tests" +} + +Write-Comment -prefix "." -text "Running the Coyote tests" -color "yellow" +foreach ($kvp in $targets.GetEnumerator()) { + if (($test -ne "all") -and ($test -ne $($kvp.Name))) { + continue + } + + foreach ($f in $frameworks) { + if (($framework -ne "all") -and ($f -ne $framework)) { + continue + } + + $target = "$PSScriptRoot\..\Tests\$($kvp.Value)\$($kvp.Value).csproj" + Invoke-DotnetTest -dotnet $dotnet -project $($kvp.Name) -target $target -filter $filter -framework $f -verbosity $v + } +} + +Write-Comment -prefix "." -text "Done" -color "green" diff --git a/Scripts/upload-benchmark-results.ps1 b/Scripts/upload-benchmark-results.ps1 new file mode 100644 index 000000000..2b1a32c51 --- /dev/null +++ b/Scripts/upload-benchmark-results.ps1 @@ -0,0 +1,33 @@ +param( + [string]$results = "", + [string]$account_name = "", + [string]$account_key = "", + [string]$share_name = "" +) + +Import-Module $PSScriptRoot\powershell\common.psm1 + +Write-Comment -prefix "." -text "Uploading the Coyote performance benchmark results to Azure Storage" -color "yellow" + +if ($results -eq "") { + $results = $($env:vso_benchmark_results) + if ($results -eq "") { + Write-Error "Unable to find the benchmark results ($results)." + exit + } +} + +if (-not (Test-Path $results)) { + Write-Error "Unable to find the benchmark results ($results)." + exit +} + +Write-Comment -prefix "." -text "Compressing the results" -color "yellow" +Compress-Archive -Path "$results" -DestinationPath "$results" + +Write-Comment -prefix "." -text "Uploading the results" -color "yellow" +$result_file = Split-Path $results -leaf +$context = New-AzureStorageContext -StorageAccountName $account_name -StorageAccountKey $account_key +Set-AzureStorageFileContent -Context $context -ShareName $share_name -Source "$results.zip" -Path "benchmarks\$result_file.zip" + +Write-Comment -prefix "." -text "Done" -color "green" diff --git a/Source/Core/Core.csproj b/Source/Core/Core.csproj new file mode 100644 index 000000000..5ff49a3c9 --- /dev/null +++ b/Source/Core/Core.csproj @@ -0,0 +1,24 @@ + + + + + The Coyote framework core libraries and runtime. + Microsoft.Coyote + Microsoft.Coyote + true + coyote;state-machines;asynchronous;event-driven;dotnet;csharp + ..\..\bin\ + + + netstandard2.0;net46;net47 + + + netstandard2.0 + + + + + + + + \ No newline at end of file diff --git a/Source/Core/IO/Debugging/Debug.cs b/Source/Core/IO/Debugging/Debug.cs new file mode 100644 index 000000000..290f17a14 --- /dev/null +++ b/Source/Core/IO/Debugging/Debug.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Globalization; + +namespace Microsoft.Coyote.IO +{ + /// + /// Static class implementing debug reporting methods. + /// + internal static class Debug + { + /// + /// Checks if debugging is enabled. + /// + internal static bool IsEnabled = false; + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void Write(string format, object arg0) + { + if (IsEnabled) + { + Console.Write(string.Format(CultureInfo.InvariantCulture, format, arg0)); + } + } + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void Write(string format, object arg0, object arg1) + { + if (IsEnabled) + { + Console.Write(string.Format(CultureInfo.InvariantCulture, format, arg0, arg1)); + } + } + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void Write(string format, object arg0, object arg1, object arg2) + { + if (IsEnabled) + { + Console.Write(string.Format(CultureInfo.InvariantCulture, format, arg0, arg1, arg2)); + } + } + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void Write(string format, params object[] args) + { + if (IsEnabled) + { + Console.Write(string.Format(CultureInfo.InvariantCulture, format, args)); + } + } + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void WriteLine(string format, object arg0) + { + if (IsEnabled) + { + Console.WriteLine(string.Format(CultureInfo.InvariantCulture, format, arg0)); + } + } + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void WriteLine(string format, object arg0, object arg1) + { + if (IsEnabled) + { + Console.WriteLine(string.Format(CultureInfo.InvariantCulture, format, arg0, arg1)); + } + } + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void WriteLine(string format, object arg0, object arg1, object arg2) + { + if (IsEnabled) + { + Console.WriteLine(string.Format(CultureInfo.InvariantCulture, format, arg0, arg1, arg2)); + } + } + + /// + /// Writes the debugging information, followed by the current line terminator, + /// to the output stream. The print occurs only if debugging is enabled. + /// + public static void WriteLine(string format, params object[] args) + { + if (IsEnabled) + { + Console.WriteLine(string.Format(CultureInfo.InvariantCulture, format, args)); + } + } + } +} diff --git a/Source/Core/IO/Debugging/Error.cs b/Source/Core/IO/Debugging/Error.cs new file mode 100644 index 000000000..be5c430e2 --- /dev/null +++ b/Source/Core/IO/Debugging/Error.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Globalization; + +namespace Microsoft.Coyote.IO +{ + /// + /// Static class implementing error reporting methods. + /// + internal static class Error + { + /// + /// Reports a generic error to the user. + /// + public static void Report(string format, params object[] args) + { + string message = string.Format(CultureInfo.InvariantCulture, format, args); + Write(ConsoleColor.Red, "Error: "); + Write(ConsoleColor.Yellow, message); + Console.Error.WriteLine(string.Empty); + } + + /// + /// Reports a generic error to the user and exits. + /// + public static void ReportAndExit(string value) + { + Write(ConsoleColor.Red, "Error: "); + Write(ConsoleColor.Yellow, value); + Console.Error.WriteLine(string.Empty); + Environment.Exit(1); + } + + /// + /// Reports a generic error to the user and exits. + /// + public static void ReportAndExit(string format, params object[] args) + { + string message = string.Format(CultureInfo.InvariantCulture, format, args); + Write(ConsoleColor.Red, "Error: "); + Write(ConsoleColor.Yellow, message); + Console.Error.WriteLine(string.Empty); + Environment.Exit(1); + } + + /// + /// Writes the specified string value to the output stream. + /// + private static void Write(ConsoleColor color, string value) + { + var previousForegroundColor = Console.ForegroundColor; + Console.ForegroundColor = color; + Console.Error.Write(value); + Console.ForegroundColor = previousForegroundColor; + } + } +} diff --git a/Source/Core/IO/Logging/ConsoleLogger.cs b/Source/Core/IO/Logging/ConsoleLogger.cs new file mode 100644 index 000000000..c5c13bdc4 --- /dev/null +++ b/Source/Core/IO/Logging/ConsoleLogger.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.IO +{ + /// + /// Logger that writes text to the console. + /// + public sealed class ConsoleLogger : ILogger + { + /// + /// If true, then messages are logged. The default value is true. + /// + public bool IsVerbose { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ConsoleLogger() + { + this.IsVerbose = true; + } + + /// + /// Writes the specified string value. + /// + public void Write(string value) + { + if (this.IsVerbose) + { + Console.Write(value); + } + } + + /// + /// Writes the text representation of the specified argument. + /// + public void Write(string format, object arg0) + { + if (this.IsVerbose) + { + Console.Write(format, arg0.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + Console.Write(format, arg0.ToString(), arg1.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + Console.Write(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + + /// + /// Writes the text representation of the specified array of objects. + /// + public void Write(string format, params object[] args) + { + if (this.IsVerbose) + { + Console.Write(format, args); + } + } + + /// + /// Writes the specified string value, followed by the + /// current line terminator. + /// + public void WriteLine(string value) + { + if (this.IsVerbose) + { + Console.WriteLine(value); + } + } + + /// + /// Writes the text representation of the specified argument, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0) + { + if (this.IsVerbose) + { + Console.WriteLine(format, arg0.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + Console.WriteLine(format, arg0.ToString(), arg1.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + Console.WriteLine(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + + /// + /// Writes the text representation of the specified array of objects, + /// followed by the current line terminator. + /// + public void WriteLine(string format, params object[] args) + { + if (this.IsVerbose) + { + Console.WriteLine(format, args); + } + } + + /// + /// Disposes the logger. + /// + public void Dispose() + { + } + } +} diff --git a/Source/Core/IO/Logging/ILogger.cs b/Source/Core/IO/Logging/ILogger.cs new file mode 100644 index 000000000..6755e7949 --- /dev/null +++ b/Source/Core/IO/Logging/ILogger.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.IO +{ + /// + /// Interface of the runtime logger. + /// + public interface ILogger : IDisposable + { + /// + /// If true, then messages are logged. + /// + bool IsVerbose { get; set; } + + /// + /// Writes the specified string value. + /// + void Write(string value); + + /// + /// Writes the text representation of the specified argument. + /// + void Write(string format, object arg0); + + /// + /// Writes the text representation of the specified arguments. + /// + void Write(string format, object arg0, object arg1); + + /// + /// Writes the text representation of the specified arguments. + /// + void Write(string format, object arg0, object arg1, object arg2); + + /// + /// Writes the text representation of the specified array of objects. + /// + void Write(string format, params object[] args); + + /// + /// Writes the specified string value, followed by the + /// current line terminator. + /// + void WriteLine(string value); + + /// + /// Writes the text representation of the specified argument, followed by the + /// current line terminator. + /// + void WriteLine(string format, object arg0); + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + void WriteLine(string format, object arg0, object arg1); + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + void WriteLine(string format, object arg0, object arg1, object arg2); + + /// + /// Writes the text representation of the specified array of objects, + /// followed by the current line terminator. + /// + void WriteLine(string format, params object[] args); + } +} diff --git a/Source/Core/IO/Logging/InMemoryLogger.cs b/Source/Core/IO/Logging/InMemoryLogger.cs new file mode 100644 index 000000000..a14ad1eea --- /dev/null +++ b/Source/Core/IO/Logging/InMemoryLogger.cs @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; + +namespace Microsoft.Coyote.IO +{ + /// + /// Thread safe logger that writes text in-memory. + /// + public sealed class InMemoryLogger : ILogger + { + /// + /// Underlying string writer. + /// + private readonly StringWriter Writer; + + /// + /// Serializes access to the string writer. + /// + private readonly object Lock; + + /// + /// If true, then messages are logged. The default value is true. + /// + public bool IsVerbose { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public InMemoryLogger() + { + this.Writer = new StringWriter(); + this.Lock = new object(); + this.IsVerbose = true; + } + + /// + /// Writes the specified string value. + /// + public void Write(string value) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.Write(value); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified argument. + /// + public void Write(string format, object arg0) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.Write(format, arg0.ToString()); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.Write(format, arg0.ToString(), arg1.ToString()); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.Write(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified array of objects. + /// + public void Write(string format, params object[] args) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.Write(format, args); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the specified string value, followed by the + /// current line terminator. + /// + public void WriteLine(string value) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.WriteLine(value); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified argument, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.WriteLine(format, arg0.ToString()); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.WriteLine(format, arg0.ToString(), arg1.ToString()); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.WriteLine(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Writes the text representation of the specified array of objects, + /// followed by the current line terminator. + /// + public void WriteLine(string format, params object[] args) + { + if (this.IsVerbose) + { + try + { + lock (this.Lock) + { + this.Writer.WriteLine(format, args); + } + } + catch (ObjectDisposedException) + { + // The writer was disposed. + } + } + } + + /// + /// Returns the logged text as a string. + /// + public override string ToString() + { + lock (this.Lock) + { + return this.Writer.ToString(); + } + } + + /// + /// Disposes the logger. + /// + public void Dispose() + { + this.Writer.Dispose(); + } + } +} diff --git a/Source/Core/IO/Logging/LogWriter.cs b/Source/Core/IO/Logging/LogWriter.cs new file mode 100644 index 000000000..fe95d85bd --- /dev/null +++ b/Source/Core/IO/Logging/LogWriter.cs @@ -0,0 +1,339 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Text; + +namespace Microsoft.Coyote.IO +{ + /// + /// Text writer that writes to the specified logger. + /// + internal sealed class LogWriter : TextWriter + { + /// + /// The installed logger. + /// + private readonly ILogger Logger; + + /// + /// Initializes a new instance of the class. + /// + /// ILogger + internal LogWriter(ILogger logger) + { + this.Logger = logger; + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(bool value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(char value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(char[] buffer) + { + this.Logger.Write(new string(buffer)); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(char[] buffer, int index, int count) + { + this.Logger.Write(new string(buffer, index, count)); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(decimal value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(double value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(float value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(int value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(long value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(object value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(string format, object arg0) + { + this.Logger.Write(string.Format(format, arg0.ToString())); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(string format, object arg0, object arg1) + { + this.Logger.Write(string.Format(format, arg0.ToString(), arg1.ToString())); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(string format, object arg0, object arg1, object arg2) + { + this.Logger.Write(string.Format(format, arg0.ToString(), arg1.ToString(), arg2.ToString())); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(string format, params object[] args) + { + this.Logger.Write(string.Format(format, args)); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(string value) + { + this.Logger.Write(value); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(uint value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger. + /// + public override void Write(ulong value) + { + this.Logger.Write(value.ToString()); + } + + /// + /// Writes a new line to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine() + { + this.Logger.WriteLine(string.Empty); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(bool value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(char value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(char[] buffer) + { + this.Logger.WriteLine(new string(buffer)); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(char[] buffer, int index, int count) + { + this.Logger.WriteLine(new string(buffer, index, count)); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(decimal value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(double value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(float value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(int value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(long value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(object value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(string format, object arg0) + { + this.Logger.WriteLine(string.Format(format, arg0.ToString())); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(string format, object arg0, object arg1) + { + this.Logger.WriteLine(string.Format(format, arg0.ToString(), arg1.ToString())); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(string format, object arg0, object arg1, object arg2) + { + this.Logger.WriteLine(string.Format(format, arg0.ToString(), arg1.ToString(), arg2.ToString())); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(string format, params object[] args) + { + this.Logger.Write(string.Format(format, args)); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(string value) + { + this.Logger.WriteLine(value); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(uint value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// Writes the specified input to the runtime logger, + /// followed by the current line terminator. + /// + public override void WriteLine(ulong value) + { + this.Logger.WriteLine(value.ToString()); + } + + /// + /// The character encoding in which the output is written. + /// + public override Encoding Encoding => Encoding.ASCII; + + /// + /// Returns the logged text as a string. + /// + public override string ToString() + { + return this.Logger.ToString(); + } + } +} diff --git a/Source/Core/IO/Logging/NulLogger.cs b/Source/Core/IO/Logging/NulLogger.cs new file mode 100644 index 000000000..b70e38ba3 --- /dev/null +++ b/Source/Core/IO/Logging/NulLogger.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.IO +{ + /// + /// Logger that disposes all written text. + /// + internal sealed class NulLogger : ILogger + { + /// + /// If true, then messages are logged. This logger ignores + /// this value and always disposes any written text. + /// + public bool IsVerbose { get; set; } = false; + + /// + /// Writes the specified string value. + /// + public void Write(string value) + { + } + + /// + /// Writes the text representation of the specified argument. + /// + public void Write(string format, object arg0) + { + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1) + { + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1, object arg2) + { + } + + /// + /// Writes the text representation of the specified array of objects. + /// + public void Write(string format, params object[] args) + { + } + + /// + /// Writes the specified string value, followed by the + /// current line terminator. + /// + public void WriteLine(string value) + { + } + + /// + /// Writes the text representation of the specified argument, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0) + { + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1) + { + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1, object arg2) + { + } + + /// + /// Writes the text representation of the specified array of objects, + /// followed by the current line terminator. + /// + public void WriteLine(string format, params object[] args) + { + } + + /// + /// Disposes the logger. + /// + public void Dispose() + { + } + } +} diff --git a/Source/Core/IO/Logging/RuntimeLogWriter.cs b/Source/Core/IO/Logging/RuntimeLogWriter.cs new file mode 100644 index 000000000..e4d78fddf --- /dev/null +++ b/Source/Core/IO/Logging/RuntimeLogWriter.cs @@ -0,0 +1,756 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.IO +{ + /// + /// Default implementation of a log writer that logs runtime + /// messages using the installed . + /// + public class RuntimeLogWriter : IDisposable + { + /// + /// Used to log messages. To set a custom logger, use the runtime + /// method . + /// + protected internal ILogger Logger { get; internal set; } + + /// + /// Initializes a new instance of the class + /// with the default logger. + /// + public RuntimeLogWriter() + { + this.Logger = new NulLogger(); + } + + /// + /// Called when an event is about to be enqueued to a machine. + /// + /// Id of the machine that the event is being enqueued to. + /// Name of the event. + public virtual void OnEnqueue(MachineId machineId, string eventName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnEnqueueLogMessage(machineId, eventName)); + } + } + + /// + /// Called when an event is dequeued by a machine. + /// + /// Id of the machine that the event is being dequeued by. + /// The name of the current state of the machine, if any. + /// Name of the event. + public virtual void OnDequeue(MachineId machineId, string currStateName, string eventName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnDequeueLogMessage(machineId, currStateName, eventName)); + } + } + + /// + /// Called when the default event handler for a state is about to be executed. + /// + /// Id of the machine that the state will execute in. + /// Name of the current state of the machine. + public virtual void OnDefault(MachineId machineId, string currStateName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnDefaultLogMessage(machineId, currStateName)); + } + } + + /// + /// Called when a machine transitions states via a 'goto'. + /// + /// Id of the machine. + /// The name of the current state of the machine, if any. + /// The target state of goto. + public virtual void OnGoto(MachineId machineId, string currStateName, string newStateName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnGotoLogMessage(machineId, currStateName, newStateName)); + } + } + + /// + /// Called when a machine is being pushed to a state. + /// + /// Id of the machine being pushed to the state. + /// The name of the current state of the machine, if any. + /// The state the machine is pushed to. + public virtual void OnPush(MachineId machineId, string currStateName, string newStateName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnPushLogMessage(machineId, currStateName, newStateName)); + } + } + + /// + /// Called when a machine has been popped from a state. + /// + /// Id of the machine that the pop executed in. + /// The name of the current state of the machine, if any. + /// The name of the state being re-entered, if any + public virtual void OnPop(MachineId machineId, string currStateName, string restoredStateName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnPopLogMessage(machineId, currStateName, restoredStateName)); + } + } + + /// + /// When an event cannot be handled in the current state, its exit handler is executed and then the state is + /// popped and any previous "current state" is reentered. This handler is called when that pop has been done. + /// + /// Id of the machine that the pop executed in. + /// The name of the current state of the machine, if any. + /// The name of the event that cannot be handled. + public virtual void OnPopUnhandledEvent(MachineId machineId, string currStateName, string eventName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnPopUnhandledEventLogMessage(machineId, currStateName, eventName)); + } + } + + /// + /// Called when an event is received by a machine. + /// + /// Id of the machine that received the event. + /// The name of the current state of the machine, if any. + /// The name of the event. + /// The machine was waiting for one or more specific events, + /// and was one of them + public virtual void OnReceive(MachineId machineId, string currStateName, string eventName, bool wasBlocked) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnReceiveLogMessage(machineId, currStateName, eventName, wasBlocked)); + } + } + + /// + /// Called when a machine waits to receive an event of a specified type. + /// + /// Id of the machine that is entering the wait state. + /// The name of the current state of the machine, if any. + /// The type of the event being waited for. + public virtual void OnWait(MachineId machineId, string currStateName, Type eventType) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnWaitLogMessage(machineId, currStateName, eventType)); + } + } + + /// + /// Called when a machine waits to receive an event of one of the specified types. + /// + /// Id of the machine that is entering the wait state. + /// The name of the current state of the machine, if any. + /// The types of the events being waited for, if any. + public virtual void OnWait(MachineId machineId, string currStateName, params Type[] eventTypes) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnWaitLogMessage(machineId, currStateName, eventTypes)); + } + } + + /// + /// Called when an event is sent to a target machine. + /// + /// Id of the target machine. + /// The id of the machine that sent the event, if any. + /// The name of the current state of the sender machine, if any. + /// The event being sent. + /// Id used to identify the send operation. + /// Is the target machine halted. + public virtual void OnSend(MachineId targetMachineId, MachineId senderId, string senderStateName, string eventName, + Guid opGroupId, bool isTargetHalted) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnSendLogMessage(targetMachineId, senderId, senderStateName, eventName, opGroupId, isTargetHalted)); + } + } + + /// + /// Called when a machine has been created. + /// + /// The id of the machine that has been created. + /// Id of the creator machine, null otherwise. + public virtual void OnCreateMachine(MachineId machineId, MachineId creator) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnCreateMachineLogMessage(machineId, creator)); + } + } + + /// + /// Called when a monitor has been created. + /// + /// The name of the type of the monitor that has been created. + /// The id of the monitor that has been created. + public virtual void OnCreateMonitor(string monitorTypeName, MachineId monitorId) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnCreateMonitorLogMessage(monitorTypeName, monitorId)); + } + } + + /// + /// Called when a machine timer has been created. + /// + /// Handle that contains information about the timer. + public virtual void OnCreateTimer(TimerInfo info) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnCreateTimerLogMessage(info)); + } + } + + /// + /// Called when a machine timer has been stopped. + /// + /// Handle that contains information about the timer. + public virtual void OnStopTimer(TimerInfo info) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnStopTimerLogMessage(info)); + } + } + + /// + /// Called when a machine has been halted. + /// + /// The id of the machine that has been halted. + /// Approximate size of the machine inbox. + public virtual void OnHalt(MachineId machineId, int inboxSize) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnHaltLogMessage(machineId, inboxSize)); + } + } + + /// + /// Called when a random result has been obtained. + /// + /// The id of the source machine, if any; otherwise, the runtime itself was the source. + /// The random result (may be bool or int). + public virtual void OnRandom(MachineId machineId, object result) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnRandomLogMessage(machineId, result)); + } + } + + /// + /// Called when a machine enters or exits a state. + /// + /// The id of the machine entering or exiting the state. + /// The name of the state being entered or exited. + /// If true, this is called for a state entry; otherwise, exit. + public virtual void OnMachineState(MachineId machineId, string stateName, bool isEntry) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMachineStateLogMessage(machineId, stateName, isEntry)); + } + } + + /// + /// Called when a machine raises an event. + /// + /// The id of the machine raising the event. + /// The name of the state in which the action is being executed. + /// The name of the event being raised. + public virtual void OnMachineEvent(MachineId machineId, string currStateName, string eventName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMachineEventLogMessage(machineId, currStateName, eventName)); + } + } + + /// + /// Called when a machine executes an action. + /// + /// The id of the machine executing the action. + /// The name of the state in which the action is being executed. + /// The name of the action being executed. + public virtual void OnMachineAction(MachineId machineId, string currStateName, string actionName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMachineActionLogMessage(machineId, currStateName, actionName)); + } + } + + /// + /// Called when a machine throws an exception + /// + /// The id of the machine that threw the exception. + /// The name of the current machine state. + /// The name of the action being executed. + /// The exception. + public virtual void OnMachineExceptionThrown(MachineId machineId, string currStateName, string actionName, Exception ex) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMachineExceptionThrownLogMessage(machineId, currStateName, actionName, ex)); + } + } + + /// + /// Called when a machine's OnException method is used to handle a thrown exception + /// + /// The id of the machine that threw the exception. + /// The name of the current machine state. + /// The name of the action being executed. + /// The exception. + public virtual void OnMachineExceptionHandled(MachineId machineId, string currStateName, string actionName, Exception ex) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMachineExceptionHandledLogMessage(machineId, currStateName, actionName, ex)); + } + } + + /// + /// Called when a monitor enters or exits a state. + /// + /// The name of the type of the monitor entering or exiting the state + /// The ID of the monitor entering or exiting the state + /// The name of the state being entered or exited; if + /// is not null, then the temperature is appended to the statename in brackets, e.g. "stateName[hot]". + /// If true, this is called for a state entry; otherwise, exit. + /// If true, the monitor is in a hot state; if false, the monitor is in a cold state; + /// else no liveness state is available. + public virtual void OnMonitorState(string monitorTypeName, MachineId monitorId, string stateName, + bool isEntry, bool? isInHotState) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMonitorStateLogMessage(monitorTypeName, monitorId, stateName, isEntry, isInHotState)); + } + } + + /// + /// Called when a monitor is about to process or has raised an event. + /// + /// Name of type of the monitor that will process or has raised the event. + /// ID of the monitor that will process or has raised the event + /// The name of the state in which the event is being raised. + /// The name of the event. + /// If true, the monitor is processing the event; otherwise it has raised it. + public virtual void OnMonitorEvent(string monitorTypeName, MachineId monitorId, string currStateName, + string eventName, bool isProcessing) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMonitorEventLogMessage(monitorTypeName, monitorId, currStateName, eventName, isProcessing)); + } + } + + /// + /// Called when a monitor executes an action. + /// + /// Name of type of the monitor that is executing the action. + /// ID of the monitor that is executing the action + /// The name of the state in which the action is being executed. + /// The name of the action being executed. + public virtual void OnMonitorAction(string monitorTypeName, MachineId monitorId, string currStateName, string actionName) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnMonitorActionLogMessage(monitorTypeName, monitorId, currStateName, actionName)); + } + } + + /// + /// Called for general error reporting via pre-constructed text. + /// + /// The text of the error report. + public virtual void OnError(string text) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnErrorLogMessage(text)); + } + } + + /// + /// Called for errors detected by a specific scheduling strategy. + /// + /// The scheduling strategy that was used. + /// More information about the scheduling strategy. + public virtual void OnStrategyError(SchedulingStrategy strategy, string strategyDescription) + { + if (this.Logger.IsVerbose) + { + this.Logger.WriteLine(this.FormatOnStrategyErrorLogMessage(strategy, strategyDescription)); + } + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that the event is being enqueued to. + /// Name of the event. + protected virtual string FormatOnEnqueueLogMessage(MachineId machineId, string eventName) => + $" Machine '{machineId}' enqueued event '{eventName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that the event is being dequeued by. + /// The name of the current state of the machine, if any. + /// Name of the event. + protected virtual string FormatOnDequeueLogMessage(MachineId machineId, string currStateName, string eventName) => + $" Machine '{machineId}' in state '{currStateName}' dequeued event '{eventName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that the state will execute in. + /// Name of the current state of the machine. + protected virtual string FormatOnDefaultLogMessage(MachineId machineId, string currStateName) => + $" Machine '{machineId}' in state '{currStateName}' is executing the default handler."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine. + /// The name of the current state of the machine, if any. + /// The target state of goto. + protected virtual string FormatOnGotoLogMessage(MachineId machineId, string currStateName, string newStateName) => + $" Machine '{machineId}' is transitioning from state '{currStateName}' to state '{newStateName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine being pushed to the state. + /// The name of the current state of the machine, if any. + /// The state the machine is pushed to. + protected virtual string FormatOnPushLogMessage(MachineId machineId, string currStateName, string newStateName) => + $" Machine '{machineId}' pushed from state '{currStateName}' to state '{newStateName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that the pop executed in. + /// The name of the current state of the machine, if any. + /// The name of the state being re-entered, if any + protected virtual string FormatOnPopLogMessage(MachineId machineId, string currStateName, string restoredStateName) + { + currStateName = string.IsNullOrEmpty(currStateName) ? "[not recorded]" : currStateName; + var reenteredStateName = restoredStateName ?? string.Empty; + return $" Machine '{machineId}' popped state '{currStateName}' and reentered state '{reenteredStateName}'."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that the pop executed in. + /// The name of the current state of the machine, if any. + /// The name of the event that cannot be handled. + protected virtual string FormatOnPopUnhandledEventLogMessage(MachineId machineId, string currStateName, string eventName) + { + var reenteredStateName = string.IsNullOrEmpty(currStateName) + ? string.Empty + : $" and reentered state '{currStateName}"; + return $" Machine '{machineId}' popped with unhandled event '{eventName}'{reenteredStateName}."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that received the event. + /// The name of the current state of the machine, if any. + /// The name of the event. + /// The machine was waiting for one or more specific events, + /// and was one of them + protected virtual string FormatOnReceiveLogMessage(MachineId machineId, string currStateName, string eventName, bool wasBlocked) + { + var unblocked = wasBlocked ? " and unblocked" : string.Empty; + return $" Machine '{machineId}' in state '{currStateName}' dequeued event '{eventName}'{unblocked}."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that is entering the wait state. + /// The name of the current state of the machine, if any. + /// The type of the event being waited for. + protected virtual string FormatOnWaitLogMessage(MachineId machineId, string currStateName, Type eventType) => + $" Machine '{machineId}' in state '{currStateName}' is waiting to dequeue an event of type '{eventType.FullName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the machine that is entering the wait state. + /// The name of the current state of the machine, if any. + /// The types of the events being waited for, if any. + protected virtual string FormatOnWaitLogMessage(MachineId machineId, string currStateName, params Type[] eventTypes) + { + string eventNames; + if (eventTypes.Length == 0) + { + eventNames = "''"; + } + else if (eventTypes.Length == 1) + { + eventNames = "'" + eventTypes[0].FullName + "'"; + } + else if (eventTypes.Length == 2) + { + eventNames = "'" + eventTypes[0].FullName + "' or '" + eventTypes[1].FullName + "'"; + } + else if (eventTypes.Length == 3) + { + eventNames = "'" + eventTypes[0].FullName + "', '" + eventTypes[1].FullName + "' or '" + eventTypes[2].FullName + "'"; + } + else + { + string[] eventNameArray = new string[eventTypes.Length - 1]; + for (int i = 0; i < eventTypes.Length - 2; i++) + { + eventNameArray[i] = eventTypes[i].FullName; + } + + eventNames = "'" + string.Join("', '", eventNameArray) + "' or '" + eventTypes[eventTypes.Length - 1].FullName + "'"; + } + + return $" Machine '{machineId}' in state '{currStateName}' is waiting to dequeue an event of type {eventNames}."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Id of the target machine. + /// The id of the machine that sent the event, if any. + /// The name of the current state of the sender machine, if any. + /// The event being sent. + /// Id used to identify the send operation. + /// Is the target machine halted. + protected virtual string FormatOnSendLogMessage(MachineId targetMachineId, MachineId senderId, string senderStateName, + string eventName, Guid opGroupId, bool isTargetHalted) + { + var opGroupIdMsg = opGroupId != Guid.Empty ? $" (operation group '{opGroupId}')" : string.Empty; + var target = isTargetHalted ? $"halted machine '{targetMachineId}'" : $"machine '{targetMachineId}'"; + var sender = senderId != null ? $"Machine '{senderId}' in state '{senderStateName}'" : $"The runtime"; + return $" {sender} sent event '{eventName}' to {target}{opGroupIdMsg}."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the machine that has been created. + /// Id of the creator machine, null otherwise. + protected virtual string FormatOnCreateMachineLogMessage(MachineId machineId, MachineId creator) + { + var source = creator is null ? "the runtime" : $"machine '{creator.Name}'"; + return $" Machine '{machineId}' was created by {source}."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The name of the type of the monitor that has been created. + /// The id of the monitor that has been created. + protected virtual string FormatOnCreateMonitorLogMessage(string monitorTypeName, MachineId monitorId) => + $" Monitor '{monitorTypeName}' with id '{monitorId}' was created."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Handle that contains information about the timer. + protected virtual string FormatOnCreateTimerLogMessage(TimerInfo info) + { + var source = info.OwnerId is null ? "the runtime" : $"machine '{info.OwnerId.Name}'"; + if (info.Period.TotalMilliseconds >= 0) + { + return $" Timer '{info}' (due-time:{info.DueTime.TotalMilliseconds}ms; " + + $"period :{info.Period.TotalMilliseconds}ms) was created by {source}."; + } + else + { + return $" Timer '{info}' (due-time:{info.DueTime.TotalMilliseconds}ms) was created by {source}."; + } + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Handle that contains information about the timer. + protected virtual string FormatOnStopTimerLogMessage(TimerInfo info) + { + var source = info.OwnerId is null ? "the runtime" : $"machine '{info.OwnerId.Name}'"; + return $" Timer '{info}' was stopped and disposed by {source}."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the machine that has been halted. + /// Approximate size of the machine inbox. + protected virtual string FormatOnHaltLogMessage(MachineId machineId, int inboxSize) => + $" Machine '{machineId}' halted with '{inboxSize}' events in its inbox."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the source machine, if any; otherwise, the runtime itself was the source. + /// The random result (may be bool or int). + protected virtual string FormatOnRandomLogMessage(MachineId machineId, object result) + { + var source = machineId != null ? $"Machine '{machineId}'" : "Runtime"; + return $" {source} nondeterministically chose '{result}'."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the machine entering or exiting the state. + /// The name of the state being entered or exited. + /// If true, this is called for a state entry; otherwise, exit. + protected virtual string FormatOnMachineStateLogMessage(MachineId machineId, string stateName, bool isEntry) + { + var direction = isEntry ? "enters" : "exits"; + return $" Machine '{machineId}' {direction} state '{stateName}'."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the machine raising the event. + /// The name of the state in which the action is being executed. + /// The name of the event being raised. + protected virtual string FormatOnMachineEventLogMessage(MachineId machineId, string currStateName, string eventName) => + $" Machine '{machineId}' in state '{currStateName}' raised event '{eventName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the machine executing the action. + /// The name of the state in which the action is being executed. + /// The name of the action being executed. + protected virtual string FormatOnMachineActionLogMessage(MachineId machineId, string currStateName, string actionName) => + $" Machine '{machineId}' in state '{currStateName}' invoked action '{actionName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the machine that threw the exception. + /// The name of the current machine state. + /// The name of the action being executed. + /// The exception. + protected virtual string FormatOnMachineExceptionThrownLogMessage(MachineId machineId, string currStateName, string actionName, Exception ex) => + $" Machine '{machineId}' in state '{currStateName}' running action '{actionName}' threw an exception '{ex.GetType().Name}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The id of the machine that threw the exception. + /// The name of the current machine state. + /// The name of the action being executed. + /// The exception. + protected virtual string FormatOnMachineExceptionHandledLogMessage(MachineId machineId, string currStateName, string actionName, Exception ex) => + $" Machine '{machineId}' in state '{currStateName}' running action '{actionName}' chose to handle the exception '{ex.GetType().Name}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The name of the type of the monitor entering or exiting the state + /// The ID of the monitor entering or exiting the state + /// The name of the state being entered or exited; if + /// is not null, then the temperature is appended to the statename in brackets, e.g. "stateName[hot]". + /// If true, this is called for a state entry; otherwise, exit. + /// If true, the monitor is in a hot state; if false, the monitor is in a cold state; + /// else no liveness state is available. + protected virtual string FormatOnMonitorStateLogMessage(string monitorTypeName, MachineId monitorId, string stateName, bool isEntry, bool? isInHotState) + { + var liveness = isInHotState.HasValue ? (isInHotState.Value ? "'hot' " : "'cold' ") : string.Empty; + var direction = isEntry ? "enters" : "exits"; + return $" Monitor '{monitorTypeName}' with id '{monitorId}' {direction} {liveness}state '{stateName}'."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Name of type of the monitor that will process or has raised the event. + /// ID of the monitor that will process or has raised the event + /// The name of the state in which the event is being raised. + /// The name of the event. + /// If true, the monitor is processing the event; otherwise it has raised it. + protected virtual string FormatOnMonitorEventLogMessage(string monitorTypeName, MachineId monitorId, string currStateName, string eventName, bool isProcessing) + { + var activity = isProcessing ? "is processing" : "raised"; + return $" Monitor '{monitorTypeName}' with id '{monitorId}' in state '{currStateName}' {activity} event '{eventName}'."; + } + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// Name of type of the monitor that is executing the action. + /// ID of the monitor that is executing the action + /// The name of the state in which the action is being executed. + /// The name of the action being executed. + protected virtual string FormatOnMonitorActionLogMessage(string monitorTypeName, MachineId monitorId, string currStateName, string actionName) => + $" Monitor '{monitorTypeName}' with id '{monitorId}' in state '{currStateName}' executed action '{actionName}'."; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The text of the error report. + protected virtual string FormatOnErrorLogMessage(string text) => text; + + /// + /// Returns a string formatted for the log message and its parameters. + /// + /// The scheduling strategy that was used. + /// More information about the scheduling strategy. + protected virtual string FormatOnStrategyErrorLogMessage(SchedulingStrategy strategy, string strategyDescription) + { + var desc = string.IsNullOrEmpty(strategyDescription) ? $" Description: {strategyDescription}" : string.Empty; + return $" Found bug using '{strategy}' strategy.{desc}"; + } + + /// + /// Disposes the log writer. + /// + public virtual void Dispose(bool disposing) + { + } + + /// + /// Disposes the log writer. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Source/Core/Machines/AsyncMachine.cs b/Source/Core/Machines/AsyncMachine.cs new file mode 100644 index 000000000..1d2e8d591 --- /dev/null +++ b/Source/Core/Machines/AsyncMachine.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Diagnostics; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Implements a machine that can execute asynchronously. + /// This type is intended for runtime use only. + /// + [DebuggerStepThrough] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public abstract class AsyncMachine + { + /// + /// The runtime that executes this machine. + /// + internal CoyoteRuntime Runtime { get; private set; } + + /// + /// The unique machine id. + /// + protected internal MachineId Id { get; private set; } + + /// + /// Id used to identify subsequent operations performed by this machine. + /// + protected internal abstract Guid OperationGroupId { get; set; } + + /// + /// The logger installed to the Coyote runtime. + /// + protected ILogger Logger => this.Runtime.Logger; + + /// + /// Initializes this machine. + /// + internal void Initialize(CoyoteRuntime runtime, MachineId mid) + { + this.Runtime = runtime; + this.Id = mid; + } + + /// + /// Returns the cached state of this machine. + /// + internal virtual int GetCachedState() => 0; + + /// + /// Determines whether the specified object is equal to the current object. + /// + public override bool Equals(object obj) + { + if (obj is AsyncMachine m && + this.GetType() == m.GetType()) + { + return this.Id.Value == m.Id.Value; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() + { + return this.Id.Value.GetHashCode(); + } + + /// + /// Returns a string that represents the current machine. + /// + public override string ToString() + { + return this.Id.Name; + } + } +} diff --git a/Source/Core/Machines/DequeueStatus.cs b/Source/Core/Machines/DequeueStatus.cs new file mode 100644 index 000000000..3b8401677 --- /dev/null +++ b/Source/Core/Machines/DequeueStatus.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// The status returned as the result of a dequeue operation. + /// + internal enum DequeueStatus + { + /// + /// An event was successfully dequeued. + /// + Success = 0, + + /// + /// The raised event was dequeued. + /// + Raised, + + /// + /// The default event was dequeued. + /// + Default, + + /// + /// No event available to dequeue. + /// + NotAvailable + } +} diff --git a/Source/Core/Machines/EnqueueStatus.cs b/Source/Core/Machines/EnqueueStatus.cs new file mode 100644 index 000000000..4d28ea114 --- /dev/null +++ b/Source/Core/Machines/EnqueueStatus.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// The status returned as the result of an enqueue operation. + /// + internal enum EnqueueStatus + { + /// + /// The event handler is already running. + /// + EventHandlerRunning = 0, + + /// + /// The event handler is not running. + /// + EventHandlerNotRunning, + + /// + /// The event was used to wake a machine at a receive statement. + /// + Received, + + /// + /// There is no next event available to dequeue and handle. + /// + NextEventUnavailable, + + /// + /// The event was dropped. + /// + Dropped + } +} diff --git a/Source/Core/Machines/EventQueues/EventQueue.cs b/Source/Core/Machines/EventQueues/EventQueue.cs new file mode 100644 index 000000000..1da73d409 --- /dev/null +++ b/Source/Core/Machines/EventQueues/EventQueue.cs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Implements a queue of events. + /// + internal sealed class EventQueue : IEventQueue + { + /// + /// Manages the state of the machine that owns this queue. + /// + private readonly IMachineStateManager MachineStateManager; + + /// + /// The internal queue. + /// + private readonly LinkedList<(Event e, Guid opGroupId)> Queue; + + /// + /// The raised event and its metadata, or null if no event has been raised. + /// + private (Event e, Guid opGroupId) RaisedEvent; + + /// + /// Map from the types of events that the owner of the queue is waiting to receive + /// to an optional predicate. If an event of one of these types is enqueued, then + /// if there is no predicate, or if there is a predicate and evaluates to true, then + /// the event is received, else the event is deferred. + /// + private Dictionary> EventWaitTypes; + + /// + /// Task completion source that contains the event obtained using an explicit receive. + /// + private TaskCompletionSource ReceiveCompletionSource; + + /// + /// Checks if the queue is accepting new events. + /// + private bool IsClosed; + + /// + /// The size of the queue. + /// + public int Size => this.Queue.Count; + + /// + /// Checks if an event has been raised. + /// + public bool IsEventRaised => this.RaisedEvent != default; + + /// + /// Initializes a new instance of the class. + /// + internal EventQueue(IMachineStateManager machineStateManager) + { + this.MachineStateManager = machineStateManager; + this.Queue = new LinkedList<(Event, Guid)>(); + this.EventWaitTypes = new Dictionary>(); + this.IsClosed = false; + } + + /// + /// Enqueues the specified event and its optional metadata. + /// + public EnqueueStatus Enqueue(Event e, Guid opGroupId, EventInfo info) + { + EnqueueStatus enqueueStatus = EnqueueStatus.EventHandlerRunning; + lock (this.Queue) + { + if (this.IsClosed) + { + return EnqueueStatus.Dropped; + } + + if (this.EventWaitTypes != null && + this.EventWaitTypes.TryGetValue(e.GetType(), out Func predicate) && + (predicate is null || predicate(e))) + { + this.EventWaitTypes = null; + enqueueStatus = EnqueueStatus.Received; + } + else + { + this.Queue.AddLast((e, opGroupId)); + if (!this.MachineStateManager.IsEventHandlerRunning) + { + this.MachineStateManager.IsEventHandlerRunning = true; + enqueueStatus = EnqueueStatus.EventHandlerNotRunning; + } + } + } + + if (enqueueStatus is EnqueueStatus.Received) + { + this.MachineStateManager.OnReceiveEvent(e, opGroupId, info); + this.ReceiveCompletionSource.SetResult(e); + return enqueueStatus; + } + else + { + this.MachineStateManager.OnEnqueueEvent(e, opGroupId, info); + } + + return enqueueStatus; + } + + /// + /// Dequeues the next event, if there is one available. + /// + public (DequeueStatus status, Event e, Guid opGroupId, EventInfo info) Dequeue() + { + // Try to get the raised event, if there is one. Raised events + // have priority over the events in the inbox. + if (this.RaisedEvent != default) + { + if (this.MachineStateManager.IsEventIgnoredInCurrentState(this.RaisedEvent.e, this.RaisedEvent.opGroupId, null)) + { + // TODO: should the user be able to raise an ignored event? + // The raised event is ignored in the current state. + this.RaisedEvent = default; + } + else + { + (Event e, Guid opGroupId) = this.RaisedEvent; + this.RaisedEvent = default; + return (DequeueStatus.Raised, e, opGroupId, null); + } + } + + lock (this.Queue) + { + // Try to dequeue the next event, if there is one. + var node = this.Queue.First; + while (node != null) + { + // Iterates through the events in the inbox. + if (this.MachineStateManager.IsEventIgnoredInCurrentState(node.Value.e, node.Value.opGroupId, null)) + { + // Removes an ignored event. + var nextNode = node.Next; + this.Queue.Remove(node); + node = nextNode; + continue; + } + else if (this.MachineStateManager.IsEventDeferredInCurrentState(node.Value.e, node.Value.opGroupId, null)) + { + // Skips a deferred event. + node = node.Next; + continue; + } + + // Found next event that can be dequeued. + this.Queue.Remove(node); + return (DequeueStatus.Success, node.Value.e, node.Value.opGroupId, null); + } + + // No event can be dequeued, so check if there is a default event handler. + if (!this.MachineStateManager.IsDefaultHandlerInstalledInCurrentState()) + { + // There is no default event handler installed, so do not return an event. + // Setting 'IsEventHandlerRunning' must happen inside the lock as it needs + // to be synchronized with the enqueue and starting a new event handler. + this.MachineStateManager.IsEventHandlerRunning = false; + return (DequeueStatus.NotAvailable, null, Guid.Empty, null); + } + } + + // TODO: check op-id of default event. + // A default event handler exists. + return (DequeueStatus.Default, Default.Event, Guid.Empty, null); + } + + /// + /// Enqueues the specified raised event. + /// + public void Raise(Event e, Guid opGroupId) + { + this.RaisedEvent = (e, opGroupId); + this.MachineStateManager.OnRaiseEvent(e, opGroupId, null); + } + + /// + /// Waits to receive an event of the specified type that satisfies an optional predicate. + /// + public Task ReceiveAsync(Type eventType, Func predicate = null) + { + var eventWaitTypes = new Dictionary> + { + { eventType, predicate } + }; + + return this.ReceiveAsync(eventWaitTypes); + } + + /// + /// Waits to receive an event of the specified types. + /// + public Task ReceiveAsync(params Type[] eventTypes) + { + var eventWaitTypes = new Dictionary>(); + foreach (var type in eventTypes) + { + eventWaitTypes.Add(type, null); + } + + return this.ReceiveAsync(eventWaitTypes); + } + + /// + /// Waits to receive an event of the specified types that satisfy the specified predicates. + /// + public Task ReceiveAsync(params Tuple>[] events) + { + var eventWaitTypes = new Dictionary>(); + foreach (var e in events) + { + eventWaitTypes.Add(e.Item1, e.Item2); + } + + return this.ReceiveAsync(eventWaitTypes); + } + + /// + /// Waits for an event to be enqueued based on the conditions defined in the event wait types. + /// + private Task ReceiveAsync(Dictionary> eventWaitTypes) + { + (Event e, Guid opGroupId) receivedEvent = default; + lock (this.Queue) + { + var node = this.Queue.First; + while (node != null) + { + // Dequeue the first event that the caller waits to receive, if there is one in the queue. + if (eventWaitTypes.TryGetValue(node.Value.e.GetType(), out Func predicate) && + (predicate is null || predicate(node.Value.e))) + { + receivedEvent = node.Value; + this.Queue.Remove(node); + break; + } + + node = node.Next; + } + + if (receivedEvent == default) + { + this.ReceiveCompletionSource = new TaskCompletionSource(); + this.EventWaitTypes = eventWaitTypes; + } + } + + if (receivedEvent == default) + { + // Note that 'EventWaitTypes' is racy, so should not be accessed outside + // the lock, this is why we access 'eventWaitTypes' instead. + this.MachineStateManager.OnWaitEvent(eventWaitTypes.Keys); + return this.ReceiveCompletionSource.Task; + } + + this.MachineStateManager.OnReceiveEventWithoutWaiting(receivedEvent.e, receivedEvent.opGroupId, null); + return Task.FromResult(receivedEvent.e); + } + + /// + /// Returns the cached state of the queue. + /// + public int GetCachedState() => 0; + + /// + /// Closes the queue, which stops any further event enqueues. + /// + public void Close() + { + lock (this.Queue) + { + this.IsClosed = true; + } + } + + /// + /// Disposes the queue resources. + /// + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + foreach (var (e, opGroupId) in this.Queue) + { + this.MachineStateManager.OnDropEvent(e, opGroupId, null); + } + + this.Queue.Clear(); + } + + /// + /// Disposes the queue resources. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Source/Core/Machines/EventQueues/IEventQueue.cs b/Source/Core/Machines/EventQueues/IEventQueue.cs new file mode 100644 index 000000000..0257452ec --- /dev/null +++ b/Source/Core/Machines/EventQueues/IEventQueue.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Interface of a queue of events. + /// + internal interface IEventQueue : IDisposable + { + /// + /// The size of the queue. + /// + int Size { get; } + + /// + /// Checks if an event has been raised. + /// + bool IsEventRaised { get; } + + /// + /// Enqueues the specified event and its optional metadata. + /// + EnqueueStatus Enqueue(Event e, Guid opGroupId, EventInfo info); + + /// + /// Dequeues the next event, if there is one available. + /// + (DequeueStatus status, Event e, Guid opGroupId, EventInfo info) Dequeue(); + + /// + /// Enqueues the specified raised event. + /// + void Raise(Event e, Guid opGroupId); + + /// + /// Waits to receive an event of the specified type that satisfies an optional predicate. + /// + Task ReceiveAsync(Type eventType, Func predicate = null); + + /// + /// Waits to receive an event of the specified types. + /// + Task ReceiveAsync(params Type[] eventTypes); + + /// + /// Waits to receive an event of the specified types that satisfy the specified predicates. + /// + Task ReceiveAsync(params Tuple>[] events); + + /// + /// Returns the cached state of the queue. + /// + int GetCachedState(); + + /// + /// Closes the queue, which stops any further event enqueues. + /// + void Close(); + } +} diff --git a/Source/Core/Machines/Events/Default.cs b/Source/Core/Machines/Events/Default.cs new file mode 100644 index 000000000..f90ac6f82 --- /dev/null +++ b/Source/Core/Machines/Events/Default.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Machines +{ + /// + /// The default event. + /// + [DataContract] + public sealed class Default : Event + { + /// + /// Gets an instance of the default event. + /// + public static Default Event { get; } = new Default(); + + /// + /// Initializes a new instance of the class. + /// + private Default() + : base() + { + } + } +} diff --git a/Source/Core/Machines/Events/GotoStateEvent.cs b/Source/Core/Machines/Events/GotoStateEvent.cs new file mode 100644 index 000000000..9c2d38e13 --- /dev/null +++ b/Source/Core/Machines/Events/GotoStateEvent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Machines +{ + /// + /// The goto state event. + /// + [DataContract] + internal sealed class GotoStateEvent : Event + { + /// + /// Type of the state to transition to. + /// + public readonly Type State; + + /// + /// Initializes a new instance of the class. + /// + /// Type of the state. + public GotoStateEvent(Type s) + : base() + { + this.State = s; + } + } +} diff --git a/Source/Core/Machines/Events/Halt.cs b/Source/Core/Machines/Events/Halt.cs new file mode 100644 index 000000000..3945a2072 --- /dev/null +++ b/Source/Core/Machines/Events/Halt.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Machines +{ + /// + /// The halt event. + /// + [DataContract] + public sealed class Halt : Event + { + /// + /// Initializes a new instance of the class. + /// + public Halt() + : base() + { + } + } +} diff --git a/Source/Core/Machines/Events/PushStateEvent.cs b/Source/Core/Machines/Events/PushStateEvent.cs new file mode 100644 index 000000000..c2ac9ca54 --- /dev/null +++ b/Source/Core/Machines/Events/PushStateEvent.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Machines +{ + /// + /// The push state event. + /// + [DataContract] + internal sealed class PushStateEvent : Event + { + /// + /// Type of the state to transition to. + /// + public Type State; + + /// + /// Initializes a new instance of the class. + /// + /// Type of the state. + public PushStateEvent(Type s) + : base() + { + this.State = s; + } + } +} diff --git a/Source/Core/Machines/Events/QuiescentEvent.cs b/Source/Core/Machines/Events/QuiescentEvent.cs new file mode 100644 index 000000000..40460c281 --- /dev/null +++ b/Source/Core/Machines/Events/QuiescentEvent.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Signals that a machine has reached quiescence. + /// + [DataContract] + internal sealed class QuiescentEvent : Event + { + /// + /// The id of the machine that has reached quiescence. + /// + public MachineId MachineId; + + /// + /// Initializes a new instance of the class. + /// + /// The id of the machine that has reached quiescence. + public QuiescentEvent(MachineId mid) + { + this.MachineId = mid; + } + } +} diff --git a/Source/Core/Machines/Events/WildcardEvent.cs b/Source/Core/Machines/Events/WildcardEvent.cs new file mode 100644 index 000000000..c9d36a717 --- /dev/null +++ b/Source/Core/Machines/Events/WildcardEvent.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Machines +{ + /// + /// The wild card event. + /// + [DataContract] + public sealed class WildCardEvent : Event + { + /// + /// Initializes a new instance of the class. + /// + public WildCardEvent() + : base() + { + } + } +} diff --git a/Source/Core/Machines/Handlers/ActionBinding.cs b/Source/Core/Machines/Handlers/ActionBinding.cs new file mode 100644 index 000000000..01e9fdb61 --- /dev/null +++ b/Source/Core/Machines/Handlers/ActionBinding.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// Defines an action binding. + /// + internal sealed class ActionBinding : EventActionHandler + { + /// + /// Name of the action. + /// + public string Name; + + /// + /// Initializes a new instance of the class. + /// + public ActionBinding(string actionName) + { + this.Name = actionName; + } + } +} diff --git a/Source/Core/Machines/Handlers/CachedDelegate.cs b/Source/Core/Machines/Handlers/CachedDelegate.cs new file mode 100644 index 000000000..c5a3e6f8c --- /dev/null +++ b/Source/Core/Machines/Handlers/CachedDelegate.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.Coyote.Machines +{ + /// + /// A machine delegate that has been cached for performance optimization. + /// + internal class CachedDelegate + { + internal readonly MethodInfo MethodInfo; + internal readonly Delegate Handler; + + internal CachedDelegate(MethodInfo methodInfo, Machine machine) + { + this.MethodInfo = methodInfo; + + // MethodInfo.Invoke catches the exception to wrap it in a TargetInvocationException. + // This unwinds the stack before Machine.ExecuteAction's exception filter is invoked, + // so call through the delegate instead (which is also much faster than Invoke). + if (methodInfo.ReturnType == typeof(void)) + { + this.Handler = Delegate.CreateDelegate(typeof(Action), machine, methodInfo); + } + else if (methodInfo.ReturnType == typeof(Task)) + { + this.Handler = Delegate.CreateDelegate(typeof(Func), machine, methodInfo); + } + else + { + throw new InvalidOperationException($"Machine '{machine.Id}' is trying to cache invalid delegate '{methodInfo.Name}'."); + } + } + } +} diff --git a/Source/Core/Machines/Handlers/DeferAction.cs b/Source/Core/Machines/Handlers/DeferAction.cs new file mode 100644 index 000000000..57c5dd3f1 --- /dev/null +++ b/Source/Core/Machines/Handlers/DeferAction.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// Defines a defer action. + /// + internal sealed class DeferAction : EventActionHandler + { + } +} diff --git a/Source/Core/Machines/Handlers/EventActionHandler.cs b/Source/Core/Machines/Handlers/EventActionHandler.cs new file mode 100644 index 000000000..9a1403735 --- /dev/null +++ b/Source/Core/Machines/Handlers/EventActionHandler.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// An abstract event handler. + /// + internal abstract class EventActionHandler + { + } +} diff --git a/Source/Core/Machines/Handlers/EventHandlerStatus.cs b/Source/Core/Machines/Handlers/EventHandlerStatus.cs new file mode 100644 index 000000000..a7c9fa902 --- /dev/null +++ b/Source/Core/Machines/Handlers/EventHandlerStatus.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// The status of the machine event handler. + /// + internal enum EventHandlerStatus + { + /// + /// The machine has dequeued an event. + /// + EventDequeued = 0, + + /// + /// The machine has handled an event. + /// + EventHandled, + + /// + /// The machine has dequeued an event that cannot be handled. + /// + EventUnhandled + } +} diff --git a/Source/Core/Machines/Handlers/GotoStateTransition.cs b/Source/Core/Machines/Handlers/GotoStateTransition.cs new file mode 100644 index 000000000..6b5ed2d77 --- /dev/null +++ b/Source/Core/Machines/Handlers/GotoStateTransition.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Defines a goto state transition. + /// + internal sealed class GotoStateTransition + { + /// + /// The target state. + /// + public Type TargetState; + + /// + /// An optional lambda function that executes after the + /// on-exit handler of the exiting state. + /// + public string Lambda; + + /// + /// Initializes a new instance of the class. + /// + /// The target state. + /// Lambda function that executes after the on-exit handler of the exiting state. + public GotoStateTransition(Type targetState, string lambda) + { + this.TargetState = targetState; + this.Lambda = lambda; + } + + /// + /// Initializes a new instance of the class. + /// + /// The target state. + public GotoStateTransition(Type targetState) + { + this.TargetState = targetState; + this.Lambda = null; + } + } +} diff --git a/Source/Core/Machines/Handlers/IgnoreAction.cs b/Source/Core/Machines/Handlers/IgnoreAction.cs new file mode 100644 index 000000000..dfde96bda --- /dev/null +++ b/Source/Core/Machines/Handlers/IgnoreAction.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// Defines a skip action binding (for ignore). + /// + internal sealed class IgnoreAction : EventActionHandler + { + } +} diff --git a/Source/Core/Machines/Handlers/PushStateTransition.cs b/Source/Core/Machines/Handlers/PushStateTransition.cs new file mode 100644 index 000000000..9b020f9c6 --- /dev/null +++ b/Source/Core/Machines/Handlers/PushStateTransition.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Defines a push state transition. + /// + internal sealed class PushStateTransition + { + /// + /// The target state. + /// + public Type TargetState; + + /// + /// Initializes a new instance of the class. + /// + /// The target state. + public PushStateTransition(Type targetState) + { + this.TargetState = targetState; + } + } +} diff --git a/Source/Core/Machines/IMachineStateManager.cs b/Source/Core/Machines/IMachineStateManager.cs new file mode 100644 index 000000000..104613872 --- /dev/null +++ b/Source/Core/Machines/IMachineStateManager.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Interface for managing the state of a machine. + /// + internal interface IMachineStateManager + { + /// + /// True if the event handler of the machine is running, else false. + /// + bool IsEventHandlerRunning { get; set; } + + /// + /// Id used to identify subsequent operations performed by the machine. + /// + Guid OperationGroupId { get; set; } + + /// + /// Returns the cached state of the machine. + /// + int GetCachedState(); + + /// + /// Checks if the specified event is ignored in the current machine state. + /// + bool IsEventIgnoredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo); + + /// + /// Checks if the specified event is deferred in the current machine state. + /// + bool IsEventDeferredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo); + + /// + /// Checks if a default handler is installed in the current machine state. + /// + bool IsDefaultHandlerInstalledInCurrentState(); + + /// + /// Notifies the machine that an event has been enqueued. + /// + void OnEnqueueEvent(Event e, Guid opGroupId, EventInfo eventInfo); + + /// + /// Notifies the machine that an event has been raised. + /// + void OnRaiseEvent(Event e, Guid opGroupId, EventInfo eventInfo); + + /// + /// Notifies the machine that it is waiting to receive an event of one of the specified types. + /// + void OnWaitEvent(IEnumerable eventTypes); + + /// + /// Notifies the machine that an event it was waiting to receive has been enqueued. + /// + void OnReceiveEvent(Event e, Guid opGroupId, EventInfo eventInfo); + + /// + /// Notifies the machine that an event it was waiting to receive was already in the + /// event queue when the machine invoked the receive statement. + /// + void OnReceiveEventWithoutWaiting(Event e, Guid opGroupId, EventInfo eventInfo); + + /// + /// Notifies the machine that an event has been dropped. + /// + void OnDropEvent(Event e, Guid opGroupId, EventInfo eventInfo); + + /// + /// Asserts if the specified condition holds. + /// + void Assert(bool predicate, string s, object arg0); + + /// + /// Asserts if the specified condition holds. + /// + void Assert(bool predicate, string s, object arg0, object arg1); + + /// + /// Asserts if the specified condition holds. + /// + void Assert(bool predicate, string s, object arg0, object arg1, object arg2); + + /// + /// Asserts if the specified condition holds. + /// + void Assert(bool predicate, string s, params object[] args); + } +} diff --git a/Source/Core/Machines/Machine.cs b/Source/Core/Machines/Machine.cs new file mode 100644 index 000000000..557646f4a --- /dev/null +++ b/Source/Core/Machines/Machine.cs @@ -0,0 +1,1748 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Utilities; + +using EventInfo = Microsoft.Coyote.Runtime.EventInfo; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Implements an asynchronous communicating state machine. Inherit from this class + /// to declare states, state transitions and event handlers. + /// + public abstract class Machine : AsyncMachine + { + /// + /// Map from machine types to a set of all possible states types. + /// + private static readonly ConcurrentDictionary> StateTypeMap = + new ConcurrentDictionary>(); + + /// + /// Map from machine types to a set of all available states. + /// + private static readonly ConcurrentDictionary> StateMap = + new ConcurrentDictionary>(); + + /// + /// Map from machine types to a set of all available actions. + /// + private static readonly ConcurrentDictionary> MachineActionMap = + new ConcurrentDictionary>(); + + /// + /// Checks if the machine state is cached. + /// + private static readonly ConcurrentDictionary MachineStateCached = + new ConcurrentDictionary(); + + /// + /// Manages the state of the machine. + /// + internal IMachineStateManager StateManager { get; private set; } + + /// + /// A stack of machine states. The state on the top of + /// the stack represents the current state. + /// + private readonly Stack StateStack; + + /// + /// A stack of maps that determine event handling action for + /// each event type. These maps do not keep transition handlers. + /// This stack has always the same height as StateStack. + /// + private readonly Stack> ActionHandlerStack; + + /// + /// Dictionary containing all the current goto state transitions. + /// + internal Dictionary GotoTransitions; + + /// + /// Dictionary containing all the current push state transitions. + /// + internal Dictionary PushTransitions; + + /// + /// Map from action names to cached delegates. + /// + private readonly Dictionary ActionMap; + + /// + /// The inbox of the machine. Incoming events are enqueued here. + /// Events are dequeued to be processed. + /// + private IEventQueue Inbox; + + /// + /// Map that contains the active timers. + /// + private readonly Dictionary Timers; + + /// + /// Is the machine halted. + /// + internal volatile bool IsHalted; + + /// + /// Is pop invoked in the current action. + /// + private bool IsPopInvoked; + + /// + /// User OnException asked for the machine to be gracefully halted + /// (suppressing the exception) + /// + private bool OnExceptionRequestedGracefulHalt; + + /// + /// Gets the of the current state. + /// + protected internal Type CurrentState + { + get + { + if (this.StateStack.Count == 0) + { + return null; + } + + return this.StateStack.Peek().GetType(); + } + } + + /// + /// Gets the current action handler map. + /// + private Dictionary CurrentActionHandlerMap + { + get + { + if (this.ActionHandlerStack.Count == 0) + { + return null; + } + + return this.ActionHandlerStack.Peek(); + } + } + + /// + /// Gets the name of the current state. + /// + internal string CurrentStateName => NameResolver.GetQualifiedStateName(this.CurrentState); + + /// + /// Gets the latest received , or null if + /// no has been received. + /// + protected internal Event ReceivedEvent { get; private set; } + + /// + /// Id used to identify subsequent operations performed by this machine. This value is + /// initially either or the specified upon + /// machine creation. This value is automatically set to the operation group id of the + /// last dequeue, raise or receive operation, if it is not . + /// This value can also be manually set using the property. + /// + protected internal override Guid OperationGroupId + { + get => this.StateManager.OperationGroupId; + + set + { + this.StateManager.OperationGroupId = value; + } + } + + /// + /// User-defined hashed state of the machine. Override to improve the + /// accuracy of liveness checking when state-caching is enabled. + /// + protected virtual int HashedState => 0; + + /// + /// Initializes a new instance of the class. + /// + protected Machine() + { + this.StateStack = new Stack(); + this.ActionHandlerStack = new Stack>(); + this.ActionMap = new Dictionary(); + this.Timers = new Dictionary(); + this.IsHalted = false; + this.IsPopInvoked = false; + this.OnExceptionRequestedGracefulHalt = false; + } + + /// + /// Initializes this machine. + /// + internal void Initialize(CoyoteRuntime runtime, MachineId mid, IMachineStateManager stateManager, IEventQueue inbox) + { + this.Initialize(runtime, mid); + this.StateManager = stateManager; + this.Inbox = inbox; + } + + /// + /// Creates a new machine of the specified type and with the specified + /// optional . This can only be + /// used to access its payload, and cannot be handled. + /// + /// Type of the machine. + /// Optional initialization event. + /// Optional id that can be used to identify this operation. + /// The unique machine id. + protected MachineId CreateMachine(Type type, Event e = null, Guid opGroupId = default) => + this.Runtime.CreateMachine(null, type, null, e, this, opGroupId); + + /// + /// Creates a new machine of the specified type and name, and with the + /// specified optional . This can + /// only be used to access its payload, and cannot be handled. + /// + /// Type of the machine. + /// Optional friendly machine name used for logging. + /// Optional initialization event. + /// Optional id that can be used to identify this operation. + /// The unique machine id. + protected MachineId CreateMachine(Type type, string friendlyName, Event e = null, Guid opGroupId = default) => + this.Runtime.CreateMachine(null, type, friendlyName, e, this, opGroupId); + + /// + /// Creates a new machine of the specified and name, using the specified + /// unbound machine id, and passes the specified optional . This event + /// can only be used to access its payload, and cannot be handled. + /// + /// Unbound machine id. + /// Type of the machine. + /// Optional friendly machine name used for logging. + /// Optional initialization event. + /// Optional id that can be used to identify this operation. + protected void CreateMachine(MachineId mid, Type type, string friendlyName, Event e = null, Guid opGroupId = default) => + this.Runtime.CreateMachine(mid, type, friendlyName, e, this, opGroupId); + + /// + /// Sends an asynchronous to a machine. + /// + /// The id of the target machine. + /// The event to send. + /// Optional id that can be used to identify this operation. + /// Optional configuration of a send operation. + protected void Send(MachineId mid, Event e, Guid opGroupId = default, SendOptions options = null) => + this.Runtime.SendEvent(mid, e, this, opGroupId, options); + + /// + /// Raises an internally at the end of the current action. + /// + /// The event to raise. + /// Optional id that can be used to identify this operation. + protected void Raise(Event e, Guid opGroupId = default) + { + this.Assert(!this.IsHalted, "Machine '{0}' invoked Raise while halted.", this.Id); + this.Assert(e != null, "Machine '{0}' is raising a null event.", this.Id); + + // The operation group id of this operation is set using the following precedence: + // (1) To the specified raise operation group id, if it is non-empty. + // (2) To the operation group id of this machine. + this.Inbox.Raise(e, opGroupId != Guid.Empty ? opGroupId : this.OperationGroupId); + } + + /// + /// Waits to receive an of the specified type + /// that satisfies an optional predicate. + /// + /// The event type. + /// The optional predicate. + /// The received event. + protected internal Task Receive(Type eventType, Func predicate = null) + { + this.Assert(!this.IsHalted, "Machine '{0}' invoked Receive while halted.", this.Id); + this.Runtime.NotifyReceiveCalled(this); + return this.Inbox.ReceiveAsync(eventType, predicate); + } + + /// + /// Waits to receive an of the specified types. + /// + /// The event types to wait for. + /// The received event. + protected internal Task Receive(params Type[] eventTypes) + { + this.Assert(!this.IsHalted, "Machine '{0}' invoked Receive while halted.", this.Id); + this.Runtime.NotifyReceiveCalled(this); + return this.Inbox.ReceiveAsync(eventTypes); + } + + /// + /// Waits to receive an of the specified types + /// that satisfy the specified predicates. + /// + /// Event types and predicates. + /// The received event. + protected internal Task Receive(params Tuple>[] events) + { + this.Assert(!this.IsHalted, "Machine '{0}' invoked Receive while halted.", this.Id); + this.Runtime.NotifyReceiveCalled(this); + return this.Inbox.ReceiveAsync(events); + } + + /// + /// Transitions the machine to the specified + /// at the end of the current action. + /// + /// Type of the state. + protected void Goto() + where S : MachineState + { +#pragma warning disable 618 + this.Goto(typeof(S)); +#pragma warning restore 618 + } + + /// + /// Transitions the machine to the specified + /// at the end of the current action. + /// + /// Type of the state. + [Obsolete("Goto(typeof(T)) is deprecated; use Goto() instead.")] + protected void Goto(Type s) + { + this.Assert(!this.IsHalted, "Machine '{0}' invoked Goto while halted.", this.Id); + this.Assert(StateTypeMap[this.GetType()].Any(val => val.DeclaringType.Equals(s.DeclaringType) && val.Name.Equals(s.Name)), + "Machine '{0}' is trying to transition to non-existing state '{1}'.", this.Id, s.Name); + this.Raise(new GotoStateEvent(s)); + } + + /// + /// Transitions the machine to the specified + /// at the end of the current action, pushing current state on the stack. + /// + /// Type of the state. + protected void Push() + where S : MachineState + { +#pragma warning disable 618 + this.Push(typeof(S)); +#pragma warning restore 618 + } + + /// + /// Transitions the machine to the specified + /// at the end of the current action, pushing current state on the stack. + /// + /// Type of the state. + [Obsolete("Push(typeof(T)) is deprecated; use Push() instead.")] + protected void Push(Type s) + { + this.Assert(!this.IsHalted, "Machine '{0}' invoked Push while halted.", this.Id); + this.Assert(StateTypeMap[this.GetType()].Any(val => val.DeclaringType.Equals(s.DeclaringType) && val.Name.Equals(s.Name)), + "Machine '{0}' is trying to transition to non-existing state '{1}'.", this.Id, s.Name); + this.Raise(new PushStateEvent(s)); + } + + /// + /// Pops the current from the state stack + /// at the end of the current action. + /// + protected void Pop() + { + this.Runtime.NotifyPop(this); + this.IsPopInvoked = true; + } + + /// + /// Starts a timer that sends a to this machine after the + /// specified due time. The timer accepts an optional payload to be used during timeout. + /// The timer is automatically disposed after it timeouts. To manually stop and dispose + /// the timer, invoke the method. + /// + /// The amount of time to wait before sending the first timeout event. + /// Optional payload of the timeout event. + /// Handle that contains information about the timer. + protected TimerInfo StartTimer(TimeSpan dueTime, object payload = null) + { + // The specified due time and period must be valid. + this.Assert(dueTime.TotalMilliseconds >= 0, "Machine '{0}' registered a timer with a negative due time.", this.Id); + return this.RegisterTimer(dueTime, Timeout.InfiniteTimeSpan, payload); + } + + /// + /// Starts a periodic timer that sends a to this machine + /// after the specified due time, and then repeats after each specified period. The timer + /// accepts an optional payload to be used during timeout. The timer can be stopped by + /// invoking the method. + /// + /// The amount of time to wait before sending the first timeout event. + /// The time interval between timeout events. + /// Optional payload of the timeout event. + /// Handle that contains information about the timer. + protected TimerInfo StartPeriodicTimer(TimeSpan dueTime, TimeSpan period, object payload = null) + { + // The specified due time and period must be valid. + this.Assert(dueTime.TotalMilliseconds >= 0, "Machine '{0}' registered a periodic timer with a negative due time.", this.Id); + this.Assert(period.TotalMilliseconds >= 0, "Machine '{0}' registered a periodic timer with a negative period.", this.Id); + return this.RegisterTimer(dueTime, period, payload); + } + + /// + /// Stops and disposes the specified timer. + /// + /// Handle that contains information about the timer. + protected void StopTimer(TimerInfo info) + { + this.Assert(info.OwnerId == this.Id, "Machine '{0}' is not allowed to dispose timer '{1}', which is owned by machine '{2}'.", + this.Id, info, info.OwnerId); + this.UnregisterTimer(info); + } + + /// + /// Returns a nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + /// The controlled nondeterministic choice. + protected bool Random() + { + return this.Runtime.GetNondeterministicBooleanChoice(this, 2); + } + + /// + /// Returns a nondeterministic boolean choice, that can be + /// controlled during analysis or testing. The value is used + /// to generate a number in the range [0..maxValue), where 0 + /// triggers true. + /// + /// The max value. + /// The controlled nondeterministic choice. + protected bool Random(int maxValue) + { + return this.Runtime.GetNondeterministicBooleanChoice(this, maxValue); + } + + /// + /// Returns a fair nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + /// The controlled nondeterministic choice. + protected bool FairRandom( + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + var havocId = string.Format(CultureInfo.InvariantCulture, "{0}_{1}_{2}_{3}_{4}", + this.Id.Name, this.CurrentStateName, callerMemberName, callerFilePath, callerLineNumber.ToString()); + return this.Runtime.GetFairNondeterministicBooleanChoice(this, havocId); + } + + /// + /// Returns a nondeterministic integer, that can be controlled during + /// analysis or testing. The value is used to generate an integer in + /// the range [0..maxValue). + /// + /// The max value. + /// The controlled nondeterministic integer. + protected int RandomInteger(int maxValue) + { + return this.Runtime.GetNondeterministicIntegerChoice(this, maxValue); + } + + /// + /// Invokes the specified monitor with the specified . + /// + /// Type of the monitor. + /// The event to send. + protected void Monitor(Event e) + { + this.Monitor(typeof(T), e); + } + + /// + /// Invokes the specified monitor with the specified event. + /// + /// Type of the monitor. + /// The event to send. + protected void Monitor(Type type, Event e) + { + this.Assert(e != null, "Machine '{0}' is sending a null event.", this.Id); + this.Runtime.Monitor(type, this, e); + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + protected void Assert(bool predicate) + { + this.Runtime.Assert(predicate); + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + protected void Assert(bool predicate, string s, object arg0) + { + this.Runtime.Assert(predicate, s, arg0); + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + protected void Assert(bool predicate, string s, object arg0, object arg1) + { + this.Runtime.Assert(predicate, s, arg0, arg1); + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + protected void Assert(bool predicate, string s, object arg0, object arg1, object arg2) + { + this.Runtime.Assert(predicate, s, arg0, arg1, arg2); + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + protected void Assert(bool predicate, string s, params object[] args) + { + this.Runtime.Assert(predicate, s, args); + } + + /// + /// Enqueues the specified event and its metadata. + /// + internal EnqueueStatus Enqueue(Event e, Guid opGroupId, EventInfo info) + { + if (this.IsHalted) + { + return EnqueueStatus.Dropped; + } + + return this.Inbox.Enqueue(e, opGroupId, info); + } + + /// + /// Runs the event handler. The handler terminates if there + /// is no next event to process or if the machine is halted. + /// + internal async Task RunEventHandlerAsync() + { + if (this.IsHalted) + { + return; + } + + Event lastDequeuedEvent = null; + while (!this.IsHalted && this.Runtime.IsRunning) + { + (DequeueStatus status, Event e, Guid opGroupId, EventInfo info) = this.Inbox.Dequeue(); + if (opGroupId != Guid.Empty) + { + // Inherit the operation group id of the dequeue or raise operation, if it is non-empty. + this.StateManager.OperationGroupId = opGroupId; + } + + if (status is DequeueStatus.Success) + { + // Notify the runtime for a new event to handle. This is only used + // during bug-finding and operation bounding, because the runtime + // has to schedule a machine when a new operation is dequeued. + this.Runtime.NotifyDequeuedEvent(this, e, info); + } + else if (status is DequeueStatus.Raised) + { + this.Runtime.NotifyHandleRaisedEvent(this, e); + } + else if (status is DequeueStatus.Default) + { + this.Runtime.LogWriter.OnDefault(this.Id, this.CurrentStateName); + + // If the default event was handled, then notify the runtime. + // This is only used during bug-finding, because the runtime + // has to schedule a machine between default handlers. + this.Runtime.NotifyDefaultHandlerFired(this); + } + else if (status is DequeueStatus.NotAvailable) + { + break; + } + + // Assigns the received event. + this.ReceivedEvent = e; + + if (status is DequeueStatus.Success) + { + // Inform the user of a successful dequeue once ReceivedEvent is set. + lastDequeuedEvent = e; + await this.ExecuteUserCallbackAsync(EventHandlerStatus.EventDequeued, lastDequeuedEvent); + } + + if (e is TimerElapsedEvent timeoutEvent && + timeoutEvent.Info.Period.TotalMilliseconds < 0) + { + // If the timer is not periodic, then dispose it. + this.UnregisterTimer(timeoutEvent.Info); + } + + // Handles next event. + if (!this.IsHalted) + { + await this.HandleEvent(e); + } + + if (!this.Inbox.IsEventRaised && lastDequeuedEvent != null && !this.IsHalted) + { + // Inform the user that the machine is done handling the current event. + // The machine will either go idle or dequeue its next event. + await this.ExecuteUserCallbackAsync(EventHandlerStatus.EventHandled, lastDequeuedEvent); + lastDequeuedEvent = null; + } + } + } + + /// + /// Handles the specified . + /// + private async Task HandleEvent(Event e) + { + Type currentState = this.CurrentState; + + while (true) + { + if (this.CurrentState is null) + { + // If the stack of states is empty and the event + // is halt, then terminate the machine. + if (e.GetType().Equals(typeof(Halt))) + { + this.HaltMachine(); + return; + } + + string currentStateName = NameResolver.GetQualifiedStateName(currentState); + await this.ExecuteUserCallbackAsync(EventHandlerStatus.EventUnhandled, e, currentStateName); + if (this.IsHalted) + { + // Invoking a user callback caused the machine to halt. + return; + } + + var unhandledEx = new UnhandledEventException(currentStateName, e, "Unhandled Event"); + if (this.OnUnhandledEventExceptionHandler(nameof(this.HandleEvent), unhandledEx)) + { + this.HaltMachine(); + return; + } + else + { + // If the event cannot be handled then report an error and exit. + this.Assert(false, "Machine '{0}' received event '{1}' that cannot be handled.", + this.Id, e.GetType().FullName); + } + } + + if (e.GetType() == typeof(GotoStateEvent)) + { + // Checks if the event is a goto state event. + Type targetState = (e as GotoStateEvent).State; + await this.GotoState(targetState, null); + } + else if (e.GetType() == typeof(PushStateEvent)) + { + // Checks if the event is a push state event. + Type targetState = (e as PushStateEvent).State; + await this.PushState(targetState); + } + else if (this.GotoTransitions.ContainsKey(e.GetType())) + { + // Checks if the event can trigger a goto state transition. + var transition = this.GotoTransitions[e.GetType()]; + await this.GotoState(transition.TargetState, transition.Lambda); + } + else if (this.GotoTransitions.ContainsKey(typeof(WildCardEvent))) + { + var transition = this.GotoTransitions[typeof(WildCardEvent)]; + await this.GotoState(transition.TargetState, transition.Lambda); + } + else if (this.PushTransitions.ContainsKey(e.GetType())) + { + // Checks if the event can trigger a push state transition. + Type targetState = this.PushTransitions[e.GetType()].TargetState; + await this.PushState(targetState); + } + else if (this.PushTransitions.ContainsKey(typeof(WildCardEvent))) + { + Type targetState = this.PushTransitions[typeof(WildCardEvent)].TargetState; + await this.PushState(targetState); + } + else if (this.CurrentActionHandlerMap.ContainsKey(e.GetType()) && + this.CurrentActionHandlerMap[e.GetType()] is ActionBinding) + { + // Checks if the event can trigger an action. + var handler = this.CurrentActionHandlerMap[e.GetType()] as ActionBinding; + await this.Do(handler.Name); + } + else if (this.CurrentActionHandlerMap.ContainsKey(typeof(WildCardEvent)) + && this.CurrentActionHandlerMap[typeof(WildCardEvent)] is ActionBinding) + { + var handler = this.CurrentActionHandlerMap[typeof(WildCardEvent)] as ActionBinding; + await this.Do(handler.Name); + } + else + { + // If the current state cannot handle the event. + await this.ExecuteCurrentStateOnExit(null); + if (this.IsHalted) + { + return; + } + + this.DoStatePop(); + this.Runtime.LogWriter.OnPopUnhandledEvent(this.Id, this.CurrentStateName, e.GetType().FullName); + continue; + } + + break; + } + } + + /// + /// Invokes an action. + /// + private async Task Do(string actionName) + { + CachedDelegate cachedAction = this.ActionMap[actionName]; + this.Runtime.NotifyInvokedAction(this, cachedAction.MethodInfo, this.ReceivedEvent); + await this.ExecuteAction(cachedAction); + this.Runtime.NotifyCompletedAction(this, cachedAction.MethodInfo, this.ReceivedEvent); + + if (this.IsPopInvoked) + { + // Performs the state transition, if pop was invoked during the action. + await this.PopState(); + } + } + + /// + /// Executes the on entry action of the current state. + /// + private async Task ExecuteCurrentStateOnEntry() + { + this.Runtime.NotifyEnteredState(this); + + CachedDelegate entryAction = null; + if (this.StateStack.Peek().EntryAction != null) + { + entryAction = this.ActionMap[this.StateStack.Peek().EntryAction]; + } + + // Invokes the entry action of the new state, + // if there is one available. + if (entryAction != null) + { + this.Runtime.NotifyInvokedOnEntryAction(this, entryAction.MethodInfo, this.ReceivedEvent); + await this.ExecuteAction(entryAction); + this.Runtime.NotifyCompletedOnEntryAction(this, entryAction.MethodInfo, this.ReceivedEvent); + } + + if (this.IsPopInvoked) + { + // Performs the state transition, if pop was invoked during the action. + await this.PopState(); + } + } + + /// + /// Executes the on exit action of the current state. + /// + /// Action name + private async Task ExecuteCurrentStateOnExit(string eventHandlerExitActionName) + { + this.Runtime.NotifyExitedState(this); + + CachedDelegate exitAction = null; + if (this.StateStack.Peek().ExitAction != null) + { + exitAction = this.ActionMap[this.StateStack.Peek().ExitAction]; + } + + // Invokes the exit action of the current state, + // if there is one available. + if (exitAction != null) + { + this.Runtime.NotifyInvokedOnExitAction(this, exitAction.MethodInfo, this.ReceivedEvent); + await this.ExecuteAction(exitAction); + this.Runtime.NotifyCompletedOnExitAction(this, exitAction.MethodInfo, this.ReceivedEvent); + } + + // Invokes the exit action of the event handler, + // if there is one available. + if (eventHandlerExitActionName != null) + { + CachedDelegate eventHandlerExitAction = this.ActionMap[eventHandlerExitActionName]; + this.Runtime.NotifyInvokedOnExitAction(this, eventHandlerExitAction.MethodInfo, this.ReceivedEvent); + await this.ExecuteAction(eventHandlerExitAction); + this.Runtime.NotifyCompletedOnExitAction(this, eventHandlerExitAction.MethodInfo, this.ReceivedEvent); + } + } + + /// + /// An exception filter that calls , + /// which can choose to fast-fail the app to get a full dump. + /// + /// The machine action being executed when the failure occurred. + /// The exception being tested. + private bool InvokeOnFailureExceptionFilter(CachedDelegate action, Exception ex) + { + // This is called within the exception filter so the stack has not yet been unwound. + // If OnFailure does not fail-fast, return false to process the exception normally. + this.Runtime.RaiseOnFailureEvent(new MachineActionExceptionFilterException(action.MethodInfo.Name, ex)); + return false; + } + + /// + /// Executes the specified action. + /// + private async Task ExecuteAction(CachedDelegate cachedAction) + { + try + { + if (cachedAction.Handler is Action action) + { + // Use an exception filter to call OnFailure before the stack has been unwound. + try + { + action(); + } + catch (Exception ex) when (this.OnExceptionHandler(cachedAction.MethodInfo.Name, ex)) + { + // User handled the exception, return normally. + } + catch (Exception ex) when (!this.OnExceptionRequestedGracefulHalt && this.InvokeOnFailureExceptionFilter(cachedAction, ex)) + { + // If InvokeOnFailureExceptionFilter does not fail-fast, it returns + // false to process the exception normally. + } + } + else if (cachedAction.Handler is Func taskFunc) + { + try + { + // We have no reliable stack for awaited operations. + Task task = taskFunc(); + this.Runtime.NotifyWaitTask(this, task); + await task; + } + catch (Exception ex) when (this.OnExceptionHandler(cachedAction.MethodInfo.Name, ex)) + { + // User handled the exception, return normally. + } + } + } + catch (Exception ex) + { + Exception innerException = ex; + while (innerException is TargetInvocationException) + { + innerException = innerException.InnerException; + } + + if (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + if (innerException is ExecutionCanceledException) + { + this.IsHalted = true; + Debug.WriteLine($" ExecutionCanceledException was thrown from machine '{this.Id}'."); + } + else if (innerException is TaskSchedulerException) + { + this.IsHalted = true; + Debug.WriteLine($" TaskSchedulerException was thrown from machine '{this.Id}'."); + } + else if (this.OnExceptionRequestedGracefulHalt) + { + // Gracefully halt. + this.HaltMachine(); + } + else + { + // Reports the unhandled exception. + this.ReportUnhandledException(innerException, cachedAction.MethodInfo.Name); + } + } + } + + /// + /// Executes the specified event handler user callback. + /// + private Task ExecuteUserCallbackAsync(EventHandlerStatus eventHandlerStatus, Event lastDequeuedEvent, string currentState = default) + { + try + { + if (eventHandlerStatus is EventHandlerStatus.EventDequeued) + { + try + { + return this.OnEventDequeueAsync(lastDequeuedEvent); + } + catch (Exception ex) when (this.OnExceptionHandler(nameof(this.OnEventDequeueAsync), ex)) + { + // User handled the exception, return normally. + } + } + else if (eventHandlerStatus is EventHandlerStatus.EventHandled) + { + try + { + return this.OnEventHandledAsync(lastDequeuedEvent); + } + catch (Exception ex) when (this.OnExceptionHandler(nameof(this.OnEventHandledAsync), ex)) + { + // User handled the exception, return normally. + } + } + else if (eventHandlerStatus is EventHandlerStatus.EventUnhandled) + { + try + { + return this.OnEventUnhandledAsync(lastDequeuedEvent, currentState); + } + catch (Exception ex) when (this.OnExceptionHandler(nameof(this.OnEventUnhandledAsync), ex)) + { + // User handled the exception, return normally. + } + } + } + catch (Exception ex) + { + Exception innerException = ex; + while (innerException is TargetInvocationException) + { + innerException = innerException.InnerException; + } + + if (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + if (innerException is ExecutionCanceledException) + { + this.IsHalted = true; + Debug.WriteLine($" ExecutionCanceledException was thrown from machine '{this.Id}'."); + } + else if (innerException is TaskSchedulerException) + { + this.IsHalted = true; + Debug.WriteLine($" TaskSchedulerException was thrown from machine '{this.Id}'."); + } + else if (this.OnExceptionRequestedGracefulHalt) + { + // Gracefully halt. + this.HaltMachine(); + } + else + { + // Reports the unhandled exception. + if (eventHandlerStatus is EventHandlerStatus.EventDequeued) + { + this.ReportUnhandledException(innerException, nameof(this.OnEventDequeueAsync)); + } + else if (eventHandlerStatus is EventHandlerStatus.EventHandled) + { + this.ReportUnhandledException(innerException, nameof(this.OnEventHandledAsync)); + } + else if (eventHandlerStatus is EventHandlerStatus.EventUnhandled) + { + this.ReportUnhandledException(innerException, nameof(this.OnEventUnhandledAsync)); + } + } + } + + return Task.CompletedTask; + } + + /// + /// Performs a goto transition to the specified state. + /// + private async Task GotoState(Type s, string onExitActionName) + { + this.Runtime.LogWriter.OnGoto(this.Id, this.CurrentStateName, + $"{s.DeclaringType}.{NameResolver.GetStateNameForLogging(s)}"); + + // The machine performs the on exit action of the current state. + await this.ExecuteCurrentStateOnExit(onExitActionName); + if (this.IsHalted) + { + return; + } + + this.DoStatePop(); + + var nextState = StateMap[this.GetType()].First(val + => val.GetType().Equals(s)); + + // The machine transitions to the new state. + this.DoStatePush(nextState); + + // The machine performs the on entry action of the new state. + await this.ExecuteCurrentStateOnEntry(); + } + + /// + /// Performs a push transition to the specified state. + /// + private async Task PushState(Type s) + { + this.Runtime.LogWriter.OnPush(this.Id, this.CurrentStateName, s.FullName); + + var nextState = StateMap[this.GetType()].First(val => val.GetType().Equals(s)); + this.DoStatePush(nextState); + + // The machine performs the on entry statements of the new state. + await this.ExecuteCurrentStateOnEntry(); + } + + /// + /// Performs a pop transition from the current state. + /// + private async Task PopState() + { + this.IsPopInvoked = false; + var prevStateName = this.CurrentStateName; + + // The machine performs the on exit action of the current state. + await this.ExecuteCurrentStateOnExit(null); + if (this.IsHalted) + { + return; + } + + this.DoStatePop(); + this.Runtime.LogWriter.OnPop(this.Id, prevStateName, this.CurrentStateName); + + // Watch out for an extra pop. + this.Assert(this.CurrentState != null, "Machine '{0}' popped with no matching push.", this.Id); + } + + /// + /// Configures the state transitions of the machine when a state is pushed into the stack. + /// + private void DoStatePush(MachineState state) + { + this.GotoTransitions = state.GotoTransitions; + this.PushTransitions = state.PushTransitions; + + // Gets existing map for actions. + var eventHandlerMap = this.CurrentActionHandlerMap is null ? + new Dictionary() : + new Dictionary(this.CurrentActionHandlerMap); + + // Updates the map with defer annotations. + foreach (var deferredEvent in state.DeferredEvents) + { + if (deferredEvent.Equals(typeof(WildCardEvent))) + { + eventHandlerMap.Clear(); + eventHandlerMap[deferredEvent] = new DeferAction(); + break; + } + + eventHandlerMap[deferredEvent] = new DeferAction(); + } + + // Updates the map with actions. + foreach (var actionBinding in state.ActionBindings) + { + if (actionBinding.Key.Equals(typeof(WildCardEvent))) + { + eventHandlerMap.Clear(); + eventHandlerMap[actionBinding.Key] = actionBinding.Value; + break; + } + + eventHandlerMap[actionBinding.Key] = actionBinding.Value; + } + + // Updates the map with ignores. + foreach (var ignoredEvent in state.IgnoredEvents) + { + if (ignoredEvent.Equals(typeof(WildCardEvent))) + { + eventHandlerMap.Clear(); + eventHandlerMap[ignoredEvent] = new IgnoreAction(); + break; + } + + eventHandlerMap[ignoredEvent] = new IgnoreAction(); + } + + // Removes the events on which push transitions are defined. + foreach (var eventType in this.PushTransitions.Keys) + { + if (eventType.Equals(typeof(WildCardEvent))) + { + eventHandlerMap.Clear(); + break; + } + + eventHandlerMap.Remove(eventType); + } + + // Removes the events on which goto transitions are defined. + foreach (var eventType in this.GotoTransitions.Keys) + { + if (eventType.Equals(typeof(WildCardEvent))) + { + eventHandlerMap.Clear(); + break; + } + + eventHandlerMap.Remove(eventType); + } + + this.StateStack.Push(state); + this.ActionHandlerStack.Push(eventHandlerMap); + } + + /// + /// Configures the state transitions of the machine + /// when a state is popped. + /// + private void DoStatePop() + { + this.StateStack.Pop(); + this.ActionHandlerStack.Pop(); + + if (this.StateStack.Count > 0) + { + this.GotoTransitions = this.StateStack.Peek().GotoTransitions; + this.PushTransitions = this.StateStack.Peek().PushTransitions; + } + else + { + this.GotoTransitions = null; + this.PushTransitions = null; + } + } + + /// + /// Checks if the specified event is ignored in the current machine state. + /// + internal bool IsEventIgnoredInCurrentState(Event e) + { + if (e is TimerElapsedEvent timeoutEvent && !this.Timers.ContainsKey(timeoutEvent.Info)) + { + // The timer that created this timeout event is not active. + return true; + } + + Type eventType = e.GetType(); + + if (eventType.IsGenericType) + { + var genericTypeDefinition = eventType.GetGenericTypeDefinition(); + foreach (var kvp in this.CurrentActionHandlerMap) + { + if (!(kvp.Value is IgnoreAction)) + { + continue; + } + + // TODO: make sure this logic and/or simplify. + if (kvp.Key.IsGenericType && kvp.Key.GetGenericTypeDefinition().Equals( + genericTypeDefinition.GetGenericTypeDefinition())) + { + return true; + } + } + } + + // If a transition is defined, then the event is not ignored. + if (this.GotoTransitions.ContainsKey(eventType) || + this.PushTransitions.ContainsKey(eventType) || + this.GotoTransitions.ContainsKey(typeof(WildCardEvent)) || + this.PushTransitions.ContainsKey(typeof(WildCardEvent))) + { + return false; + } + + if (this.CurrentActionHandlerMap.ContainsKey(eventType)) + { + return this.CurrentActionHandlerMap[eventType] is IgnoreAction; + } + + if (this.CurrentActionHandlerMap.ContainsKey(typeof(WildCardEvent)) && + this.CurrentActionHandlerMap[typeof(WildCardEvent)] is IgnoreAction) + { + return true; + } + + return false; + } + + /// + /// Checks if the specified event is deferred in the current machine state. + /// + internal bool IsEventDeferredInCurrentState(Event e) + { + Type eventType = e.GetType(); + + // If a transition is defined, then the event is not deferred. + if (this.GotoTransitions.ContainsKey(eventType) || this.PushTransitions.ContainsKey(eventType) || + this.GotoTransitions.ContainsKey(typeof(WildCardEvent)) || + this.PushTransitions.ContainsKey(typeof(WildCardEvent))) + { + return false; + } + + if (this.CurrentActionHandlerMap.ContainsKey(eventType)) + { + return this.CurrentActionHandlerMap[eventType] is DeferAction; + } + + if (this.CurrentActionHandlerMap.ContainsKey(typeof(WildCardEvent)) && + this.CurrentActionHandlerMap[typeof(WildCardEvent)] is DeferAction) + { + return true; + } + + return false; + } + + /// + /// Checks if a default handler is installed in current state. + /// + internal bool IsDefaultHandlerInstalledInCurrentState() => + this.CurrentActionHandlerMap.ContainsKey(typeof(Default)) || + this.GotoTransitions.ContainsKey(typeof(Default)) || + this.PushTransitions.ContainsKey(typeof(Default)); + + /// + /// Returns the cached state of this machine. + /// + internal override int GetCachedState() + { + unchecked + { + var hash = 19; + hash = (hash * 31) + this.GetType().GetHashCode(); + hash = (hash * 31) + this.Id.Value.GetHashCode(); + hash = (hash * 31) + this.IsHalted.GetHashCode(); + + hash = (hash * 31) + this.StateManager.GetCachedState(); + + foreach (var state in this.StateStack) + { + hash = (hash * 31) + state.GetType().GetHashCode(); + } + + hash = (hash * 31) + this.Inbox.GetCachedState(); + + if (this.Runtime.Configuration.EnableUserDefinedStateHashing) + { + // Adds the user-defined hashed machine state. + hash = (hash * 31) + this.HashedState; + } + + return hash; + } + } + + /// + /// Transitions to the start state, and executes the + /// entry action, if there is any. + /// + internal Task GotoStartState(Event e) + { + this.ReceivedEvent = e; + return this.ExecuteCurrentStateOnEntry(); + } + + /// + /// Initializes information about the states of the machine. + /// + internal void InitializeStateInformation() + { + Type machineType = this.GetType(); + + if (MachineStateCached.TryAdd(machineType, false)) + { + // Caches the available state types for this machine type. + if (StateTypeMap.TryAdd(machineType, new HashSet())) + { + Type baseType = machineType; + while (baseType != typeof(Machine)) + { + foreach (var s in baseType.GetNestedTypes(BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.DeclaredOnly)) + { + this.ExtractStateTypes(s); + } + + baseType = baseType.BaseType; + } + } + + // Caches the available state instances for this machine type. + if (StateMap.TryAdd(machineType, new HashSet())) + { + foreach (var type in StateTypeMap[machineType]) + { + Type stateType = type; + if (type.IsAbstract) + { + continue; + } + + if (type.IsGenericType) + { + // If the state type is generic (only possible if inherited by a + // generic machine declaration), then iterate through the base + // machine classes to identify the runtime generic type, and use + // it to instantiate the runtime state type. This type can be + // then used to create the state constructor. + Type declaringType = this.GetType(); + while (!declaringType.IsGenericType || + !type.DeclaringType.FullName.Equals(declaringType.FullName.Substring( + 0, declaringType.FullName.IndexOf('[')))) + { + declaringType = declaringType.BaseType; + } + + if (declaringType.IsGenericType) + { + stateType = type.MakeGenericType(declaringType.GetGenericArguments()); + } + } + + ConstructorInfo constructor = stateType.GetConstructor(Type.EmptyTypes); + var lambda = Expression.Lambda>( + Expression.New(constructor)).Compile(); + MachineState state = lambda(); + + try + { + state.InitializeState(); + } + catch (InvalidOperationException ex) + { + this.Assert(false, "Machine '{0}' {1} in state '{2}'.", this.Id, ex.Message, state); + } + + StateMap[machineType].Add(state); + } + } + + // Caches the actions declarations for this machine type. + if (MachineActionMap.TryAdd(machineType, new Dictionary())) + { + foreach (var state in StateMap[machineType]) + { + if (state.EntryAction != null && + !MachineActionMap[machineType].ContainsKey(state.EntryAction)) + { + MachineActionMap[machineType].Add( + state.EntryAction, + this.GetActionWithName(state.EntryAction)); + } + + if (state.ExitAction != null && + !MachineActionMap[machineType].ContainsKey(state.ExitAction)) + { + MachineActionMap[machineType].Add( + state.ExitAction, + this.GetActionWithName(state.ExitAction)); + } + + foreach (var transition in state.GotoTransitions) + { + if (transition.Value.Lambda != null && + !MachineActionMap[machineType].ContainsKey(transition.Value.Lambda)) + { + MachineActionMap[machineType].Add( + transition.Value.Lambda, + this.GetActionWithName(transition.Value.Lambda)); + } + } + + foreach (var action in state.ActionBindings) + { + if (!MachineActionMap[machineType].ContainsKey(action.Value.Name)) + { + MachineActionMap[machineType].Add( + action.Value.Name, + this.GetActionWithName(action.Value.Name)); + } + } + } + } + + // Cache completed. + lock (MachineStateCached) + { + MachineStateCached[machineType] = true; + System.Threading.Monitor.PulseAll(MachineStateCached); + } + } + else if (!MachineStateCached[machineType]) + { + lock (MachineStateCached) + { + while (!MachineStateCached[machineType]) + { + System.Threading.Monitor.Wait(MachineStateCached); + } + } + } + + // Populates the map of actions for this machine instance. + foreach (var kvp in MachineActionMap[machineType]) + { + this.ActionMap.Add(kvp.Key, new CachedDelegate(kvp.Value, this)); + } + + var initialStates = StateMap[machineType].Where(state => state.IsStart).ToList(); + this.Assert(initialStates.Count != 0, "Machine '{0}' must declare a start state.", this.Id); + this.Assert(initialStates.Count is 1, "Machine '{0}' can not declare more than one start states.", this.Id); + + this.DoStatePush(initialStates[0]); + + this.AssertStateValidity(); + } + + /// + /// Registers a new timer using the specified configuration. + /// + private TimerInfo RegisterTimer(TimeSpan dueTime, TimeSpan period, object payload) + { + var info = new TimerInfo(this.Id, dueTime, period, payload); + var timer = this.Runtime.CreateMachineTimer(info, this); + this.Runtime.LogWriter.OnCreateTimer(info); + this.Timers.Add(info, timer); + return info; + } + + /// + /// Unregisters the specified timer. + /// + private void UnregisterTimer(TimerInfo info) + { + if (!this.Timers.TryGetValue(info, out IMachineTimer timer)) + { + this.Assert(info.OwnerId == this.Id, "Timer '{0}' is already disposed.", info); + } + + this.Runtime.LogWriter.OnStopTimer(info); + this.Timers.Remove(info); + timer.Dispose(); + } + + /// + /// Returns the type of the state at the specified state + /// stack index, if there is one. + /// + internal Type GetStateTypeAtStackIndex(int index) + { + return this.StateStack.ElementAtOrDefault(index)?.GetType(); + } + + /// + /// Processes a type, looking for machine states. + /// + private void ExtractStateTypes(Type type) + { + Stack stack = new Stack(); + stack.Push(type); + + while (stack.Count > 0) + { + Type nextType = stack.Pop(); + + if (nextType.IsClass && nextType.IsSubclassOf(typeof(MachineState))) + { + StateTypeMap[this.GetType()].Add(nextType); + } + else if (nextType.IsClass && nextType.IsSubclassOf(typeof(StateGroup))) + { + // Adds the contents of the group of states to the stack. + foreach (var t in nextType.GetNestedTypes(BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.DeclaredOnly)) + { + this.Assert(t.IsSubclassOf(typeof(StateGroup)) || t.IsSubclassOf(typeof(MachineState)), + "'{0}' is neither a group of states nor a state.", t.Name); + stack.Push(t); + } + } + } + } + + /// + /// Returns the action with the specified name. + /// + private MethodInfo GetActionWithName(string actionName) + { + MethodInfo method; + Type machineType = this.GetType(); + + do + { + method = machineType.GetMethod(actionName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, + Type.DefaultBinder, Array.Empty(), null); + machineType = machineType.BaseType; + } + while (method is null && machineType != typeof(Machine)); + + this.Assert(method != null, "Cannot detect action declaration '{0}' in machine '{1}'.", actionName, this.GetType().Name); + this.Assert(method.GetParameters().Length is 0, "Action '{0}' in machine '{1}' must have 0 formal parameters.", + method.Name, this.GetType().Name); + + if (method.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) != null) + { + this.Assert(method.ReturnType == typeof(Task), + "Async action '{0}' in machine '{1}' must have 'Task' return type.", + method.Name, this.GetType().Name); + } + else + { + this.Assert(method.ReturnType == typeof(void), "Action '{0}' in machine '{1}' must have 'void' return type.", + method.Name, this.GetType().Name); + } + + return method; + } + + /// + /// Returns the set of all states in the machine (for code coverage). + /// + internal HashSet GetAllStates() + { + this.Assert(StateMap.ContainsKey(this.GetType()), "Machine '{0}' hasn't populated its states yet.", this.Id); + + var allStates = new HashSet(); + foreach (var state in StateMap[this.GetType()]) + { + allStates.Add(NameResolver.GetQualifiedStateName(state.GetType())); + } + + return allStates; + } + + /// + /// Returns the set of all (states, registered event) pairs in the machine (for code coverage). + /// + internal HashSet> GetAllStateEventPairs() + { + this.Assert(StateMap.ContainsKey(this.GetType()), "Machine '{0}' hasn't populated its states yet.", this.Id); + + var pairs = new HashSet>(); + foreach (var state in StateMap[this.GetType()]) + { + foreach (var binding in state.ActionBindings) + { + pairs.Add(Tuple.Create(NameResolver.GetQualifiedStateName(state.GetType()), binding.Key.FullName)); + } + + foreach (var transition in state.GotoTransitions) + { + pairs.Add(Tuple.Create(NameResolver.GetQualifiedStateName(state.GetType()), transition.Key.FullName)); + } + + foreach (var pushtransition in state.PushTransitions) + { + pairs.Add(Tuple.Create(NameResolver.GetQualifiedStateName(state.GetType()), pushtransition.Key.FullName)); + } + } + + return pairs; + } + + /// + /// Check machine for state related errors. + /// + private void AssertStateValidity() + { + this.Assert(StateTypeMap[this.GetType()].Count > 0, "Machine '{0}' must have one or more states.", this.Id); + this.Assert(this.StateStack.Peek() != null, "Machine '{0}' must not have a null current state.", this.Id); + } + + /// + /// Wraps the unhandled exception inside an + /// exception, and throws it to the user. + /// + private void ReportUnhandledException(Exception ex, string actionName) + { + string state = ""; + if (this.CurrentState != null) + { + state = this.CurrentStateName; + } + + this.Runtime.WrapAndThrowException(ex, $"Exception '{ex.GetType()}' was thrown " + + $"in machine '{this.Id}', state '{state}', action '{actionName}', " + + $"'{ex.Source}':\n" + + $" {ex.Message}\n" + + $"The stack trace is:\n{ex.StackTrace}"); + } + + /// + /// Invokes user callback when a machine receives an event that it cannot handle. + /// + /// The handler (outermost) that threw the exception. + /// The exception thrown by the machine. + /// False if the exception should continue to get thrown, true if the machine should gracefully halt. + private bool OnUnhandledEventExceptionHandler(string methodName, UnhandledEventException ex) + { + this.Runtime.LogWriter.OnMachineExceptionThrown(this.Id, ex.CurrentStateName, methodName, ex); + + var ret = this.OnException(methodName, ex); + this.OnExceptionRequestedGracefulHalt = false; + switch (ret) + { + case OnExceptionOutcome.HaltMachine: + case OnExceptionOutcome.HandledException: + this.Runtime.LogWriter.OnMachineExceptionHandled(this.Id, ex.CurrentStateName, methodName, ex); + this.OnExceptionRequestedGracefulHalt = true; + return true; + case OnExceptionOutcome.ThrowException: + return false; + } + + return false; + } + + /// + /// Invokes user callback when a machine throws an exception. + /// + /// The handler (outermost) that threw the exception. + /// The exception thrown by the machine. + /// False if the exception should continue to get thrown, true if it was handled in this method. + private bool OnExceptionHandler(string methodName, Exception ex) + { + if (ex is ExecutionCanceledException) + { + // Internal exception, used during testing. + return false; + } + + this.Runtime.LogWriter.OnMachineExceptionThrown(this.Id, this.CurrentStateName, methodName, ex); + + var ret = this.OnException(methodName, ex); + this.OnExceptionRequestedGracefulHalt = false; + + switch (ret) + { + case OnExceptionOutcome.ThrowException: + return false; + case OnExceptionOutcome.HandledException: + this.Runtime.LogWriter.OnMachineExceptionHandled(this.Id, this.CurrentStateName, methodName, ex); + return true; + case OnExceptionOutcome.HaltMachine: + this.OnExceptionRequestedGracefulHalt = true; + return false; + } + + return false; + } + + /// + /// User callback when a machine throws an exception. + /// + /// The handler (outermost) that threw the exception. + /// The exception thrown by the machine. + /// The action that the runtime should take. + protected virtual OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.ThrowException; + } + + /// + /// User callback that is invoked when the machine successfully dequeues + /// an event from its inbox. This method is not called when the dequeue + /// happens via a Receive statement. + /// + /// The event that was dequeued. + protected virtual Task OnEventDequeueAsync(Event e) => Task.CompletedTask; + + /// + /// User callback that is invoked when the machine finishes handling a dequeued event, + /// unless the handler of the dequeued event raised an event or caused the machine to + /// halt (either normally or due to an exception). Unless this callback raises an event, + /// the machine will either become idle or dequeue the next event from its inbox. + /// + /// The event that was handled. + protected virtual Task OnEventHandledAsync(Event e) => Task.CompletedTask; + + /// + /// User callback that is invoked when the machine receives an event that it is not prepared + /// to handle. The callback is invoked first, after which the machine will necessarily throw + /// an + /// + /// The event that was unhandled. + /// The state of the machine when the event was dequeued. + protected virtual Task OnEventUnhandledAsync(Event e, string currentState) => Task.CompletedTask; + + /// + /// User callback that is invoked when a machine halts. + /// + protected virtual void OnHalt() + { + } + + /// + /// Resets the static caches. + /// + internal static void ResetCaches() + { + StateTypeMap.Clear(); + StateMap.Clear(); + MachineActionMap.Clear(); + } + + /// + /// Halts the machine. + /// + private void HaltMachine() + { + this.IsHalted = true; + this.ReceivedEvent = null; + + // Close the inbox, which will stop any subsequent enqueues. + this.Inbox.Close(); + + this.Runtime.LogWriter.OnHalt(this.Id, this.Inbox.Size); + this.Runtime.NotifyHalted(this); + + // Dispose any held resources. + this.Inbox.Dispose(); + foreach (var timer in this.Timers.Keys.ToList()) + { + this.UnregisterTimer(timer); + } + + // Invoke user callback. + this.OnHalt(); + } + } +} diff --git a/Source/Core/Machines/MachineFactory.cs b/Source/Core/Machines/MachineFactory.cs new file mode 100644 index 000000000..448415ca7 --- /dev/null +++ b/Source/Core/Machines/MachineFactory.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Factory for creating machines. + /// + internal static class MachineFactory + { + /// + /// Cache storing machine constructors. + /// + private static readonly Dictionary> MachineConstructorCache = + new Dictionary>(); + + /// + /// Creates a new of the specified type. + /// + /// Type of the machine. + /// The created machine. + public static Machine Create(Type type) + { + lock (MachineConstructorCache) + { + if (!MachineConstructorCache.TryGetValue(type, out Func constructor)) + { + constructor = Expression.Lambda>( + Expression.New(type.GetConstructor(Type.EmptyTypes))).Compile(); + MachineConstructorCache.Add(type, constructor); + } + + return constructor(); + } + } + } +} diff --git a/Source/Core/Machines/MachineId.cs b/Source/Core/Machines/MachineId.cs new file mode 100644 index 000000000..ef8a37245 --- /dev/null +++ b/Source/Core/Machines/MachineId.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.Serialization; +using System.Threading; + +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Unique machine id. + /// + [DataContract] + [DebuggerStepThrough] + public sealed class MachineId : IEquatable, IComparable + { + /// + /// The runtime that executes the machine with this id. + /// + public IMachineRuntime Runtime { get; private set; } + + /// + /// Unique id, when is empty. + /// + [DataMember] + public readonly ulong Value; + + /// + /// Unique id, when non-empty. + /// + [DataMember] + public readonly string NameValue; + + /// + /// Type of the machine with this id. + /// + [DataMember] + public readonly string Type; + + /// + /// Name of the machine used for logging. + /// + [DataMember] + public readonly string Name; + + /// + /// Generation of the runtime that created this machine id. + /// + [DataMember] + public readonly ulong Generation; + + /// + /// Endpoint. + /// + [DataMember] + public readonly string Endpoint; + + /// + /// True if is used as the unique id, else false. + /// + public bool IsNameUsedForHashing => this.NameValue.Length > 0; + + /// + /// Initializes a new instance of the class. + /// + internal MachineId(Type type, string machineName, CoyoteRuntime runtime, bool useNameForHashing = false) + { + this.Runtime = runtime; + this.Endpoint = string.Empty; + + if (useNameForHashing) + { + this.Value = 0; + this.NameValue = machineName; + this.Runtime.Assert(!string.IsNullOrEmpty(this.NameValue), "Input machine name cannot be null when used as id."); + } + else + { + // Atomically increments and safely wraps into an unsigned long. + this.Value = (ulong)Interlocked.Increment(ref runtime.MachineIdCounter) - 1; + this.NameValue = string.Empty; + + // Checks for overflow. + this.Runtime.Assert(this.Value != ulong.MaxValue, "Detected machine id overflow."); + } + + this.Generation = runtime.Configuration.RuntimeGeneration; + + this.Type = type.FullName; + if (this.IsNameUsedForHashing) + { + this.Name = this.NameValue; + } + else + { + this.Name = string.Format(CultureInfo.InvariantCulture, "{0}({1})", + string.IsNullOrEmpty(machineName) ? this.Type : machineName, this.Value.ToString()); + } + } + + /// + /// Bind the machine id. + /// + internal void Bind(CoyoteRuntime runtime) + { + this.Runtime = runtime; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + public override bool Equals(object obj) + { + if (obj is MachineId mid) + { + // Use same machanism for hashing. + if (this.IsNameUsedForHashing != mid.IsNameUsedForHashing) + { + return false; + } + + return this.IsNameUsedForHashing ? + this.NameValue.Equals(mid.NameValue) && this.Generation == mid.Generation : + this.Value == mid.Value && this.Generation == mid.Generation; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() + { + int hash = 17; + hash = (hash * 23) + (this.IsNameUsedForHashing ? this.NameValue.GetHashCode() : this.Value.GetHashCode()); + hash = (hash * 23) + this.Generation.GetHashCode(); + return hash; + } + + /// + /// Returns a string that represents the current machine id. + /// + public override string ToString() => this.Name; + + /// + /// Indicates whether the specified is equal + /// to the current . + /// + public bool Equals(MachineId other) => this.Equals((object)other); + + /// + /// Compares the specified with the current + /// for ordering or sorting purposes. + /// + public int CompareTo(MachineId other) => string.Compare(this.Name, other?.Name); + + bool IEquatable.Equals(MachineId other) => this.Equals(other); + + int IComparable.CompareTo(MachineId other) => string.Compare(this.Name, other?.Name); + } +} diff --git a/Source/Core/Machines/MachineState.cs b/Source/Core/Machines/MachineState.cs new file mode 100644 index 000000000..124e37e76 --- /dev/null +++ b/Source/Core/Machines/MachineState.cs @@ -0,0 +1,600 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Abstract class representing a state of a Coyote machine. + /// + public abstract class MachineState + { + /// + /// Attribute for declaring that a state of a machine + /// is the start one. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class StartAttribute : Attribute + { + } + + /// + /// Attribute for declaring what action to perform + /// when entering a machine state. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class OnEntryAttribute : Attribute + { + /// + /// Action name. + /// + internal readonly string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Action name + public OnEntryAttribute(string actionName) + { + this.Action = actionName; + } + } + + /// + /// Attribute for declaring what action to perform + /// when exiting a machine state. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class OnExitAttribute : Attribute + { + /// + /// Action name. + /// + internal string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Action name + public OnExitAttribute(string actionName) + { + this.Action = actionName; + } + } + + /// + /// Attribute for declaring which state a machine should transition to + /// when it receives an event in a given state. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + protected sealed class OnEventGotoStateAttribute : Attribute + { + /// + /// Event type. + /// + internal readonly Type Event; + + /// + /// State type. + /// + internal readonly Type State; + + /// + /// Action name. + /// + internal readonly string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Event type + /// State type + public OnEventGotoStateAttribute(Type eventType, Type stateType) + { + this.Event = eventType; + this.State = stateType; + } + + /// + /// Initializes a new instance of the class. + /// + /// Event type + /// State type + /// Name of action to perform on exit + public OnEventGotoStateAttribute(Type eventType, Type stateType, string actionName) + { + this.Event = eventType; + this.State = stateType; + this.Action = actionName; + } + } + + /// + /// Attribute for declaring which state a machine should push transition to + /// when it receives an event in a given state. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + protected sealed class OnEventPushStateAttribute : Attribute + { + /// + /// Event type. + /// + internal Type Event; + + /// + /// State type. + /// + internal Type State; + + /// + /// Initializes a new instance of the class. + /// + /// Event type + /// State type + public OnEventPushStateAttribute(Type eventType, Type stateType) + { + this.Event = eventType; + this.State = stateType; + } + } + + /// + /// Attribute for declaring what action a machine should perform + /// when it receives an event in a given state. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + protected sealed class OnEventDoActionAttribute : Attribute + { + /// + /// Event type. + /// + internal Type Event; + + /// + /// Action name. + /// + internal string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Event type + /// Action name + public OnEventDoActionAttribute(Type eventType, string actionName) + { + this.Event = eventType; + this.Action = actionName; + } + } + + /// + /// Attribute for declaring what events should be deferred in + /// a machine state. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class DeferEventsAttribute : Attribute + { + /// + /// Event types. + /// + internal Type[] Events; + + /// + /// Initializes a new instance of the class. + /// + /// Event types + public DeferEventsAttribute(params Type[] eventTypes) + { + this.Events = eventTypes; + } + } + + /// + /// Attribute for declaring what events should be ignored in + /// a machine state. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class IgnoreEventsAttribute : Attribute + { + /// + /// Event types. + /// + internal Type[] Events; + + /// + /// Initializes a new instance of the class. + /// + /// Event types + public IgnoreEventsAttribute(params Type[] eventTypes) + { + this.Events = eventTypes; + } + } + + /// + /// The entry action of the state. + /// + internal string EntryAction { get; private set; } + + /// + /// The exit action of the state. + /// + internal string ExitAction { get; private set; } + + /// + /// Dictionary containing all the goto state transitions. + /// + internal Dictionary GotoTransitions; + + /// + /// Dictionary containing all the push state transitions. + /// + internal Dictionary PushTransitions; + + /// + /// Dictionary containing all the action bindings. + /// + internal Dictionary ActionBindings; + + /// + /// Set of ignored event types. + /// + internal HashSet IgnoredEvents; + + /// + /// Set of deferred event types. + /// + internal HashSet DeferredEvents; + + /// + /// True if this is the start state. + /// + internal bool IsStart { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + protected MachineState() + { + } + + /// + /// Initializes the state. + /// + internal void InitializeState() + { + this.IsStart = false; + + this.GotoTransitions = new Dictionary(); + this.PushTransitions = new Dictionary(); + this.ActionBindings = new Dictionary(); + + this.IgnoredEvents = new HashSet(); + this.DeferredEvents = new HashSet(); + + if (this.GetType().GetCustomAttribute(typeof(OnEntryAttribute), true) is OnEntryAttribute entryAttribute) + { + this.EntryAction = entryAttribute.Action; + } + + if (this.GetType().GetCustomAttribute(typeof(OnExitAttribute), true) is OnExitAttribute exitAttribute) + { + this.ExitAction = exitAttribute.Action; + } + + if (this.GetType().IsDefined(typeof(StartAttribute), false)) + { + this.IsStart = true; + } + + // Events with already declared handlers. + var handledEvents = new HashSet(); + + // Install event handlers. + this.InstallGotoTransitions(handledEvents); + this.InstallPushTransitions(handledEvents); + this.InstallActionHandlers(handledEvents); + this.InstallIgnoreHandlers(handledEvents); + this.InstallDeferHandlers(handledEvents); + } + + /// + /// Declares goto event handlers, if there are any. + /// + private void InstallGotoTransitions(HashSet handledEvents) + { + var gotoAttributes = this.GetType().GetCustomAttributes(typeof(OnEventGotoStateAttribute), false) + as OnEventGotoStateAttribute[]; + + foreach (var attr in gotoAttributes) + { + CheckEventHandlerAlreadyDeclared(attr.Event, handledEvents); + + if (attr.Action is null) + { + this.GotoTransitions.Add(attr.Event, new GotoStateTransition(attr.State)); + } + else + { + this.GotoTransitions.Add(attr.Event, new GotoStateTransition(attr.State, attr.Action)); + } + + handledEvents.Add(attr.Event); + } + + this.InheritGotoTransitions(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits goto event handlers from a base state, if there is one. + /// + private void InheritGotoTransitions(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MachineState))) + { + return; + } + + var gotoAttributesInherited = baseState.GetCustomAttributes(typeof(OnEventGotoStateAttribute), false) + as OnEventGotoStateAttribute[]; + + var gotoTransitionsInherited = new Dictionary(); + foreach (var attr in gotoAttributesInherited) + { + if (this.GotoTransitions.ContainsKey(attr.Event)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(attr.Event, baseState, handledEvents); + + if (attr.Action is null) + { + gotoTransitionsInherited.Add(attr.Event, new GotoStateTransition(attr.State)); + } + else + { + gotoTransitionsInherited.Add(attr.Event, new GotoStateTransition(attr.State, attr.Action)); + } + + handledEvents.Add(attr.Event); + } + + foreach (var kvp in gotoTransitionsInherited) + { + this.GotoTransitions.Add(kvp.Key, kvp.Value); + } + + this.InheritGotoTransitions(baseState.BaseType, handledEvents); + } + + /// + /// Declares push event handlers, if there are any. + /// + private void InstallPushTransitions(HashSet handledEvents) + { + var pushAttributes = this.GetType().GetCustomAttributes(typeof(OnEventPushStateAttribute), false) + as OnEventPushStateAttribute[]; + + foreach (var attr in pushAttributes) + { + CheckEventHandlerAlreadyDeclared(attr.Event, handledEvents); + + this.PushTransitions.Add(attr.Event, new PushStateTransition(attr.State)); + handledEvents.Add(attr.Event); + } + + this.InheritPushTransitions(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits push event handlers from a base state, if there is one. + /// + private void InheritPushTransitions(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MachineState))) + { + return; + } + + var pushAttributesInherited = baseState.GetCustomAttributes(typeof(OnEventPushStateAttribute), false) + as OnEventPushStateAttribute[]; + + var pushTransitionsInherited = new Dictionary(); + foreach (var attr in pushAttributesInherited) + { + if (this.PushTransitions.ContainsKey(attr.Event)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(attr.Event, baseState, handledEvents); + + pushTransitionsInherited.Add(attr.Event, new PushStateTransition(attr.State)); + handledEvents.Add(attr.Event); + } + + foreach (var kvp in pushTransitionsInherited) + { + this.PushTransitions.Add(kvp.Key, kvp.Value); + } + + this.InheritPushTransitions(baseState.BaseType, handledEvents); + } + + /// + /// Declares action event handlers, if there are any. + /// + private void InstallActionHandlers(HashSet handledEvents) + { + var doAttributes = this.GetType().GetCustomAttributes(typeof(OnEventDoActionAttribute), false) + as OnEventDoActionAttribute[]; + + foreach (var attr in doAttributes) + { + CheckEventHandlerAlreadyDeclared(attr.Event, handledEvents); + + this.ActionBindings.Add(attr.Event, new ActionBinding(attr.Action)); + handledEvents.Add(attr.Event); + } + + this.InheritActionHandlers(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits action event handlers from a base state, if there is one. + /// + private void InheritActionHandlers(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MachineState))) + { + return; + } + + var doAttributesInherited = baseState.GetCustomAttributes(typeof(OnEventDoActionAttribute), false) + as OnEventDoActionAttribute[]; + + var actionBindingsInherited = new Dictionary(); + foreach (var attr in doAttributesInherited) + { + if (this.ActionBindings.ContainsKey(attr.Event)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(attr.Event, baseState, handledEvents); + + actionBindingsInherited.Add(attr.Event, new ActionBinding(attr.Action)); + handledEvents.Add(attr.Event); + } + + foreach (var kvp in actionBindingsInherited) + { + this.ActionBindings.Add(kvp.Key, kvp.Value); + } + + this.InheritActionHandlers(baseState.BaseType, handledEvents); + } + + /// + /// Declares ignore event handlers, if there are any. + /// + private void InstallIgnoreHandlers(HashSet handledEvents) + { + if (this.GetType().GetCustomAttribute(typeof(IgnoreEventsAttribute), false) is IgnoreEventsAttribute ignoreEventsAttribute) + { + foreach (var e in ignoreEventsAttribute.Events) + { + CheckEventHandlerAlreadyDeclared(e, handledEvents); + } + + this.IgnoredEvents.UnionWith(ignoreEventsAttribute.Events); + handledEvents.UnionWith(ignoreEventsAttribute.Events); + } + + this.InheritIgnoreHandlers(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits ignore event handlers from a base state, if there is one. + /// + private void InheritIgnoreHandlers(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MachineState))) + { + return; + } + + if (baseState.GetCustomAttribute(typeof(IgnoreEventsAttribute), false) is IgnoreEventsAttribute ignoreEventsAttribute) + { + foreach (var e in ignoreEventsAttribute.Events) + { + if (this.IgnoredEvents.Contains(e)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(e, baseState, handledEvents); + } + + this.IgnoredEvents.UnionWith(ignoreEventsAttribute.Events); + handledEvents.UnionWith(ignoreEventsAttribute.Events); + } + + this.InheritIgnoreHandlers(baseState.BaseType, handledEvents); + } + + /// + /// Declares defer event handlers, if there are any. + /// + private void InstallDeferHandlers(HashSet handledEvents) + { + if (this.GetType().GetCustomAttribute(typeof(DeferEventsAttribute), false) is DeferEventsAttribute deferEventsAttribute) + { + foreach (var e in deferEventsAttribute.Events) + { + CheckEventHandlerAlreadyDeclared(e, handledEvents); + } + + this.DeferredEvents.UnionWith(deferEventsAttribute.Events); + handledEvents.UnionWith(deferEventsAttribute.Events); + } + + this.InheritDeferHandlers(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits defer event handlers from a base state, if there is one. + /// + private void InheritDeferHandlers(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MachineState))) + { + return; + } + + if (baseState.GetCustomAttribute(typeof(DeferEventsAttribute), false) is DeferEventsAttribute deferEventsAttribute) + { + foreach (var e in deferEventsAttribute.Events) + { + if (this.DeferredEvents.Contains(e)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(e, baseState, handledEvents); + } + + this.DeferredEvents.UnionWith(deferEventsAttribute.Events); + handledEvents.UnionWith(deferEventsAttribute.Events); + } + + this.InheritDeferHandlers(baseState.BaseType, handledEvents); + } + + /// + /// Checks if an event handler has been already declared. + /// + private static void CheckEventHandlerAlreadyDeclared(Type e, HashSet handledEvents) + { + if (handledEvents.Contains(e)) + { + throw new InvalidOperationException($"declared multiple handlers for event '{e}'"); + } + } + + /// + /// Checks if an event handler has been already inherited. + /// + private static void CheckEventHandlerAlreadyInherited(Type e, Type baseState, HashSet handledEvents) + { + if (handledEvents.Contains(e)) + { + throw new InvalidOperationException($"inherited multiple handlers for event '{e}' from state '{baseState}'"); + } + } + } +} diff --git a/Source/Core/Machines/MachineStateManager.cs b/Source/Core/Machines/MachineStateManager.cs new file mode 100644 index 000000000..8e00d1f15 --- /dev/null +++ b/Source/Core/Machines/MachineStateManager.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Manages the state of a machine. + /// + internal class MachineStateManager : IMachineStateManager + { + /// + /// The runtime that executes the machine being managed. + /// + private readonly CoyoteRuntime Runtime; + + /// + /// The machine being managed. + /// + private readonly Machine Machine; + + /// + /// True if the event handler of the machine is running, else false. + /// + public bool IsEventHandlerRunning { get; set; } + + /// + /// Id used to identify subsequent operations performed by the machine. + /// + public Guid OperationGroupId { get; set; } + + /// + /// Initializes a new instance of the class. + /// + internal MachineStateManager(CoyoteRuntime runtime, Machine machine, Guid operationGroupId) + { + this.Runtime = runtime; + this.Machine = machine; + this.IsEventHandlerRunning = true; + this.OperationGroupId = operationGroupId; + } + + /// + /// Returns the cached state of the machine. + /// + public int GetCachedState() => 0; + + /// + /// Checks if the specified event is ignored in the current machine state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEventIgnoredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Machine.IsEventIgnoredInCurrentState(e); + + /// + /// Checks if the specified event is deferred in the current machine state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEventDeferredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Machine.IsEventDeferredInCurrentState(e); + + /// + /// Checks if a default handler is installed in the current machine state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDefaultHandlerInstalledInCurrentState() => this.Machine.IsDefaultHandlerInstalledInCurrentState(); + + /// + /// Notifies the machine that an event has been enqueued. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnEnqueueEvent(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Runtime.LogWriter.OnEnqueue(this.Machine.Id, e.GetType().FullName); + + /// + /// Notifies the machine that an event has been raised. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnRaiseEvent(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Runtime.NotifyRaisedEvent(this.Machine, e, eventInfo); + + /// + /// Notifies the machine that it is waiting to receive an event of one of the specified types. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnWaitEvent(IEnumerable eventTypes) => + this.Runtime.NotifyWaitEvent(this.Machine, eventTypes); + + /// + /// Notifies the machine that an event it was waiting to receive has been enqueued. + /// + public void OnReceiveEvent(Event e, Guid opGroupId, EventInfo eventInfo) + { + if (opGroupId != Guid.Empty) + { + // Inherit the operation group id of the receive operation, if it is non-empty. + this.OperationGroupId = opGroupId; + } + + this.Runtime.NotifyReceivedEvent(this.Machine, e, eventInfo); + } + + /// + /// Notifies the machine that an event it was waiting to receive was already in the + /// event queue when the machine invoked the receive statement. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnReceiveEventWithoutWaiting(Event e, Guid opGroupId, EventInfo eventInfo) + { + if (opGroupId != Guid.Empty) + { + // Inherit the operation group id of the receive operation, if it is non-empty. + this.OperationGroupId = opGroupId; + } + + this.Runtime.NotifyReceivedEventWithoutWaiting(this.Machine, e, eventInfo); + } + + /// + /// Notifies the machine that an event has been dropped. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnDropEvent(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Runtime.TryHandleDroppedEvent(e, this.Machine.Id); + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, object arg0) => this.Runtime.Assert(predicate, s, arg0); + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, object arg0, object arg1) => this.Runtime.Assert(predicate, s, arg0, arg1); + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, object arg0, object arg1, object arg2) => + this.Runtime.Assert(predicate, s, arg0, arg1, arg2); + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, params object[] args) => this.Runtime.Assert(predicate, s, args); + } +} diff --git a/Source/Core/Machines/SendOptions.cs b/Source/Core/Machines/SendOptions.cs new file mode 100644 index 000000000..f0a0a02ea --- /dev/null +++ b/Source/Core/Machines/SendOptions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// Represents a send event configuration that is used during testing. + /// + public class SendOptions + { + /// + /// The default send options. + /// + public static SendOptions Default { get; } = new SendOptions(); + + /// + /// True if this event must always be handled, else false. + /// + public bool MustHandle { get; private set; } + + /// + /// Specifies that there must not be more than N instances of the + /// event in the inbox queue of the receiver machine. + /// + public int Assert { get; private set; } + + /// + /// Specifies that during testing, an execution that increases the cardinality of the + /// event beyond N in the receiver machine inbox queue must not be generated. + /// + public int Assume { get; private set; } + + /// + /// User-defined hash of the event payload. The default value is 0. Set it to a custom value + /// to improve the accuracy of liveness checking when state-caching is enabled. + /// + public int HashedState { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + public SendOptions(bool mustHandle = false, int assert = -1, int assume = -1, int hashedState = 0) + { + this.MustHandle = mustHandle; + this.Assert = assert; + this.Assume = assume; + this.HashedState = hashedState; + } + + /// + /// A string that represents the current options. + /// + public override string ToString() => + string.Format("SendOptions[MustHandle='{0}', Assert='{1}', Assume='{2}', HashedState='{3}']", + this.MustHandle, this.Assert, this.Assume, this.HashedState); + } +} diff --git a/Source/Core/Machines/SingleStateMachine.cs b/Source/Core/Machines/SingleStateMachine.cs new file mode 100644 index 000000000..17ad4d6e6 --- /dev/null +++ b/Source/Core/Machines/SingleStateMachine.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; + +namespace Microsoft.Coyote.Machines +{ + /// + /// Abstract class representing a single-state machine. + /// + public abstract class SingleStateMachine : Machine + { + [Start] + [OnEntry(nameof(HandleInitOnEntry))] + [OnEventGotoState(typeof(Halt), typeof(Terminating))] + [OnEventDoAction(typeof(WildCardEvent), nameof(HandleProcessEvent))] + private sealed class Init : MachineState + { + } + + [OnEntry(nameof(TerminatingOnEntry))] + private sealed class Terminating : MachineState + { + } + + /// + /// Initilizes the state machine on creation. + /// + private async Task HandleInitOnEntry() + { + await this.InitOnEntry(this.ReceivedEvent); + } + + /// + /// Initilizes the state machine on creation. + /// + /// Initial event provided on machine creation, or null otherwise. + protected virtual Task InitOnEntry(Event e) => Task.CompletedTask; + + /// + /// Process incoming event. + /// + private async Task HandleProcessEvent() + { + await this.ProcessEvent(this.ReceivedEvent); + } + + /// + /// Process incoming event. + /// + /// Event. + protected abstract Task ProcessEvent(Event e); + + /// + /// Halts the machine. + /// + private void TerminatingOnEntry() + { + this.Raise(new Halt()); + } + } +} diff --git a/Source/Core/Machines/StateGroup.cs b/Source/Core/Machines/StateGroup.cs new file mode 100644 index 000000000..b510ac0ac --- /dev/null +++ b/Source/Core/Machines/StateGroup.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines +{ + /// + /// Abstract class used for representing a group of related states. + /// + public abstract class StateGroup + { + } +} diff --git a/Source/Core/Machines/Timers/IMachineTimer.cs b/Source/Core/Machines/Timers/IMachineTimer.cs new file mode 100644 index 000000000..1def1c551 --- /dev/null +++ b/Source/Core/Machines/Timers/IMachineTimer.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.Machines.Timers +{ + /// + /// Interface of a timer that can send timeout events to its owner machine. + /// + internal interface IMachineTimer : IDisposable + { + /// + /// Stores information about this timer. + /// + TimerInfo Info { get; } + } +} diff --git a/Source/Core/Machines/Timers/MachineTimer.cs b/Source/Core/Machines/Timers/MachineTimer.cs new file mode 100644 index 000000000..4accbd218 --- /dev/null +++ b/Source/Core/Machines/Timers/MachineTimer.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; + +namespace Microsoft.Coyote.Machines.Timers +{ + /// + /// A timer that can send timeout events to its owner machine. + /// + internal sealed class MachineTimer : IMachineTimer + { + /// + /// Stores information about this timer. + /// + public TimerInfo Info { get; private set; } + + /// + /// The machine that owns this timer. + /// + private readonly Machine Owner; + + /// + /// The internal timer. + /// + private readonly Timer InternalTimer; + + /// + /// The timeout event. + /// + private readonly TimerElapsedEvent TimeoutEvent; + + /// + /// Initializes a new instance of the class. + /// + /// Stores information about this timer. + /// The machine that owns this timer. + public MachineTimer(TimerInfo info, Machine owner) + { + this.Info = info; + this.Owner = owner; + + this.TimeoutEvent = new TimerElapsedEvent(this.Info); + + // To avoid a race condition between assigning the field of the timer + // and HandleTimeout accessing the field before the assignment happens, + // we first create a timer that cannot get triggered, then assign it to + // the field, and finally we start the timer. + this.InternalTimer = new Timer(this.HandleTimeout, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + this.InternalTimer.Change(this.Info.DueTime, Timeout.InfiniteTimeSpan); + } + + /// + /// Handles the timeout. + /// + private void HandleTimeout(object state) + { + // Send a timeout event. + this.Owner.Runtime.SendEvent(this.Owner.Id, this.TimeoutEvent); + + if (this.Info.Period.TotalMilliseconds >= 0) + { + // The timer is periodic, so schedule the next timeout. + try + { + // Start the next timeout period. + this.InternalTimer.Change(this.Info.Period, Timeout.InfiniteTimeSpan); + } + catch (ObjectDisposedException) + { + // Benign race condition while disposing the timer. + } + } + } + + /// + /// Determines whether the specified System.Object is equal + /// to the current System.Object. + /// + public override bool Equals(object obj) + { + if (obj is MachineTimer timer) + { + return this.Info == timer.Info; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => this.Info.GetHashCode(); + + /// + /// Returns a string that represents the current instance. + /// + public override string ToString() => this.Info.ToString(); + + /// + /// Indicates whether the specified is equal + /// to the current . + /// + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; otherwise, false. + public bool Equals(MachineTimer other) + { + return this.Equals((object)other); + } + + /// + /// Disposes the resources held by this timer. + /// + public void Dispose() + { + this.InternalTimer.Dispose(); + } + } +} diff --git a/Source/Core/Machines/Timers/TimerElapsedEvent.cs b/Source/Core/Machines/Timers/TimerElapsedEvent.cs new file mode 100644 index 000000000..e511cf8ac --- /dev/null +++ b/Source/Core/Machines/Timers/TimerElapsedEvent.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Machines.Timers +{ + /// + /// Defines a timer elapsed event that is sent from a timer to the machine that owns the timer. + /// + public class TimerElapsedEvent : Event + { + /// + /// Stores information about the timer. + /// + public readonly TimerInfo Info; + + /// + /// Initializes a new instance of the class. + /// + /// Stores information about the timer. + internal TimerElapsedEvent(TimerInfo info) + { + this.Info = info; + } + } +} diff --git a/Source/Core/Machines/Timers/TimerInfo.cs b/Source/Core/Machines/Timers/TimerInfo.cs new file mode 100644 index 000000000..350df1a5a --- /dev/null +++ b/Source/Core/Machines/Timers/TimerInfo.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.Machines.Timers +{ + /// + /// Stores information about a timer that can send timeout events to its owner machine. + /// + public class TimerInfo : IEquatable + { + /// + /// The unique id of the timer. + /// + private readonly Guid Id; + + /// + /// The id of the machine that owns the timer. + /// + public readonly MachineId OwnerId; + + /// + /// The amount of time to wait before sending the first timeout event. + /// + public readonly TimeSpan DueTime; + + /// + /// The time interval between timeout events. + /// + public readonly TimeSpan Period; + + /// + /// The optional payload of the timer. This is null if there is no payload. + /// + public readonly object Payload; + + /// + /// Initializes a new instance of the class. + /// + /// The id of the machine that owns this timer. + /// The amount of time to wait before sending the first timeout event. + /// The time interval between timeout events. + /// Optional payload of the timeout event. + internal TimerInfo(MachineId ownerId, TimeSpan dueTime, TimeSpan period, object payload) + { + this.Id = Guid.NewGuid(); + this.OwnerId = ownerId; + this.DueTime = dueTime; + this.Period = period; + this.Payload = payload; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + public override bool Equals(object obj) + { + if (obj is TimerInfo timerInfo) + { + return this.Id == timerInfo.Id; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// Returns a string that represents the current instance. + /// + public override string ToString() => this.Id.ToString(); + + /// + /// Indicates whether the specified is equal + /// to the current . + /// + public bool Equals(TimerInfo other) + { + return this.Equals((object)other); + } + + bool IEquatable.Equals(TimerInfo other) + { + return this.Equals(other); + } + } +} diff --git a/Source/Core/Properties/InternalsVisibleTo.cs b/Source/Core/Properties/InternalsVisibleTo.cs new file mode 100644 index 000000000..4664a97a4 --- /dev/null +++ b/Source/Core/Properties/InternalsVisibleTo.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +// Libraries +[assembly: InternalsVisibleTo("Microsoft.Coyote.SharedObjects,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("Microsoft.Coyote.TestingServices,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] + +// Tools +[assembly: InternalsVisibleTo("CoyoteTester,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("CoyoteReplayer,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("CoyoteBenchmarkRunner,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("CoyoteTestLauncher,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] + +// Tests +[assembly: InternalsVisibleTo("Microsoft.Coyote.Core.Tests,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("Microsoft.Coyote.SharedObjects.Tests,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("Microsoft.Coyote.TestingServices.Tests,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] diff --git a/Source/Core/Runtime/Configuration.cs b/Source/Core/Runtime/Configuration.cs new file mode 100644 index 000000000..5d282e328 --- /dev/null +++ b/Source/Core/Runtime/Configuration.cs @@ -0,0 +1,433 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote +{ +#pragma warning disable CA1724 // Type names should not match namespaces + /// + /// The Coyote project configurations. + /// + [DataContract] + [Serializable] + public class Configuration + { + /// + /// The output path. + /// + [DataMember] + public string OutputFilePath; + + /// + /// Timeout in seconds. + /// + [DataMember] + public int Timeout; + + /// + /// The current runtime generation. + /// + [DataMember] + public ulong RuntimeGeneration; + + /// + /// The assembly to be analyzed for bugs. + /// + [DataMember] + public string AssemblyToBeAnalyzed; + + /// + /// The assembly that contains the testing runtime. + /// By default it is empty, which uses the default + /// testing runtime of Coyote. + /// + [DataMember] + public string TestingRuntimeAssembly; + + /// + /// Test method to be used. + /// + [DataMember] + public string TestMethodName; + + /// + /// Scheduling strategy to use with the Coyote tester. + /// + [DataMember] + public SchedulingStrategy SchedulingStrategy; + + /// + /// Number of scheduling iterations. + /// + [DataMember] + public int SchedulingIterations; + + /// + /// Seed for random scheduling strategies. + /// + [DataMember] + public int? RandomSchedulingSeed; + + /// + /// If true, the seed will increment in each + /// testing iteration. + /// + [DataMember] + public bool IncrementalSchedulingSeed; + + /// + /// If true, the Coyote tester performs a full exploration, + /// and does not stop when it finds a bug. + /// + [DataMember] + public bool PerformFullExploration; + + /// + /// The maximum scheduling steps to explore + /// for fair schedulers. + /// By default there is no bound. + /// + [DataMember] + public int MaxFairSchedulingSteps; + + /// + /// The maximum scheduling steps to explore + /// for unfair schedulers. + /// By default there is no bound. + /// + [DataMember] + public int MaxUnfairSchedulingSteps; + + /// + /// The maximum scheduling steps to explore + /// for both fair and unfair schedulers. + /// By default there is no bound. + /// + public int MaxSchedulingSteps + { + set + { + this.MaxUnfairSchedulingSteps = value; + this.MaxFairSchedulingSteps = value; + } + } + + /// + /// True if the user has explicitly set the + /// fair scheduling steps bound. + /// + [DataMember] + public bool UserExplicitlySetMaxFairSchedulingSteps; + + /// + /// Number of parallel bug-finding tasks. + /// By default it is 1 task. + /// + [DataMember] + public uint ParallelBugFindingTasks; + + /// + /// Runs this process as a parallel bug-finding task. + /// + [DataMember] + public bool RunAsParallelBugFindingTask; + + /// + /// The testing scheduler unique endpoint. + /// + [DataMember] + public string TestingSchedulerEndPoint; + + /// + /// The testing scheduler process id. + /// + [DataMember] + public int TestingSchedulerProcessId; + + /// + /// The unique testing process id. + /// + [DataMember] + public uint TestingProcessId; + + /// + /// If true, then the Coyote tester will consider an execution + /// that hits the depth bound as buggy. + /// + [DataMember] + public bool ConsiderDepthBoundHitAsBug; + + /// + /// The priority switch bound. By default it is 2. + /// Used by priority-based schedulers. + /// + [DataMember] + public int PrioritySwitchBound; + + /// + /// Delay bound. By default it is 2. + /// Used by delay-bounding schedulers. + /// + [DataMember] + public int DelayBound; + + /// + /// Coin-flip bound. By default it is 2. + /// + [DataMember] + public int CoinFlipBound; + + /// + /// The timeout delay used during testing. By default it is 1. + /// Increase to the make timeouts less frequent. + /// + [DataMember] + public uint TimeoutDelay; + + /// + /// Safety prefix bound. By default it is 0. + /// + [DataMember] + public int SafetyPrefixBound; + + /// + /// Enables liveness checking during bug-finding. + /// + [DataMember] + public bool EnableLivenessChecking; + + /// + /// The liveness temperature threshold. If it is 0 + /// then it is disabled. + /// + [DataMember] + public int LivenessTemperatureThreshold; + + /// + /// Enables cycle-detection using state-caching + /// for liveness checking. + /// + [DataMember] + public bool EnableCycleDetection; + + /// + /// If this option is enabled, then the user-defined state-hashing methods + /// are used to improve the accurracy of state-caching for liveness checking. + /// + [DataMember] + public bool EnableUserDefinedStateHashing; + + /// + /// Enables (safety) monitors in the production runtime. + /// + [DataMember] + public bool EnableMonitorsInProduction; + + /// + /// Attaches the debugger during trace replay. + /// + [DataMember] + public bool AttachDebugger; + + /// + /// Enables the testing assertion that a raise/goto/push/pop transition must + /// be the last API called in an event handler. + /// + [DataMember] + public bool EnableNoApiCallAfterTransitionStmtAssertion; + + /// + /// The schedule file to be replayed. + /// + public string ScheduleFile; + + /// + /// The schedule trace to be replayed. + /// + internal string ScheduleTrace; + + /// + /// Enables code coverage reporting of a Coyote program. + /// + [DataMember] + public bool ReportCodeCoverage; + + /// + /// Enables activity coverage reporting of a Coyote program. + /// + [DataMember] + public bool ReportActivityCoverage; + + /// + /// Enables activity coverage debugging. + /// + public bool DebugActivityCoverage; + + /// + /// Additional assembly specifications to instrument for code coverage, besides those in the + /// dependency graph between and the Microsoft.Coyote DLLs. + /// Key is filename, value is whether it is a list file (true) or a single file (false). + /// + public Dictionary AdditionalCodeCoverageAssemblies = new Dictionary(); + + /// + /// If true, then messages are logged. + /// + [DataMember] + public bool IsVerbose; + + /// + /// Shows warnings. + /// + [DataMember] + public bool ShowWarnings; + + /// + /// Enables debugging. + /// + [DataMember] + public bool EnableDebugging; + + /// + /// Enables profiling. + /// + [DataMember] + public bool EnableProfiling; + + /// + /// Enables colored console output. + /// + public bool EnableColoredConsoleOutput; + + /// + /// If true, then environment exit will be disabled. + /// + internal bool DisableEnvironmentExit; + + /// + /// Initializes a new instance of the class. + /// + protected Configuration() + { + this.OutputFilePath = string.Empty; + + this.Timeout = 0; + this.RuntimeGeneration = 0; + + this.AssemblyToBeAnalyzed = string.Empty; + this.TestingRuntimeAssembly = string.Empty; + this.TestMethodName = string.Empty; + + this.SchedulingStrategy = SchedulingStrategy.Random; + this.SchedulingIterations = 1; + this.RandomSchedulingSeed = null; + this.IncrementalSchedulingSeed = false; + + this.PerformFullExploration = false; + this.MaxFairSchedulingSteps = 0; + this.MaxUnfairSchedulingSteps = 0; + this.UserExplicitlySetMaxFairSchedulingSteps = false; + this.ParallelBugFindingTasks = 1; + this.RunAsParallelBugFindingTask = false; + this.TestingSchedulerEndPoint = Guid.NewGuid().ToString(); + this.TestingSchedulerProcessId = -1; + this.TestingProcessId = 0; + this.ConsiderDepthBoundHitAsBug = false; + this.PrioritySwitchBound = 0; + this.DelayBound = 0; + this.CoinFlipBound = 0; + this.TimeoutDelay = 1; + this.SafetyPrefixBound = 0; + + this.EnableLivenessChecking = true; + this.LivenessTemperatureThreshold = 0; + this.EnableCycleDetection = false; + this.EnableUserDefinedStateHashing = false; + this.EnableMonitorsInProduction = false; + this.EnableNoApiCallAfterTransitionStmtAssertion = true; + + this.AttachDebugger = false; + + this.ScheduleFile = string.Empty; + this.ScheduleTrace = string.Empty; + + this.ReportCodeCoverage = false; + this.ReportActivityCoverage = false; + this.DebugActivityCoverage = false; + + this.IsVerbose = false; + this.ShowWarnings = false; + this.EnableDebugging = false; + this.EnableProfiling = false; + + this.EnableColoredConsoleOutput = false; + this.DisableEnvironmentExit = true; + } + + /// + /// Creates a new configuration with default values. + /// + public static Configuration Create() + { + return new Configuration(); + } + + /// + /// Updates the configuration with verbose output enabled or disabled. + /// + /// If true, then messages are logged. + public Configuration WithVerbosityEnabled(bool isVerbose = true) + { + this.IsVerbose = isVerbose; + return this; + } + + /// + /// Updates the configuration with verbose output enabled or disabled. + /// + /// The verbosity level. + [Obsolete("WithVerbosityEnabled(int level) is deprecated; use WithVerbosityEnabled(bool isVerbose) instead.")] + public Configuration WithVerbosityEnabled(int level) + { + this.IsVerbose = level > 0; + return this; + } + + /// + /// Updates the configuration with the specified scheduling strategy. + /// + /// The scheduling strategy. + public Configuration WithStrategy(SchedulingStrategy strategy) + { + this.SchedulingStrategy = strategy; + return this; + } + + /// + /// Updates the configuration with the specified number of iterations to perform. + /// + /// The number of iterations to perform. + public Configuration WithNumberOfIterations(int iterations) + { + this.SchedulingIterations = iterations; + return this; + } + + /// + /// Updates the configuration with the specified number of scheduling steps + /// to perform per iteration (for both fair and unfair schedulers). + /// + /// The scheduling steps to perform per iteration. + public Configuration WithMaxSteps(int maxSteps) + { + this.MaxSchedulingSteps = maxSteps; + return this; + } + } +#pragma warning restore CA1724 // Type names should not match namespaces +} diff --git a/Source/Core/Runtime/CoyoteRuntime.cs b/Source/Core/Runtime/CoyoteRuntime.cs new file mode 100644 index 000000000..dd19cb271 --- /dev/null +++ b/Source/Core/Runtime/CoyoteRuntime.cs @@ -0,0 +1,888 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Threading; +using Microsoft.Coyote.Threading.Tasks; + +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Runtime for executing explicit and implicit asynchronous machines. + /// + internal abstract class CoyoteRuntime : IMachineRuntime + { + /// + /// Provides access to the runtime associated with the current execution context. + /// + internal static RuntimeProvider Provider { get; set; } = new RuntimeProvider(); + + /// + /// The configuration used by the runtime. + /// + internal readonly Configuration Configuration; + + /// + /// Map from unique machine ids to machines. + /// + protected readonly ConcurrentDictionary MachineMap; + + /// + /// Map from task ids to objects. + /// + protected readonly ConcurrentDictionary TaskMap; + + /// + /// Monotonically increasing machine id counter. + /// + internal long MachineIdCounter; + + /// + /// Monotonically increasing lock id counter. + /// + internal long LockIdCounter; + + /// + /// Records if the runtime is running. + /// + internal volatile bool IsRunning; + + /// + /// Returns the id of the currently executing . + /// + internal virtual int? CurrentTaskId => Task.CurrentId; + + /// + /// The log writer. + /// + protected internal RuntimeLogWriter LogWriter { get; private set; } + + /// + /// The installed logger. + /// + public ILogger Logger => this.LogWriter.Logger; + + /// + /// Callback that is fired when the Coyote program throws an exception. + /// + public event OnFailureHandler OnFailure; + + /// + /// Callback that is fired when a Coyote event is dropped. + /// + public event OnEventDroppedHandler OnEventDropped; + + /// + /// Initializes a new instance of the class. + /// + protected CoyoteRuntime(Configuration configuration) + { + this.Configuration = configuration; + this.MachineMap = new ConcurrentDictionary(); + this.TaskMap = new ConcurrentDictionary(); + this.MachineIdCounter = 0; + this.LockIdCounter = 0; + this.LogWriter = new RuntimeLogWriter + { + Logger = configuration.IsVerbose ? (ILogger)new ConsoleLogger() : new NulLogger() + }; + + this.IsRunning = true; + } + + /// + /// Creates a fresh machine id that has not yet been bound to any machine. + /// + public MachineId CreateMachineId(Type type, string machineName = null) => new MachineId(type, machineName, this); + + /// + /// Creates a machine id that is uniquely tied to the specified unique name. The + /// returned machine id can either be a fresh id (not yet bound to any machine), + /// or it can be bound to a previously created machine. In the second case, this + /// machine id can be directly used to communicate with the corresponding machine. + /// + public abstract MachineId CreateMachineIdFromName(Type type, string machineName); + + /// + /// Creates a new machine of the specified and with + /// the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public abstract MachineId CreateMachine(Type type, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified and name, and + /// with the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public abstract MachineId CreateMachine(Type type, string machineName, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified type, using the specified . + /// This method optionally passes an to the new machine, which can only + /// be used to access its payload, and cannot be handled. + /// + public abstract MachineId CreateMachine(MachineId mid, Type type, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified and with the + /// specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when + /// the machine is initialized and the (if any) is handled. + /// + public abstract Task CreateMachineAndExecuteAsync(Type type, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified and name, and with + /// the specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when the + /// machine is initialized and the (if any) is handled. + /// + public abstract Task CreateMachineAndExecuteAsync(Type type, string machineName, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified , using the specified + /// unbound machine id, and passes the specified optional . This + /// event can only be used to access its payload, and cannot be handled. The method + /// returns only when the machine is initialized and the (if any) + /// is handled. + /// + public abstract Task CreateMachineAndExecuteAsync(MachineId mid, Type type, Event e = null, Guid opGroupId = default); + + /// + /// Sends an asynchronous to a machine. + /// + public abstract void SendEvent(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null); + + /// + /// Sends an to a machine. Returns immediately if the target machine was already + /// running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + public abstract Task SendEventAndExecuteAsync(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null); + + /// + /// Registers a new specification monitor of the specified . + /// + public void RegisterMonitor(Type type) + { + this.TryCreateMonitor(type); + } + + /// + /// Invokes the specified monitor with the specified . + /// + public void InvokeMonitor(Event e) + { + this.InvokeMonitor(typeof(T), e); + } + + /// + /// Invokes the specified monitor with the specified . + /// + public void InvokeMonitor(Type type, Event e) + { + // If the event is null then report an error and exit. + this.Assert(e != null, "Cannot monitor a null event."); + this.Monitor(type, null, e); + } + + /// + /// Returns a nondeterministic boolean choice, that can be controlled + /// during analysis or testing. + /// + public bool Random() + { + return this.GetNondeterministicBooleanChoice(null, 2); + } + + /// + /// Returns a fair nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + public bool FairRandom( + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + var havocId = string.Format("Runtime_{0}_{1}_{2}", + callerMemberName, callerFilePath, callerLineNumber.ToString()); + return this.GetFairNondeterministicBooleanChoice(null, havocId); + } + + /// + /// Returns a nondeterministic boolean choice, that can be controlled + /// during analysis or testing. The value is used to generate a number + /// in the range [0..maxValue), where 0 triggers true. + /// + public bool Random(int maxValue) + { + return this.GetNondeterministicBooleanChoice(null, maxValue); + } + + /// + /// Returns a nondeterministic integer, that can be controlled during + /// analysis or testing. The value is used to generate an integer in + /// the range [0..maxValue). + /// + public int RandomInteger(int maxValue) + { + return this.GetNondeterministicIntegerChoice(null, maxValue); + } + + /// + /// Returns the operation group id of the specified machine. During testing, + /// the runtime asserts that the specified machine is currently executing. + /// + public abstract Guid GetCurrentOperationGroupId(MachineId currentMachine); + + /// + /// Terminates the runtime and notifies each active machine to halt execution. + /// + public void Stop() + { + this.IsRunning = false; + } + + /// + /// Creates a new of the specified . + /// + /// MachineId + internal abstract MachineId CreateMachine(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId); + + /// + /// Creates a new of the specified . The + /// method returns only when the machine is initialized and the + /// (if any) is handled. + /// + internal abstract Task CreateMachineAndExecuteAsync(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId); + + /// + /// Sends an asynchronous to a machine. + /// + internal abstract void SendEvent(MachineId target, Event e, AsyncMachine sender, Guid opGroupId, SendOptions options); + + /// + /// Sends an asynchronous to a machine. Returns immediately if the target machine was + /// already running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + internal abstract Task SendEventAndExecuteAsync(MachineId target, Event e, AsyncMachine sender, + Guid opGroupId, SendOptions options); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal abstract ControlledTask CreateControlledTask(Action action, CancellationToken cancellationToken); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal abstract ControlledTask CreateControlledTask(Func function, CancellationToken cancellationToken); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal abstract ControlledTask CreateControlledTask(Func function, + CancellationToken cancellationToken); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal abstract ControlledTask CreateControlledTask(Func> function, + CancellationToken cancellationToken); + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + internal abstract ControlledTask CreateControlledTaskDelay(int millisecondsDelay, CancellationToken cancellationToken); + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + internal abstract ControlledTask CreateControlledTaskDelay(TimeSpan delay, CancellationToken cancellationToken); + + /// + /// Creates a associated with a completion source. + /// + internal abstract ControlledTask CreateControlledTaskCompletionSource(Task task); + + /// + /// Creates a associated with a completion source. + /// + internal abstract ControlledTask CreateControlledTaskCompletionSource(Task task); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(params Task[] tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(IEnumerable tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(IEnumerable tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(params Task[] tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(IEnumerable> tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask WaitAllTasksAsync(IEnumerable> tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal abstract ControlledTask WaitAnyTaskAsync(params ControlledTask[] tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal abstract ControlledTask WaitAnyTaskAsync(params Task[] tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask WaitAnyTaskAsync(IEnumerable tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask WaitAnyTaskAsync(IEnumerable tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal abstract ControlledTask> WaitAnyTaskAsync(params ControlledTask[] tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal abstract ControlledTask> WaitAnyTaskAsync(params Task[] tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal abstract ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks); + + /// + /// Waits for any of the provided objects to complete execution. + /// + internal abstract int WaitAnyTask(params ControlledTask[] tasks); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds. + /// + internal abstract int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds or until a cancellation + /// token is cancelled. + /// + internal abstract int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout, CancellationToken cancellationToken); + + /// + /// Waits for any of the provided objects to complete + /// execution unless the wait is cancelled. + /// + internal abstract int WaitAnyTask(ControlledTask[] tasks, CancellationToken cancellationToken); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified time interval. + /// + internal abstract int WaitAnyTask(ControlledTask[] tasks, TimeSpan timeout); + + /// + /// Creates a controlled awaiter that switches into a target environment. + /// + internal abstract ControlledYieldAwaitable.ControlledYieldAwaiter CreateControlledYieldAwaiter(); + + /// + /// Ends the wait for the completion of the yield operation. + /// + internal abstract void OnGetYieldResult(YieldAwaitable.YieldAwaiter awaiter); + + /// + /// Sets the action to perform when the yield operation completes. + /// + internal abstract void OnYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter); + + /// + /// Schedules the continuation action that is invoked when the yield operation completes. + /// + internal abstract void OnUnsafeYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter); + + /// + /// Creates a mutual exclusion lock that is compatible with objects. + /// + internal abstract ControlledLock CreateControlledLock(); + + /// + /// Creates a new timer that sends a to its owner machine. + /// + internal abstract IMachineTimer CreateMachineTimer(TimerInfo info, Machine owner); + + /// + /// Tries to create a new specification monitor of the specified . + /// + internal abstract void TryCreateMonitor(Type type); + + /// + /// Invokes the specification monitor with the specified . + /// + internal abstract void Monitor(Type type, AsyncMachine sender, Event e); + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public virtual void Assert(bool predicate) + { + if (!predicate) + { + throw new AssertionFailureException("Detected an assertion failure."); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public virtual void Assert(bool predicate, string s, object arg0) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public virtual void Assert(bool predicate, string s, object arg0, object arg1) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString(), arg1.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public virtual void Assert(bool predicate, string s, object arg0, object arg1, object arg2) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString(), arg1.ToString(), arg2.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public virtual void Assert(bool predicate, string s, params object[] args) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, args)); + } + } + + /// + /// Asserts that the currently executing controlled task is awaiting a controlled awaiter. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void AssertAwaitingControlledAwaiter(ref TAwaiter awaiter) + where TAwaiter : INotifyCompletion + { + } + + /// + /// Asserts that the currently executing controlled task is awaiting a controlled awaiter. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void AssertAwaitingUnsafeControlledAwaiter(ref TAwaiter awaiter) + where TAwaiter : ICriticalNotifyCompletion + { + } + + /// + /// Returns a nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal abstract bool GetNondeterministicBooleanChoice(AsyncMachine machine, int maxValue); + + /// + /// Returns a fair nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal abstract bool GetFairNondeterministicBooleanChoice(AsyncMachine machine, string uniqueId); + + /// + /// Returns a nondeterministic integer choice, that can be + /// controlled during analysis or testing. + /// + internal abstract int GetNondeterministicIntegerChoice(AsyncMachine machine, int maxValue); + + /// + /// Injects a context switch point that can be systematically explored during testing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void ExploreContextSwitch() + { + } + + /// + /// Gets the machine of type with the specified id, + /// or null if no such machine exists. + /// + internal TMachine GetMachineFromId(MachineId id) + where TMachine : AsyncMachine => + id != null && this.MachineMap.TryGetValue(id, out AsyncMachine value) && + value is TMachine machine ? machine : null; + + /// + /// Notifies that a machine entered a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyEnteredState(Machine machine) + { + } + + /// + /// Notifies that a monitor entered a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyEnteredState(Monitor monitor) + { + } + + /// + /// Notifies that a machine exited a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyExitedState(Machine machine) + { + } + + /// + /// Notifies that a monitor exited a state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyExitedState(Monitor monitor) + { + } + + /// + /// Notifies that a machine invoked an action. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyInvokedAction(Machine machine, MethodInfo action, Event receivedEvent) + { + } + + /// + /// Notifies that a machine completed invoking an action. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyCompletedAction(Machine machine, MethodInfo action, Event receivedEvent) + { + } + + /// + /// Notifies that a machine invoked an action. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyInvokedOnEntryAction(Machine machine, MethodInfo action, Event receivedEvent) + { + } + + /// + /// Notifies that a machine completed invoking an action. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyCompletedOnEntryAction(Machine machine, MethodInfo action, Event receivedEvent) + { + } + + /// + /// Notifies that a machine invoked an action. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyInvokedOnExitAction(Machine machine, MethodInfo action, Event receivedEvent) + { + } + + /// + /// Notifies that a machine completed invoking an action. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyCompletedOnExitAction(Machine machine, MethodInfo action, Event receivedEvent) + { + } + + /// + /// Notifies that a monitor invoked an action. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyInvokedAction(Monitor monitor, MethodInfo action, Event receivedEvent) + { + } + + /// + /// Notifies that a machine raised an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyRaisedEvent(Machine machine, Event e, EventInfo eventInfo) + { + } + + /// + /// Notifies that a monitor raised an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyRaisedEvent(Monitor monitor, Event e, EventInfo eventInfo) + { + } + + /// + /// Notifies that a machine dequeued an . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyDequeuedEvent(Machine machine, Event e, EventInfo eventInfo) + { + } + + /// + /// Notifies that a machine invoked pop. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyPop(Machine machine) + { + } + + /// + /// Notifies that a machine called Receive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyReceiveCalled(Machine machine) + { + } + + /// + /// Notifies that a machine is handling a raised . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyHandleRaisedEvent(Machine machine, Event e) + { + } + + /// + /// Notifies that a machine is waiting for the specified task to complete. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyWaitTask(Machine machine, Task task) + { + } + + /// + /// Notifies that a is waiting for the specified task to complete. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyWaitTask(ControlledTaskMachine machine, Task task) + { + } + + /// + /// Notifies that a machine is waiting to receive an event of one of the specified types. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyWaitEvent(Machine machine, IEnumerable eventTypes) + { + } + + /// + /// Notifies that a machine enqueued an event that it was waiting to receive. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyReceivedEvent(Machine machine, Event e, EventInfo eventInfo) + { + } + + /// + /// Notifies that a machine received an event without waiting because the event + /// was already in the inbox when the machine invoked the receive statement. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyReceivedEventWithoutWaiting(Machine machine, Event e, EventInfo eventInfo) + { + } + + /// + /// Notifies that a machine has halted. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyHalted(Machine machine) + { + } + + /// + /// Notifies that the inbox of the specified machine is about to be + /// checked to see if the default event handler should fire. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyDefaultEventHandlerCheck(Machine machine) + { + } + + /// + /// Notifies that the default handler of the specified machine has been fired. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void NotifyDefaultHandlerFired(Machine machine) + { + } + + /// + /// Use this method to abstract the default + /// for logging runtime messages. + /// + public RuntimeLogWriter SetLogWriter(RuntimeLogWriter logWriter) + { + var logger = this.LogWriter.Logger; + var prevLogWriter = this.LogWriter; + this.LogWriter = logWriter ?? throw new InvalidOperationException("Cannot install a null log writer."); + this.SetLogger(logger); + return prevLogWriter; + } + + /// + /// Use this method to abstract the default for logging messages. + /// + public ILogger SetLogger(ILogger logger) + { + var prevLogger = this.LogWriter.Logger; + if (this.LogWriter != null) + { + this.LogWriter.Logger = logger ?? throw new InvalidOperationException("Cannot install a null logger."); + } + else + { + throw new InvalidOperationException("Cannot install a logger on a null log writer."); + } + + return prevLogger; + } + + /// + /// Raises the event with the specified . + /// + protected internal void RaiseOnFailureEvent(Exception exception) + { + if (this.Configuration.AttachDebugger && exception is MachineActionExceptionFilterException && + !((exception as MachineActionExceptionFilterException).InnerException is RuntimeException)) + { + System.Diagnostics.Debugger.Break(); + this.Configuration.AttachDebugger = false; + } + + this.OnFailure?.Invoke(exception); + } + + /// + /// Tries to handle the specified dropped . + /// + internal void TryHandleDroppedEvent(Event e, MachineId mid) + { + this.OnEventDropped?.Invoke(e, mid); + } + + /// + /// Throws an exception + /// containing the specified exception. + /// + internal virtual void WrapAndThrowException(Exception exception, string s, params object[] args) + { + throw (exception is AssertionFailureException) + ? exception + : new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, args), exception); + } + + /// + /// Disposes runtime resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.MachineIdCounter = 0; + } + } + + /// + /// Disposes runtime resources. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Source/Core/Runtime/Events/Event.cs b/Source/Core/Runtime/Events/Event.cs new file mode 100644 index 000000000..bc0d014ae --- /dev/null +++ b/Source/Core/Runtime/Events/Event.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote +{ + /// + /// Abstract class representing an event. + /// + [DataContract] + public abstract class Event + { + } +} diff --git a/Source/Core/Runtime/Events/EventInfo.cs b/Source/Core/Runtime/Events/EventInfo.cs new file mode 100644 index 000000000..0eb0848e8 --- /dev/null +++ b/Source/Core/Runtime/Events/EventInfo.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Contains an , and its associated metadata. + /// + [DataContract] + internal class EventInfo + { + /// + /// Event name. + /// + [DataMember] + internal string EventName { get; private set; } + + /// + /// Information regarding the event origin. + /// + [DataMember] + internal EventOriginInfo OriginInfo { get; private set; } + + /// + /// True if this event must always be handled, else false. + /// + internal bool MustHandle { get; set; } + + /// + /// Specifies that there must not be more than N instances of the + /// event in the inbox queue of the receiver machine. + /// + internal int Assert { get; set; } + + /// + /// Specifies that during testing, an execution that increases the cardinality of the + /// event beyond N in the receiver machine inbox queue must not be generated. + /// + internal int Assume { get; set; } + + /// + /// User-defined hash of the event payload. The default value is 0. Set it to a custom value + /// to improve the accuracy of liveness checking when state-caching is enabled. + /// + internal int HashedState { get; set; } + + /// + /// The step from which this event was sent. + /// + internal int SendStep { get; set; } + + /// + /// Initializes a new instance of the class. + /// + internal EventInfo(Event e) + { + this.EventName = e.GetType().FullName; + this.MustHandle = false; + this.Assert = -1; + this.Assume = -1; + this.HashedState = 0; + } + + /// + /// Initializes a new instance of the class. + /// + internal EventInfo(Event e, EventOriginInfo originInfo) + : this(e) + { + this.OriginInfo = originInfo; + } + } +} diff --git a/Source/Core/Runtime/Events/EventOriginInfo.cs b/Source/Core/Runtime/Events/EventOriginInfo.cs new file mode 100644 index 000000000..4cffd434a --- /dev/null +++ b/Source/Core/Runtime/Events/EventOriginInfo.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Contains the origin information of an . + /// + [DataContract] + internal class EventOriginInfo + { + /// + /// The sender machine id. + /// + [DataMember] + internal MachineId SenderMachineId { get; private set; } + + /// + /// The sender machine name. + /// + [DataMember] + internal string SenderMachineName { get; private set; } + + /// + /// The sender machine state name. + /// + [DataMember] + internal string SenderStateName { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + internal EventOriginInfo(MachineId senderMachineId, string senderMachineName, string senderStateName) + { + this.SenderMachineId = senderMachineId; + this.SenderMachineName = senderMachineName; + this.SenderStateName = senderStateName; + } + } +} diff --git a/Source/Core/Runtime/Exceptions/AssertionFailureException.cs b/Source/Core/Runtime/Exceptions/AssertionFailureException.cs new file mode 100644 index 000000000..0d52ab4f0 --- /dev/null +++ b/Source/Core/Runtime/Exceptions/AssertionFailureException.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// The exception that is thrown by the Coyote runtime upon assertion failure. + /// + internal sealed class AssertionFailureException : RuntimeException + { + /// + /// Initializes a new instance of the class. + /// + /// Message + internal AssertionFailureException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Message + /// Inner exception + internal AssertionFailureException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Source/Core/Runtime/Exceptions/ExecutionCanceledException.cs b/Source/Core/Runtime/Exceptions/ExecutionCanceledException.cs new file mode 100644 index 000000000..85ddae441 --- /dev/null +++ b/Source/Core/Runtime/Exceptions/ExecutionCanceledException.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// The exception that is thrown in a Coyote machine upon cancellation + /// of execution by the Coyote runtime. + /// + [DebuggerStepThrough] + public sealed class ExecutionCanceledException : RuntimeException + { + /// + /// Initializes a new instance of the class. + /// + internal ExecutionCanceledException() + { + } + } +} diff --git a/Source/Core/Runtime/Exceptions/MachineActionExceptionFilterException.cs b/Source/Core/Runtime/Exceptions/MachineActionExceptionFilterException.cs new file mode 100644 index 000000000..e9ce37e01 --- /dev/null +++ b/Source/Core/Runtime/Exceptions/MachineActionExceptionFilterException.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// The exception that is thrown by the Coyote runtime upon a machine action failure. + /// + internal sealed class MachineActionExceptionFilterException : RuntimeException + { + /// + /// Initializes a new instance of the class. + /// + /// Message + internal MachineActionExceptionFilterException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Message + /// Inner exception + internal MachineActionExceptionFilterException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Source/Core/Runtime/Exceptions/OnEventDroppedHandler.cs b/Source/Core/Runtime/Exceptions/OnEventDroppedHandler.cs new file mode 100644 index 000000000..588471e71 --- /dev/null +++ b/Source/Core/Runtime/Exceptions/OnEventDroppedHandler.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Handles the event. + /// + public delegate void OnEventDroppedHandler(Event e, MachineId target); +} diff --git a/Source/Core/Runtime/Exceptions/OnExceptionOutcome.cs b/Source/Core/Runtime/Exceptions/OnExceptionOutcome.cs new file mode 100644 index 000000000..c040e0c4f --- /dev/null +++ b/Source/Core/Runtime/Exceptions/OnExceptionOutcome.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Runtime +{ + /// + /// The outcome when a machine throws an exception. + /// + public enum OnExceptionOutcome + { + /// + /// Throw the exception causing the runtime to fail. + /// + ThrowException = 0, + + /// + /// The exception was handled and Machine should continue execution. + /// + HandledException = 1, + + /// + /// Halt the machine (do not throw the exception). + /// + HaltMachine = 2 + } +} diff --git a/Source/Core/Runtime/Exceptions/OnFailureHandler.cs b/Source/Core/Runtime/Exceptions/OnFailureHandler.cs new file mode 100644 index 000000000..cfa4df78e --- /dev/null +++ b/Source/Core/Runtime/Exceptions/OnFailureHandler.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Handles the event. + /// + public delegate void OnFailureHandler(Exception ex); +} diff --git a/Source/Core/Runtime/Exceptions/RuntimeException.cs b/Source/Core/Runtime/Exceptions/RuntimeException.cs new file mode 100644 index 000000000..94ab97a77 --- /dev/null +++ b/Source/Core/Runtime/Exceptions/RuntimeException.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// An exception that is thrown by the Coyote runtime. + /// + [Serializable] + [DebuggerStepThrough] + public class RuntimeException : Exception + { + /// + /// Initializes a new instance of the class. + /// + internal RuntimeException() + { + } + + /// + /// Initializes a new instance of the class. + /// + internal RuntimeException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + internal RuntimeException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + protected RuntimeException(SerializationInfo serializationInfo, StreamingContext streamingContext) + : base(serializationInfo, streamingContext) + { + } + } +} diff --git a/Source/Core/Runtime/Exceptions/UnhandledEventException.cs b/Source/Core/Runtime/Exceptions/UnhandledEventException.cs new file mode 100644 index 000000000..4a427bfb9 --- /dev/null +++ b/Source/Core/Runtime/Exceptions/UnhandledEventException.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Signals that a machine received an unhandled event. + /// + public sealed class UnhandledEventException : RuntimeException + { + /// + /// Name of the current state of the machine. + /// + public string CurrentStateName; + + /// + /// The event. + /// + public Event UnhandledEvent; + + /// + /// Initializes a new instance of the class. + /// + /// Current state name. + /// The event that was unhandled. + /// The exception message. + internal UnhandledEventException(string currentStateName, Event unhandledEvent, string message) + : base(message) + { + this.CurrentStateName = currentStateName; + this.UnhandledEvent = unhandledEvent; + } + } +} diff --git a/Source/Core/Runtime/IMachineRuntime.cs b/Source/Core/Runtime/IMachineRuntime.cs new file mode 100644 index 000000000..dd9c545c4 --- /dev/null +++ b/Source/Core/Runtime/IMachineRuntime.cs @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote +{ + /// + /// Interface that exposes runtime methods for creating and executing + /// asynchronous communicating state-machines. + /// + public interface IMachineRuntime : IDisposable + { + /// + /// The installed logger. + /// + ILogger Logger { get; } + + /// + /// Callback that is fired when the runtime throws an exception. + /// + event OnFailureHandler OnFailure; + + /// + /// Callback that is fired when an event is dropped. + /// + event OnEventDroppedHandler OnEventDropped; + + /// + /// Creates a fresh machine id that has not yet been bound to any machine. + /// + /// Type of the machine. + /// Optional machine name used for logging. + /// The result is the machine id. + MachineId CreateMachineId(Type type, string machineName = null); + + /// + /// Creates a machine id that is uniquely tied to the specified unique name. The + /// returned machine id can either be a fresh id (not yet bound to any machine), + /// or it can be bound to a previously created machine. In the second case, this + /// machine id can be directly used to communicate with the corresponding machine. + /// + /// Type of the machine. + /// Unique name used to create or get the machine id. + /// The result is the machine id. + MachineId CreateMachineIdFromName(Type type, string machineName); + + /// + /// Creates a new machine of the specified and with + /// the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + /// Type of the machine. + /// Optional event used during initialization. + /// Optional id that can be used to identify this operation. + /// The result is the machine id. + MachineId CreateMachine(Type type, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified and name, and + /// with the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + /// Type of the machine. + /// Optional machine name used for logging. + /// Optional event used during initialization. + /// Optional id that can be used to identify this operation. + /// The result is the machine id. + MachineId CreateMachine(Type type, string machineName, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified type, using the specified . + /// This method optionally passes an to the new machine, which can only + /// be used to access its payload, and cannot be handled. + /// + /// Unbound machine id. + /// Type of the machine. + /// Optional event used during initialization. + /// Optional id that can be used to identify this operation. + /// The result is the machine id. + MachineId CreateMachine(MachineId mid, Type type, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified and with the + /// specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when + /// the machine is initialized and the (if any) is handled. + /// + /// Type of the machine. + /// Optional event used during initialization. + /// Optional id that can be used to identify this operation. + /// Task that represents the asynchronous operation. The task result is the machine id. + Task CreateMachineAndExecuteAsync(Type type, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified and name, and with + /// the specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when the + /// machine is initialized and the (if any) is handled. + /// + /// Type of the machine. + /// Optional machine name used for logging. + /// Optional event used during initialization. + /// Optional id that can be used to identify this operation. + /// Task that represents the asynchronous operation. The task result is the machine id. + Task CreateMachineAndExecuteAsync(Type type, string machineName, Event e = null, Guid opGroupId = default); + + /// + /// Creates a new machine of the specified , using the specified + /// unbound machine id, and passes the specified optional . This + /// event can only be used to access its payload, and cannot be handled. The method + /// returns only when the machine is initialized and the (if any) + /// is handled. + /// + /// Unbound machine id. + /// Type of the machine. + /// Optional event used during initialization. + /// Optional id that can be used to identify this operation. + /// Task that represents the asynchronous operation. The task result is the machine id. + Task CreateMachineAndExecuteAsync(MachineId mid, Type type, Event e = null, Guid opGroupId = default); + + /// + /// Sends an asynchronous to a machine. + /// + /// The id of the target machine. + /// The event to send. + /// Optional id that can be used to identify this operation. + /// Optional configuration of a send operation. + void SendEvent(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null); + + /// + /// Sends an to a machine. Returns immediately if the target machine was already + /// running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + /// The id of the target machine. + /// The event to send. + /// Optional id that can be used to identify this operation. + /// Optional configuration of a send operation. + /// Task that represents the asynchronous operation. The task result is true if + /// the event was handled, false if the event was only enqueued. + Task SendEventAndExecuteAsync(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null); + + /// + /// Registers a new specification monitor of the specified . + /// + /// Type of the monitor. + void RegisterMonitor(Type type); + + /// + /// Invokes the specified monitor with the specified . + /// + /// Type of the monitor. + /// Event + void InvokeMonitor(Event e); + + /// + /// Invokes the specified monitor with the specified . + /// + /// Type of the monitor. + /// Event + void InvokeMonitor(Type type, Event e); + + /// + /// Returns a nondeterministic boolean choice, that can be controlled + /// during analysis or testing. + /// + /// The nondeterministic boolean choice. + bool Random(); + + /// + /// Returns a fair nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + /// CallerMemberName + /// CallerFilePath + /// CallerLineNumber + /// The controlled nondeterministic choice. + bool FairRandom( + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0); + + /// + /// Returns a nondeterministic boolean choice, that can be controlled + /// during analysis or testing. The value is used to generate a number + /// in the range [0..maxValue), where 0 triggers true. + /// + /// The max value. + /// The nondeterministic boolean choice. + bool Random(int maxValue); + + /// + /// Returns a nondeterministic integer choice, that can be + /// controlled during analysis or testing. The value is used + /// to generate an integer in the range [0..maxValue). + /// + /// The max value. + /// The nondeterministic integer choice. + int RandomInteger(int maxValue); + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + /// The predicate to check. + void Assert(bool predicate); + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + /// The predicate to check. + /// The message to print if the assertion fails. + /// The first argument. + void Assert(bool predicate, string s, object arg0); + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + /// The predicate to check. + /// The message to print if the assertion fails. + /// The first argument. + /// The second argument. + void Assert(bool predicate, string s, object arg0, object arg1); + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + /// The predicate to check. + /// The message to print if the assertion fails. + /// The first argument. + /// The second argument. + /// The third argument. + void Assert(bool predicate, string s, object arg0, object arg1, object arg2); + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + /// The predicate to check. + /// The message to print if the assertion fails. + /// The message arguments. + void Assert(bool predicate, string s, params object[] args); + + /// + /// Returns the operation group id of the specified machine id. Returns + /// if the id is not set, or if the is not associated with this runtime. + /// During testing, the runtime asserts that the specified machine is currently executing. + /// + /// The id of the currently executing machine. + /// The unique identifier. + Guid GetCurrentOperationGroupId(MachineId currentMachineId); + + /// + /// Use this method to override the default + /// for logging runtime messages. + /// + /// The runtime log writer to install. + /// The previously installed runtime log writer. + RuntimeLogWriter SetLogWriter(RuntimeLogWriter logWriter); + + /// + /// Use this method to override the default for logging messages. + /// + /// The logger to install. + /// The previously installed logger. + ILogger SetLogger(ILogger logger); + + /// + /// Terminates the runtime and notifies each active machine to halt execution. + /// + void Stop(); + } +} diff --git a/Source/Core/Runtime/MachineRuntimeFactory.cs b/Source/Core/Runtime/MachineRuntimeFactory.cs new file mode 100644 index 000000000..d03a3041c --- /dev/null +++ b/Source/Core/Runtime/MachineRuntimeFactory.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote +{ + /// + /// The runtime for creating and executing asynchronous communicating state-machines. + /// + public static class MachineRuntimeFactory + { + /// + /// Creates a new runtime. + /// + /// The created runtime. + public static IMachineRuntime Create() + { + return new ProductionRuntime(Configuration.Create()); + } + + /// + /// Creates a new runtime with the specified . + /// + /// The runtime configuration to use. + /// The created runtime. + public static IMachineRuntime Create(Configuration configuration) + { + return new ProductionRuntime(configuration ?? Configuration.Create()); + } + } +} diff --git a/Source/Core/Runtime/ProductionRuntime.cs b/Source/Core/Runtime/ProductionRuntime.cs new file mode 100644 index 000000000..26c4fc450 --- /dev/null +++ b/Source/Core/Runtime/ProductionRuntime.cs @@ -0,0 +1,910 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Threading; +using Microsoft.Coyote.Threading.Tasks; + +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Runtime for executing machines in production. + /// + internal sealed class ProductionRuntime : CoyoteRuntime + { + /// + /// List of monitors in the program. + /// + private readonly List Monitors; + + /// + /// Initializes a new instance of the class. + /// + internal ProductionRuntime() + : this(Configuration.Create()) + { + } + + /// + /// Initializes a new instance of the class. + /// + internal ProductionRuntime(Configuration configuration) + : base(configuration) + { + this.Monitors = new List(); + } + + /// + /// Creates a machine id that is uniquely tied to the specified unique name. The + /// returned machine id can either be a fresh id (not yet bound to any machine), + /// or it can be bound to a previously created machine. In the second case, this + /// machine id can be directly used to communicate with the corresponding machine. + /// + public override MachineId CreateMachineIdFromName(Type type, string machineName) => new MachineId(type, machineName, this, true); + + /// + /// Creates a new machine of the specified and with + /// the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(Type type, Event e = null, Guid opGroupId = default) => + this.CreateMachine(null, type, null, e, null, opGroupId); + + /// + /// Creates a new machine of the specified and name, and + /// with the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(Type type, string machineName, Event e = null, Guid opGroupId = default) => + this.CreateMachine(null, type, machineName, e, null, opGroupId); + + /// + /// Creates a new machine of the specified type, using the specified . + /// This method optionally passes an to the new machine, which can only + /// be used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(MachineId mid, Type type, Event e = null, Guid opGroupId = default) => + this.CreateMachine(mid, type, null, e, null, opGroupId); + + /// + /// Creates a new machine of the specified and with the + /// specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when + /// the machine is initialized and the (if any) is handled. + /// + public override Task CreateMachineAndExecuteAsync(Type type, Event e = null, Guid opGroupId = default) => + this.CreateMachineAndExecuteAsync(null, type, null, e, null, opGroupId); + + /// + /// Creates a new machine of the specified and name, and with + /// the specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when the + /// machine is initialized and the (if any) is handled. + /// + public override Task CreateMachineAndExecuteAsync(Type type, string machineName, Event e = null, Guid opGroupId = default) => + this.CreateMachineAndExecuteAsync(null, type, machineName, e, null, opGroupId); + + /// + /// Creates a new machine of the specified , using the specified + /// unbound machine id, and passes the specified optional . This + /// event can only be used to access its payload, and cannot be handled. The method + /// returns only when the machine is initialized and the (if any) + /// is handled. + /// + public override Task CreateMachineAndExecuteAsync(MachineId mid, Type type, Event e = null, Guid opGroupId = default) => + this.CreateMachineAndExecuteAsync(mid, type, null, e, null, opGroupId); + + /// + /// Sends an asynchronous to a machine. + /// + public override void SendEvent(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null) => + this.SendEvent(target, e, null, opGroupId, options); + + /// + /// Sends an to a machine. Returns immediately if the target machine was already + /// running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + public override Task SendEventAndExecuteAsync(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null) => + this.SendEventAndExecuteAsync(target, e, null, opGroupId, options); + + /// + /// Returns the operation group id of the specified machine. Returns + /// if the id is not set, or if the is not associated with this runtime. + /// During testing, the runtime asserts that the specified machine is currently executing. + /// + public override Guid GetCurrentOperationGroupId(MachineId currentMachine) + { + Machine machine = this.GetMachineFromId(currentMachine); + return machine is null ? Guid.Empty : machine.OperationGroupId; + } + + /// + /// Creates a new of the specified . + /// + internal override MachineId CreateMachine(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId) + { + Machine machine = this.CreateMachine(mid, type, machineName, creator, opGroupId); + this.LogWriter.OnCreateMachine(machine.Id, creator?.Id); + this.RunMachineEventHandler(machine, e, true); + return machine.Id; + } + + /// + /// Creates a new of the specified . The + /// method returns only when the created machine reaches quiescence. + /// + internal override async Task CreateMachineAndExecuteAsync(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId) + { + Machine machine = this.CreateMachine(mid, type, machineName, creator, opGroupId); + this.LogWriter.OnCreateMachine(machine.Id, creator?.Id); + await this.RunMachineEventHandlerAsync(machine, e, true); + return machine.Id; + } + + /// + /// Creates a new of the specified . + /// + private Machine CreateMachine(MachineId mid, Type type, string machineName, Machine creator, Guid opGroupId) + { + if (!type.IsSubclassOf(typeof(Machine))) + { + this.Assert(false, "Type '{0}' is not a machine.", type.FullName); + } + + if (mid is null) + { + mid = new MachineId(type, machineName, this); + } + else if (mid.Runtime != null && mid.Runtime != this) + { + this.Assert(false, "Unbound machine id '{0}' was created by another runtime.", mid.Value); + } + else if (mid.Type != type.FullName) + { + this.Assert(false, "Cannot bind machine id '{0}' of type '{1}' to a machine of type '{2}'.", + mid.Value, mid.Type, type.FullName); + } + else + { + mid.Bind(this); + } + + // The operation group id of the machine is set using the following precedence: + // (1) To the specified machine creation operation group id, if it is non-empty. + // (2) To the operation group id of the creator machine, if it exists. + // (3) To the empty operation group id. + if (opGroupId == Guid.Empty && creator != null) + { + opGroupId = creator.OperationGroupId; + } + + Machine machine = MachineFactory.Create(type); + IMachineStateManager stateManager = new MachineStateManager(this, machine, opGroupId); + IEventQueue eventQueue = new EventQueue(stateManager); + + machine.Initialize(this, mid, stateManager, eventQueue); + machine.InitializeStateInformation(); + + if (!this.MachineMap.TryAdd(mid, machine)) + { + string info = "This typically occurs if either the machine id was created by another runtime instance, " + + "or if a machine id from a previous runtime generation was deserialized, but the current runtime " + + "has not increased its generation value."; + this.Assert(false, "Machine with id '{0}' was already created in generation '{1}'. {2}", mid.Value, mid.Generation, info); + } + + return machine; + } + + /// + /// Sends an asynchronous to a machine. + /// + internal override void SendEvent(MachineId target, Event e, AsyncMachine sender, Guid opGroupId, SendOptions options) + { + EnqueueStatus enqueueStatus = this.EnqueueEvent(target, e, sender, opGroupId, out Machine targetMachine); + if (enqueueStatus is EnqueueStatus.EventHandlerNotRunning) + { + this.RunMachineEventHandler(targetMachine, null, false); + } + } + + /// + /// Sends an asynchronous to a machine. Returns immediately if the target machine was + /// already running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + internal override async Task SendEventAndExecuteAsync(MachineId target, Event e, AsyncMachine sender, + Guid opGroupId, SendOptions options) + { + EnqueueStatus enqueueStatus = this.EnqueueEvent(target, e, sender, opGroupId, out Machine targetMachine); + if (enqueueStatus is EnqueueStatus.EventHandlerNotRunning) + { + await this.RunMachineEventHandlerAsync(targetMachine, null, false); + return true; + } + + return enqueueStatus is EnqueueStatus.Dropped; + } + + /// + /// Enqueues an event to the machine with the specified id. + /// + private EnqueueStatus EnqueueEvent(MachineId target, Event e, AsyncMachine sender, Guid opGroupId, out Machine targetMachine) + { + if (target is null) + { + string message = sender != null ? + string.Format("Machine '{0}' is sending to a null machine.", sender.Id.ToString()) : + "Cannot send to a null machine."; + this.Assert(false, message); + } + + if (e is null) + { + string message = sender != null ? + string.Format("Machine '{0}' is sending a null event.", sender.Id.ToString()) : + "Cannot send a null event."; + this.Assert(false, message); + } + + // The operation group id of this operation is set using the following precedence: + // (1) To the specified send operation group id, if it is non-empty. + // (2) To the operation group id of the sender machine, if it exists and is non-empty. + // (3) To the empty operation group id. + if (opGroupId == Guid.Empty && sender != null) + { + opGroupId = sender.OperationGroupId; + } + + targetMachine = this.GetMachineFromId(target); + if (targetMachine is null) + { + this.LogWriter.OnSend(target, sender?.Id, (sender as Machine)?.CurrentStateName ?? string.Empty, + e.GetType().FullName, opGroupId, isTargetHalted: true); + this.TryHandleDroppedEvent(e, target); + return EnqueueStatus.Dropped; + } + + this.LogWriter.OnSend(target, sender?.Id, (sender as Machine)?.CurrentStateName ?? string.Empty, + e.GetType().FullName, opGroupId, isTargetHalted: false); + + EnqueueStatus enqueueStatus = targetMachine.Enqueue(e, opGroupId, null); + if (enqueueStatus == EnqueueStatus.Dropped) + { + this.TryHandleDroppedEvent(e, target); + } + + return enqueueStatus; + } + + /// + /// Runs a new asynchronous machine event handler. + /// This is a fire and forget invocation. + /// + private void RunMachineEventHandler(Machine machine, Event initialEvent, bool isFresh) + { + Task.Run(async () => + { + try + { + if (isFresh) + { + await machine.GotoStartState(initialEvent); + } + + await machine.RunEventHandlerAsync(); + } + catch (Exception ex) + { + this.IsRunning = false; + this.RaiseOnFailureEvent(ex); + } + finally + { + if (machine.IsHalted) + { + this.MachineMap.TryRemove(machine.Id, out AsyncMachine _); + } + } + }); + } + + /// + /// Runs a new asynchronous machine event handler. + /// + private async Task RunMachineEventHandlerAsync(Machine machine, Event initialEvent, bool isFresh) + { + try + { + if (isFresh) + { + await machine.GotoStartState(initialEvent); + } + + await machine.RunEventHandlerAsync(); + } + catch (Exception ex) + { + this.IsRunning = false; + this.RaiseOnFailureEvent(ex); + return; + } + } + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTask(Action action, CancellationToken cancellationToken) => + new ControlledTask(Task.Run(action, cancellationToken)); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTask(Func function, CancellationToken cancellationToken) + { + return new ControlledTask(Task.Run(async () => + { + await function(); + }, cancellationToken)); + } + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTask(Func function, + CancellationToken cancellationToken) => + new ControlledTask(Task.Run(function, cancellationToken)); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTask(Func> function, + CancellationToken cancellationToken) + { + return new ControlledTask(Task.Run(async () => + { + return await function(); + }, cancellationToken)); + } + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTaskDelay(int millisecondsDelay, CancellationToken cancellationToken) => + new ControlledTask(Task.Delay(millisecondsDelay, cancellationToken)); + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTaskDelay(TimeSpan delay, CancellationToken cancellationToken) => + new ControlledTask(Task.Delay(delay, cancellationToken)); + + /// + /// Creates a associated with a completion source. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTaskCompletionSource(Task task) => new ControlledTask(task); + + /// + /// Creates a associated with a completion source. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask CreateControlledTaskCompletionSource(Task task) => + new ControlledTask(task); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks) => + new ControlledTask(Task.WhenAll(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(params Task[] tasks) => + new ControlledTask(Task.WhenAll(tasks)); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(IEnumerable tasks) => + new ControlledTask(Task.WhenAll(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(IEnumerable tasks) => + new ControlledTask(Task.WhenAll(tasks)); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks) => + new ControlledTask(Task.WhenAll(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(params Task[] tasks) => + new ControlledTask(Task.WhenAll(tasks)); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(IEnumerable> tasks) => + new ControlledTask(Task.WhenAll(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAllTasksAsync(IEnumerable> tasks) => + new ControlledTask(Task.WhenAll(tasks)); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAnyTaskAsync(params ControlledTask[] tasks) => + new ControlledTask(Task.WhenAny(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAnyTaskAsync(params Task[] tasks) => + new ControlledTask(Task.WhenAny(tasks)); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAnyTaskAsync(IEnumerable tasks) => + new ControlledTask(Task.WhenAny(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask WaitAnyTaskAsync(IEnumerable tasks) => + new ControlledTask(Task.WhenAny(tasks)); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask> WaitAnyTaskAsync(params ControlledTask[] tasks) => + new ControlledTask>(Task.WhenAny(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask> WaitAnyTaskAsync(params Task[] tasks) => + new ControlledTask>(Task.WhenAny(tasks)); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks) => + new ControlledTask>(Task.WhenAny(tasks.Select(t => t.AwaiterTask))); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks) => + new ControlledTask>(Task.WhenAny(tasks)); + + /// + /// Waits for any of the provided objects to complete execution. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override int WaitAnyTask(params ControlledTask[] tasks) => + Task.WaitAny(tasks.Select(t => t.AwaiterTask).ToArray()); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout) => + Task.WaitAny(tasks.Select(t => t.AwaiterTask).ToArray(), millisecondsTimeout); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds or until a cancellation + /// token is cancelled. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout, CancellationToken cancellationToken) => + Task.WaitAny(tasks.Select(t => t.AwaiterTask).ToArray(), millisecondsTimeout, cancellationToken); + + /// + /// Waits for any of the provided objects to complete + /// execution unless the wait is cancelled. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override int WaitAnyTask(ControlledTask[] tasks, CancellationToken cancellationToken) => + Task.WaitAny(tasks.Select(t => t.AwaiterTask).ToArray(), cancellationToken); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified time interval. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override int WaitAnyTask(ControlledTask[] tasks, TimeSpan timeout) => + Task.WaitAny(tasks.Select(t => t.AwaiterTask).ToArray(), timeout); + + /// + /// Creates a controlled awaiter that switches into a target environment. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledYieldAwaitable.ControlledYieldAwaiter CreateControlledYieldAwaiter() => + new ControlledYieldAwaitable.ControlledYieldAwaiter(this, default); + + /// + /// Ends the wait for the completion of the yield operation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override void OnGetYieldResult(YieldAwaitable.YieldAwaiter awaiter) => awaiter.GetResult(); + + /// + /// Sets the action to perform when the yield operation completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override void OnYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter) => + awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action that is invoked when the yield operation completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override void OnUnsafeYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter) => + awaiter.UnsafeOnCompleted(continuation); + + /// + /// Creates a mutual exclusion lock that is compatible with objects. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal override ControlledLock CreateControlledLock() + { + var id = (ulong)Interlocked.Increment(ref this.LockIdCounter) - 1; + return new ControlledLock(id); + } + + /// + /// Creates a new timer that sends a to its owner machine. + /// + internal override IMachineTimer CreateMachineTimer(TimerInfo info, Machine owner) => new MachineTimer(info, owner); + + /// + /// Tries to create a new of the specified . + /// + internal override void TryCreateMonitor(Type type) + { + // Check if monitors are enabled in production. + if (!this.Configuration.EnableMonitorsInProduction) + { + return; + } + + lock (this.Monitors) + { + if (this.Monitors.Any(m => m.GetType() == type)) + { + // Idempotence: only one monitor per type can exist. + return; + } + } + + this.Assert(type.IsSubclassOf(typeof(Monitor)), "Type '{0}' is not a subclass of Monitor.", type.FullName); + + MachineId mid = new MachineId(type, null, this); + Monitor monitor = (Monitor)Activator.CreateInstance(type); + + monitor.Initialize(this, mid); + monitor.InitializeStateInformation(); + + lock (this.Monitors) + { + this.Monitors.Add(monitor); + } + + this.LogWriter.OnCreateMonitor(type.FullName, monitor.Id); + + monitor.GotoStartState(); + } + + /// + /// Invokes the specified with the specified . + /// + internal override void Monitor(Type type, AsyncMachine sender, Event e) + { + // Check if monitors are enabled in production. + if (!this.Configuration.EnableMonitorsInProduction) + { + return; + } + + Monitor monitor = null; + + lock (this.Monitors) + { + foreach (var m in this.Monitors) + { + if (m.GetType() == type) + { + monitor = m; + break; + } + } + } + + if (monitor != null) + { + lock (monitor) + { + monitor.MonitorEvent(e); + } + } + } + + /// + /// Returns a nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal override bool GetNondeterministicBooleanChoice(AsyncMachine machine, int maxValue) + { + Random random = new Random(DateTime.Now.Millisecond); + + bool result = false; + if (random.Next(maxValue) == 0) + { + result = true; + } + + this.LogWriter.OnRandom(machine?.Id, result); + + return result; + } + + /// + /// Returns a fair nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal override bool GetFairNondeterministicBooleanChoice(AsyncMachine machine, string uniqueId) + { + return this.GetNondeterministicBooleanChoice(machine, 2); + } + + /// + /// Returns a nondeterministic integer choice, that can be + /// controlled during analysis or testing. + /// + internal override int GetNondeterministicIntegerChoice(AsyncMachine machine, int maxValue) + { + Random random = new Random(DateTime.Now.Millisecond); + var result = random.Next(maxValue); + + this.LogWriter.OnRandom(machine?.Id, result); + + return result; + } + + /// + /// Notifies that a machine entered a state. + /// + internal override void NotifyEnteredState(Machine machine) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineState(machine.Id, machine.CurrentStateName, isEntry: true); + } + } + + /// + /// Notifies that a monitor entered a state. + /// + internal override void NotifyEnteredState(Monitor monitor) + { + if (this.Configuration.IsVerbose) + { + string monitorState = monitor.CurrentStateNameWithTemperature; + this.LogWriter.OnMonitorState(monitor.GetType().FullName, monitor.Id, monitorState, true, monitor.GetHotState()); + } + } + + /// + /// Notifies that a machine exited a state. + /// + internal override void NotifyExitedState(Machine machine) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineState(machine.Id, machine.CurrentStateName, isEntry: false); + } + } + + /// + /// Notifies that a monitor exited a state. + /// + internal override void NotifyExitedState(Monitor monitor) + { + if (this.Configuration.IsVerbose) + { + string monitorState = monitor.CurrentStateNameWithTemperature; + this.LogWriter.OnMonitorState(monitor.GetType().FullName, monitor.Id, monitorState, false, monitor.GetHotState()); + } + } + + /// + /// Notifies that a machine invoked an action. + /// + internal override void NotifyInvokedAction(Machine machine, MethodInfo action, Event receivedEvent) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineAction(machine.Id, machine.CurrentStateName, action.Name); + } + } + + /// + /// Notifies that a machine invoked an action. + /// + internal override void NotifyInvokedOnEntryAction(Machine machine, MethodInfo action, Event receivedEvent) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineAction(machine.Id, machine.CurrentStateName, action.Name); + } + } + + /// + /// Notifies that a machine invoked an action. + /// + internal override void NotifyInvokedOnExitAction(Machine machine, MethodInfo action, Event receivedEvent) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineAction(machine.Id, machine.CurrentStateName, action.Name); + } + } + + /// + /// Notifies that a monitor invoked an action. + /// + internal override void NotifyInvokedAction(Monitor monitor, MethodInfo action, Event receivedEvent) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMonitorAction(monitor.GetType().FullName, monitor.Id, action.Name, monitor.CurrentStateName); + } + } + + /// + /// Notifies that a machine raised an . + /// + internal override void NotifyRaisedEvent(Machine machine, Event e, EventInfo eventInfo) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineEvent(machine.Id, machine.CurrentStateName, e.GetType().FullName); + } + } + + /// + /// Notifies that a monitor raised an . + /// + internal override void NotifyRaisedEvent(Monitor monitor, Event e, EventInfo eventInfo) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMonitorEvent(monitor.GetType().FullName, monitor.Id, monitor.CurrentStateName, + e.GetType().FullName, isProcessing: false); + } + } + + /// + /// Notifies that a machine dequeued an . + /// + internal override void NotifyDequeuedEvent(Machine machine, Event e, EventInfo eventInfo) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnDequeue(machine.Id, machine.CurrentStateName, e.GetType().FullName); + } + } + + /// + /// Notifies that a machine is waiting to receive an event of one of the specified types. + /// + internal override void NotifyWaitEvent(Machine machine, IEnumerable eventTypes) + { + if (this.Configuration.IsVerbose) + { + var eventWaitTypesArray = eventTypes.ToArray(); + if (eventWaitTypesArray.Length == 1) + { + this.LogWriter.OnWait(machine.Id, machine.CurrentStateName, eventWaitTypesArray[0]); + } + else + { + this.LogWriter.OnWait(machine.Id, machine.CurrentStateName, eventWaitTypesArray); + } + } + } + + /// + /// Notifies that a machine enqueued an event that it was waiting to receive. + /// + internal override void NotifyReceivedEvent(Machine machine, Event e, EventInfo eventInfo) + { + this.LogWriter.OnReceive(machine.Id, machine.CurrentStateName, e.GetType().FullName, wasBlocked: true); + } + + /// + /// Notifies that a machine received an event without waiting because the event + /// was already in the inbox when the machine invoked the receive statement. + /// + internal override void NotifyReceivedEventWithoutWaiting(Machine machine, Event e, EventInfo eventInfo) + { + this.LogWriter.OnReceive(machine.Id, machine.CurrentStateName, e.GetType().FullName, wasBlocked: false); + } + + /// + /// Disposes runtime resources. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.Monitors.Clear(); + this.MachineMap.Clear(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Source/Core/Runtime/Providers/AsyncLocalRuntimeProvider.cs b/Source/Core/Runtime/Providers/AsyncLocalRuntimeProvider.cs new file mode 100644 index 000000000..3c130e824 --- /dev/null +++ b/Source/Core/Runtime/Providers/AsyncLocalRuntimeProvider.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Provides access to the runtime associated with the current asynchronous control flow. + /// + internal class AsyncLocalRuntimeProvider : RuntimeProvider + { + /// + /// Stores the runtime executing an asynchronous control flow. + /// + private static readonly AsyncLocal AsyncLocalRuntime = new AsyncLocal(); + + /// + /// The currently executing runtime. + /// + internal override CoyoteRuntime Current => AsyncLocalRuntime.Value ?? + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, + "Uncontrolled task with id '{0}' tried to access the runtime. Please make sure to avoid using concurrency " + + "APIs such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers or controlled tasks. If you " + + "are using external libraries that are executing concurrently, you will need to mock them during testing.", + Task.CurrentId.HasValue ? Task.CurrentId.Value.ToString() : "")); + + /// + /// Initializes a new instance of the class. + /// + internal AsyncLocalRuntimeProvider(CoyoteRuntime runtime) + : base() + { + this.SetCurrentRuntime(runtime); + } + + /// + /// Sets the runtime associated with the current execution context. + /// + internal override void SetCurrentRuntime(CoyoteRuntime runtime) => AsyncLocalRuntime.Value = runtime; + } +} diff --git a/Source/Core/Runtime/Providers/RuntimeProvider.cs b/Source/Core/Runtime/Providers/RuntimeProvider.cs new file mode 100644 index 000000000..baaee545a --- /dev/null +++ b/Source/Core/Runtime/Providers/RuntimeProvider.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.Runtime +{ + /// + /// Provides access to the runtime associated with the current execution context. + /// + internal class RuntimeProvider + { + /// + /// The default executing runtime. + /// + private static readonly CoyoteRuntime Default = new ProductionRuntime(Configuration.Create()); + + /// + /// The currently executing runtime. + /// + /// + /// This can only be set/get internally today -- in testing, everything is serialized, and before + /// each iteration, this runtime is set in a local async context, so that should be thread safe. + /// In production, its a singleton that is only set when this type is loaded in the very beginning. + /// If we allow the user to override/set it manually, we should have some assertion that its not done + /// after the runtime "starts", else it could become really expensive to have a lock here to check it + /// every time we call these properties. + /// + internal virtual CoyoteRuntime Current => Default; + + /// + /// Sets the runtime associated with the current execution context. + /// + internal virtual void SetCurrentRuntime(CoyoteRuntime runtime) + { + } + } +} diff --git a/Source/Core/Runtime/TestAttributes.cs b/Source/Core/Runtime/TestAttributes.cs new file mode 100644 index 000000000..022dad35d --- /dev/null +++ b/Source/Core/Runtime/TestAttributes.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote +{ + /// + /// Attribute for declaring the entry point to + /// a Coyote program test. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class TestAttribute : Attribute + { + } + + /// + /// Attribute for declaring the initialization + /// method to be called before testing starts. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class TestInitAttribute : Attribute + { + } + + /// + /// Attribute for declaring a cleanup method to be + /// called when all test iterations terminate. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class TestDisposeAttribute : Attribute + { + } + + /// + /// Attribute for declaring a cleanup method to be + /// called when each test iteration terminates. + /// + [AttributeUsage(AttributeTargets.Method)] + public sealed class TestIterationDisposeAttribute : Attribute + { + } + + /// + /// Attribute for declaring the factory method that creates + /// the Coyote testing runtime. This is an advanced feature, + /// only to be used for replacing the original Coyote testing + /// runtime with an alternative implementation. + /// + [AttributeUsage(AttributeTargets.Method)] + internal sealed class TestRuntimeCreateAttribute : Attribute + { + } +} diff --git a/Source/Core/Specifications/Monitors/Monitor.cs b/Source/Core/Specifications/Monitors/Monitor.cs new file mode 100644 index 000000000..ad8d5a4bf --- /dev/null +++ b/Source/Core/Specifications/Monitors/Monitor.cs @@ -0,0 +1,846 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Utilities; + +using EventInfo = Microsoft.Coyote.Runtime.EventInfo; + +namespace Microsoft.Coyote.Specifications +{ + /// + /// Abstract class representing a Coyote monitor. + /// + public abstract class Monitor + { + /// + /// Map from monitor types to a set of all + /// possible states types. + /// + private static readonly ConcurrentDictionary> StateTypeMap = + new ConcurrentDictionary>(); + + /// + /// Map from monitor types to a set of all + /// available states. + /// + private static readonly ConcurrentDictionary> StateMap = + new ConcurrentDictionary>(); + + /// + /// Map from monitor types to a set of all + /// available actions. + /// + private static readonly ConcurrentDictionary> MonitorActionMap = + new ConcurrentDictionary>(); + + /// + /// The runtime that executes this monitor. + /// + private CoyoteRuntime Runtime; + + /// + /// The monitor state. + /// + private MonitorState State; + + /// + /// Dictionary containing all the current goto state transitions. + /// + internal Dictionary GotoTransitions; + + /// + /// Dictionary containing all the current action bindings. + /// + internal Dictionary ActionBindings; + + /// + /// Map from action names to actions. + /// + private readonly Dictionary ActionMap; + + /// + /// Set of currently ignored event types. + /// + private HashSet IgnoredEvents; + + /// + /// A counter that increases in each step of the execution, + /// as long as the monitor remains in a hot state. If the + /// temperature reaches the specified limit, then a potential + /// liveness bug has been found. + /// + private int LivenessTemperature; + + /// + /// The unique monitor id. + /// + internal MachineId Id { get; private set; } + + /// + /// Gets the name of this monitor. + /// + protected internal string Name => this.Id.Name; + + /// + /// The logger installed to the Coyote runtime. + /// + protected ILogger Logger => this.Runtime.Logger; + + /// + /// Gets the current state. + /// + protected internal Type CurrentState + { + get + { + if (this.State is null) + { + return null; + } + + return this.State.GetType(); + } + } + + /// + /// Gets the current state name. + /// + internal string CurrentStateName + { + get => NameResolver.GetQualifiedStateName(this.CurrentState); + } + + /// + /// Gets the current state name with temperature. + /// + internal string CurrentStateNameWithTemperature + { + get + { + return this.CurrentStateName + + (this.IsInHotState() ? "[hot]" : + this.IsInColdState() ? "[cold]" : + string.Empty); + } + } + + /// + /// Returns a nullable boolean indicating liveness temperature: true for hot, false for cold, else null. + /// + internal bool? GetHotState() + { + return this.IsInHotState() ? true : + this.IsInColdState() ? (bool?)false : + null; + } + + /// + /// Gets the latest received event, or null if no event + /// has been received. + /// + protected internal Event ReceivedEvent { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + protected Monitor() + : base() + { + this.ActionMap = new Dictionary(); + this.LivenessTemperature = 0; + } + + /// + /// Initializes this monitor. + /// + /// The runtime that executes this monitor. + /// The monitor id. + internal void Initialize(CoyoteRuntime runtime, MachineId mid) + { + this.Id = mid; + this.Runtime = runtime; + } + + /// + /// Returns from the execution context, and transitions + /// the monitor to the given . + /// + /// Type of the state. + protected void Goto() + where S : MonitorState + { +#pragma warning disable 618 + this.Goto(typeof(S)); +#pragma warning restore 618 + } + + /// + /// Returns from the execution context, and transitions + /// the monitor to the given . + /// + /// Type of the state. + [Obsolete("Goto(typeof(T)) is deprecated; use Goto() instead.")] + protected void Goto(Type s) + { + // If the state is not a state of the monitor, then report an error and exit. + this.Assert(StateTypeMap[this.GetType()].Any(val => val.DeclaringType.Equals(s.DeclaringType) && val.Name.Equals(s.Name)), + "Monitor '{0}' is trying to transition to non-existing state '{1}'.", this.GetType().Name, s.Name); + this.Raise(new GotoStateEvent(s)); + } + + /// + /// Raises an internally and returns from the execution context. + /// + /// The event to raise. + protected void Raise(Event e) + { + // If the event is null, then report an error and exit. + this.Assert(e != null, "Monitor '{0}' is raising a null event.", this.GetType().Name); + + var eventOrigin = new EventOriginInfo(this.Id, this.GetType().FullName, NameResolver.GetQualifiedStateName(this.CurrentState)); + EventInfo raisedEvent = new EventInfo(e, eventOrigin); + this.Runtime.NotifyRaisedEvent(this, e, raisedEvent); + this.HandleEvent(e); + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + protected void Assert(bool predicate) + { + this.Runtime.Assert(predicate); + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + protected void Assert(bool predicate, string s, params object[] args) + { + this.Runtime.Assert(predicate, s, args); + } + + /// + /// Notifies the monitor to handle the received event. + /// + /// The event to monitor. + internal void MonitorEvent(Event e) + { + this.Runtime.LogWriter.OnMonitorEvent(this.GetType().Name, this.Id, this.CurrentStateName, + e.GetType().FullName, isProcessing: true); + this.HandleEvent(e); + } + + /// + /// Handles the given event. + /// + private void HandleEvent(Event e) + { + // Do not process an ignored event. + if (this.IsEventIgnoredInCurrentState(e)) + { + return; + } + + // Assigns the receieved event. + this.ReceivedEvent = e; + + while (true) + { + if (this.State is null) + { + // If the event cannot be handled, then report an error and exit. + this.Assert(false, "Monitor '{0}' received event '{1}' that cannot be handled.", + this.GetType().Name, e.GetType().FullName); + } + + // If current state cannot handle the event then null the state. + if (!this.CanHandleEvent(e.GetType())) + { + this.Runtime.NotifyExitedState(this); + this.State = null; + continue; + } + + if (e.GetType() == typeof(GotoStateEvent)) + { + // Checks if the event is a goto state event. + Type targetState = (e as GotoStateEvent).State; + this.GotoState(targetState, null); + } + else if (this.GotoTransitions.ContainsKey(e.GetType())) + { + // Checks if the event can trigger a goto state transition. + var transition = this.GotoTransitions[e.GetType()]; + this.GotoState(transition.TargetState, transition.Lambda); + } + else if (this.GotoTransitions.ContainsKey(typeof(WildCardEvent))) + { + // Checks if the event can trigger a goto state transition. + var transition = this.GotoTransitions[typeof(WildCardEvent)]; + this.GotoState(transition.TargetState, transition.Lambda); + } + else if (this.ActionBindings.ContainsKey(e.GetType())) + { + // Checks if the event can trigger an action. + var handler = this.ActionBindings[e.GetType()]; + this.Do(handler.Name); + } + else if (this.ActionBindings.ContainsKey(typeof(WildCardEvent))) + { + // Checks if the event can trigger an action. + var handler = this.ActionBindings[typeof(WildCardEvent)]; + this.Do(handler.Name); + } + + break; + } + } + + /// + /// Checks if the specified event is ignored in the current monitor state. + /// + private bool IsEventIgnoredInCurrentState(Event e) + { + if (this.IgnoredEvents.Contains(e.GetType()) || + this.IgnoredEvents.Contains(typeof(WildCardEvent))) + { + return true; + } + + return false; + } + + /// + /// Invokes an action. + /// + [System.Diagnostics.DebuggerStepThrough] + private void Do(string actionName) + { + MethodInfo action = this.ActionMap[actionName]; + this.Runtime.NotifyInvokedAction(this, action, this.ReceivedEvent); + this.ExecuteAction(action); + } + + /// + /// Executes the on entry function of the current state. + /// + [System.Diagnostics.DebuggerStepThrough] + private void ExecuteCurrentStateOnEntry() + { + this.Runtime.NotifyEnteredState(this); + + MethodInfo entryAction = null; + if (this.State.EntryAction != null) + { + entryAction = this.ActionMap[this.State.EntryAction]; + } + + // Invokes the entry action of the new state, + // if there is one available. + if (entryAction != null) + { + this.ExecuteAction(entryAction); + } + } + + /// + /// Executes the on exit function of the current state. + /// + [System.Diagnostics.DebuggerStepThrough] + private void ExecuteCurrentStateOnExit(string eventHandlerExitActionName) + { + this.Runtime.NotifyExitedState(this); + + MethodInfo exitAction = null; + if (this.State.ExitAction != null) + { + exitAction = this.ActionMap[this.State.ExitAction]; + } + + // Invokes the exit action of the current state, + // if there is one available. + if (exitAction != null) + { + this.ExecuteAction(exitAction); + } + + // Invokes the exit action of the event handler, + // if there is one available. + if (eventHandlerExitActionName != null) + { + MethodInfo eventHandlerExitAction = this.ActionMap[eventHandlerExitActionName]; + this.ExecuteAction(eventHandlerExitAction); + } + } + + /// + /// Executes the specified action. + /// + [System.Diagnostics.DebuggerStepThrough] + private void ExecuteAction(MethodInfo action) + { + try + { + action.Invoke(this, null); + } + catch (Exception ex) + { + Exception innerException = ex; + while (innerException is TargetInvocationException) + { + innerException = innerException.InnerException; + } + + if (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + if (innerException is ExecutionCanceledException || + innerException is TaskSchedulerException) + { +#pragma warning disable CA2200 // Rethrow to preserve stack details. + throw ex; +#pragma warning restore CA2200 // Rethrow to preserve stack details. + } + else + { + // Reports the unhandled exception. + this.ReportUnhandledException(innerException, action.Name); + } + } + } + + /// + /// Performs a goto transition to the given state. + /// + private void GotoState(Type s, string onExitActionName) + { + // The monitor performs the on exit statements of the current state. + this.ExecuteCurrentStateOnExit(onExitActionName); + + var nextState = StateMap[this.GetType()].First(val => val.GetType().Equals(s)); + this.ConfigureStateTransitions(nextState); + + // The monitor transitions to the new state. + this.State = nextState; + + if (nextState.IsCold) + { + this.LivenessTemperature = 0; + } + + // The monitor performs the on entry statements of the new state. + this.ExecuteCurrentStateOnEntry(); + } + + /// + /// Checks if the state can handle the given event type. An event + /// can be handled if it is deferred, or leads to a transition or + /// action binding. + /// + private bool CanHandleEvent(Type e) + { + if (this.GotoTransitions.ContainsKey(e) || + this.GotoTransitions.ContainsKey(typeof(WildCardEvent)) || + this.ActionBindings.ContainsKey(e) || + this.ActionBindings.ContainsKey(typeof(WildCardEvent)) || + e == typeof(GotoStateEvent)) + { + return true; + } + + return false; + } + + /// + /// Checks the liveness temperature of the monitor and report + /// a potential liveness bug if the temperature passes the + /// specified threshold. Only works in a liveness monitor. + /// + internal void CheckLivenessTemperature() + { + if (this.State.IsHot && + this.Runtime.Configuration.LivenessTemperatureThreshold > 0) + { + this.LivenessTemperature++; + this.Runtime.Assert( + this.LivenessTemperature <= this.Runtime. + Configuration.LivenessTemperatureThreshold, + "Monitor '{0}' detected potential liveness bug in hot state '{1}'.", + this.GetType().Name, this.CurrentStateName); + } + } + + /// + /// Checks the liveness temperature of the monitor and report + /// a potential liveness bug if the temperature passes the + /// specified threshold. Only works in a liveness monitor. + /// + internal void CheckLivenessTemperature(int livenessTemperature) + { + if (livenessTemperature > this.Runtime.Configuration.LivenessTemperatureThreshold) + { + this.Runtime.Assert( + livenessTemperature <= this.Runtime.Configuration.LivenessTemperatureThreshold, + $"Monitor '{this.GetType().Name}' detected infinite execution that violates a liveness property."); + } + } + + /// + /// Returns true if the monitor is in a hot state. + /// + internal bool IsInHotState() => this.State.IsHot; + + /// + /// Returns true if the monitor is in a hot state. Also outputs + /// the name of the current state. + /// + internal bool IsInHotState(out string stateName) + { + stateName = this.CurrentStateName; + return this.State.IsHot; + } + + /// + /// Returns true if the monitor is in a cold state. + /// + internal bool IsInColdState() => this.State.IsCold; + + /// + /// Returns true if the monitor is in a cold state. Also outputs + /// the name of the current state. + /// + internal bool IsInColdState(out string stateName) + { + stateName = this.CurrentStateName; + return this.State.IsCold; + } + + /// + /// Returns the hashed state of this monitor. + /// + protected virtual int GetHashedState() + { + return 0; + } + + /// + /// Returns the cached state of this monitor. + /// + internal int GetCachedState() + { + unchecked + { + var hash = 19; + + hash = (hash * 31) + this.GetType().GetHashCode(); + hash = (hash * 31) + this.CurrentState.GetHashCode(); + + // Adds the user-defined hashed state. + hash = (hash * 31) + this.GetHashedState(); + + return hash; + } + } + + /// + /// Returns a string that represents the current monitor. + /// + public override string ToString() => this.GetType().Name; + + /// + /// Transitions to the start state, and executes the + /// entry action, if there is any. + /// + internal void GotoStartState() + { + this.ExecuteCurrentStateOnEntry(); + } + + /// + /// Initializes information about the states of the monitor. + /// + internal void InitializeStateInformation() + { + Type monitorType = this.GetType(); + + // Caches the available state types for this monitor type. + if (StateTypeMap.TryAdd(monitorType, new HashSet())) + { + Type baseType = monitorType; + while (baseType != typeof(Monitor)) + { + foreach (var s in baseType.GetNestedTypes(BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.DeclaredOnly)) + { + this.ExtractStateTypes(s); + } + + baseType = baseType.BaseType; + } + } + + // Caches the available state instances for this monitor type. + if (StateMap.TryAdd(monitorType, new HashSet())) + { + foreach (var type in StateTypeMap[monitorType]) + { + Type stateType = type; + if (type.IsAbstract) + { + continue; + } + + if (type.IsGenericType) + { + // If the state type is generic (only possible if inherited by a + // generic monitor declaration), then iterate through the base + // monitor classes to identify the runtime generic type, and use + // it to instantiate the runtime state type. This type can be + // then used to create the state constructor. + Type declaringType = this.GetType(); + while (!declaringType.IsGenericType || + !type.DeclaringType.FullName.Equals(declaringType.FullName.Substring( + 0, declaringType.FullName.IndexOf('[')))) + { + declaringType = declaringType.BaseType; + } + + if (declaringType.IsGenericType) + { + stateType = type.MakeGenericType(declaringType.GetGenericArguments()); + } + } + + ConstructorInfo constructor = stateType.GetConstructor(Type.EmptyTypes); + var lambda = Expression.Lambda>( + Expression.New(constructor)).Compile(); + MonitorState state = lambda(); + + state.InitializeState(); + + this.Assert( + (state.IsCold && !state.IsHot) || + (!state.IsCold && state.IsHot) || + (!state.IsCold && !state.IsHot), + "State '{0}' of monitor '{1}' cannot be both cold and hot.", type.FullName, this.GetType().Name); + + StateMap[monitorType].Add(state); + } + } + + // Caches the actions declarations for this monitor type. + if (MonitorActionMap.TryAdd(monitorType, new Dictionary())) + { + foreach (var state in StateMap[monitorType]) + { + if (state.EntryAction != null && + !MonitorActionMap[monitorType].ContainsKey(state.EntryAction)) + { + MonitorActionMap[monitorType].Add( + state.EntryAction, + this.GetActionWithName(state.EntryAction)); + } + + if (state.ExitAction != null && + !MonitorActionMap[monitorType].ContainsKey(state.ExitAction)) + { + MonitorActionMap[monitorType].Add( + state.ExitAction, + this.GetActionWithName(state.ExitAction)); + } + + foreach (var transition in state.GotoTransitions) + { + if (transition.Value.Lambda != null && + !MonitorActionMap[monitorType].ContainsKey(transition.Value.Lambda)) + { + MonitorActionMap[monitorType].Add( + transition.Value.Lambda, + this.GetActionWithName(transition.Value.Lambda)); + } + } + + foreach (var action in state.ActionBindings) + { + if (!MonitorActionMap[monitorType].ContainsKey(action.Value.Name)) + { + MonitorActionMap[monitorType].Add( + action.Value.Name, + this.GetActionWithName(action.Value.Name)); + } + } + } + } + + // Populates the map of actions for this monitor instance. + foreach (var kvp in MonitorActionMap[monitorType]) + { + this.ActionMap.Add(kvp.Key, kvp.Value); + } + + var initialStates = StateMap[monitorType].Where(state => state.IsStart).ToList(); + this.Assert(initialStates.Count != 0, "Monitor '{0}' must declare a start state.", this.GetType().Name); + this.Assert(initialStates.Count == 1, "Monitor '{0}' can not declare more than one start states.", this.GetType().Name); + + this.ConfigureStateTransitions(initialStates.Single()); + this.State = initialStates.Single(); + + this.AssertStateValidity(); + } + + /// + /// Processes a type, looking for monitor states. + /// + private void ExtractStateTypes(Type type) + { + Stack stack = new Stack(); + stack.Push(type); + + while (stack.Count > 0) + { + Type nextType = stack.Pop(); + + if (nextType.IsClass && nextType.IsSubclassOf(typeof(MonitorState))) + { + StateTypeMap[this.GetType()].Add(nextType); + } + else if (nextType.IsClass && nextType.IsSubclassOf(typeof(StateGroup))) + { + // Adds the contents of the group of states to the stack. + foreach (var t in nextType.GetNestedTypes(BindingFlags.Instance | + BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.DeclaredOnly)) + { + this.Assert(t.IsSubclassOf(typeof(StateGroup)) || t.IsSubclassOf(typeof(MonitorState)), + "'{0}' is neither a group of states nor a state.", t.Name); + stack.Push(t); + } + } + } + } + + /// + /// Configures the state transitions of the monitor. + /// + private void ConfigureStateTransitions(MonitorState state) + { + this.GotoTransitions = state.GotoTransitions; + this.ActionBindings = state.ActionBindings; + this.IgnoredEvents = state.IgnoredEvents; + } + + /// + /// Returns the action with the specified name. + /// + private MethodInfo GetActionWithName(string actionName) + { + MethodInfo method; + Type monitorType = this.GetType(); + + do + { + method = monitorType.GetMethod( + actionName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy, + Type.DefaultBinder, Array.Empty(), null); + monitorType = monitorType.BaseType; + } + while (method is null && monitorType != typeof(Monitor)); + + this.Assert(method != null, "Cannot detect action declaration '{0}' in monitor '{1}'.", + actionName, this.GetType().Name); + this.Assert(method.GetParameters().Length == 0, "Action '{0}' in monitor '{1}' must have 0 formal parameters.", + method.Name, this.GetType().Name); + this.Assert(method.ReturnType == typeof(void), "Action '{0}' in monitor '{1}' must have 'void' return type.", + method.Name, this.GetType().Name); + + return method; + } + + /// + /// Check monitor for state related errors. + /// + private void AssertStateValidity() + { + this.Assert(StateTypeMap[this.GetType()].Count > 0, "Monitor '{0}' must have one or more states.", this.GetType().Name); + this.Assert(this.State != null, "Monitor '{0}' must not have a null current state.", this.GetType().Name); + } + + /// + /// Wraps the unhandled exception inside an + /// exception, and throws it to the user. + /// + private void ReportUnhandledException(Exception ex, string actionName) + { + var state = this.CurrentState is null ? "" : this.CurrentStateName; + this.Runtime.WrapAndThrowException(ex, $"Exception '{ex.GetType()}' was thrown " + + $"in monitor '{this.GetType().Name}', state '{state}', action '{actionName}', " + + $"'{ex.Source}':\n" + + $" {ex.Message}\n" + + $"The stack trace is:\n{ex.StackTrace}"); + } + + /// + /// Returns the set of all states in the monitor (for code coverage). + /// + internal HashSet GetAllStates() + { + this.Assert(StateMap.ContainsKey(this.GetType()), "Monitor '{0}' hasn't populated its states yet.", this.GetType().Name); + + var allStates = new HashSet(); + foreach (var state in StateMap[this.GetType()]) + { + allStates.Add(NameResolver.GetQualifiedStateName(state.GetType())); + } + + return allStates; + } + + /// + /// Returns the set of all (states, registered event) pairs in the monitor (for code coverage). + /// + internal HashSet> GetAllStateEventPairs() + { + this.Assert(StateMap.ContainsKey(this.GetType()), "Monitor '{0}' hasn't populated its states yet.", this.GetType().Name); + + var pairs = new HashSet>(); + foreach (var state in StateMap[this.GetType()]) + { + foreach (var binding in state.ActionBindings) + { + pairs.Add(Tuple.Create(NameResolver.GetQualifiedStateName(state.GetType()), binding.Key.FullName)); + } + + foreach (var transition in state.GotoTransitions) + { + pairs.Add(Tuple.Create(NameResolver.GetQualifiedStateName(state.GetType()), transition.Key.FullName)); + } + } + + return pairs; + } + + /// + /// Resets the static caches. + /// + internal static void ResetCaches() + { + StateTypeMap.Clear(); + StateMap.Clear(); + MonitorActionMap.Clear(); + } + } +} diff --git a/Source/Core/Specifications/Monitors/MonitorState.cs b/Source/Core/Specifications/Monitors/MonitorState.cs new file mode 100644 index 000000000..db858a758 --- /dev/null +++ b/Source/Core/Specifications/Monitors/MonitorState.cs @@ -0,0 +1,474 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.Specifications +{ + /// + /// Abstract class representing a state of a specification monitor. + /// + public abstract class MonitorState + { + /// + /// Attribute for declaring that a state of a monitor + /// is the start one. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class StartAttribute : Attribute + { + } + + /// + /// Attribute for declaring what action to perform + /// when entering a monitor state. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class OnEntryAttribute : Attribute + { + /// + /// Action name. + /// + internal readonly string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Action name + public OnEntryAttribute(string actionName) + { + this.Action = actionName; + } + } + + /// + /// Attribute for declaring what action to perform + /// when exiting a monitor state. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class OnExitAttribute : Attribute + { + /// + /// Action name. + /// + internal string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Action name + public OnExitAttribute(string actionName) + { + this.Action = actionName; + } + } + + /// + /// Attribute for declaring which state a monitor should transition to + /// when it receives an event in a given state. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + protected sealed class OnEventGotoStateAttribute : Attribute + { + /// + /// Event type. + /// + internal readonly Type Event; + + /// + /// State type. + /// + internal readonly Type State; + + /// + /// Action name. + /// + internal readonly string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Event type + /// State type + public OnEventGotoStateAttribute(Type eventType, Type stateType) + { + this.Event = eventType; + this.State = stateType; + } + + /// + /// Initializes a new instance of the class. + /// + /// Event type + /// State type + /// Name of action to perform on exit + public OnEventGotoStateAttribute(Type eventType, Type stateType, string actionName) + { + this.Event = eventType; + this.State = stateType; + this.Action = actionName; + } + } + + /// + /// Attribute for declaring what action a monitor should perform + /// when it receives an event in a given state. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + protected sealed class OnEventDoActionAttribute : Attribute + { + /// + /// Event type. + /// + internal Type Event; + + /// + /// Action name. + /// + internal string Action; + + /// + /// Initializes a new instance of the class. + /// + /// Event type + /// Action name + public OnEventDoActionAttribute(Type eventType, string actionName) + { + this.Event = eventType; + this.Action = actionName; + } + } + + /// + /// Attribute for declaring what events should be ignored in + /// a monitor state. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class IgnoreEventsAttribute : Attribute + { + /// + /// Event types. + /// + internal Type[] Events; + + /// + /// Initializes a new instance of the class. + /// + /// Event types + public IgnoreEventsAttribute(params Type[] eventTypes) + { + this.Events = eventTypes; + } + } + + /// + /// Attribute for declaring a cold monitor state. A monitor that + /// is in a cold state satisfies a liveness property. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class ColdAttribute : Attribute + { + } + + /// + /// Attribute for declaring a hot monitor state. A monitor that + /// is in a hot state violates a liveness property. + /// + [AttributeUsage(AttributeTargets.Class)] + protected sealed class HotAttribute : Attribute + { + } + + /// + /// The entry action of the state. + /// + internal string EntryAction { get; private set; } + + /// + /// The exit action of the state. + /// + internal string ExitAction { get; private set; } + + /// + /// Dictionary containing all the goto state transitions. + /// + internal Dictionary GotoTransitions; + + /// + /// Dictionary containing all the action bindings. + /// + internal Dictionary ActionBindings; + + /// + /// Set of ignored event types. + /// + internal HashSet IgnoredEvents; + + /// + /// True if this is the start state. + /// + internal bool IsStart { get; private set; } + + /// + /// Returns true if this is a hot state. + /// + internal bool IsHot { get; private set; } + + /// + /// Returns true if this is a cold state. + /// + internal bool IsCold { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + protected MonitorState() + { + } + + /// + /// Initializes the state. + /// + internal void InitializeState() + { + this.IsStart = false; + this.IsHot = false; + this.IsCold = false; + + this.GotoTransitions = new Dictionary(); + this.ActionBindings = new Dictionary(); + + this.IgnoredEvents = new HashSet(); + + if (this.GetType().GetCustomAttribute(typeof(OnEntryAttribute), true) is OnEntryAttribute entryAttribute) + { + this.EntryAction = entryAttribute.Action; + } + + if (this.GetType().GetCustomAttribute(typeof(OnExitAttribute), true) is OnExitAttribute exitAttribute) + { + this.ExitAction = exitAttribute.Action; + } + + if (this.GetType().IsDefined(typeof(StartAttribute), false)) + { + this.IsStart = true; + } + + if (this.GetType().IsDefined(typeof(HotAttribute), false)) + { + this.IsHot = true; + } + + if (this.GetType().IsDefined(typeof(ColdAttribute), false)) + { + this.IsCold = true; + } + + // Events with already declared handlers. + var handledEvents = new HashSet(); + + // Install event handlers. + this.InstallGotoTransitions(handledEvents); + this.InstallActionHandlers(handledEvents); + this.InstallIgnoreHandlers(handledEvents); + } + + /// + /// Declares goto event handlers, if there are any. + /// + private void InstallGotoTransitions(HashSet handledEvents) + { + var gotoAttributes = this.GetType().GetCustomAttributes(typeof(OnEventGotoStateAttribute), false) + as OnEventGotoStateAttribute[]; + + foreach (var attr in gotoAttributes) + { + CheckEventHandlerAlreadyDeclared(attr.Event, handledEvents); + + if (attr.Action is null) + { + this.GotoTransitions.Add(attr.Event, new GotoStateTransition(attr.State)); + } + else + { + this.GotoTransitions.Add(attr.Event, new GotoStateTransition(attr.State, attr.Action)); + } + + handledEvents.Add(attr.Event); + } + + this.InheritGotoTransitions(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits goto event handlers from a base state, if there is one. + /// + private void InheritGotoTransitions(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MonitorState))) + { + return; + } + + var gotoAttributesInherited = baseState.GetCustomAttributes(typeof(OnEventGotoStateAttribute), false) + as OnEventGotoStateAttribute[]; + + var gotoTransitionsInherited = new Dictionary(); + foreach (var attr in gotoAttributesInherited) + { + if (this.GotoTransitions.ContainsKey(attr.Event)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(attr.Event, baseState, handledEvents); + + if (attr.Action is null) + { + gotoTransitionsInherited.Add(attr.Event, new GotoStateTransition(attr.State)); + } + else + { + gotoTransitionsInherited.Add(attr.Event, new GotoStateTransition(attr.State, attr.Action)); + } + + handledEvents.Add(attr.Event); + } + + foreach (var kvp in gotoTransitionsInherited) + { + this.GotoTransitions.Add(kvp.Key, kvp.Value); + } + + this.InheritGotoTransitions(baseState.BaseType, handledEvents); + } + + /// + /// Declares action event handlers, if there are any. + /// + private void InstallActionHandlers(HashSet handledEvents) + { + var doAttributes = this.GetType().GetCustomAttributes(typeof(OnEventDoActionAttribute), false) + as OnEventDoActionAttribute[]; + + foreach (var attr in doAttributes) + { + CheckEventHandlerAlreadyDeclared(attr.Event, handledEvents); + + this.ActionBindings.Add(attr.Event, new ActionBinding(attr.Action)); + handledEvents.Add(attr.Event); + } + + this.InheritActionHandlers(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits action event handlers from a base state, if there is one. + /// + private void InheritActionHandlers(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MonitorState))) + { + return; + } + + var doAttributesInherited = baseState.GetCustomAttributes(typeof(OnEventDoActionAttribute), false) + as OnEventDoActionAttribute[]; + + var actionBindingsInherited = new Dictionary(); + foreach (var attr in doAttributesInherited) + { + if (this.ActionBindings.ContainsKey(attr.Event)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(attr.Event, baseState, handledEvents); + + actionBindingsInherited.Add(attr.Event, new ActionBinding(attr.Action)); + handledEvents.Add(attr.Event); + } + + foreach (var kvp in actionBindingsInherited) + { + this.ActionBindings.Add(kvp.Key, kvp.Value); + } + + this.InheritActionHandlers(baseState.BaseType, handledEvents); + } + + /// + /// Declares ignore event handlers, if there are any. + /// + private void InstallIgnoreHandlers(HashSet handledEvents) + { + if (this.GetType().GetCustomAttribute(typeof(IgnoreEventsAttribute), false) is IgnoreEventsAttribute ignoreEventsAttribute) + { + foreach (var e in ignoreEventsAttribute.Events) + { + CheckEventHandlerAlreadyDeclared(e, handledEvents); + } + + this.IgnoredEvents.UnionWith(ignoreEventsAttribute.Events); + handledEvents.UnionWith(ignoreEventsAttribute.Events); + } + + this.InheritIgnoreHandlers(this.GetType().BaseType, handledEvents); + } + + /// + /// Inherits ignore event handlers from a base state, if there is one. + /// + private void InheritIgnoreHandlers(Type baseState, HashSet handledEvents) + { + if (!baseState.IsSubclassOf(typeof(MonitorState))) + { + return; + } + + if (baseState.GetCustomAttribute(typeof(IgnoreEventsAttribute), false) is IgnoreEventsAttribute ignoreEventsAttribute) + { + foreach (var e in ignoreEventsAttribute.Events) + { + if (this.IgnoredEvents.Contains(e)) + { + continue; + } + + CheckEventHandlerAlreadyInherited(e, baseState, handledEvents); + } + + this.IgnoredEvents.UnionWith(ignoreEventsAttribute.Events); + handledEvents.UnionWith(ignoreEventsAttribute.Events); + } + + this.InheritIgnoreHandlers(baseState.BaseType, handledEvents); + } + + /// + /// Checks if an event handler has been already declared. + /// + private static void CheckEventHandlerAlreadyDeclared(Type e, HashSet handledEvents) + { + if (handledEvents.Contains(e)) + { + throw new InvalidOperationException($"declared multiple handlers for event '{e}'"); + } + } + + /// + /// Checks if an event handler has been already inherited. + /// + private static void CheckEventHandlerAlreadyInherited(Type e, Type baseState, HashSet handledEvents) + { + if (handledEvents.Contains(e)) + { + throw new InvalidOperationException($"inherited multiple handlers for event '{e}' from state '{baseState}'"); + } + } + } +} diff --git a/Source/Core/Specifications/Specification.cs b/Source/Core/Specifications/Specification.cs new file mode 100644 index 000000000..24791ea79 --- /dev/null +++ b/Source/Core/Specifications/Specification.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Specifications +{ + /// + /// Provides static methods that are useful for writing specifications + /// and interacting with the systematic testing engine. + /// + public static class Specification + { + /// + /// Checks if the predicate holds, and if not, throws an exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Assert(bool predicate, string s, object arg0) => + CoyoteRuntime.Provider.Current.Assert(predicate, s, arg0); + + /// + /// Checks if the predicate holds, and if not, throws an exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Assert(bool predicate, string s, object arg0, object arg1) => + CoyoteRuntime.Provider.Current.Assert(predicate, s, arg0, arg1); + + /// + /// Checks if the predicate holds, and if not, throws an exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Assert(bool predicate, string s, object arg0, object arg1, object arg2) => + CoyoteRuntime.Provider.Current.Assert(predicate, s, arg0, arg1, arg2); + + /// + /// Checks if the predicate holds, and if not, throws an exception. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Assert(bool predicate, string s, params object[] args) => + CoyoteRuntime.Provider.Current.Assert(predicate, s, args); + + /// + /// Returns a nondeterministic boolean choice, that can be controlled during analysis or testing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ChooseRandomBoolean() => CoyoteRuntime.Provider.Current.GetNondeterministicBooleanChoice(null, 2); + + /// + /// Returns a nondeterministic boolean choice, that can be controlled during analysis or testing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ChooseRandomBoolean(int maxValue) => CoyoteRuntime.Provider.Current.GetNondeterministicBooleanChoice(null, maxValue); + + /// + /// Returns a nondeterministic integer, that can be controlled during analysis or testing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ChooseRandomInteger(int maxValue) => CoyoteRuntime.Provider.Current.GetNondeterministicIntegerChoice(null, maxValue); + + /// + /// Registers a new safety or liveness monitor. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RegisterMonitor() + where T : Monitor => + CoyoteRuntime.Provider.Current.RegisterMonitor(typeof(T)); + + /// + /// Invokes the specified monitor with the given event. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Monitor(Event e) + where T : Monitor => + CoyoteRuntime.Provider.Current.Monitor(typeof(T), null, e); + } +} diff --git a/Source/Core/Threading/ControlledLock.cs b/Source/Core/Threading/ControlledLock.cs new file mode 100644 index 000000000..94b9092a9 --- /dev/null +++ b/Source/Core/Threading/ControlledLock.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.Threading +{ + /// + /// A mutual exclusion lock that can be acquired asynchronously + /// by a . + /// + public class ControlledLock + { + /// + /// Unique id of the lock. + /// + public readonly ulong Id; + + /// + /// Queue of tasks awaiting to acquire the lock. + /// + private readonly Queue> Awaiters; + + /// + /// True if the lock has been acquired, else false. + /// + protected internal bool IsAcquired; + + /// + /// Initializes a new instance of the class. + /// + internal ControlledLock(ulong id) + { + this.Id = id; + this.Awaiters = new Queue>(); + this.IsAcquired = false; + } + + /// + /// Creates a new mutual exclusion lock. + /// + /// The mutual exclusion lock. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledLock Create() => CoyoteRuntime.Provider.Current.CreateControlledLock(); + + /// + /// Tries to acquire the lock asynchronously, and returns a task that completes + /// when the lock has been acquired. The returned task contains a releaser that + /// releases the lock when disposed. + /// + public virtual ControlledTask AcquireAsync() + { + lock (this.Awaiters) + { + if (!this.IsAcquired) + { + this.IsAcquired = true; + return ControlledTask.FromResult(new Releaser(this)); + } + else + { + var waiter = new TaskCompletionSource(); + this.Awaiters.Enqueue(waiter); + return waiter.Task.ContinueWith((_, state) => new Releaser((ControlledLock)state), this, + CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default). + ToControlledTask(); + } + } + } + + /// + /// Releases the lock. + /// + protected virtual void Release() + { + TaskCompletionSource awaiter = null; + lock (this.Awaiters) + { + if (this.Awaiters.Count > 0) + { + awaiter = this.Awaiters.Dequeue(); + } + else + { + this.IsAcquired = false; + } + } + + if (awaiter != null) + { + awaiter.SetResult(null); + } + } + + /// + /// Releases the acquired when disposed. + /// + public struct Releaser : IDisposable + { + /// + /// The acquired lock. + /// + private readonly ControlledLock Lock; + + /// + /// Initializes a new instance of the struct. + /// + internal Releaser(ControlledLock taskLock) + { + this.Lock = taskLock; + } + + /// + /// Releases the acquired lock. + /// + public void Dispose() => this.Lock?.Release(); + } + } +} diff --git a/Source/Core/Threading/Tasks/AsyncControlledTaskMethodBuilder.cs b/Source/Core/Threading/Tasks/AsyncControlledTaskMethodBuilder.cs new file mode 100644 index 000000000..e5a25b319 --- /dev/null +++ b/Source/Core/Threading/Tasks/AsyncControlledTaskMethodBuilder.cs @@ -0,0 +1,294 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Threading.Tasks +{ + /// + /// Represents a builder for asynchronous methods that return a . + /// This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + [StructLayout(LayoutKind.Auto)] + public struct AsyncControlledTaskMethodBuilder + { + /// + /// The to which most operations are delegated. + /// +#pragma warning disable IDE0044 // Add readonly modifier + private AsyncTaskMethodBuilder MethodBuilder; +#pragma warning restore IDE0044 // Add readonly modifier + + /// + /// True, if completed synchronously and successfully, else false. + /// + private bool IsCompleted; + + /// + /// True, if the builder should be used for setting/getting the result, else false. + /// + private bool UseBuilder; + + /// + /// Gets the task for this builder. + /// + public ControlledTask Task + { + [DebuggerHidden] + get + { + if (this.IsCompleted) + { + IO.Debug.WriteLine(" Creating completed builder task '{0}' (isCompleted {1}) from task '{2}'.", + this.MethodBuilder.Task.Id, this.MethodBuilder.Task.IsCompleted, System.Threading.Tasks.Task.CurrentId); + return ControlledTask.CompletedTask; + } + else + { + IO.Debug.WriteLine(" Creating builder task '{0}' (isCompleted {1}) from task '{2}'.", + this.MethodBuilder.Task.Id, this.MethodBuilder.Task.IsCompleted, System.Threading.Tasks.Task.CurrentId); + this.UseBuilder = true; + return CoyoteRuntime.Provider.Current.CreateControlledTaskCompletionSource(this.MethodBuilder.Task); + } + } + } + + /// + /// Creates an instance of the struct. + /// + [DebuggerHidden] + public static AsyncControlledTaskMethodBuilder Create() + { + IO.Debug.WriteLine(" Creating async builder from task '{0}'.", System.Threading.Tasks.Task.CurrentId); + return default; + } + + /// + /// Begins running the builder with the associated state machine. + /// + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable CA1822 // Mark members as static + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine + { + IO.Debug.WriteLine(" Move next from task '{0}'.", System.Threading.Tasks.Task.CurrentId); + this.MethodBuilder.Start(ref stateMachine); + } +#pragma warning restore CA1822 // Mark members as static + + /// + /// Associates the builder with the specified state machine. + /// + [DebuggerHidden] + public void SetStateMachine(IAsyncStateMachine stateMachine) + { + IO.Debug.WriteLine(" Set state machine from task '{0}'.", System.Threading.Tasks.Task.CurrentId); + this.MethodBuilder.SetStateMachine(stateMachine); + } + + /// + /// Marks the task as successfully completed. + /// + [DebuggerHidden] + public void SetResult() + { + if (this.UseBuilder) + { + IO.Debug.WriteLine(" Set result of task '{0}' from task '{1}'.", + this.MethodBuilder.Task.Id, System.Threading.Tasks.Task.CurrentId); + this.MethodBuilder.SetResult(); + } + else + { + IO.Debug.WriteLine(" Set result (completed) from task '{0}'.", System.Threading.Tasks.Task.CurrentId); + this.IsCompleted = true; + } + } + + /// + /// Marks the task as failed and binds the specified exception to the task. + /// + [DebuggerHidden] + public void SetException(Exception exception) => this.MethodBuilder.SetException(exception); + + /// + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// + [DebuggerHidden] + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + this.UseBuilder = true; + CoyoteRuntime.Provider.Current.AssertAwaitingControlledAwaiter(ref awaiter); + this.MethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); + } + + /// + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// + [DebuggerHidden] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + this.UseBuilder = true; + CoyoteRuntime.Provider.Current.AssertAwaitingUnsafeControlledAwaiter(ref awaiter); + this.MethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + } + } + + /// + /// Represents a builder for asynchronous methods that return a . + /// This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + [StructLayout(LayoutKind.Auto)] + public struct AsyncControlledTaskMethodBuilder + { + /// + /// The to which most operations are delegated. + /// +#pragma warning disable IDE0044 // Add readonly modifier + private AsyncTaskMethodBuilder MethodBuilder; +#pragma warning restore IDE0044 // Add readonly modifier + + /// + /// The result for this builder, if it's completed before any awaits occur. + /// + private TResult Result; + + /// + /// True, if completed synchronously and successfully, else false. + /// + private bool IsCompleted; + + /// + /// True, if the builder should be used for setting/getting the result, else false. + /// + private bool UseBuilder; + + /// + /// Gets the task for this builder. + /// + public ControlledTask Task + { + [DebuggerHidden] + get + { + if (this.IsCompleted) + { + IO.Debug.WriteLine(" Creating completed builder task '{0}' (completed '{1}', result '{2}', result type '{3}') from task '{4}'.", + this.MethodBuilder.Task.Id, this.MethodBuilder.Task.IsCompleted, this.Result, typeof(TResult), System.Threading.Tasks.Task.CurrentId); + return ControlledTask.FromResult(this.Result); + } + else + { + IO.Debug.WriteLine(" Creating builder task '{0}' (completed '{1}', result '{2}', result type '{3}') from task '{4}'.", + this.MethodBuilder.Task.Id, this.MethodBuilder.Task.IsCompleted, this.Result, typeof(TResult), System.Threading.Tasks.Task.CurrentId); + this.UseBuilder = true; + return CoyoteRuntime.Provider.Current.CreateControlledTaskCompletionSource(this.MethodBuilder.Task); + } + } + } + + /// + /// Creates an instance of the struct. + /// +#pragma warning disable CA1000 // Do not declare static members on generic types + [DebuggerHidden] + public static AsyncControlledTaskMethodBuilder Create() + { + IO.Debug.WriteLine(" Creating async builder with result type '{0}' from task '{1}'.", + typeof(TResult), System.Threading.Tasks.Task.CurrentId); + return default; + } +#pragma warning restore CA1000 // Do not declare static members on generic types + + /// + /// Begins running the builder with the associated state machine. + /// + [DebuggerStepThrough] + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable CA1822 // Mark members as static + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine + { + IO.Debug.WriteLine(" Move next from task '{0}' (result type '{1}').", + System.Threading.Tasks.Task.CurrentId, typeof(TResult)); + this.MethodBuilder.Start(ref stateMachine); + } +#pragma warning restore CA1822 // Mark members as static + + /// + /// Associates the builder with the specified state machine. + /// + [DebuggerHidden] + public void SetStateMachine(IAsyncStateMachine stateMachine) + { + IO.Debug.WriteLine(" Set state machine with result type '{0}' from task '{1}'.", + typeof(TResult), System.Threading.Tasks.Task.CurrentId); + this.MethodBuilder.SetStateMachine(stateMachine); + } + + /// + /// Marks the task as successfully completed. + /// + /// The result to use to complete the task. + [DebuggerHidden] + public void SetResult(TResult result) + { + if (this.UseBuilder) + { + IO.Debug.WriteLine(" Set result with type '{0}' of task '{1}' from task '{2}'.", + typeof(TResult), this.MethodBuilder.Task.Id, System.Threading.Tasks.Task.CurrentId); + this.MethodBuilder.SetResult(result); + } + else + { + IO.Debug.WriteLine(" Set completed result '{0}' with type '{1}' from task '{2}'.", + result, typeof(TResult), System.Threading.Tasks.Task.CurrentId); + this.Result = result; + this.IsCompleted = true; + } + } + + /// + /// Marks the task as failed and binds the specified exception to the task. + /// + [DebuggerHidden] + public void SetException(Exception exception) => this.MethodBuilder.SetException(exception); + + /// + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// + [DebuggerHidden] + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + this.UseBuilder = true; + CoyoteRuntime.Provider.Current.AssertAwaitingControlledAwaiter(ref awaiter); + this.MethodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine); + } + + /// + /// Schedules the state machine to proceed to the next action when the specified awaiter completes. + /// + [DebuggerHidden] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + this.UseBuilder = true; + CoyoteRuntime.Provider.Current.AssertAwaitingUnsafeControlledAwaiter(ref awaiter); + this.MethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); + } + } +} diff --git a/Source/Core/Threading/Tasks/ConfiguredControlledTaskAwaitable.cs b/Source/Core/Threading/Tasks/ConfiguredControlledTaskAwaitable.cs new file mode 100644 index 000000000..0fd910d19 --- /dev/null +++ b/Source/Core/Threading/Tasks/ConfiguredControlledTaskAwaitable.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.Coyote.Threading.Tasks +{ + /// + /// Provides an awaitable object that is the outcome of invoking . + /// This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + public struct ConfiguredControlledTaskAwaitable + { + /// + /// The task awaiter. + /// + private readonly ConfiguredControlledTaskAwaiter Awaiter; + + /// + /// Initializes a new instance of the struct. + /// + internal ConfiguredControlledTaskAwaitable(ControlledTask task, Task awaiterTask, bool continueOnCapturedContext) + { + this.Awaiter = new ConfiguredControlledTaskAwaiter(task, awaiterTask, continueOnCapturedContext); + } + + /// + /// Returns an awaiter for this awaitable object. + /// + /// The awaiter. + public ConfiguredControlledTaskAwaiter GetAwaiter() => this.Awaiter; + + /// + /// Provides an awaiter for an awaitable object. This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + public struct ConfiguredControlledTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion + { + /// + /// The controlled task being awaited. + /// + private readonly ControlledTask ControlledTask; + + /// + /// The task awaiter. + /// + private readonly ConfiguredTaskAwaitable.ConfiguredTaskAwaiter Awaiter; + + /// + /// Gets a value that indicates whether the controlled task has completed. + /// + public bool IsCompleted => this.ControlledTask.IsCompleted; + + /// + /// Initializes a new instance of the struct. + /// + internal ConfiguredControlledTaskAwaiter(ControlledTask task, Task awaiterTask, bool continueOnCapturedContext) + { + this.ControlledTask = task; + this.Awaiter = awaiterTask.ConfigureAwait(continueOnCapturedContext).GetAwaiter(); + } + + /// + /// Ends the await on the completed task. + /// + public void GetResult() => this.ControlledTask.GetResult(this.Awaiter); + + /// + /// Schedules the continuation action for the task associated with this awaiter. + /// + /// The action to invoke when the await operation completes. + public void OnCompleted(Action continuation) => + this.ControlledTask.OnCompleted(continuation, this.Awaiter); + + /// + /// Schedules the continuation action for the task associated with this awaiter. + /// + /// The action to invoke when the await operation completes. + public void UnsafeOnCompleted(Action continuation) => + this.ControlledTask.UnsafeOnCompleted(continuation, this.Awaiter); + } + } + + /// + /// Provides an awaitable object that enables configured awaits on a . + /// This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + public struct ConfiguredControlledTaskAwaitable + { + /// + /// The task awaiter. + /// + private readonly ConfiguredControlledTaskAwaiter Awaiter; + + /// + /// Initializes a new instance of the struct. + /// + internal ConfiguredControlledTaskAwaitable(ControlledTask task, Task awaiterTask, + bool continueOnCapturedContext) + { + this.Awaiter = new ConfiguredControlledTaskAwaiter(task, awaiterTask, continueOnCapturedContext); + } + + /// + /// Returns an awaiter for this awaitable object. + /// + /// The awaiter. + public ConfiguredControlledTaskAwaiter GetAwaiter() => this.Awaiter; + + /// + /// Provides an awaiter for an awaitable object. This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + public struct ConfiguredControlledTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion + { + /// + /// The controlled task being awaited. + /// + private readonly ControlledTask ControlledTask; + + /// + /// The task awaiter. + /// + private readonly ConfiguredTaskAwaitable.ConfiguredTaskAwaiter Awaiter; + + /// + /// Gets a value that indicates whether the controlled task has completed. + /// + public bool IsCompleted => this.ControlledTask.IsCompleted; + + /// + /// Initializes a new instance of the struct. + /// + internal ConfiguredControlledTaskAwaiter(ControlledTask task, Task awaiterTask, + bool continueOnCapturedContext) + { + this.ControlledTask = task; + this.Awaiter = awaiterTask.ConfigureAwait(continueOnCapturedContext).GetAwaiter(); + } + + /// + /// Ends the await on the completed task. + /// + public TResult GetResult() => this.ControlledTask.GetResult(this.Awaiter); + + /// + /// Schedules the continuation action for the task associated with this awaiter. + /// + /// The action to invoke when the await operation completes. + public void OnCompleted(Action continuation) => + this.ControlledTask.OnCompleted(continuation, this.Awaiter); + + /// + /// Schedules the continuation action for the task associated with this awaiter. + /// + /// The action to invoke when the await operation completes. + public void UnsafeOnCompleted(Action continuation) => + this.ControlledTask.UnsafeOnCompleted(continuation, this.Awaiter); + } + } +} diff --git a/Source/Core/Threading/Tasks/ControlledTask.cs b/Source/Core/Threading/Tasks/ControlledTask.cs new file mode 100644 index 000000000..c91b97f6e --- /dev/null +++ b/Source/Core/Threading/Tasks/ControlledTask.cs @@ -0,0 +1,637 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Threading.Tasks +{ + /// + /// Represents an asynchronous operation. Each is a thin wrapper + /// over and each call simply invokes the wrapped task. During testing, a + /// is controlled by the runtime and systematically interleaved + /// with other asynchronous operations to find bugs. + /// + [AsyncMethodBuilder(typeof(AsyncControlledTaskMethodBuilder))] + public class ControlledTask : IDisposable + { + /// + /// A that has completed successfully. + /// + public static ControlledTask CompletedTask { get; } = new ControlledTask(Task.CompletedTask); + + /// + /// Returns the id of the currently executing . + /// + public static int? CurrentId => CoyoteRuntime.Provider.Current.CurrentTaskId; + + /// + /// Internal task used to execute the work. + /// + private protected readonly Task InternalTask; + + /// + /// The id of this task. + /// + public int Id => this.InternalTask.Id; + + /// + /// Task that provides access to the completed work. + /// + internal Task AwaiterTask => this.InternalTask; + + /// + /// Value that indicates whether the task has completed. + /// + public bool IsCompleted => this.InternalTask.IsCompleted; + + /// + /// Value that indicates whether the task completed execution due to being canceled. + /// + public bool IsCanceled => this.InternalTask.IsCanceled; + + /// + /// Value that indicates whether the task completed due to an unhandled exception. + /// + public bool IsFaulted => this.InternalTask.IsFaulted; + + /// + /// Gets the that caused the task + /// to end prematurely. If the task completed successfully or has not yet + /// thrown any exceptions, this will return null. + /// + public AggregateException Exception => this.InternalTask.Exception; + + /// + /// The status of this task. + /// + public TaskStatus Status => this.InternalTask.Status; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal ControlledTask(Task task) + { + this.InternalTask = task ?? throw new ArgumentNullException(nameof(task)); + } + + /// + /// Creates a that is completed successfully with the specified result. + /// + /// The type of the result returned by the task. + /// The result to store into the completed task. + /// The successfully completed task. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask FromResult(TResult result) => + new ControlledTask(Task.FromResult(result)); + + /// + /// Creates a that is completed due to + /// cancellation with a specified cancellation token. + /// + /// The cancellation token with which to complete the task. + /// The canceled task. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask FromCanceled(CancellationToken cancellationToken) => + new ControlledTask(Task.FromCanceled(cancellationToken)); + + /// + /// Creates a that is completed due to + /// cancellation with a specified cancellation token. + /// + /// The type of the result returned by the task. + /// The cancellation token with which to complete the task. + /// The canceled task. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask FromCanceled(CancellationToken cancellationToken) => + new ControlledTask(Task.FromCanceled(cancellationToken)); + + /// + /// Creates a that is completed with a specified exception. + /// + /// The exception with which to complete the task. + /// The faulted task. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask FromException(Exception exception) => + new ControlledTask(Task.FromException(exception)); + + /// + /// Creates a that is completed with a specified exception. + /// + /// The type of the result returned by the task. + /// The exception with which to complete the task. + /// The faulted task. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask FromException(Exception exception) => + new ControlledTask(Task.FromException(exception)); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. A cancellation token allows the work to be cancelled. + /// + /// The work to execute asynchronously. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Action action) => + CoyoteRuntime.Provider.Current.CreateControlledTask(action, default); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. + /// + /// The work to execute asynchronously. + /// Cancellation token that can be used to cancel the work. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Action action, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.CreateControlledTask(action, cancellationToken); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. + /// + /// The work to execute asynchronously. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Func function) => + CoyoteRuntime.Provider.Current.CreateControlledTask(function, default); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. A cancellation token allows the work to be cancelled. + /// + /// The work to execute asynchronously. + /// Cancellation token that can be used to cancel the work. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Func function, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.CreateControlledTask(function, cancellationToken); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. + /// + /// The result type of the task. + /// The work to execute asynchronously. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Func function) => + CoyoteRuntime.Provider.Current.CreateControlledTask(function, default); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. A cancellation token allows the work to be cancelled. + /// + /// The result type of the task. + /// The work to execute asynchronously. + /// Cancellation token that can be used to cancel the work. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Func function, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.CreateControlledTask(function, cancellationToken); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. + /// + /// The result type of the task. + /// The work to execute asynchronously. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Func> function) => + CoyoteRuntime.Provider.Current.CreateControlledTask(function, default); + + /// + /// Queues the specified work to run on the thread pool and returns a + /// object that represents that work. A cancellation token allows the work to be cancelled. + /// + /// The result type of the task. + /// The work to execute asynchronously. + /// Cancellation token that can be used to cancel the work. + /// Task that represents the work to run asynchronously. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Run(Func> function, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.CreateControlledTask(function, cancellationToken); + + /// + /// Creates a that completes after a time delay. + /// + /// + /// The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely. + /// + /// Task that represents the time delay. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Delay(int millisecondsDelay) => + CoyoteRuntime.Provider.Current.CreateControlledTaskDelay(millisecondsDelay, default); + + /// + /// Creates a that completes after a time delay. + /// + /// + /// The number of milliseconds to wait before completing the returned task, or -1 to wait indefinitely. + /// + /// Cancellation token that can be used to cancel the delay. + /// Task that represents the time delay. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Delay(int millisecondsDelay, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.CreateControlledTaskDelay(millisecondsDelay, cancellationToken); + + /// + /// Creates a that completes after a specified time interval. + /// + /// + /// The time span to wait before completing the returned task, or TimeSpan.FromMilliseconds(-1) + /// to wait indefinitely. + /// + /// Task that represents the time delay. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Delay(TimeSpan delay) => + CoyoteRuntime.Provider.Current.CreateControlledTaskDelay(delay, default); + + /// + /// Creates a that completes after a specified time interval. + /// + /// + /// The time span to wait before completing the returned task, or TimeSpan.FromMilliseconds(-1) + /// to wait indefinitely. + /// + /// Cancellation token that can be used to cancel the delay. + /// Task that represents the time delay. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask Delay(TimeSpan delay, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.CreateControlledTaskDelay(delay, cancellationToken); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(params ControlledTask[] tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(params Task[] tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(IEnumerable tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(IEnumerable tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + /// The result type of the task. + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(params ControlledTask[] tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + /// The result type of the task. + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(params Task[] tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + /// The result type of the task. + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(IEnumerable> tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + /// The result type of the task. + /// The tasks to wait for completion. + /// Task that represents the completion of all of the specified tasks. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAll(IEnumerable> tasks) => + CoyoteRuntime.Provider.Current.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAny(params ControlledTask[] tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAny(params Task[] tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAny(IEnumerable tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask WhenAny(IEnumerable tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask> WhenAny(params ControlledTask[] tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask> WhenAny(params Task[] tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask> WhenAny(IEnumerable> tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledTask> WhenAny(IEnumerable> tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTaskAsync(tasks); + + /// + /// Waits for any of the provided objects to complete execution. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WaitAny(params ControlledTask[] tasks) => + CoyoteRuntime.Provider.Current.WaitAnyTask(tasks); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WaitAny(ControlledTask[] tasks, int millisecondsTimeout) => + CoyoteRuntime.Provider.Current.WaitAnyTask(tasks, millisecondsTimeout); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds or until a cancellation + /// token is cancelled. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WaitAny(ControlledTask[] tasks, int millisecondsTimeout, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.WaitAnyTask(tasks, millisecondsTimeout, cancellationToken); + + /// + /// Waits for any of the provided objects to complete + /// execution unless the wait is cancelled. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WaitAny(ControlledTask[] tasks, CancellationToken cancellationToken) => + CoyoteRuntime.Provider.Current.WaitAnyTask(tasks, cancellationToken); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified time interval. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WaitAny(ControlledTask[] tasks, TimeSpan timeout) => + CoyoteRuntime.Provider.Current.WaitAnyTask(tasks, timeout); + + /// + /// Creates an awaitable that asynchronously yields back to the current context when awaited. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ControlledYieldAwaitable Yield() => default; + + /// + /// Converts the specified into a . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task ToTask() => this.InternalTask; + + /// + /// Gets an awaiter for this awaitable. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual ControlledTaskAwaiter GetAwaiter() => new ControlledTaskAwaiter(this, this.InternalTask); + + /// + /// Ends the wait for the completion of the task. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void GetResult(TaskAwaiter awaiter) => awaiter.GetResult(); + + /// + /// Sets the action to perform when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void OnCompleted(Action continuation, TaskAwaiter awaiter) => awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void UnsafeOnCompleted(Action continuation, TaskAwaiter awaiter) => + awaiter.UnsafeOnCompleted(continuation); + + /// + /// Configures an awaiter used to await this task. + /// + /// + /// True to attempt to marshal the continuation back to the original context captured; otherwise, false. + /// + public virtual ConfiguredControlledTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => + new ConfiguredControlledTaskAwaitable(this, this.InternalTask, continueOnCapturedContext); + + /// + /// Injects a context switch point that can be systematically explored during testing. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ExploreContextSwitch() => CoyoteRuntime.Provider.Current.ExploreContextSwitch(); + + /// + /// Ends the wait for the completion of the task. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void GetResult(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => awaiter.GetResult(); + + /// + /// Sets the action to perform when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void OnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void UnsafeOnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + awaiter.UnsafeOnCompleted(continuation); + + /// + /// Disposes the , releasing all of its unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } + + /// + /// Disposes the , releasing all of its unmanaged resources. + /// + /// + /// Unlike most of the members of , this method is not thread-safe. + /// + public void Dispose() + { + this.InternalTask.Dispose(); + this.Dispose(true); + GC.SuppressFinalize(this); + } + } + + /// + /// Represents an asynchronous operation that can return a value. Each + /// is a thin wrapper over and each call simply invokes the wrapped task. During + /// testing, a is controlled by the runtime and systematically interleaved with + /// other asynchronous operations to find bugs. + /// + /// The type of the produced result. + [AsyncMethodBuilder(typeof(AsyncControlledTaskMethodBuilder<>))] + public class ControlledTask : ControlledTask + { + /// + /// Task that provides access to the completed work. + /// + internal new Task AwaiterTask => this.InternalTask as Task; + + /// + /// Gets the result value of this task. + /// + public TResult Result => this.AwaiterTask.Result; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal ControlledTask(Task task) + : base(task) + { + } + + /// + /// Gets an awaiter for this awaitable. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public new virtual ControlledTaskAwaiter GetAwaiter() => + new ControlledTaskAwaiter(this, this.AwaiterTask); + + /// + /// Ends the wait for the completion of the task. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual TResult GetResult(TaskAwaiter awaiter) => awaiter.GetResult(); + + /// + /// Sets the action to perform when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void OnCompleted(Action continuation, TaskAwaiter awaiter) => + awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void UnsafeOnCompleted(Action continuation, TaskAwaiter awaiter) => + awaiter.UnsafeOnCompleted(continuation); + + /// + /// Configures an awaiter used to await this task. + /// + /// + /// True to attempt to marshal the continuation back to the original context captured; otherwise, false. + /// + public new virtual ConfiguredControlledTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) => + new ConfiguredControlledTaskAwaitable(this, this.AwaiterTask, continueOnCapturedContext); + + /// + /// Ends the wait for the completion of the task. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual TResult GetResult(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + awaiter.GetResult(); + + /// + /// Sets the action to perform when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void OnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + awaiter.OnCompleted(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal virtual void UnsafeOnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + awaiter.UnsafeOnCompleted(continuation); + } +} diff --git a/Source/Core/Threading/Tasks/ControlledTaskAwaiter.cs b/Source/Core/Threading/Tasks/ControlledTaskAwaiter.cs new file mode 100644 index 000000000..4eff252eb --- /dev/null +++ b/Source/Core/Threading/Tasks/ControlledTaskAwaiter.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.Coyote.Threading.Tasks +{ + /// + /// Implements a awaiter. This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + public readonly struct ControlledTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion + { + // WARNING: The layout must remain the same, as the struct is used to access + // the generic ControlledTaskAwaiter<> as ControlledTaskAwaiter. + + /// + /// The controlled task being awaited. + /// + private readonly ControlledTask ControlledTask; + + /// + /// The task awaiter. + /// + private readonly TaskAwaiter Awaiter; + + /// + /// Gets a value that indicates whether the controlled task has completed. + /// + public bool IsCompleted => this.ControlledTask.IsCompleted; + + /// + /// Initializes a new instance of the struct. + /// + [DebuggerStepThrough] + internal ControlledTaskAwaiter(ControlledTask task, Task awaiterTask) + { + this.ControlledTask = task; + this.Awaiter = awaiterTask.GetAwaiter(); + } + + /// + /// Ends the wait for the completion of the controlled task. + /// + [DebuggerHidden] + public void GetResult() => this.ControlledTask.GetResult(this.Awaiter); + + /// + /// Sets the action to perform when the controlled task completes. + /// + [DebuggerHidden] + public void OnCompleted(Action continuation) => + this.ControlledTask.OnCompleted(continuation, this.Awaiter); + + /// + /// Schedules the continuation action that is invoked when the controlled task completes. + /// + [DebuggerHidden] + public void UnsafeOnCompleted(Action continuation) => + this.ControlledTask.UnsafeOnCompleted(continuation, this.Awaiter); + } + + /// + /// Implements a awaiter. This type is intended for compiler use only. + /// + /// The type of the produced result. + /// This type is intended for compiler use rather than use directly in code. + public readonly struct ControlledTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion + { + // WARNING: The layout must remain the same, as the struct is used to access + // the generic ControlledTaskAwaiter<> as ControlledTaskAwaiter. + + /// + /// The controlled task being awaited. + /// + private readonly ControlledTask ControlledTask; + + /// + /// The task awaiter. + /// + private readonly TaskAwaiter Awaiter; + + /// + /// Gets a value that indicates whether the controlled task has completed. + /// + public bool IsCompleted => this.ControlledTask.IsCompleted; + + /// + /// Initializes a new instance of the struct. + /// + [DebuggerStepThrough] + internal ControlledTaskAwaiter(ControlledTask task, Task awaiterTask) + { + this.ControlledTask = task; + this.Awaiter = awaiterTask.GetAwaiter(); + } + + /// + /// Ends the wait for the completion of the controlled task. + /// + [DebuggerHidden] + public TResult GetResult() => this.ControlledTask.GetResult(this.Awaiter); + + /// + /// Sets the action to perform when the controlled task completes. + /// + [DebuggerHidden] + public void OnCompleted(Action continuation) => + this.ControlledTask.OnCompleted(continuation, this.Awaiter); + + /// + /// Schedules the continuation action that is invoked when the controlled task completes. + /// + [DebuggerHidden] + public void UnsafeOnCompleted(Action continuation) => + this.ControlledTask.UnsafeOnCompleted(continuation, this.Awaiter); + } +} diff --git a/Source/Core/Threading/Tasks/ControlledTaskMachine.cs b/Source/Core/Threading/Tasks/ControlledTaskMachine.cs new file mode 100644 index 000000000..452ff3d55 --- /dev/null +++ b/Source/Core/Threading/Tasks/ControlledTaskMachine.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Threading.Tasks +{ + /// + /// Abstract machine that can execute a asynchronously. + /// + internal abstract class ControlledTaskMachine : AsyncMachine + { + /// + /// The id of the task that provides access to the completed work. + /// + internal abstract int AwaiterTaskId { get; } + + /// + /// Id used to identify subsequent operations performed by this machine. + /// + protected internal override Guid OperationGroupId { get; set; } = Guid.Empty; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal ControlledTaskMachine(CoyoteRuntime runtime) + { + var mid = new MachineId(this.GetType(), "ControlledTask", runtime); + this.Initialize(runtime, mid); + } + + /// + /// Executes the work asynchronously. + /// + internal abstract Task ExecuteAsync(); + + /// + /// Tries to complete the machine with the specified exception. + /// + internal abstract void TryCompleteWithException(Exception exception); + } +} diff --git a/Source/Core/Threading/Tasks/ControlledYieldAwaitable.cs b/Source/Core/Threading/Tasks/ControlledYieldAwaitable.cs new file mode 100644 index 000000000..cab6d3902 --- /dev/null +++ b/Source/Core/Threading/Tasks/ControlledYieldAwaitable.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.CompilerServices; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Threading.Tasks +{ + /// + /// Implements an awaitable that asynchronously yields back to the current context when awaited. + /// This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + public readonly struct ControlledYieldAwaitable + { + /// + /// Gets an awaiter for this awaitable. + /// +#pragma warning disable CA1822 // Mark members as static + public ControlledYieldAwaiter GetAwaiter() => CoyoteRuntime.Provider.Current.CreateControlledYieldAwaiter(); +#pragma warning restore CA1822 // Mark members as static + + /// + /// Provides an awaiter that switches into a target environment. + /// This type is intended for compiler use only. + /// + /// This type is intended for compiler use rather than use directly in code. + public readonly struct ControlledYieldAwaiter : ICriticalNotifyCompletion, INotifyCompletion + { + /// + /// The runtime executing this awaiter. + /// + private readonly CoyoteRuntime Runtime; + + /// + /// The internal yield awaiter. + /// + private readonly YieldAwaitable.YieldAwaiter Awaiter; + + /// + /// Gets a value that indicates whether a yield is not required. + /// +#pragma warning disable CA1822 // Mark members as static + public bool IsCompleted => false; +#pragma warning restore CA1822 // Mark members as static + + /// + /// Initializes a new instance of the struct. + /// + internal ControlledYieldAwaiter(CoyoteRuntime runtime, YieldAwaitable.YieldAwaiter awaiter) + { + this.Runtime = runtime; + this.Awaiter = awaiter; + } + + /// + /// Ends the await operation. + /// + public void GetResult() => this.Runtime.OnGetYieldResult(this.Awaiter); + + /// + /// Posts the continuation action back to the current context. + /// + public void OnCompleted(Action continuation) => this.Runtime.OnYieldCompleted(continuation, this.Awaiter); + + /// + /// Posts the continuation action back to the current context. + /// + public void UnsafeOnCompleted(Action continuation) => this.Runtime.OnUnsafeYieldCompleted(continuation, this.Awaiter); + } + } +} diff --git a/Source/Core/Threading/Tasks/TaskExtensions.cs b/Source/Core/Threading/Tasks/TaskExtensions.cs new file mode 100644 index 000000000..d14afb1bf --- /dev/null +++ b/Source/Core/Threading/Tasks/TaskExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; + +namespace Microsoft.Coyote.Threading.Tasks +{ + /// + /// Extension methods for and objects. + /// + public static class TaskExtensions + { + /// + /// Converts the specified into a . + /// + public static ControlledTask ToControlledTask(this Task @this) => new ControlledTask(@this); + + /// + /// Converts the specified into a . + /// + public static ControlledTask ToControlledTask(this Task @this) => + new ControlledTask(@this); + } +} diff --git a/Source/Core/Utilities/ErrorReporter.cs b/Source/Core/Utilities/ErrorReporter.cs new file mode 100644 index 000000000..69d0a1cb3 --- /dev/null +++ b/Source/Core/Utilities/ErrorReporter.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.Utilities +{ + /// + /// Reports errors and warnings to the user. + /// + public sealed class ErrorReporter + { + /// + /// Configuration. + /// + private readonly Configuration Configuration; + + /// + /// The installed logger. + /// + internal ILogger Logger { get; set; } + + /// + /// Initializes a new instance of the class. + /// + internal ErrorReporter(Configuration configuration, ILogger logger) + { + this.Configuration = configuration; + this.Logger = logger ?? new ConsoleLogger(); + } + + /// + /// Reports an error, followed by the current line terminator. + /// + public void WriteErrorLine(string value) + { + this.Write("Error: ", ConsoleColor.Red); + this.Write(value, ConsoleColor.Yellow); + this.Logger.WriteLine(string.Empty); + } + + /// + /// Reports a warning, followed by the current line terminator. + /// + public void WriteWarningLine(string value) + { + if (this.Configuration.ShowWarnings) + { + this.Write("Warning: ", ConsoleColor.Red); + this.Write(value, ConsoleColor.Yellow); + this.Logger.WriteLine(string.Empty); + } + } + + /// + /// Writes the specified string value. + /// + private void Write(string value, ConsoleColor color) + { + ConsoleColor previousForegroundColor = default; + if (this.Configuration.EnableColoredConsoleOutput) + { + previousForegroundColor = Console.ForegroundColor; + Console.ForegroundColor = color; + } + + this.Logger.Write(value); + + if (this.Configuration.EnableColoredConsoleOutput) + { + Console.ForegroundColor = previousForegroundColor; + } + } + } +} diff --git a/Source/Core/Utilities/NameResolver.cs b/Source/Core/Utilities/NameResolver.cs new file mode 100644 index 000000000..9df679ea0 --- /dev/null +++ b/Source/Core/Utilities/NameResolver.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Globalization; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.Utilities +{ + /// + /// Utility class for resolving names. + /// + internal static class NameResolver + { + /// + /// Cache of state names. + /// + private static readonly ConcurrentDictionary StateNamesCache = + new ConcurrentDictionary(); + + /// + /// Returns the qualified (i.e. ) name of the specified + /// machine or monitor state, or the empty string if there is no such name. + /// + internal static string GetQualifiedStateName(Type state) + { + if (state is null) + { + return string.Empty; + } + + if (!StateNamesCache.TryGetValue(state, out string name)) + { + name = state.Name; + + var nextState = state; + while (nextState.DeclaringType != null) + { + if (!nextState.DeclaringType.IsSubclassOf(typeof(StateGroup))) + { + break; + } + + name = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", nextState.DeclaringType.Name, name); + nextState = nextState.DeclaringType; + } + + StateNamesCache.GetOrAdd(state, name); + } + + return name; + } + + /// + /// Returns the state name to be used for logging purposes. + /// + internal static string GetStateNameForLogging(Type state) => state is null ? "None" : GetQualifiedStateName(state); + } +} diff --git a/Source/Core/Utilities/Profiler.cs b/Source/Core/Utilities/Profiler.cs new file mode 100644 index 000000000..c1733248f --- /dev/null +++ b/Source/Core/Utilities/Profiler.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace Microsoft.Coyote.Utilities +{ + /// + /// The Coyote profiler. + /// + public sealed class Profiler + { + private Stopwatch StopWatch = null; + + /// + /// Starts measuring execution time. + /// + public void StartMeasuringExecutionTime() + { + this.StopWatch = new Stopwatch(); + this.StopWatch.Start(); + } + + /// + /// Stops measuring execution time. + /// + public void StopMeasuringExecutionTime() + { + if (this.StopWatch != null) + { + this.StopWatch.Stop(); + } + } + + /// + /// Returns profilling results. + /// + public double Results() => + this.StopWatch != null ? this.StopWatch.Elapsed.TotalSeconds : 0; + } +} diff --git a/Source/Core/Utilities/Tooling/BaseCommandLineOptions.cs b/Source/Core/Utilities/Tooling/BaseCommandLineOptions.cs new file mode 100644 index 000000000..fbbef1bcd --- /dev/null +++ b/Source/Core/Utilities/Tooling/BaseCommandLineOptions.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text.RegularExpressions; +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.Utilities +{ + /// + /// The Coyote base command line options. + /// + public abstract class BaseCommandLineOptions + { + /// + /// Configuration. + /// + protected Configuration Configuration; + + /// + /// Command line options. + /// + protected string[] Options; + + /// + /// Initializes a new instance of the class. + /// + /// Array of arguments + public BaseCommandLineOptions(string[] args) + { + this.Configuration = Configuration.Create(); + this.Options = args; + } + + /// + /// Parses the command line options and returns a configuration. + /// + /// Configuration + public Configuration Parse() + { + for (int idx = 0; idx < this.Options.Length; idx++) + { + this.ParseOption(this.Options[idx]); + } + + this.CheckForParsingErrors(); + this.UpdateConfiguration(); + return this.Configuration; + } + + /// + /// Parses the given option. + /// + /// Option + protected virtual void ParseOption(string option) + { + if (IsMatch(option, @"^[\/|-]?$")) + { + this.ShowHelp(); + Environment.Exit(0); + } + else if (IsMatch(option, @"^[\/|-]o:") && option.Length > 3) + { + this.Configuration.OutputFilePath = option.Substring(3); + } + else if (IsMatch(option, @"^[\/|-]v$")) + { + this.Configuration.IsVerbose = true; + } + else if (IsMatch(option, @"^[\/|-]v:") && option.Length > 3) + { + if (!int.TryParse(option.Substring(3), out int i) && i > 0 && i <= 3) + { + Error.ReportAndExit("This option is deprecated; please use '-v'."); + } + + this.Configuration.IsVerbose = i > 0; + } + else if (IsMatch(option, @"^[\/|-]debug$")) + { + this.Configuration.EnableDebugging = true; + Debug.IsEnabled = true; + } + else if (IsMatch(option, @"^[\/|-]warnings-on$")) + { + this.Configuration.ShowWarnings = true; + } + else if (IsMatch(option, @"^[\/|-]timeout:") && option.Length > 9) + { + if (!int.TryParse(option.Substring(9), out int i) && + i > 0) + { + Error.ReportAndExit("Please give a valid timeout '-timeout:[x]', where [x] > 0 seconds."); + } + + this.Configuration.Timeout = i; + } + else + { + this.ShowHelp(); + Error.ReportAndExit("cannot recognise command line option '" + option + "'."); + } + } + + /// + /// Checks for parsing errors. + /// + protected abstract void CheckForParsingErrors(); + + /// + /// Updates the configuration depending on the + /// user specified options. + /// + protected abstract void UpdateConfiguration(); + + /// + /// Shows help. + /// + protected abstract void ShowHelp(); + + /// + /// Checks if the given input is a matches the specified pattern. + /// + /// The input to match. + /// The pattern to match. + /// True if the input matches the pattern. + protected static bool IsMatch(string input, string pattern) + { + return Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase); + } + } +} diff --git a/Source/Core/Utilities/Tooling/SchedulingStrategy.cs b/Source/Core/Utilities/Tooling/SchedulingStrategy.cs new file mode 100644 index 000000000..5a03bfada --- /dev/null +++ b/Source/Core/Utilities/Tooling/SchedulingStrategy.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.Utilities +{ + /// + /// Coyote runtime scheduling strategy. + /// + [DataContract] + public enum SchedulingStrategy + { + /// + /// Interactive scheduling. + /// + [EnumMember(Value = "Interactive")] + Interactive = 0, + + /// + /// Replay scheduling. + /// + [EnumMember(Value = "Replay")] + Replay, + + /// + /// Portfolio scheduling. + /// + [EnumMember(Value = "Portfolio")] + Portfolio, + + /// + /// Random scheduling. + /// + [EnumMember(Value = "Random")] + Random, + + /// + /// Probabilistic random-walk scheduling. + /// + [EnumMember(Value = "ProbabilisticRandom")] + ProbabilisticRandom, + + /// + /// Prioritized scheduling. + /// + [EnumMember(Value = "PCT")] + PCT, + + /// + /// Prioritized scheduling with Random tail. + /// + [EnumMember(Value = "FairPCT")] + FairPCT, + + /// + /// Depth-first search scheduling. + /// + [EnumMember(Value = "DFS")] + DFS, + + /// + /// Depth-first search scheduling with + /// iterative deepening. + /// + [EnumMember(Value = "IDDFS")] + IDDFS, + + /// + /// Delay-bounding scheduling. + /// + [EnumMember(Value = "DelayBounding")] + DelayBounding, + + /// + /// Random delay-bounding scheduling. + /// + [EnumMember(Value = "RandomDelayBounding")] + RandomDelayBounding + } +} diff --git a/Source/SharedObjects/SharedCounter/ISharedCounter.cs b/Source/SharedObjects/SharedCounter/ISharedCounter.cs new file mode 100644 index 000000000..d94dad408 --- /dev/null +++ b/Source/SharedObjects/SharedCounter/ISharedCounter.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Interface of a shared counter. + /// + public interface ISharedCounter + { + /// + /// Increments the shared counter. + /// + void Increment(); + + /// + /// Decrements the shared counter. + /// + void Decrement(); + + /// + /// Gets the current value of the shared counter. + /// + /// Current value + int GetValue(); + + /// + /// Adds a value to the counter atomically. + /// + /// Value to add + /// The new value of the counter + int Add(int value); + + /// + /// Sets the counter to a value atomically. + /// + /// Value to set + /// The original value of the counter + int Exchange(int value); + + /// + /// Sets the counter to a value atomically if it is equal to a given value. + /// + /// Value to set + /// Value to compare against + /// The original value of the counter + int CompareExchange(int value, int comparand); + } +} diff --git a/Source/SharedObjects/SharedCounter/MockSharedCounter.cs b/Source/SharedObjects/SharedCounter/MockSharedCounter.cs new file mode 100644 index 000000000..a52dfb540 --- /dev/null +++ b/Source/SharedObjects/SharedCounter/MockSharedCounter.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices.Runtime; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// A wrapper for a shared counter modeled using a state-machine for testing. + /// + internal sealed class MockSharedCounter : ISharedCounter + { + /// + /// Machine modeling the shared counter. + /// + private readonly MachineId CounterMachine; + + /// + /// The testing runtime hosting this shared counter. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// Initializes a new instance of the class. + /// + public MockSharedCounter(int value, SystematicTestingRuntime runtime) + { + this.Runtime = runtime; + this.CounterMachine = this.Runtime.CreateMachine(typeof(SharedCounterMachine)); + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.CounterMachine, SharedCounterEvent.SetEvent(currentMachine.Id, value)); + currentMachine.Receive(typeof(SharedCounterResponseEvent)).Wait(); + } + + /// + /// Increments the shared counter. + /// + public void Increment() + { + this.Runtime.SendEvent(this.CounterMachine, SharedCounterEvent.IncrementEvent()); + } + + /// + /// Decrements the shared counter. + /// + public void Decrement() + { + this.Runtime.SendEvent(this.CounterMachine, SharedCounterEvent.DecrementEvent()); + } + + /// + /// Gets the current value of the shared counter. + /// + public int GetValue() + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.CounterMachine, SharedCounterEvent.GetEvent(currentMachine.Id)); + var response = currentMachine.Receive(typeof(SharedCounterResponseEvent)).Result; + return (response as SharedCounterResponseEvent).Value; + } + + /// + /// Adds a value to the counter atomically. + /// + public int Add(int value) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.CounterMachine, SharedCounterEvent.AddEvent(currentMachine.Id, value)); + var response = currentMachine.Receive(typeof(SharedCounterResponseEvent)).Result; + return (response as SharedCounterResponseEvent).Value; + } + + /// + /// Sets the counter to a value atomically. + /// + public int Exchange(int value) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.CounterMachine, SharedCounterEvent.SetEvent(currentMachine.Id, value)); + var response = currentMachine.Receive(typeof(SharedCounterResponseEvent)).Result; + return (response as SharedCounterResponseEvent).Value; + } + + /// + /// Sets the counter to a value atomically if it is equal to a given value. + /// + public int CompareExchange(int value, int comparand) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.CounterMachine, SharedCounterEvent.CasEvent(currentMachine.Id, value, comparand)); + var response = currentMachine.Receive(typeof(SharedCounterResponseEvent)).Result; + return (response as SharedCounterResponseEvent).Value; + } + } +} diff --git a/Source/SharedObjects/SharedCounter/ProductionSharedCounter.cs b/Source/SharedObjects/SharedCounter/ProductionSharedCounter.cs new file mode 100644 index 000000000..b74a91ac6 --- /dev/null +++ b/Source/SharedObjects/SharedCounter/ProductionSharedCounter.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Implements a shared counter to be used in production. + /// + internal sealed class ProductionSharedCounter : ISharedCounter + { + /// + /// The value of the shared counter. + /// + private volatile int Counter; + + /// + /// Initializes a new instance of the class. + /// + public ProductionSharedCounter(int value) + { + this.Counter = value; + } + + /// + /// Increments the shared counter. + /// + public void Increment() + { + Interlocked.Increment(ref this.Counter); + } + + /// + /// Decrements the shared counter. + /// + public void Decrement() + { + Interlocked.Decrement(ref this.Counter); + } + + /// + /// Gets the current value of the shared counter. + /// + public int GetValue() => this.Counter; + + /// + /// Adds a value to the counter atomically. + /// + public int Add(int value) => Interlocked.Add(ref this.Counter, value); + + /// + /// Sets the counter to a value atomically. + /// + public int Exchange(int value) => Interlocked.Exchange(ref this.Counter, value); + + /// + /// Sets the counter to a value atomically if it is equal to a given value. + /// + public int CompareExchange(int value, int comparand) => + Interlocked.CompareExchange(ref this.Counter, value, comparand); + } +} diff --git a/Source/SharedObjects/SharedCounter/SharedCounter.cs b/Source/SharedObjects/SharedCounter/SharedCounter.cs new file mode 100644 index 000000000..f40bb797a --- /dev/null +++ b/Source/SharedObjects/SharedCounter/SharedCounter.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.TestingServices.Runtime; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Shared counter that can be safely shared by multiple Coyote machines. + /// + public static class SharedCounter + { + /// + /// Creates a new shared counter. + /// + /// The machine runtime. + /// The initial value. + public static ISharedCounter Create(IMachineRuntime runtime, int value = 0) + { + if (runtime is ProductionRuntime) + { + return new ProductionSharedCounter(value); + } + else if (runtime is SystematicTestingRuntime testingRuntime) + { + return new MockSharedCounter(value, testingRuntime); + } + else + { + throw new RuntimeException("Unknown runtime object of type: " + runtime.GetType().Name + "."); + } + } + } +} diff --git a/Source/SharedObjects/SharedCounter/SharedCounterEvent.cs b/Source/SharedObjects/SharedCounter/SharedCounterEvent.cs new file mode 100644 index 000000000..8b7a539b5 --- /dev/null +++ b/Source/SharedObjects/SharedCounter/SharedCounterEvent.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Event used to communicate with a shared counter machine. + /// + internal class SharedCounterEvent : Event + { + /// + /// Supported shared counter operations. + /// + internal enum SharedCounterOperation + { + GET, + SET, + INC, + DEC, + ADD, + CAS + } + + /// + /// The operation stored in this event. + /// + public SharedCounterOperation Operation { get; private set; } + + /// + /// The shared counter value stored in this event. + /// + public int Value { get; private set; } + + /// + /// Comparand value stored in this event. + /// + public int Comparand { get; private set; } + + /// + /// The sender machine stored in this event. + /// + public MachineId Sender { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + private SharedCounterEvent(SharedCounterOperation op, int value, int comparand, MachineId sender) + { + this.Operation = op; + this.Value = value; + this.Comparand = comparand; + this.Sender = sender; + } + + /// + /// Creates a new event for the 'INC' operation. + /// + public static SharedCounterEvent IncrementEvent() + { + return new SharedCounterEvent(SharedCounterOperation.INC, 0, 0, null); + } + + /// + /// Creates a new event for the 'DEC' operation. + /// + public static SharedCounterEvent DecrementEvent() + { + return new SharedCounterEvent(SharedCounterOperation.DEC, 0, 0, null); + } + + /// + /// Creates a new event for the 'SET' operation. + /// + public static SharedCounterEvent SetEvent(MachineId sender, int value) + { + return new SharedCounterEvent(SharedCounterOperation.SET, value, 0, sender); + } + + /// + /// Creates a new event for the 'GET' operation. + /// + public static SharedCounterEvent GetEvent(MachineId sender) + { + return new SharedCounterEvent(SharedCounterOperation.GET, 0, 0, sender); + } + + /// + /// Creates a new event for the 'ADD' operation. + /// + public static SharedCounterEvent AddEvent(MachineId sender, int value) + { + return new SharedCounterEvent(SharedCounterOperation.ADD, value, 0, sender); + } + + /// + /// Creates a new event for the 'CAS' operation. + /// + public static SharedCounterEvent CasEvent(MachineId sender, int value, int comparand) + { + return new SharedCounterEvent(SharedCounterOperation.CAS, value, comparand, sender); + } + } +} diff --git a/Source/SharedObjects/SharedCounter/SharedCounterMachine.cs b/Source/SharedObjects/SharedCounter/SharedCounterMachine.cs new file mode 100644 index 000000000..a5287c57b --- /dev/null +++ b/Source/SharedObjects/SharedCounter/SharedCounterMachine.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// A shared counter modeled using a state-machine for testing. + /// + internal sealed class SharedCounterMachine : Machine + { + /// + /// The value of the shared counter. + /// + private int Counter; + + /// + /// The start state of this machine. + /// + [Start] + [OnEntry(nameof(Initialize))] + [OnEventDoAction(typeof(SharedCounterEvent), nameof(ProcessEvent))] + private class Init : MachineState + { + } + + /// + /// Initializes the machine. + /// + private void Initialize() + { + this.Counter = 0; + } + + /// + /// Processes the next dequeued event. + /// + private void ProcessEvent() + { + var e = this.ReceivedEvent as SharedCounterEvent; + switch (e.Operation) + { + case SharedCounterEvent.SharedCounterOperation.SET: + this.Send(e.Sender, new SharedCounterResponseEvent(this.Counter)); + this.Counter = e.Value; + break; + + case SharedCounterEvent.SharedCounterOperation.GET: + this.Send(e.Sender, new SharedCounterResponseEvent(this.Counter)); + break; + + case SharedCounterEvent.SharedCounterOperation.INC: + this.Counter++; + break; + + case SharedCounterEvent.SharedCounterOperation.DEC: + this.Counter--; + break; + + case SharedCounterEvent.SharedCounterOperation.ADD: + this.Counter += e.Value; + this.Send(e.Sender, new SharedCounterResponseEvent(this.Counter)); + break; + + case SharedCounterEvent.SharedCounterOperation.CAS: + this.Send(e.Sender, new SharedCounterResponseEvent(this.Counter)); + if (this.Counter == e.Comparand) + { + this.Counter = e.Value; + } + + break; + + default: + throw new System.ArgumentOutOfRangeException("Unsupported SharedCounter operation: " + e.Operation); + } + } + } +} diff --git a/Source/SharedObjects/SharedCounter/SharedCounterResponseEvent.cs b/Source/SharedObjects/SharedCounter/SharedCounterResponseEvent.cs new file mode 100644 index 000000000..d92638090 --- /dev/null +++ b/Source/SharedObjects/SharedCounter/SharedCounterResponseEvent.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Event containing the value of a shared counter. + /// + internal class SharedCounterResponseEvent : Event + { + /// + /// Value. + /// + internal int Value; + + /// + /// Initializes a new instance of the class. + /// + internal SharedCounterResponseEvent(int value) + { + this.Value = value; + } + } +} diff --git a/Source/SharedObjects/SharedDictionary/ISharedDictionary.cs b/Source/SharedObjects/SharedDictionary/ISharedDictionary.cs new file mode 100644 index 000000000..6f9d2734b --- /dev/null +++ b/Source/SharedObjects/SharedDictionary/ISharedDictionary.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Interface of a shared dictionary. + /// + public interface ISharedDictionary + { + /// + /// Adds a new key to the dictionary, if it doesn’t already exist in the dictionary. + /// + /// Key + /// Value + /// True or false depending on whether the new key/value pair was added. + bool TryAdd(TKey key, TValue value); + + /// + /// Updates the value for an existing key in the dictionary, if that key has a specific value. + /// + /// Key + /// New value + /// Old value + /// True if the value with key was equal to comparisonValue and was replaced with newValue; otherwise, false. + bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue); + + /// + /// Attempts to get the value associated with the specified key. + /// + /// Key + /// Value associated with the key, or the default value if the key does not exist + /// True if the key was found; otherwise, false. + bool TryGetValue(TKey key, out TValue value); + + /// + /// Gets or sets the value associated with the specified key. + /// + /// Key + /// Value + TValue this[TKey key] { get; set; } + + /// + /// Removes the specified key from the dictionary. + /// + /// Key + /// Value associated with the key if present, or the default value otherwise. + /// True if the element is successfully removed; otherwise, false. + bool TryRemove(TKey key, out TValue value); + + /// + /// Gets the number of elements in the dictionary. + /// + /// Size + int Count { get; } + } +} diff --git a/Source/SharedObjects/SharedDictionary/MockSharedDictionary.cs b/Source/SharedObjects/SharedDictionary/MockSharedDictionary.cs new file mode 100644 index 000000000..782403fb4 --- /dev/null +++ b/Source/SharedObjects/SharedDictionary/MockSharedDictionary.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices.Runtime; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// A wrapper for a shared dictionary modeled using a state-machine for testing. + /// + internal sealed class MockSharedDictionary : ISharedDictionary + { + /// + /// Machine modeling the shared dictionary. + /// + private readonly MachineId DictionaryMachine; + + /// + /// The testing runtime hosting this shared dictionary. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// Initializes a new instance of the class. + /// + public MockSharedDictionary(IEqualityComparer comparer, SystematicTestingRuntime runtime) + { + this.Runtime = runtime; + if (comparer != null) + { + this.DictionaryMachine = this.Runtime.CreateMachine( + typeof(SharedDictionaryMachine), + SharedDictionaryEvent.InitEvent(comparer)); + } + else + { + this.DictionaryMachine = this.Runtime.CreateMachine(typeof(SharedDictionaryMachine)); + } + } + + /// + /// Adds a new key to the dictionary, if it doesn’t already exist in the dictionary. + /// + public bool TryAdd(TKey key, TValue value) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.DictionaryMachine, SharedDictionaryEvent.TryAddEvent(key, value, currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedDictionaryResponseEvent)).Result as SharedDictionaryResponseEvent; + return e.Value; + } + + /// + /// Updates the value for an existing key in the dictionary, if that key has a specific value. + /// + public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.DictionaryMachine, SharedDictionaryEvent.TryUpdateEvent(key, newValue, comparisonValue, currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedDictionaryResponseEvent)).Result as SharedDictionaryResponseEvent; + return e.Value; + } + + /// + /// Attempts to get the value associated with the specified key. + /// + public bool TryGetValue(TKey key, out TValue value) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.DictionaryMachine, SharedDictionaryEvent.TryGetEvent(key, currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedDictionaryResponseEvent>)).Result + as SharedDictionaryResponseEvent>; + value = e.Value.Item2; + return e.Value.Item1; + } + + /// + /// Gets or sets the value associated with the specified key. + /// + public TValue this[TKey key] + { + get + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.DictionaryMachine, SharedDictionaryEvent.GetEvent(key, currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedDictionaryResponseEvent)).Result as SharedDictionaryResponseEvent; + return e.Value; + } + + set + { + this.Runtime.SendEvent(this.DictionaryMachine, SharedDictionaryEvent.SetEvent(key, value)); + } + } + + /// + /// Removes the specified key from the dictionary. + /// + public bool TryRemove(TKey key, out TValue value) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.DictionaryMachine, SharedDictionaryEvent.TryRemoveEvent(key, currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedDictionaryResponseEvent>)).Result + as SharedDictionaryResponseEvent>; + value = e.Value.Item2; + return e.Value.Item1; + } + + /// + /// Gets the number of elements in the dictionary. + /// + public int Count + { + get + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.DictionaryMachine, SharedDictionaryEvent.CountEvent(currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedDictionaryResponseEvent)).Result as SharedDictionaryResponseEvent; + return e.Value; + } + } + } +} diff --git a/Source/SharedObjects/SharedDictionary/ProductionSharedDictionary.cs b/Source/SharedObjects/SharedDictionary/ProductionSharedDictionary.cs new file mode 100644 index 000000000..429451642 --- /dev/null +++ b/Source/SharedObjects/SharedDictionary/ProductionSharedDictionary.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Implements a shared dictionary to be used in production. + /// + internal sealed class ProductionSharedDictionary : ISharedDictionary + { + /// + /// The dictionary. + /// + private readonly ConcurrentDictionary Dictionary; + + /// + /// Initializes a new instance of the class. + /// + internal ProductionSharedDictionary() + { + this.Dictionary = new ConcurrentDictionary(); + } + + /// + /// Initializes a new instance of the class. + /// + internal ProductionSharedDictionary(IEqualityComparer comparer) + { + this.Dictionary = new ConcurrentDictionary(comparer); + } + + /// + /// Adds a new key to the dictionary, if it doesn’t already exist in the dictionary. + /// + public bool TryAdd(TKey key, TValue value) => this.Dictionary.TryAdd(key, value); + + /// + /// Updates the value for an existing key in the dictionary, if that key has a specific value. + /// + public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue) => + this.Dictionary.TryUpdate(key, newValue, comparisonValue); + + /// + /// Attempts to get the value associated with the specified key. + /// + public bool TryGetValue(TKey key, out TValue value) => this.Dictionary.TryGetValue(key, out value); + + /// + /// Gets or sets the value associated with the specified key. + /// + public TValue this[TKey key] + { + get => this.Dictionary[key]; + + set + { + this.Dictionary[key] = value; + } + } + + /// + /// Removes the specified key from the dictionary. + /// + public bool TryRemove(TKey key, out TValue value) => this.Dictionary.TryRemove(key, out value); + + /// + /// Gets the number of elements in the dictionary. + /// + public int Count + { + get => this.Dictionary.Count; + } + } +} diff --git a/Source/SharedObjects/SharedDictionary/SharedDictionary.cs b/Source/SharedObjects/SharedDictionary/SharedDictionary.cs new file mode 100644 index 000000000..f17543e76 --- /dev/null +++ b/Source/SharedObjects/SharedDictionary/SharedDictionary.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.TestingServices.Runtime; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Shared dictionary that can be safely shared by multiple Coyote machines. + /// + public static class SharedDictionary + { + /// + /// Creates a new shared dictionary. + /// + /// The machine runtime. + public static ISharedDictionary Create(IMachineRuntime runtime) + { + if (runtime is ProductionRuntime) + { + return new ProductionSharedDictionary(); + } + else if (runtime is SystematicTestingRuntime testingRuntime) + { + return new MockSharedDictionary(null, testingRuntime); + } + else + { + throw new RuntimeException("Unknown runtime object of type: " + runtime.GetType().Name + "."); + } + } + + /// + /// Creates a new shared dictionary. + /// + /// The key comparer. + /// The machine runtime. + public static ISharedDictionary Create(IEqualityComparer comparer, IMachineRuntime runtime) + { + if (runtime is ProductionRuntime) + { + return new ProductionSharedDictionary(comparer); + } + else if (runtime is SystematicTestingRuntime testingRuntime) + { + return new MockSharedDictionary(comparer, testingRuntime); + } + else + { + throw new RuntimeException("Unknown runtime object of type: " + runtime.GetType().Name + "."); + } + } + } +} diff --git a/Source/SharedObjects/SharedDictionary/SharedDictionaryEvent.cs b/Source/SharedObjects/SharedDictionary/SharedDictionaryEvent.cs new file mode 100644 index 000000000..04c38bc5e --- /dev/null +++ b/Source/SharedObjects/SharedDictionary/SharedDictionaryEvent.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Event used to communicate with a shared counter machine. + /// + internal class SharedDictionaryEvent : Event + { + /// + /// Supported shared dictionary operations. + /// + internal enum SharedDictionaryOperation + { + INIT, + GET, + SET, + TRYADD, + TRYGET, + TRYUPDATE, + TRYREMOVE, + COUNT + } + + /// + /// The operation stored in this event. + /// + internal SharedDictionaryOperation Operation { get; private set; } + + /// + /// The shared dictionary key stored in this event. + /// + internal object Key { get; private set; } + + /// + /// The shared dictionary value stored in this event. + /// + internal object Value { get; private set; } + + /// + /// The shared dictionary comparison value stored in this event. + /// + internal object ComparisonValue { get; private set; } + + /// + /// The sender machine stored in this event. + /// + internal MachineId Sender { get; private set; } + + /// + /// The comparer stored in this event. + /// + internal object Comparer { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + private SharedDictionaryEvent(SharedDictionaryOperation op, object key, object value, object comparisonValue, MachineId sender, object comparer) + { + this.Operation = op; + this.Key = key; + this.Value = value; + this.ComparisonValue = comparisonValue; + this.Sender = sender; + this.Comparer = comparer; + } + + /// + /// Creates a new event for the 'INIT' operation. + /// + internal static SharedDictionaryEvent InitEvent(object comparer) => + new SharedDictionaryEvent(SharedDictionaryOperation.INIT, null, null, null, null, comparer); + + /// + /// Creates a new event for the 'TRYADD' operation. + /// + internal static SharedDictionaryEvent TryAddEvent(object key, object value, MachineId sender) => + new SharedDictionaryEvent(SharedDictionaryOperation.TRYADD, key, value, null, sender, null); + + /// + /// Creates a new event for the 'TRYUPDATE' operation. + /// + internal static SharedDictionaryEvent TryUpdateEvent(object key, object value, object comparisonValue, MachineId sender) => + new SharedDictionaryEvent(SharedDictionaryOperation.TRYUPDATE, key, value, comparisonValue, sender, null); + + /// + /// Creates a new event for the 'GET' operation. + /// + internal static SharedDictionaryEvent GetEvent(object key, MachineId sender) => + new SharedDictionaryEvent(SharedDictionaryOperation.GET, key, null, null, sender, null); + + /// + /// Creates a new event for the 'TRYGET' operation. + /// + internal static SharedDictionaryEvent TryGetEvent(object key, MachineId sender) => + new SharedDictionaryEvent(SharedDictionaryOperation.TRYGET, key, null, null, sender, null); + + /// + /// Creates a new event for the 'SET' operation. + /// + internal static SharedDictionaryEvent SetEvent(object key, object value) => + new SharedDictionaryEvent(SharedDictionaryOperation.SET, key, value, null, null, null); + + /// + /// Creates a new event for the 'COUNT' operation. + /// + internal static SharedDictionaryEvent CountEvent(MachineId sender) => + new SharedDictionaryEvent(SharedDictionaryOperation.COUNT, null, null, null, sender, null); + + /// + /// Creates a new event for the 'TRYREMOVE' operation. + /// + internal static SharedDictionaryEvent TryRemoveEvent(object key, MachineId sender) => + new SharedDictionaryEvent(SharedDictionaryOperation.TRYREMOVE, key, null, null, sender, null); + } +} diff --git a/Source/SharedObjects/SharedDictionary/SharedDictionaryMachine.cs b/Source/SharedObjects/SharedDictionary/SharedDictionaryMachine.cs new file mode 100644 index 000000000..ad596c1de --- /dev/null +++ b/Source/SharedObjects/SharedDictionary/SharedDictionaryMachine.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// A shared dictionary modeled using a state-machine for testing. + /// + internal sealed class SharedDictionaryMachine : Machine + { + /// + /// The internal shared dictionary. + /// + private Dictionary Dictionary; + + /// + /// The start state of this machine. + /// + [Start] + [OnEntry(nameof(Initialize))] + [OnEventDoAction(typeof(SharedDictionaryEvent), nameof(ProcessEvent))] + private class Init : MachineState + { + } + + /// + /// Initializes the machine. + /// + private void Initialize() + { + if (this.ReceivedEvent is SharedDictionaryEvent e) + { + if (e.Operation == SharedDictionaryEvent.SharedDictionaryOperation.INIT && e.Comparer != null) + { + this.Dictionary = new Dictionary(e.Comparer as IEqualityComparer); + } + else + { + throw new ArgumentException("Incorrect arguments provided to SharedDictionary."); + } + } + else + { + this.Dictionary = new Dictionary(); + } + } + + /// + /// Processes the next dequeued event. + /// + private void ProcessEvent() + { + var e = this.ReceivedEvent as SharedDictionaryEvent; + switch (e.Operation) + { + case SharedDictionaryEvent.SharedDictionaryOperation.TRYADD: + if (this.Dictionary.ContainsKey((TKey)e.Key)) + { + this.Send(e.Sender, new SharedDictionaryResponseEvent(false)); + } + else + { + this.Dictionary[(TKey)e.Key] = (TValue)e.Value; + this.Send(e.Sender, new SharedDictionaryResponseEvent(true)); + } + + break; + + case SharedDictionaryEvent.SharedDictionaryOperation.TRYUPDATE: + if (!this.Dictionary.ContainsKey((TKey)e.Key)) + { + this.Send(e.Sender, new SharedDictionaryResponseEvent(false)); + } + else + { + var currentValue = this.Dictionary[(TKey)e.Key]; + if (currentValue.Equals((TValue)e.ComparisonValue)) + { + this.Dictionary[(TKey)e.Key] = (TValue)e.Value; + this.Send(e.Sender, new SharedDictionaryResponseEvent(true)); + } + else + { + this.Send(e.Sender, new SharedDictionaryResponseEvent(false)); + } + } + + break; + + case SharedDictionaryEvent.SharedDictionaryOperation.TRYGET: + if (!this.Dictionary.ContainsKey((TKey)e.Key)) + { + this.Send(e.Sender, new SharedDictionaryResponseEvent>(Tuple.Create(false, default(TValue)))); + } + else + { + this.Send(e.Sender, new SharedDictionaryResponseEvent>(Tuple.Create(true, this.Dictionary[(TKey)e.Key]))); + } + + break; + + case SharedDictionaryEvent.SharedDictionaryOperation.GET: + this.Send(e.Sender, new SharedDictionaryResponseEvent(this.Dictionary[(TKey)e.Key])); + break; + + case SharedDictionaryEvent.SharedDictionaryOperation.SET: + this.Dictionary[(TKey)e.Key] = (TValue)e.Value; + break; + + case SharedDictionaryEvent.SharedDictionaryOperation.COUNT: + this.Send(e.Sender, new SharedDictionaryResponseEvent(this.Dictionary.Count)); + break; + + case SharedDictionaryEvent.SharedDictionaryOperation.TRYREMOVE: + if (this.Dictionary.ContainsKey((TKey)e.Key)) + { + var value = this.Dictionary[(TKey)e.Key]; + this.Dictionary.Remove((TKey)e.Key); + this.Send(e.Sender, new SharedDictionaryResponseEvent>(Tuple.Create(true, value))); + } + else + { + this.Send(e.Sender, new SharedDictionaryResponseEvent>(Tuple.Create(false, default(TValue)))); + } + + break; + } + } + } +} diff --git a/Source/SharedObjects/SharedDictionary/SharedDictionaryResponseEvent.cs b/Source/SharedObjects/SharedDictionary/SharedDictionaryResponseEvent.cs new file mode 100644 index 000000000..f1c3cb2dc --- /dev/null +++ b/Source/SharedObjects/SharedDictionary/SharedDictionaryResponseEvent.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Event containing the value of a shared dictionary. + /// + internal class SharedDictionaryResponseEvent : Event + { + /// + /// Value. + /// + internal T Value; + + /// + /// Initializes a new instance of the class. + /// + internal SharedDictionaryResponseEvent(T value) + { + this.Value = value; + } + } +} diff --git a/Source/SharedObjects/SharedObjects.csproj b/Source/SharedObjects/SharedObjects.csproj new file mode 100644 index 000000000..d3dc3e7d4 --- /dev/null +++ b/Source/SharedObjects/SharedObjects.csproj @@ -0,0 +1,21 @@ + + + + + The Coyote shared objects library. + Microsoft.Coyote.SharedObjects + Microsoft.Coyote.SharedObjects + true + asynchronous;event-driven;state-machines;systematic-testing;dotnet;csharp + ..\..\bin\ + + + netstandard2.0;net46;net47 + + + netstandard2.0 + + + + + diff --git a/Source/SharedObjects/SharedRegister/ISharedRegister.cs b/Source/SharedObjects/SharedRegister/ISharedRegister.cs new file mode 100644 index 000000000..e3b4706d4 --- /dev/null +++ b/Source/SharedObjects/SharedRegister/ISharedRegister.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Interface of a shared register. + /// + /// Value type of the shared register + public interface ISharedRegister + where T : struct + { + /// + /// Reads and updates the register. + /// + /// Update function + /// Resulting value of the register + T Update(Func func); + + /// + /// Gets current value of the register. + /// + /// Current value + T GetValue(); + + /// + /// Sets current value of the register. + /// + /// Value + void SetValue(T value); + } +} diff --git a/Source/SharedObjects/SharedRegister/MockSharedRegister.cs b/Source/SharedObjects/SharedRegister/MockSharedRegister.cs new file mode 100644 index 000000000..eac766b72 --- /dev/null +++ b/Source/SharedObjects/SharedRegister/MockSharedRegister.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices.Runtime; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// A wrapper for a shared register modeled using a state-machine for testing. + /// + internal sealed class MockSharedRegister : ISharedRegister + where T : struct + { + /// + /// Machine modeling the shared register. + /// + private readonly MachineId RegisterMachine; + + /// + /// The testing runtime hosting this shared register. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// Initializes a new instance of the class. + /// + public MockSharedRegister(T value, SystematicTestingRuntime runtime) + { + this.Runtime = runtime; + this.RegisterMachine = this.Runtime.CreateMachine(typeof(SharedRegisterMachine)); + this.Runtime.SendEvent(this.RegisterMachine, SharedRegisterEvent.SetEvent(value)); + } + + /// + /// Reads and updates the register. + /// + public T Update(Func func) + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.RegisterMachine, SharedRegisterEvent.UpdateEvent(func, currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedRegisterResponseEvent)).Result as SharedRegisterResponseEvent; + return e.Value; + } + + /// + /// Gets current value of the register. + /// + public T GetValue() + { + var currentMachine = this.Runtime.GetExecutingMachine(); + this.Runtime.SendEvent(this.RegisterMachine, SharedRegisterEvent.GetEvent(currentMachine.Id)); + var e = currentMachine.Receive(typeof(SharedRegisterResponseEvent)).Result as SharedRegisterResponseEvent; + return e.Value; + } + + /// + /// Sets current value of the register. + /// + public void SetValue(T value) + { + this.Runtime.SendEvent(this.RegisterMachine, SharedRegisterEvent.SetEvent(value)); + } + } +} diff --git a/Source/SharedObjects/SharedRegister/ProductionSharedRegister.cs b/Source/SharedObjects/SharedRegister/ProductionSharedRegister.cs new file mode 100644 index 000000000..81f99f8ec --- /dev/null +++ b/Source/SharedObjects/SharedRegister/ProductionSharedRegister.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Implements a shared register to be used in production. + /// + internal sealed class ProductionSharedRegister : ISharedRegister + where T : struct + { + /// + /// Current value of the register. + /// + private T Value; + + /// + /// Initializes a new instance of the class. + /// + public ProductionSharedRegister(T value) + { + this.Value = value; + } + + /// + /// Reads and updates the register. + /// + public T Update(Func func) + { + T oldValue, newValue; + bool done = false; + + do + { + oldValue = this.Value; + newValue = func(oldValue); + + lock (this) + { + if (oldValue.Equals(this.Value)) + { + this.Value = newValue; + done = true; + } + } + } + while (!done); + + return newValue; + } + + /// + /// Gets current value of the register. + /// + public T GetValue() + { + T currentValue; + lock (this) + { + currentValue = this.Value; + } + + return currentValue; + } + + /// + /// Sets current value of the register. + /// + public void SetValue(T value) + { + lock (this) + { + this.Value = value; + } + } + } +} diff --git a/Source/SharedObjects/SharedRegister/SharedRegister.cs b/Source/SharedObjects/SharedRegister/SharedRegister.cs new file mode 100644 index 000000000..e4b7e444d --- /dev/null +++ b/Source/SharedObjects/SharedRegister/SharedRegister.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.TestingServices.Runtime; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Shared register that can be safely shared by multiple Coyote machines. + /// + public static class SharedRegister + { + /// + /// Creates a new shared register. + /// + /// The machine runtime. + /// The initial value. + public static ISharedRegister Create(IMachineRuntime runtime, T value = default) + where T : struct + { + if (runtime is ProductionRuntime) + { + return new ProductionSharedRegister(value); + } + else if (runtime is SystematicTestingRuntime testingRuntime) + { + return new MockSharedRegister(value, testingRuntime); + } + else + { + throw new RuntimeException("Unknown runtime object of type: " + runtime.GetType().Name + "."); + } + } + } +} diff --git a/Source/SharedObjects/SharedRegister/SharedRegisterEvent.cs b/Source/SharedObjects/SharedRegister/SharedRegisterEvent.cs new file mode 100644 index 000000000..1e3e8753b --- /dev/null +++ b/Source/SharedObjects/SharedRegister/SharedRegisterEvent.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Event used to communicate with a shared register machine. + /// + internal class SharedRegisterEvent : Event + { + /// + /// Supported shared register operations. + /// + internal enum SharedRegisterOperation + { + GET, + SET, + UPDATE + } + + /// + /// The operation stored in this event. + /// + public SharedRegisterOperation Operation { get; private set; } + + /// + /// The shared register value stored in this event. + /// + public object Value { get; private set; } + + /// + /// The shared register func stored in this event. + /// + public object Func { get; private set; } + + /// + /// The sender machine stored in this event. + /// + public MachineId Sender { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + private SharedRegisterEvent(SharedRegisterOperation op, object value, object func, MachineId sender) + { + this.Operation = op; + this.Value = value; + this.Func = func; + this.Sender = sender; + } + + /// + /// Creates a new event for the 'UPDATE' operation. + /// + public static SharedRegisterEvent UpdateEvent(object func, MachineId sender) + { + return new SharedRegisterEvent(SharedRegisterOperation.UPDATE, null, func, sender); + } + + /// + /// Creates a new event for the 'SET' operation. + /// + public static SharedRegisterEvent SetEvent(object value) + { + return new SharedRegisterEvent(SharedRegisterOperation.SET, value, null, null); + } + + /// + /// Creates a new event for the 'GET' operation. + /// + public static SharedRegisterEvent GetEvent(MachineId sender) + { + return new SharedRegisterEvent(SharedRegisterOperation.GET, null, null, sender); + } + } +} diff --git a/Source/SharedObjects/SharedRegister/SharedRegisterMachine.cs b/Source/SharedObjects/SharedRegister/SharedRegisterMachine.cs new file mode 100644 index 000000000..7000f90f6 --- /dev/null +++ b/Source/SharedObjects/SharedRegister/SharedRegisterMachine.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// A shared register modeled using a state-machine for testing. + /// + internal sealed class SharedRegisterMachine : Machine + where T : struct + { + /// + /// The value of the shared register. + /// + private T Value; + + /// + /// The start state of this machine. + /// + [Start] + [OnEntry(nameof(Initialize))] + [OnEventDoAction(typeof(SharedRegisterEvent), nameof(ProcessEvent))] + private class Init : MachineState + { + } + + /// + /// Initializes the machine. + /// + private void Initialize() + { + this.Value = default; + } + + /// + /// Processes the next dequeued event. + /// + private void ProcessEvent() + { + var e = this.ReceivedEvent as SharedRegisterEvent; + switch (e.Operation) + { + case SharedRegisterEvent.SharedRegisterOperation.SET: + this.Value = (T)e.Value; + break; + + case SharedRegisterEvent.SharedRegisterOperation.GET: + this.Send(e.Sender, new SharedRegisterResponseEvent(this.Value)); + break; + + case SharedRegisterEvent.SharedRegisterOperation.UPDATE: + var func = (Func)e.Func; + this.Value = func(this.Value); + this.Send(e.Sender, new SharedRegisterResponseEvent(this.Value)); + break; + } + } + } +} diff --git a/Source/SharedObjects/SharedRegister/SharedRegisterResponseEvent.cs b/Source/SharedObjects/SharedRegister/SharedRegisterResponseEvent.cs new file mode 100644 index 000000000..ac2981d50 --- /dev/null +++ b/Source/SharedObjects/SharedRegister/SharedRegisterResponseEvent.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.SharedObjects +{ + /// + /// Event containing the value of a shared register. + /// + internal class SharedRegisterResponseEvent : Event + { + /// + /// Value. + /// + internal T Value; + + /// + /// Initializes a new instance of the class. + /// + internal SharedRegisterResponseEvent(T value) + { + this.Value = value; + } + } +} diff --git a/Source/TestingServices/Engines/AbstractTestingEngine.cs b/Source/TestingServices/Engines/AbstractTestingEngine.cs new file mode 100644 index 000000000..c2a76ac8a --- /dev/null +++ b/Source/TestingServices/Engines/AbstractTestingEngine.cs @@ -0,0 +1,669 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +#if NET46 +using System.Configuration; +#endif +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.TestingServices.Scheduling; +using Microsoft.Coyote.TestingServices.Scheduling.Strategies; +using Microsoft.Coyote.TestingServices.Tracing.Schedule; +using Microsoft.Coyote.Threading.Tasks; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The Coyote abstract testing engine. + /// + [DebuggerStepThrough] + internal abstract class AbstractTestingEngine : ITestingEngine + { + /// + /// Configuration. + /// + internal Configuration Configuration; + + /// + /// The Coyote assembly to analyze. + /// + internal Assembly Assembly; + + /// + /// The assembly that provides the Coyote runtime to use during testing. + /// If its null, the engine uses the default Coyote testing runtime. + /// + internal Assembly RuntimeAssembly; + + /// + /// The Coyote test runtime factory method. + /// + internal MethodInfo TestRuntimeFactoryMethod; + + /// + /// The Coyote test initialization method. + /// + internal MethodInfo TestInitMethod; + + /// + /// The Coyote test dispose method. + /// + internal MethodInfo TestDisposeMethod; + + /// + /// The Coyote test dispose method per iteration. + /// + internal MethodInfo TestIterationDisposeMethod; + + /// + /// The method to test. + /// + internal Delegate TestMethod; + + /// + /// The name of the test. + /// + internal string TestName; + + /// + /// Set of callbacks to invoke at the end + /// of each iteration. + /// + protected ISet> PerIterationCallbacks; + + /// + /// The installed logger. + /// + protected ILogger Logger; + + /// + /// The bug-finding scheduling strategy. + /// + protected ISchedulingStrategy Strategy; + + /// + /// Random number generator used by the scheduling strategies. + /// + protected IRandomNumberGenerator RandomNumberGenerator; + + /// + /// The error reporter. + /// + protected ErrorReporter ErrorReporter; + + /// + /// The profiler. + /// + protected Profiler Profiler; + + /// + /// The testing task cancellation token source. + /// + protected CancellationTokenSource CancellationTokenSource; + + /// + /// A guard for printing info. + /// + protected int PrintGuard; + + /// + /// Data structure containing information + /// gathered during testing. + /// + public TestReport TestReport { get; set; } + + /// + /// Initializes a new instance of the class. + /// + protected AbstractTestingEngine(Configuration configuration) + { + this.Initialize(configuration); + + try + { + this.Assembly = Assembly.LoadFrom(configuration.AssemblyToBeAnalyzed); + } + catch (FileNotFoundException ex) + { + Error.ReportAndExit(ex.Message); + } + +#if NET46 + // Load config file and absorb its settings. + try + { + var configFile = ConfigurationManager.OpenExeConfiguration(configuration.AssemblyToBeAnalyzed); + + var settings = configFile.AppSettings.Settings; + foreach (var key in settings.AllKeys) + { + if (ConfigurationManager.AppSettings.Get(key) is null) + { + ConfigurationManager.AppSettings.Set(key, settings[key].Value); + } + else + { + ConfigurationManager.AppSettings.Add(key, settings[key].Value); + } + } + } + catch (ConfigurationErrorsException ex) + { + Error.Report(ex.Message); + } +#endif + + if (!string.IsNullOrEmpty(configuration.TestingRuntimeAssembly)) + { + try + { + this.RuntimeAssembly = Assembly.LoadFrom(configuration.TestingRuntimeAssembly); + } + catch (FileNotFoundException ex) + { + Error.ReportAndExit(ex.Message); + } + + this.FindRuntimeFactoryMethod(); + } + + this.FindEntryPoint(); + this.TestInitMethod = this.FindTestMethod(typeof(TestInitAttribute)); + this.TestDisposeMethod = this.FindTestMethod(typeof(TestDisposeAttribute)); + this.TestIterationDisposeMethod = this.FindTestMethod(typeof(TestIterationDisposeAttribute)); + } + + /// + /// Initializes a new instance of the class. + /// + protected AbstractTestingEngine(Configuration configuration, Assembly assembly) + { + this.Initialize(configuration); + this.Assembly = assembly; + this.FindEntryPoint(); + this.TestInitMethod = this.FindTestMethod(typeof(TestInitAttribute)); + this.TestDisposeMethod = this.FindTestMethod(typeof(TestDisposeAttribute)); + this.TestIterationDisposeMethod = this.FindTestMethod(typeof(TestIterationDisposeAttribute)); + } + + /// + /// Initializes a new instance of the class. + /// + protected AbstractTestingEngine(Configuration configuration, Delegate testMethod) + { + this.Initialize(configuration); + this.TestMethod = testMethod; + } + + /// + /// Initialized the testing engine. + /// + private void Initialize(Configuration configuration) + { + this.Configuration = configuration; + this.Logger = new ConsoleLogger(); + this.ErrorReporter = new ErrorReporter(this.Configuration, this.Logger); + this.Profiler = new Profiler(); + + this.PerIterationCallbacks = new HashSet>(); + + // Initializes scheduling strategy specific components. + this.SetRandomNumberGenerator(); + + this.TestReport = new TestReport(this.Configuration); + this.CancellationTokenSource = new CancellationTokenSource(); + this.PrintGuard = 1; + + if (this.Configuration.SchedulingStrategy == SchedulingStrategy.Interactive) + { + this.Strategy = new InteractiveStrategy(this.Configuration, this.Logger); + this.Configuration.SchedulingIterations = 1; + this.Configuration.PerformFullExploration = false; + this.Configuration.IsVerbose = true; + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.Replay) + { + var scheduleDump = this.GetScheduleForReplay(out bool isFair); + ScheduleTrace schedule = new ScheduleTrace(scheduleDump); + this.Strategy = new ReplayStrategy(this.Configuration, schedule, isFair); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.Random) + { + this.Strategy = new RandomStrategy(this.Configuration.MaxFairSchedulingSteps, this.RandomNumberGenerator); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.ProbabilisticRandom) + { + this.Strategy = new ProbabilisticRandomStrategy( + this.Configuration.MaxFairSchedulingSteps, + this.Configuration.CoinFlipBound, + this.RandomNumberGenerator); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.PCT) + { + this.Strategy = new PCTStrategy(this.Configuration.MaxUnfairSchedulingSteps, this.Configuration.PrioritySwitchBound, + this.RandomNumberGenerator); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.FairPCT) + { + var prefixLength = this.Configuration.SafetyPrefixBound == 0 ? + this.Configuration.MaxUnfairSchedulingSteps : this.Configuration.SafetyPrefixBound; + var prefixStrategy = new PCTStrategy(prefixLength, this.Configuration.PrioritySwitchBound, this.RandomNumberGenerator); + var suffixStrategy = new RandomStrategy(this.Configuration.MaxFairSchedulingSteps, this.RandomNumberGenerator); + this.Strategy = new ComboStrategy(prefixStrategy, suffixStrategy); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.DFS) + { + this.Strategy = new DFSStrategy(this.Configuration.MaxUnfairSchedulingSteps); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.IDDFS) + { + this.Strategy = new IterativeDeepeningDFSStrategy(this.Configuration.MaxUnfairSchedulingSteps); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.DelayBounding) + { + this.Strategy = new ExhaustiveDelayBoundingStrategy(this.Configuration.MaxUnfairSchedulingSteps, + this.Configuration.DelayBound, this.RandomNumberGenerator); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.RandomDelayBounding) + { + this.Strategy = new RandomDelayBoundingStrategy(this.Configuration.MaxUnfairSchedulingSteps, + this.Configuration.DelayBound, this.RandomNumberGenerator); + } + else if (this.Configuration.SchedulingStrategy == SchedulingStrategy.Portfolio) + { + Error.ReportAndExit("Portfolio testing strategy is only " + + "available in parallel testing."); + } + + if (this.Configuration.SchedulingStrategy != SchedulingStrategy.Replay && + this.Configuration.ScheduleFile.Length > 0) + { + var scheduleDump = this.GetScheduleForReplay(out bool isFair); + ScheduleTrace schedule = new ScheduleTrace(scheduleDump); + this.Strategy = new ReplayStrategy(this.Configuration, schedule, isFair, this.Strategy); + } + } + + /// + /// Runs the testing engine. + /// + public ITestingEngine Run() + { + try + { + Task task = this.CreateTestingTask(); + if (this.Configuration.Timeout > 0) + { + this.CancellationTokenSource.CancelAfter( + this.Configuration.Timeout * 1000); + } + + this.Profiler.StartMeasuringExecutionTime(); + + task.Start(); + task.Wait(this.CancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + if (this.CancellationTokenSource.IsCancellationRequested) + { + this.Logger.WriteLine($"... Task {this.Configuration.TestingProcessId} timed out."); + } + } + catch (AggregateException aex) + { + aex.Handle((ex) => + { + IO.Debug.WriteLine(ex.Message); + IO.Debug.WriteLine(ex.StackTrace); + return true; + }); + + if (aex.InnerException is FileNotFoundException) + { + Error.ReportAndExit($"{aex.InnerException.Message}"); + } + + Error.ReportAndExit("Exception thrown during testing outside the context of a " + + "machine, possibly in a test method. Please use " + + "/debug /v:2 to print more information."); + } + catch (Exception ex) + { + this.Logger.WriteLine($"... Task {this.Configuration.TestingProcessId} failed due to an internal error: {ex}"); + this.TestReport.InternalErrors.Add(ex.ToString()); + } + finally + { + this.Profiler.StopMeasuringExecutionTime(); + } + + return this; + } + + /// + /// Creates a new testing task. + /// + protected abstract Task CreateTestingTask(); + + /// + /// Stops the testing engine. + /// + public void Stop() + { + this.CancellationTokenSource.Cancel(); + } + + /// + /// Returns a report with the testing results. + /// + public abstract string GetReport(); + + /// + /// Tries to emit the testing traces, if any. + /// + public virtual void TryEmitTraces(string directory, string file) + { + // No-op, must be implemented in subclass. + } + + /// + /// Registers a callback to invoke at the end of each iteration. The callback takes as + /// a parameter an integer representing the current iteration. + /// + public void RegisterPerIterationCallBack(Action callback) + { + this.PerIterationCallbacks.Add(callback); + } + + /// + /// Finds the testing runtime factory method, if one is provided. + /// + private void FindRuntimeFactoryMethod() + { + BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | BindingFlags.InvokeMethod; + List runtimeFactoryMethods = this.FindTestMethodsWithAttribute(typeof(TestRuntimeCreateAttribute), flags, this.RuntimeAssembly); + if (runtimeFactoryMethods.Count == 0) + { + Error.ReportAndExit($"Failed to find a testing runtime factory method in the '{this.RuntimeAssembly.FullName}' assembly."); + } + else if (runtimeFactoryMethods.Count > 1) + { + Error.ReportAndExit("Only one testing runtime factory method can be declared with " + + $"the attribute '{typeof(TestRuntimeCreateAttribute).FullName}'. " + + $"'{runtimeFactoryMethods.Count}' factory methods were found instead."); + } + + if (runtimeFactoryMethods[0].ReturnType != typeof(SystematicTestingRuntime) || + runtimeFactoryMethods[0].ContainsGenericParameters || + runtimeFactoryMethods[0].IsAbstract || runtimeFactoryMethods[0].IsVirtual || + runtimeFactoryMethods[0].IsConstructor || + runtimeFactoryMethods[0].IsPublic || !runtimeFactoryMethods[0].IsStatic || + runtimeFactoryMethods[0].GetParameters().Length != 2 || + runtimeFactoryMethods[0].GetParameters()[0].ParameterType != typeof(Configuration) || + runtimeFactoryMethods[0].GetParameters()[1].ParameterType != typeof(ISchedulingStrategy)) + { + Error.ReportAndExit("Incorrect test runtime factory method declaration. Please " + + "declare the method as follows:\n" + + $" [{typeof(TestRuntimeCreateAttribute).FullName}] internal static SystematicTestingRuntime " + + $"{runtimeFactoryMethods[0].Name}(Configuration configuration, ISchedulingStrategy strategy) {{ ... }}"); + } + + this.TestRuntimeFactoryMethod = runtimeFactoryMethods[0]; + } + + /// + /// Finds the entry point to the Coyote program. + /// + private void FindEntryPoint() + { + BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.InvokeMethod; + List testMethods = this.FindTestMethodsWithAttribute(typeof(TestAttribute), flags, this.Assembly); + + // Filter by test method name + var filteredTestMethods = testMethods + .FindAll(mi => string.Format("{0}.{1}", mi.DeclaringType.FullName, mi.Name) + .EndsWith(this.Configuration.TestMethodName)); + + if (filteredTestMethods.Count == 0) + { + if (testMethods.Count > 0) + { + var msg = "Cannot detect a Coyote test method with name " + this.Configuration.TestMethodName + + ". Possible options are: " + Environment.NewLine; + foreach (var mi in testMethods) + { + msg += string.Format("{0}.{1}{2}", mi.DeclaringType.FullName, mi.Name, Environment.NewLine); + } + + Error.ReportAndExit(msg); + } + else + { + Error.ReportAndExit("Cannot detect a Coyote test method. Use the " + + $"attribute '[{typeof(TestAttribute).FullName}]' to declare a test method."); + } + } + else if (filteredTestMethods.Count > 1) + { + var msg = "Only one test method to the Coyote program can " + + $"be declared with the attribute '{typeof(TestAttribute).FullName}'. " + + $"'{testMethods.Count}' test methods were found instead. Provide " + + $"/method flag to qualify the test method name you wish to use. " + + "Possible options are: " + Environment.NewLine; + + foreach (var mi in testMethods) + { + msg += string.Format("{0}.{1}{2}", mi.DeclaringType.FullName, mi.Name, Environment.NewLine); + } + + Error.ReportAndExit(msg); + } + + MethodInfo testMethod = filteredTestMethods[0]; + ParameterInfo[] testParams = testMethod.GetParameters(); + + bool hasExpectedReturnType = (testMethod.ReturnType == typeof(void) && + testMethod.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) == null) || + (testMethod.ReturnType == typeof(ControlledTask) && + testMethod.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) != null); + bool hasExpectedParameters = !testMethod.ContainsGenericParameters && + (testParams.Length is 0 || + (testParams.Length is 1 && testParams[0].ParameterType == typeof(IMachineRuntime))); + + if (testMethod.IsAbstract || testMethod.IsVirtual || testMethod.IsConstructor || + !testMethod.IsPublic || !testMethod.IsStatic || + !hasExpectedReturnType || !hasExpectedParameters) + { + Error.ReportAndExit("Incorrect test method declaration. Please " + + "use one of the following supported declarations:\n\n" + + $" [{typeof(TestAttribute).FullName}]\n" + + $" public static void {testMethod.Name}() {{ ... }}\n\n" + + $" [{typeof(TestAttribute).FullName}]\n" + + $" public static void {testMethod.Name}(IMachineRuntime runtime) {{ ... await ... }}\n\n" + + $" [{typeof(TestAttribute).FullName}]\n" + + $" public static async ControlledTask {testMethod.Name}() {{ ... }}\n\n" + + $" [{typeof(TestAttribute).FullName}]\n" + + $" public static async ControlledTask {testMethod.Name}(IMachineRuntime runtime) {{ ... await ... }}"); + } + + if (testMethod.ReturnType == typeof(void) && testParams.Length == 1) + { + this.TestMethod = Delegate.CreateDelegate(typeof(Action), testMethod); + } + else if (testMethod.ReturnType == typeof(void)) + { + this.TestMethod = Delegate.CreateDelegate(typeof(Action), testMethod); + } + else if (testParams.Length == 1) + { + this.TestMethod = Delegate.CreateDelegate(typeof(Func), testMethod); + } + else + { + this.TestMethod = Delegate.CreateDelegate(typeof(Func), testMethod); + } + + this.TestName = $"{testMethod.DeclaringType}.{testMethod.Name}"; + } + + /// + /// Finds the test method with the specified attribute. + /// Returns null if no such method is found. + /// + private MethodInfo FindTestMethod(Type attribute) + { + BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.InvokeMethod; + List testMethods = this.FindTestMethodsWithAttribute(attribute, flags, this.Assembly); + + if (testMethods.Count == 0) + { + return null; + } + else if (testMethods.Count > 1) + { + Error.ReportAndExit("Only one test method to the Coyote program can " + + $"be declared with the attribute '{attribute.FullName}'. " + + $"'{testMethods.Count}' test methods were found instead."); + } + + if (testMethods[0].ReturnType != typeof(void) || + testMethods[0].ContainsGenericParameters || + testMethods[0].IsAbstract || testMethods[0].IsVirtual || + testMethods[0].IsConstructor || + !testMethods[0].IsPublic || !testMethods[0].IsStatic || + testMethods[0].GetParameters().Length != 0) + { + Error.ReportAndExit("Incorrect test method declaration. Please " + + "declare the test method as follows:\n" + + $" [{attribute.FullName}] public static void " + + $"{testMethods[0].Name}() {{ ... }}"); + } + + return testMethods[0]; + } + + /// + /// Finds the test methods with the specified attribute in the given assembly. + /// Returns an empty list if no such methods are found. + /// + private List FindTestMethodsWithAttribute(Type attribute, BindingFlags bindingFlags, Assembly assembly) + { + List testMethods = null; + + try + { + testMethods = assembly.GetTypes().SelectMany(t => t.GetMethods(bindingFlags)). + Where(m => m.GetCustomAttributes(attribute, false).Length > 0).ToList(); + } + catch (ReflectionTypeLoadException ex) + { + foreach (var le in ex.LoaderExceptions) + { + this.ErrorReporter.WriteErrorLine(le.Message); + } + + Error.ReportAndExit($"Failed to load assembly '{assembly.FullName}'"); + } + catch (Exception ex) + { + this.ErrorReporter.WriteErrorLine(ex.Message); + Error.ReportAndExit($"Failed to load assembly '{assembly.FullName}'"); + } + + return testMethods; + } + + /// + /// Returns the schedule to replay. + /// + private string[] GetScheduleForReplay(out bool isFair) + { + string[] scheduleDump; + if (this.Configuration.ScheduleTrace.Length > 0) + { + scheduleDump = this.Configuration.ScheduleTrace.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + } + else + { + scheduleDump = File.ReadAllLines(this.Configuration.ScheduleFile); + } + + isFair = false; + foreach (var line in scheduleDump) + { + if (!line.StartsWith("--")) + { + break; + } + + if (line.Equals("--fair-scheduling")) + { + isFair = true; + } + else if (line.Equals("--cycle-detection")) + { + this.Configuration.EnableCycleDetection = true; + } + else if (line.StartsWith("--liveness-temperature-threshold:")) + { + this.Configuration.LivenessTemperatureThreshold = + int.Parse(line.Substring("--liveness-temperature-threshold:".Length)); + } + else if (line.StartsWith("--test-method:")) + { + this.Configuration.TestMethodName = + line.Substring("--test-method:".Length); + } + } + + return scheduleDump; + } + + /// + /// Returns (and creates if it does not exist) the output directory. + /// + protected string GetOutputDirectory() + { + string directoryPath = Path.GetDirectoryName(this.Assembly.Location) + + Path.DirectorySeparatorChar + "Output" + Path.DirectorySeparatorChar; + Directory.CreateDirectory(directoryPath); + return directoryPath; + } + + /// + /// Installs the specified . + /// + public void SetLogger(IO.ILogger logger) + { + if (logger is null) + { + throw new InvalidOperationException("Cannot install a null logger."); + } + + this.Logger.Dispose(); + this.Logger = logger; + this.ErrorReporter.Logger = logger; + } + + /// + /// Sets the random number generator to be used by the scheduling strategy. + /// + private void SetRandomNumberGenerator() + { + int seed = this.Configuration.RandomSchedulingSeed ?? DateTime.Now.Millisecond; + this.RandomNumberGenerator = new DefaultRandomNumberGenerator(seed); + } + } +} diff --git a/Source/TestingServices/Engines/BugFindingEngine.cs b/Source/TestingServices/Engines/BugFindingEngine.cs new file mode 100644 index 000000000..d51afce73 --- /dev/null +++ b/Source/TestingServices/Engines/BugFindingEngine.cs @@ -0,0 +1,449 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Runtime.Serialization; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.TestingServices.Tracing.Error; +using Microsoft.Coyote.TestingServices.Tracing.Schedule; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Implementation of the bug-finding engine. + /// + [DebuggerStepThrough] + internal sealed class BugFindingEngine : AbstractTestingEngine + { + /// + /// The bug trace, if any. + /// + private BugTrace BugTrace; + + /// + /// The readable trace, if any. + /// + internal string ReadableTrace { get; private set; } + + /// + /// The reproducable trace, if any. + /// + internal string ReproducableTrace { get; private set; } + + /// + /// Creates a new bug-finding engine. + /// + internal static BugFindingEngine Create(Configuration configuration, Delegate testMethod) + { + return new BugFindingEngine(configuration, testMethod); + } + + /// + /// Creates a new bug-finding engine. + /// + internal static BugFindingEngine Create(Configuration configuration) + { + return new BugFindingEngine(configuration); + } + + /// + /// Creates a new bug-finding engine. + /// + internal static BugFindingEngine Create(Configuration configuration, Assembly assembly) + { + return new BugFindingEngine(configuration, assembly); + } + + /// + /// Initializes a new instance of the class. + /// + private BugFindingEngine(Configuration configuration) + : base(configuration) + { + this.Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + private BugFindingEngine(Configuration configuration, Assembly assembly) + : base(configuration, assembly) + { + this.Initialize(); + } + + /// + /// Initializes a new instance of the class. + /// + private BugFindingEngine(Configuration configuration, Delegate testMethod) + : base(configuration, testMethod) + { + this.Initialize(); + } + + /// + /// Initializes the bug-finding engine. + /// + private void Initialize() + { + this.ReadableTrace = string.Empty; + this.ReproducableTrace = string.Empty; + } + + /// + /// Creates a new testing task. + /// + protected override Task CreateTestingTask() + { + string options = string.Empty; + if (this.Configuration.SchedulingStrategy == SchedulingStrategy.Random || + this.Configuration.SchedulingStrategy == SchedulingStrategy.ProbabilisticRandom || + this.Configuration.SchedulingStrategy == SchedulingStrategy.PCT || + this.Configuration.SchedulingStrategy == SchedulingStrategy.FairPCT || + this.Configuration.SchedulingStrategy == SchedulingStrategy.RandomDelayBounding) + { + options = $" (seed:{this.Configuration.RandomSchedulingSeed})"; + } + + this.Logger.WriteLine($"... Task {this.Configuration.TestingProcessId} is " + + $"using '{this.Configuration.SchedulingStrategy}' strategy{options}."); + + return new Task(() => + { + try + { + if (this.TestInitMethod != null) + { + // Initializes the test state. + this.TestInitMethod.Invoke(null, Array.Empty()); + } + + int maxIterations = this.Configuration.SchedulingIterations; + for (int i = 0; i < maxIterations; i++) + { + if (this.CancellationTokenSource.IsCancellationRequested) + { + break; + } + + // Runs a new testing iteration. + this.RunNextIteration(i); + + if (!this.Configuration.PerformFullExploration && this.TestReport.NumOfFoundBugs > 0) + { + break; + } + + if (!this.Strategy.PrepareForNextIteration()) + { + break; + } + + if (this.RandomNumberGenerator != null && this.Configuration.IncrementalSchedulingSeed) + { + // Increments the seed in the random number generator (if one is used), to + // capture the seed used by the scheduling strategy in the next iteration. + this.RandomNumberGenerator.Seed += 1; + } + + // Increases iterations if there is a specified timeout + // and the default iteration given. + if (this.Configuration.SchedulingIterations == 1 && + this.Configuration.Timeout > 0) + { + maxIterations++; + } + } + + if (this.TestDisposeMethod != null) + { + // Disposes the test state. + this.TestDisposeMethod.Invoke(null, Array.Empty()); + } + } + catch (Exception ex) + { + Exception innerException = ex; + while (innerException is TargetInvocationException) + { + innerException = innerException.InnerException; + } + + if (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + if (!(innerException is TaskCanceledException)) + { + ExceptionDispatchInfo.Capture(innerException).Throw(); + } + } + }, this.CancellationTokenSource.Token); + } + + /// + /// Runs the next testing iteration. + /// + private void RunNextIteration(int iteration) + { + if (this.ShouldPrintIteration(iteration + 1)) + { + this.Logger.WriteLine($"..... Iteration #{iteration + 1}"); + + // Flush when logging to console. + if (this.Logger is ConsoleLogger) + { + Console.Out.Flush(); + } + } + + // Runtime used to serialize and test the program in this iteration. + SystematicTestingRuntime runtime = null; + + // Logger used to intercept the program output if no custom logger + // is installed and if verbosity is turned off. + InMemoryLogger runtimeLogger = null; + + // Gets a handle to the standard output and error streams. + var stdOut = Console.Out; + var stdErr = Console.Error; + + try + { + // Creates a new instance of the bug-finding runtime. + if (this.TestRuntimeFactoryMethod != null) + { + runtime = (SystematicTestingRuntime)this.TestRuntimeFactoryMethod.Invoke( + null, + new object[] { this.Configuration, this.Strategy }); + } + else + { + runtime = new SystematicTestingRuntime(this.Configuration, this.Strategy); + } + + // If verbosity is turned off, then intercept the program log, and also dispose + // the standard output and error streams. + if (!this.Configuration.IsVerbose) + { + runtimeLogger = new InMemoryLogger(); + runtime.SetLogger(runtimeLogger); + + var writer = new LogWriter(new NulLogger()); + Console.SetOut(writer); + Console.SetError(writer); + } + + // Runs the test and waits for it to terminate. + runtime.RunTest(this.TestMethod, this.TestName); + runtime.WaitAsync().Wait(); + + // Invokes user-provided cleanup for this iteration. + if (this.TestIterationDisposeMethod != null) + { + // Disposes the test state. + this.TestIterationDisposeMethod.Invoke(null, null); + } + + // Invoke the per iteration callbacks, if any. + foreach (var callback in this.PerIterationCallbacks) + { + callback(iteration); + } + + // Checks that no monitor is in a hot state at termination. Only + // checked if no safety property violations have been found. + if (!runtime.Scheduler.BugFound) + { + runtime.CheckNoMonitorInHotStateAtTermination(); + } + + if (runtime.Scheduler.BugFound) + { + this.ErrorReporter.WriteErrorLine(runtime.Scheduler.BugReport); + } + + this.GatherIterationStatistics(runtime); + + if (this.TestReport.NumOfFoundBugs > 0) + { + if (runtimeLogger != null) + { + this.ReadableTrace = runtimeLogger.ToString(); + this.ReadableTrace += this.TestReport.GetText(this.Configuration, ""); + } + + this.BugTrace = runtime.BugTrace; + this.ConstructReproducableTrace(runtime); + } + } + finally + { + if (!this.Configuration.IsVerbose) + { + // Restores the standard output and error streams. + Console.SetOut(stdOut); + Console.SetError(stdErr); + } + + if (this.Configuration.PerformFullExploration && runtime.Scheduler.BugFound) + { + this.Logger.WriteLine($"..... Iteration #{iteration + 1} " + + $"triggered bug #{this.TestReport.NumOfFoundBugs} " + + $"[task-{this.Configuration.TestingProcessId}]"); + } + + // Cleans up the runtime before the next iteration starts. + runtimeLogger?.Dispose(); + runtime?.Dispose(); + } + } + + /// + /// Returns a report with the testing results. + /// + public override string GetReport() + { + return this.TestReport.GetText(this.Configuration, "..."); + } + + /// + /// Tries to emit the testing traces, if any. + /// + public override void TryEmitTraces(string directory, string file) + { + // Emits the human readable trace, if it exists. + if (!string.IsNullOrEmpty(this.ReadableTrace)) + { + string[] readableTraces = Directory.GetFiles(directory, file + "_*.txt"). + Where(path => new Regex(@"^.*_[0-9]+.txt$").IsMatch(path)).ToArray(); + string readableTracePath = directory + file + "_" + readableTraces.Length + ".txt"; + + this.Logger.WriteLine($"..... Writing {readableTracePath}"); + File.WriteAllText(readableTracePath, this.ReadableTrace); + } + + // Emits the bug trace, if it exists. + if (this.BugTrace != null) + { + string[] bugTraces = Directory.GetFiles(directory, file + "_*.pstrace"); + string bugTracePath = directory + file + "_" + bugTraces.Length + ".pstrace"; + + using (FileStream stream = File.Open(bugTracePath, FileMode.Create)) + { + DataContractSerializer serializer = new DataContractSerializer(typeof(BugTrace)); + this.Logger.WriteLine($"..... Writing {bugTracePath}"); + serializer.WriteObject(stream, this.BugTrace); + } + } + + // Emits the reproducable trace, if it exists. + if (!string.IsNullOrEmpty(this.ReproducableTrace)) + { + string[] reproTraces = Directory.GetFiles(directory, file + "_*.schedule"); + string reproTracePath = directory + file + "_" + reproTraces.Length + ".schedule"; + + this.Logger.WriteLine($"..... Writing {reproTracePath}"); + File.WriteAllText(reproTracePath, this.ReproducableTrace); + } + + this.Logger.WriteLine($"... Elapsed {this.Profiler.Results()} sec."); + } + + /// + /// Gathers the exploration strategy statistics for the latest testing iteration. + /// + private void GatherIterationStatistics(SystematicTestingRuntime runtime) + { + TestReport report = runtime.Scheduler.GetReport(); + report.CoverageInfo.Merge(runtime.CoverageInfo); + this.TestReport.Merge(report); + } + + /// + /// Constructs a reproducable trace. + /// + private void ConstructReproducableTrace(SystematicTestingRuntime runtime) + { + StringBuilder stringBuilder = new StringBuilder(); + + if (this.Strategy.IsFair()) + { + stringBuilder.Append("--fair-scheduling").Append(Environment.NewLine); + } + + if (this.Configuration.EnableCycleDetection) + { + stringBuilder.Append("--cycle-detection").Append(Environment.NewLine); + stringBuilder.Append("--liveness-temperature-threshold:" + + this.Configuration.LivenessTemperatureThreshold). + Append(Environment.NewLine); + } + else + { + stringBuilder.Append("--liveness-temperature-threshold:" + + this.Configuration.LivenessTemperatureThreshold). + Append(Environment.NewLine); + } + + if (!string.IsNullOrEmpty(this.Configuration.TestMethodName)) + { + stringBuilder.Append("--test-method:" + + this.Configuration.TestMethodName). + Append(Environment.NewLine); + } + + for (int idx = 0; idx < runtime.Scheduler.ScheduleTrace.Count; idx++) + { + ScheduleStep step = runtime.Scheduler.ScheduleTrace[idx]; + if (step.Type == ScheduleStepType.SchedulingChoice) + { + stringBuilder.Append($"({step.ScheduledOperationId})"); + } + else if (step.BooleanChoice != null) + { + stringBuilder.Append(step.BooleanChoice.Value); + } + else + { + stringBuilder.Append(step.IntegerChoice.Value); + } + + if (idx < runtime.Scheduler.ScheduleTrace.Count - 1) + { + stringBuilder.Append(Environment.NewLine); + } + } + + this.ReproducableTrace = stringBuilder.ToString(); + } + + /// + /// Returns true if the engine should print the current iteration. + /// + private bool ShouldPrintIteration(int iteration) + { + if (iteration > this.PrintGuard * 10) + { + var count = iteration.ToString().Length - 1; + var guard = "1" + (count > 0 ? string.Concat(Enumerable.Repeat("0", count)) : string.Empty); + this.PrintGuard = int.Parse(guard); + } + + return iteration % this.PrintGuard == 0; + } + } +} diff --git a/Source/TestingServices/Engines/ITestingEngine.cs b/Source/TestingServices/Engines/ITestingEngine.cs new file mode 100644 index 000000000..15286351f --- /dev/null +++ b/Source/TestingServices/Engines/ITestingEngine.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Interface of a Coyote testing engine. + /// + public interface ITestingEngine + { + /// + /// Data structure containing information gathered during testing. + /// + TestReport TestReport { get; } + + /// + /// Runs the Coyote testing engine. + /// + ITestingEngine Run(); + + /// + /// Stops the Coyote testing engine. + /// + void Stop(); + + /// + /// Returns a report with the testing results. + /// + string GetReport(); + + /// + /// Tries to emit the testing traces, if any. + /// + void TryEmitTraces(string directory, string file); + + /// + /// Registers a callback to invoke at the end of each iteration. The callback + /// takes as a parameter an integer representing the current iteration. + /// + void RegisterPerIterationCallBack(Action callback); + } +} diff --git a/Source/TestingServices/Engines/ReplayEngine.cs b/Source/TestingServices/Engines/ReplayEngine.cs new file mode 100644 index 000000000..2d359a7d8 --- /dev/null +++ b/Source/TestingServices/Engines/ReplayEngine.cs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.TestingServices.Scheduling.Strategies; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The Coyote replay engine. + /// + [DebuggerStepThrough] + internal sealed class ReplayEngine : AbstractTestingEngine + { + /// + /// Text describing an internal replay error. + /// + internal string InternalError { get; private set; } + + /// + /// Creates a new replaying engine. + /// + internal static ReplayEngine Create(Configuration configuration) + { + configuration.SchedulingStrategy = SchedulingStrategy.Replay; + return new ReplayEngine(configuration); + } + + /// + /// Creates a new replaying engine. + /// + internal static ReplayEngine Create(Configuration configuration, Assembly assembly) + { + configuration.SchedulingStrategy = SchedulingStrategy.Replay; + return new ReplayEngine(configuration, assembly); + } + + /// + /// Creates a new replaying engine. + /// + internal static ReplayEngine Create(Configuration configuration, Delegate testMethod) + { + configuration.SchedulingStrategy = SchedulingStrategy.Replay; + return new ReplayEngine(configuration, testMethod); + } + + /// + /// Creates a new replaying engine. + /// + internal static ReplayEngine Create(Configuration configuration, Delegate testMethod, string trace) + { + configuration.SchedulingStrategy = SchedulingStrategy.Replay; + configuration.ScheduleTrace = trace; + return new ReplayEngine(configuration, testMethod); + } + + /// + /// Initializes a new instance of the class. + /// + private ReplayEngine(Configuration configuration) + : base(configuration) + { + } + + /// + /// Initializes a new instance of the class. + /// + private ReplayEngine(Configuration configuration, Assembly assembly) + : base(configuration, assembly) + { + } + + /// + /// Initializes a new instance of the class. + /// + private ReplayEngine(Configuration configuration, Delegate testMethod) + : base(configuration, testMethod) + { + } + + /// + /// Creates a new testing task. + /// + protected override Task CreateTestingTask() + { + return new Task(() => + { + // Runtime used to serialize and test the program. + SystematicTestingRuntime runtime = null; + + // Logger used to intercept the program output if no custom logger + // is installed and if verbosity is turned off. + InMemoryLogger runtimeLogger = null; + + // Gets a handle to the standard output and error streams. + var stdOut = Console.Out; + var stdErr = Console.Error; + + try + { + if (this.TestInitMethod != null) + { + // Initializes the test state. + this.TestInitMethod.Invoke(null, Array.Empty()); + } + + // Creates a new instance of the testing runtime. + if (this.TestRuntimeFactoryMethod != null) + { + runtime = (SystematicTestingRuntime)this.TestRuntimeFactoryMethod.Invoke( + null, + new object[] { this.Configuration, this.Strategy }); + } + else + { + runtime = new SystematicTestingRuntime(this.Configuration, this.Strategy); + } + + // If verbosity is turned off, then intercept the program log, and also redirect + // the standard output and error streams into the runtime logger. + if (!this.Configuration.IsVerbose) + { + runtimeLogger = new InMemoryLogger(); + runtime.SetLogger(runtimeLogger); + + var writer = new LogWriter(new NulLogger()); + Console.SetOut(writer); + Console.SetError(writer); + } + + if (this.Configuration.AttachDebugger) + { + Debugger.Launch(); + } + + // Runs the test and waits for it to terminate. + runtime.RunTest(this.TestMethod, this.TestName); + runtime.WaitAsync().Wait(); + + // Invokes user-provided cleanup for this iteration. + if (this.TestIterationDisposeMethod != null) + { + // Disposes the test state. + this.TestIterationDisposeMethod.Invoke(null, Array.Empty()); + } + + // Invokes user-provided cleanup for all iterations. + if (this.TestDisposeMethod != null) + { + // Disposes the test state. + this.TestDisposeMethod.Invoke(null, Array.Empty()); + } + + this.InternalError = (this.Strategy as ReplayStrategy).ErrorText; + + // Checks that no monitor is in a hot state at termination. Only + // checked if no safety property violations have been found. + if (!runtime.Scheduler.BugFound && this.InternalError.Length == 0) + { + runtime.CheckNoMonitorInHotStateAtTermination(); + } + + if (runtime.Scheduler.BugFound && this.InternalError.Length == 0) + { + this.ErrorReporter.WriteErrorLine(runtime.Scheduler.BugReport); + } + + TestReport report = runtime.Scheduler.GetReport(); + report.CoverageInfo.Merge(runtime.CoverageInfo); + this.TestReport.Merge(report); + } + catch (Exception ex) + { + Exception innerException = ex; + while (innerException is TargetInvocationException) + { + innerException = innerException.InnerException; + } + + if (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + if (!(innerException is TaskCanceledException)) + { + ExceptionDispatchInfo.Capture(innerException).Throw(); + } + } + finally + { + if (!this.Configuration.IsVerbose) + { + // Restores the standard output and error streams. + Console.SetOut(stdOut); + Console.SetError(stdErr); + } + + // Cleans up the runtime. + runtimeLogger?.Dispose(); + runtime?.Dispose(); + } + }, this.CancellationTokenSource.Token); + } + + /// + /// Returns a report with the testing results. + /// + public override string GetReport() + { + StringBuilder report = new StringBuilder(); + + report.AppendFormat("... Reproduced {0} bug{1}.", this.TestReport.NumOfFoundBugs, + this.TestReport.NumOfFoundBugs == 1 ? string.Empty : "s"); + report.AppendLine(); + + report.Append($"... Elapsed {this.Profiler.Results()} sec."); + + return report.ToString(); + } + } +} diff --git a/Source/TestingServices/Engines/TestingEngineFactory.cs b/Source/TestingServices/Engines/TestingEngineFactory.cs new file mode 100644 index 000000000..4448ba636 --- /dev/null +++ b/Source/TestingServices/Engines/TestingEngineFactory.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The testing engine factory. + /// + public static class TestingEngineFactory + { + /// + /// Creates a new bug-finding engine. + /// + public static ITestingEngine CreateBugFindingEngine(Configuration configuration) => + BugFindingEngine.Create(configuration); + + /// + /// Creates a new bug-finding engine. + /// + public static ITestingEngine CreateBugFindingEngine(Configuration configuration, Assembly assembly) => + BugFindingEngine.Create(configuration, assembly); + + /// + /// Creates a new bug-finding engine. + /// + public static ITestingEngine CreateBugFindingEngine(Configuration configuration, Action test) => + BugFindingEngine.Create(configuration, test); + + /// + /// Creates a new bug-finding engine. + /// + public static ITestingEngine CreateBugFindingEngine(Configuration configuration, Action test) => + BugFindingEngine.Create(configuration, test); + + /// + /// Creates a new bug-finding engine. + /// + public static ITestingEngine CreateBugFindingEngine(Configuration configuration, Func test) => + BugFindingEngine.Create(configuration, test); + + /// + /// Creates a new bug-finding engine. + /// + public static ITestingEngine CreateBugFindingEngine(Configuration configuration, Func test) => + BugFindingEngine.Create(configuration, test); + + /// + /// Creates a new replay engine. + /// + public static ITestingEngine CreateReplayEngine(Configuration configuration) => + ReplayEngine.Create(configuration); + + /// + /// Creates a new replay engine. + /// + public static ITestingEngine CreateReplayEngine(Configuration configuration, Assembly assembly) => + ReplayEngine.Create(configuration, assembly); + + /// + /// Creates a new replay engine. + /// + public static ITestingEngine CreateReplayEngine(Configuration configuration, Action test) => + ReplayEngine.Create(configuration, test); + + /// + /// Creates a new replay engine. + /// + public static ITestingEngine CreateReplayEngine(Configuration configuration, Action test) => + ReplayEngine.Create(configuration, test); + + /// + /// Creates a new replay engine. + /// + public static ITestingEngine CreateReplayEngine(Configuration configuration, Func test) => + ReplayEngine.Create(configuration, test); + + /// + /// Creates a new replay engine. + /// + public static ITestingEngine CreateReplayEngine(Configuration configuration, Func test) => + ReplayEngine.Create(configuration, test); + } +} diff --git a/Source/TestingServices/Exploration/ISchedulingStrategy.cs b/Source/TestingServices/Exploration/ISchedulingStrategy.cs new file mode 100644 index 000000000..a7e289d44 --- /dev/null +++ b/Source/TestingServices/Exploration/ISchedulingStrategy.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// Interface of a machine scheduling strategy. + /// + public interface ISchedulingStrategy + { + /// + /// Forces the next asynchronous operation to be scheduled. + /// + /// The next operation to schedule. + /// List of operations that can be scheduled. + /// The currently scheduled operation. + /// True if there is a next choice, else false. + bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current); + + /// + /// Returns the next boolean choice. + /// + /// The max value. + /// The next boolean choice. + /// True if there is a next choice, else false. + bool GetNextBooleanChoice(int maxValue, out bool next); + + /// + /// Returns the next integer choice. + /// + /// The max value. + /// The next integer choice. + /// True if there is a next choice, else false. + bool GetNextIntegerChoice(int maxValue, out int next); + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + /// The next operation to schedule. + /// List of operations that can be scheduled. + /// The currently scheduled operation. + void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current); + + /// + /// Forces the next boolean choice. + /// + /// The max value. + /// The next boolean choice. + void ForceNextBooleanChoice(int maxValue, bool next); + + /// + /// Forces the next integer choice. + /// + /// The max value. + /// The next integer choice. + void ForceNextIntegerChoice(int maxValue, int next); + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + /// True to start the next iteration. + bool PrepareForNextIteration(); + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + void Reset(); + + /// + /// Returns the scheduled steps. + /// + int GetScheduledSteps(); + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + bool HasReachedMaxSchedulingSteps(); + + /// + /// Checks if this is a fair scheduling strategy. + /// + bool IsFair(); + + /// + /// Returns a textual description of the scheduling strategy. + /// + string GetDescription(); + } +} diff --git a/Source/TestingServices/Exploration/OperationScheduler.cs b/Source/TestingServices/Exploration/OperationScheduler.cs new file mode 100644 index 000000000..334126bf9 --- /dev/null +++ b/Source/TestingServices/Exploration/OperationScheduler.cs @@ -0,0 +1,593 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.TestingServices.Scheduling.Strategies; +using Microsoft.Coyote.TestingServices.Tracing.Schedule; + +namespace Microsoft.Coyote.TestingServices.Scheduling +{ + /// + /// Implements a scheduler that serializes and schedules controlled operations. + /// + [DebuggerStepThrough] + internal sealed class OperationScheduler + { + /// + /// The configuration used by the scheduler. + /// + internal readonly Configuration Configuration; + + /// + /// The testing runtime. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// The scheduling strategy to be used for state-space exploration. + /// + private readonly ISchedulingStrategy Strategy; + + /// + /// Map from unique ids to asynchronous operations. + /// + private readonly Dictionary OperationMap; + + /// + /// Map from ids of tasks that are controlled by the runtime to operations. + /// + internal readonly ConcurrentDictionary ControlledTaskMap; + + /// + /// The program schedule trace. + /// + internal ScheduleTrace ScheduleTrace; + + /// + /// The scheduler completion source. + /// + private readonly TaskCompletionSource CompletionSource; + + /// + /// Checks if the scheduler is running. + /// + private bool IsSchedulerRunning; + + /// + /// The currently scheduled asynchronous operation. + /// + internal MachineOperation ScheduledOperation { get; private set; } + + /// + /// Number of scheduled steps. + /// + internal int ScheduledSteps => this.Strategy.GetScheduledSteps(); + + /// + /// Checks if the schedule has been fully explored. + /// + internal bool HasFullyExploredSchedule { get; private set; } + + /// + /// True if a bug was found. + /// + internal bool BugFound { get; private set; } + + /// + /// Bug report. + /// + internal string BugReport { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + internal OperationScheduler(SystematicTestingRuntime runtime, ISchedulingStrategy strategy, + ScheduleTrace trace, Configuration configuration) + { + this.Configuration = configuration; + this.Runtime = runtime; + this.Strategy = strategy; + this.OperationMap = new Dictionary(); + this.ControlledTaskMap = new ConcurrentDictionary(); + this.ScheduleTrace = trace; + this.CompletionSource = new TaskCompletionSource(); + this.IsSchedulerRunning = true; + this.BugFound = false; + this.HasFullyExploredSchedule = false; + } + + /// + /// Schedules the next enabled operation. + /// + internal void ScheduleNextEnabledOperation() + { + int? taskId = Task.CurrentId; + + // If the caller is the root task, then return. + if (taskId != null && taskId == this.Runtime.RootTaskId) + { + return; + } + + if (!this.IsSchedulerRunning) + { + this.Stop(); + throw new ExecutionCanceledException(); + } + + // Checks if concurrency not controlled by the runtime was used. + this.CheckNoExternalConcurrencyUsed(); + + // Checks if the scheduling steps bound has been reached. + this.CheckIfSchedulingStepsBoundIsReached(); + + // Get and order the operations by their id. + var ops = this.OperationMap.Values.OrderBy(op => op.SourceId).Select(op => op as IAsyncOperation).ToList(); + + // Try enable any operation that is currently waiting, but has its dependencies already satisfied. + foreach (var op in ops) + { + if (op is MachineOperation machineOp) + { + machineOp.TryEnable(); + IO.Debug.WriteLine(" Operation '{0}' has status '{1}'.", op.SourceId, op.Status); + } + } + + MachineOperation current = this.ScheduledOperation; + if (!this.Strategy.GetNext(out IAsyncOperation next, ops, current)) + { + // Checks if the program has deadlocked. + this.CheckIfProgramHasDeadlocked(ops.Select(op => op as MachineOperation)); + + IO.Debug.WriteLine(" Schedule explored."); + this.HasFullyExploredSchedule = true; + this.Stop(); + throw new ExecutionCanceledException(); + } + + this.ScheduledOperation = next as MachineOperation; + this.ScheduleTrace.AddSchedulingChoice(next.SourceId); + + IO.Debug.WriteLine($" Scheduling the next operation of '{next.SourceName}'."); + + if (current != next) + { + current.IsActive = false; + lock (next) + { + this.ScheduledOperation.IsActive = true; + System.Threading.Monitor.PulseAll(next); + } + + lock (current) + { + if (!current.IsHandlerRunning) + { + return; + } + + if (!this.ControlledTaskMap.ContainsKey(Task.CurrentId.Value)) + { + this.ControlledTaskMap.TryAdd(Task.CurrentId.Value, current); + IO.Debug.WriteLine($" Operation '{current.SourceId}' is associated with task '{Task.CurrentId}'."); + } + + while (!current.IsActive) + { + IO.Debug.WriteLine($" Sleeping the current operation of '{current.SourceName}' on task '{Task.CurrentId}'."); + System.Threading.Monitor.Wait(current); + IO.Debug.WriteLine($" Waking up the current operation of '{current.SourceName}' on task '{Task.CurrentId}'."); + } + + if (current.Status != AsyncOperationStatus.Enabled) + { + throw new ExecutionCanceledException(); + } + } + } + } + + /// + /// Returns the next nondeterministic boolean choice. + /// + internal bool GetNextNondeterministicBooleanChoice(int maxValue, string uniqueId = null) + { + // Checks if concurrency not controlled by the runtime was used. + this.CheckNoExternalConcurrencyUsed(); + + // Checks if the scheduling steps bound has been reached. + this.CheckIfSchedulingStepsBoundIsReached(); + + if (!this.Strategy.GetNextBooleanChoice(maxValue, out bool choice)) + { + IO.Debug.WriteLine(" Schedule explored."); + this.Stop(); + throw new ExecutionCanceledException(); + } + + if (uniqueId is null) + { + this.ScheduleTrace.AddNondeterministicBooleanChoice(choice); + } + else + { + this.ScheduleTrace.AddFairNondeterministicBooleanChoice(uniqueId, choice); + } + + return choice; + } + + /// + /// Returns the next nondeterministic integer choice. + /// + internal int GetNextNondeterministicIntegerChoice(int maxValue) + { + // Checks if concurrency not controlled by the runtime was used. + this.CheckNoExternalConcurrencyUsed(); + + // Checks if the scheduling steps bound has been reached. + this.CheckIfSchedulingStepsBoundIsReached(); + + if (!this.Strategy.GetNextIntegerChoice(maxValue, out int choice)) + { + IO.Debug.WriteLine(" Schedule explored."); + this.Stop(); + throw new ExecutionCanceledException(); + } + + this.ScheduleTrace.AddNondeterministicIntegerChoice(choice); + + return choice; + } + + /// + /// Waits for the specified asynchronous operation to start. + /// + internal void WaitForOperationToStart(MachineOperation op) + { + lock (op) + { + if (this.OperationMap.Count == 1) + { + op.IsActive = true; + System.Threading.Monitor.PulseAll(op); + } + else + { + while (!op.IsHandlerRunning) + { + System.Threading.Monitor.Wait(op); + } + } + } + } + + /// + /// Notify that the specified asynchronous operation has been created + /// and will start executing on the specified task. + /// + internal void NotifyOperationCreated(MachineOperation op, Task task) + { + IO.Debug.WriteLine($" Mapping operation '{op.SourceName}' to task '{task.Id}'."); + this.ControlledTaskMap.TryAdd(task.Id, op); + if (!this.OperationMap.ContainsKey(op.SourceId)) + { + if (this.OperationMap.Count == 0) + { + this.ScheduledOperation = op; + } + + this.OperationMap.Add(op.SourceId, op); + } + } + + /// + /// Notify that the specified asynchronous operation has started. + /// + internal static void NotifyOperationStarted(MachineOperation op) + { + IO.Debug.WriteLine($" Starting the current operation of '{op.SourceName}' on task '{Task.CurrentId}'."); + + lock (op) + { + op.IsHandlerRunning = true; + System.Threading.Monitor.PulseAll(op); + while (!op.IsActive) + { + IO.Debug.WriteLine($" Sleeping the current operation of '{op.SourceName}' on task '{Task.CurrentId}'."); + System.Threading.Monitor.Wait(op); + IO.Debug.WriteLine($" Waking up the current operation of '{op.SourceName}' on task '{Task.CurrentId}'."); + } + + if (op.Status != AsyncOperationStatus.Enabled) + { + throw new ExecutionCanceledException(); + } + } + } + + /// + /// Notify that an assertion has failed. + /// + [DebuggerHidden] + internal void NotifyAssertionFailure(string text, bool killTasks = true, bool cancelExecution = true) + { + if (!this.BugFound) + { + this.BugReport = text; + + this.Runtime.LogWriter.OnError($" {text}"); + this.Runtime.LogWriter.OnStrategyError(this.Configuration.SchedulingStrategy, this.Strategy.GetDescription()); + + this.BugFound = true; + + if (this.Configuration.AttachDebugger) + { + Debugger.Break(); + } + } + + if (killTasks) + { + this.Stop(); + } + + if (cancelExecution) + { + throw new ExecutionCanceledException(); + } + } + + /// + /// Returns the enabled schedulable ids. + /// + internal HashSet GetEnabledSchedulableIds() + { + var enabledSchedulableIds = new HashSet(); + foreach (var machineInfo in this.OperationMap.Values) + { + if (machineInfo.Status is AsyncOperationStatus.Enabled) + { + enabledSchedulableIds.Add(machineInfo.SourceId); + } + } + + return enabledSchedulableIds; + } + + /// + /// Returns a test report with the scheduling statistics. + /// + internal TestReport GetReport() + { + TestReport report = new TestReport(this.Configuration); + + if (this.BugFound) + { + report.NumOfFoundBugs++; + report.BugReports.Add(this.BugReport); + } + + if (this.Strategy.IsFair()) + { + report.NumOfExploredFairSchedules++; + report.TotalExploredFairSteps += this.ScheduledSteps; + + if (report.MinExploredFairSteps < 0 || + report.MinExploredFairSteps > this.ScheduledSteps) + { + report.MinExploredFairSteps = this.ScheduledSteps; + } + + if (report.MaxExploredFairSteps < this.ScheduledSteps) + { + report.MaxExploredFairSteps = this.ScheduledSteps; + } + + if (this.Strategy.HasReachedMaxSchedulingSteps()) + { + report.MaxFairStepsHitInFairTests++; + } + + if (this.ScheduledSteps >= report.Configuration.MaxUnfairSchedulingSteps) + { + report.MaxUnfairStepsHitInFairTests++; + } + } + else + { + report.NumOfExploredUnfairSchedules++; + + if (this.Strategy.HasReachedMaxSchedulingSteps()) + { + report.MaxUnfairStepsHitInUnfairTests++; + } + } + + return report; + } + + /// + /// Checks that no task that is not controlled by the runtime is currently executing. + /// + [DebuggerHidden] + internal void CheckNoExternalConcurrencyUsed() + { + if (!this.IsSchedulerRunning) + { + throw new ExecutionCanceledException(); + } + + if (!Task.CurrentId.HasValue || !this.ControlledTaskMap.ContainsKey(Task.CurrentId.Value)) + { + this.NotifyAssertionFailure(string.Format(CultureInfo.InvariantCulture, + "Uncontrolled task with id '{0}' invoked a runtime method. Please make sure to avoid using concurrency APIs " + + "such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers or controlled tasks. If you are " + + "using external libraries that are executing concurrently, you will need to mock them during testing.", + Task.CurrentId.HasValue ? Task.CurrentId.Value.ToString() : "")); + } + } + + /// + /// Checks for a deadlock. This happens when there are no more enabled operations, + /// but there is one or more blocked operations that are waiting to receive an event + /// or for a task to complete. + /// + [DebuggerHidden] + private void CheckIfProgramHasDeadlocked(IEnumerable ops) + { + var blockedOnReceiveOperations = ops.Where(op => op.Status is AsyncOperationStatus.BlockedOnReceive).ToList(); + var blockedOnWaitOperations = ops.Where(op => op.Status is AsyncOperationStatus.BlockedOnWaitAll || + op.Status is AsyncOperationStatus.BlockedOnWaitAny).ToList(); + var blockedOnResourceSynchronization = ops.Where(op => op.Status is AsyncOperationStatus.BlockedOnResource).ToList(); + if (blockedOnReceiveOperations.Count == 0 && + blockedOnWaitOperations.Count == 0 && + blockedOnResourceSynchronization.Count == 0) + { + return; + } + + string message = "Deadlock detected."; + if (blockedOnReceiveOperations.Count > 0) + { + for (int i = 0; i < blockedOnReceiveOperations.Count; i++) + { + message += string.Format(CultureInfo.InvariantCulture, " '{0}'", blockedOnReceiveOperations[i].SourceName); + if (i == blockedOnReceiveOperations.Count - 2) + { + message += " and"; + } + else if (i < blockedOnReceiveOperations.Count - 1) + { + message += ","; + } + } + + message += blockedOnReceiveOperations.Count == 1 ? " is " : " are "; + message += "waiting to receive an event, but no other controlled tasks are enabled."; + } + + if (blockedOnWaitOperations.Count > 0) + { + for (int i = 0; i < blockedOnWaitOperations.Count; i++) + { + message += string.Format(CultureInfo.InvariantCulture, " '{0}'", blockedOnWaitOperations[i].SourceName); + if (i == blockedOnWaitOperations.Count - 2) + { + message += " and"; + } + else if (i < blockedOnWaitOperations.Count - 1) + { + message += ","; + } + } + + message += blockedOnWaitOperations.Count == 1 ? " is " : " are "; + message += "waiting for a task to complete, but no other controlled tasks are enabled."; + } + + if (blockedOnResourceSynchronization.Count > 0) + { + for (int i = 0; i < blockedOnResourceSynchronization.Count; i++) + { + message += string.Format(CultureInfo.InvariantCulture, " '{0}'", blockedOnResourceSynchronization[i].SourceName); + if (i == blockedOnResourceSynchronization.Count - 2) + { + message += " and"; + } + else if (i < blockedOnResourceSynchronization.Count - 1) + { + message += ","; + } + } + + message += blockedOnResourceSynchronization.Count == 1 ? " is " : " are "; + message += "waiting to access a concurrent resource that is acquired by another task, "; + message += "but no other controlled tasks are enabled."; + } + + this.NotifyAssertionFailure(message); + } + + /// + /// Checks if the scheduling steps bound has been reached. If yes, + /// it stops the scheduler and kills all enabled machines. + /// + [DebuggerHidden] + private void CheckIfSchedulingStepsBoundIsReached() + { + if (this.Strategy.HasReachedMaxSchedulingSteps()) + { + int bound = this.Strategy.IsFair() ? this.Configuration.MaxFairSchedulingSteps : + this.Configuration.MaxUnfairSchedulingSteps; + string message = $"Scheduling steps bound of {bound} reached."; + + if (this.Configuration.ConsiderDepthBoundHitAsBug) + { + this.NotifyAssertionFailure(message); + } + else + { + IO.Debug.WriteLine($" {message}"); + this.Stop(); + throw new ExecutionCanceledException(); + } + } + } + + /// + /// Waits until the scheduler terminates. + /// + internal Task WaitAsync() => this.CompletionSource.Task; + + /// + /// Stops the scheduler. + /// + private void Stop() + { + this.IsSchedulerRunning = false; + this.KillRemainingOperations(); + + // Check if the completion source is completed. If not synchronize on + // it (as it can only be set once) and set its result. + if (!this.CompletionSource.Task.IsCompleted) + { + lock (this.CompletionSource) + { + if (!this.CompletionSource.Task.IsCompleted) + { + this.CompletionSource.SetResult(true); + } + } + } + } + + /// + /// Kills any remaining operations at the end of the schedule. + /// + private void KillRemainingOperations() + { + foreach (var op in this.OperationMap.Values) + { + op.IsActive = true; + op.Status = AsyncOperationStatus.Completed; + + if (op.IsHandlerRunning) + { + lock (op) + { + System.Threading.Monitor.PulseAll(op); + } + } + } + } + } +} diff --git a/Source/TestingServices/Exploration/RandomNumberGenerators/DefaultRandomNumberGenerator.cs b/Source/TestingServices/Exploration/RandomNumberGenerators/DefaultRandomNumberGenerator.cs new file mode 100644 index 000000000..eeec979b2 --- /dev/null +++ b/Source/TestingServices/Exploration/RandomNumberGenerators/DefaultRandomNumberGenerator.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Coyote.TestingServices.Scheduling +{ + /// + /// Default random number generator that uses the generator. + /// + public class DefaultRandomNumberGenerator : IRandomNumberGenerator + { + /// + /// Device for generating random numbers. + /// + private Random Random; + + /// + /// The seed currently used by the generator. + /// + private int RandomSeed; + + /// + /// The seed currently used by the generator. + /// + public int Seed + { + get => this.RandomSeed; + + set + { + this.RandomSeed = value; + this.Random = new Random(this.RandomSeed); + } + } + + /// + /// Initializes a new instance of the class. + /// It uses a time-dependent seed. + /// + public DefaultRandomNumberGenerator() + { + this.RandomSeed = DateTime.Now.Millisecond; + this.Random = new Random(this.RandomSeed); + } + + /// + /// Initializes a new instance of the class. + /// It uses the specified seed. + /// + public DefaultRandomNumberGenerator(int seed) + { + this.RandomSeed = seed; + this.Random = new Random(seed); + } + + /// + /// Returns a non-negative random number. + /// + public int Next() + { + return this.Random.Next(); + } + + /// + /// Returns a non-negative random number less than the specified max value. + /// + /// Exclusive upper bound. + public int Next(int maxValue) + { + return this.Random.Next(maxValue); + } + } +} diff --git a/Source/TestingServices/Exploration/RandomNumberGenerators/IRandomNumberGenerator.cs b/Source/TestingServices/Exploration/RandomNumberGenerators/IRandomNumberGenerator.cs new file mode 100644 index 000000000..543e3b141 --- /dev/null +++ b/Source/TestingServices/Exploration/RandomNumberGenerators/IRandomNumberGenerator.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.TestingServices.Scheduling +{ + /// + /// Interface for random number generators. + /// + public interface IRandomNumberGenerator + { + /// + /// The seed currently used by the generator. + /// + int Seed { get; set; } + + /// + /// Returns a non-negative random number. + /// + int Next(); + + /// + /// Returns a non-negative random number less than maxValue. + /// + /// Exclusive upper bound + int Next(int maxValue); + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Bounded/DelayBoundingStrategy.cs b/Source/TestingServices/Exploration/Strategies/Bounded/DelayBoundingStrategy.cs new file mode 100644 index 000000000..9212bddc7 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Bounded/DelayBoundingStrategy.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// An abstract delay-bounding scheduling strategy. + /// + public abstract class DelayBoundingStrategy : ISchedulingStrategy + { + /// + /// The random number generator used by the strategy. + /// + protected IRandomNumberGenerator RandomNumberGenerator; + + /// + /// The maximum number of steps to schedule. + /// + protected int MaxScheduledSteps; + + /// + /// The number of scheduled steps. + /// + protected int ScheduledSteps; + + /// + /// Length of the explored schedule across all iterations. + /// + protected int ScheduleLength; + + /// + /// The maximum number of delays. + /// + protected int MaxDelays; + + /// + /// The remaining delays. + /// + protected List RemainingDelays; + + /// + /// Initializes a new instance of the class. + /// It uses the default random number generator (seed is based on current time). + /// + public DelayBoundingStrategy(int maxSteps, int maxDelays) + : this(maxSteps, maxDelays, new DefaultRandomNumberGenerator(DateTime.Now.Millisecond)) + { + } + + /// + /// Initializes a new instance of the class. + /// It uses the specified random number generator. + /// + public DelayBoundingStrategy(int maxSteps, int maxDelays, IRandomNumberGenerator random) + { + this.RandomNumberGenerator = random; + this.MaxScheduledSteps = maxSteps; + this.ScheduledSteps = 0; + this.MaxDelays = maxDelays; + this.ScheduleLength = 0; + this.RemainingDelays = new List(); + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public virtual bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + var currentMachineIdx = ops.IndexOf(current); + var orderedMachines = ops.GetRange(currentMachineIdx, ops.Count - currentMachineIdx); + if (currentMachineIdx != 0) + { + orderedMachines.AddRange(ops.GetRange(0, currentMachineIdx)); + } + + var enabledOperations = orderedMachines.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + if (enabledOperations.Count == 0) + { + next = null; + return false; + } + + int idx = 0; + while (this.RemainingDelays.Count > 0 && this.ScheduledSteps == this.RemainingDelays[0]) + { + idx = (idx + 1) % enabledOperations.Count; + this.RemainingDelays.RemoveAt(0); + Debug.WriteLine(" Inserted delay, '{0}' remaining.", this.RemainingDelays.Count); + } + + next = enabledOperations[idx]; + + this.ScheduledSteps++; + + return true; + } + + /// + /// Returns the next boolean choice. + /// + public virtual bool GetNextBooleanChoice(int maxValue, out bool next) + { + next = false; + if (this.RemainingDelays.Count > 0 && this.ScheduledSteps == this.RemainingDelays[0]) + { + next = true; + this.RemainingDelays.RemoveAt(0); + Debug.WriteLine(" Inserted delay, '{0}' remaining.", this.RemainingDelays.Count); + } + + this.ScheduledSteps++; + + return true; + } + + /// + /// Returns the next integer choice. + /// + public virtual bool GetNextIntegerChoice(int maxValue, out int next) + { + next = this.RandomNumberGenerator.Next(maxValue); + this.ScheduledSteps++; + return true; + } + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + this.ScheduledSteps++; + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + this.ScheduledSteps++; + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + this.ScheduledSteps++; + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public abstract bool PrepareForNextIteration(); + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public virtual void Reset() + { + this.ScheduleLength = 0; + this.ScheduledSteps = 0; + this.RemainingDelays.Clear(); + } + + /// + /// Returns the scheduled steps. + /// + public int GetScheduledSteps() => this.ScheduledSteps; + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public bool HasReachedMaxSchedulingSteps() + { + if (this.MaxScheduledSteps == 0) + { + return false; + } + + return this.ScheduledSteps >= this.MaxScheduledSteps; + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public bool IsFair() => false; + + /// + /// Returns a textual description of the scheduling strategy. + /// + public abstract string GetDescription(); + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Bounded/ExhaustiveDelayBoundingStrategy.cs b/Source/TestingServices/Exploration/Strategies/Bounded/ExhaustiveDelayBoundingStrategy.cs new file mode 100644 index 000000000..7a9dc4f68 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Bounded/ExhaustiveDelayBoundingStrategy.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// An exhaustive delay-bounding scheduling strategy. + /// + public sealed class ExhaustiveDelayBoundingStrategy : DelayBoundingStrategy, ISchedulingStrategy + { + /// + /// Cache of delays across iterations. + /// + private List DelaysCache; + + /// + /// Initializes a new instance of the class. + /// It uses the default random number generator (seed is based on current time). + /// + public ExhaustiveDelayBoundingStrategy(int maxSteps, int maxDelays) + : this(maxSteps, maxDelays, new DefaultRandomNumberGenerator(DateTime.Now.Millisecond)) + { + } + + /// + /// Initializes a new instance of the class. + /// It uses the specified random number generator. + /// + public ExhaustiveDelayBoundingStrategy(int maxSteps, int maxDelays, IRandomNumberGenerator random) + : base(maxSteps, maxDelays, random) + { + this.DelaysCache = Enumerable.Repeat(0, this.MaxDelays).ToList(); + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public override bool PrepareForNextIteration() + { + this.ScheduleLength = Math.Max(this.ScheduleLength, this.ScheduledSteps); + this.ScheduledSteps = 0; + + var bound = Math.Min(this.MaxScheduledSteps, this.ScheduleLength); + for (var idx = 0; idx < this.MaxDelays; idx++) + { + if (this.DelaysCache[idx] < bound) + { + this.DelaysCache[idx] = this.DelaysCache[idx] + 1; + break; + } + + this.DelaysCache[idx] = 0; + } + + this.RemainingDelays.Clear(); + this.RemainingDelays.AddRange(this.DelaysCache); + this.RemainingDelays.Sort(); + + return true; + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public override void Reset() + { + this.DelaysCache = Enumerable.Repeat(0, this.MaxDelays).ToList(); + base.Reset(); + } + + /// + /// Returns a textual description of the scheduling strategy. + /// + public override string GetDescription() + { + var text = this.MaxDelays + "' delays, delays '["; + for (int idx = 0; idx < this.DelaysCache.Count; idx++) + { + text += this.DelaysCache[idx]; + if (idx < this.DelaysCache.Count - 1) + { + text += ", "; + } + } + + text += "]'"; + return text; + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Bounded/PCTStrategy.cs b/Source/TestingServices/Exploration/Strategies/Bounded/PCTStrategy.cs new file mode 100644 index 000000000..dc3024d06 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Bounded/PCTStrategy.cs @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// A priority-based probabilistic scheduling strategy. + /// + /// This strategy is described in the following paper: + /// https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/asplos277-pct.pdf + /// + public sealed class PCTStrategy : ISchedulingStrategy + { + /// + /// Random number generator. + /// + private readonly IRandomNumberGenerator RandomNumberGenerator; + + /// + /// The maximum number of steps to schedule. + /// + private readonly int MaxScheduledSteps; + + /// + /// The number of scheduled steps. + /// + private int ScheduledSteps; + + /// + /// Max number of priority switch points. + /// + private readonly int MaxPrioritySwitchPoints; + + /// + /// Approximate length of the schedule across all iterations. + /// + private int ScheduleLength; + + /// + /// List of prioritized operations. + /// + private readonly List PrioritizedOperations; + + /// + /// Set of priority change points. + /// + private readonly SortedSet PriorityChangePoints; + + /// + /// Initializes a new instance of the class. It uses + /// the default random number generator (seed is based on current time). + /// + public PCTStrategy(int maxSteps, int maxPrioritySwitchPoints) + : this(maxSteps, maxPrioritySwitchPoints, new DefaultRandomNumberGenerator(DateTime.Now.Millisecond)) + { + } + + /// + /// Initializes a new instance of the class. + /// It uses the specified random number generator. + /// + public PCTStrategy(int maxSteps, int maxPrioritySwitchPoints, IRandomNumberGenerator random) + { + this.RandomNumberGenerator = random; + this.MaxScheduledSteps = maxSteps; + this.ScheduledSteps = 0; + this.ScheduleLength = 0; + this.MaxPrioritySwitchPoints = maxPrioritySwitchPoints; + this.PrioritizedOperations = new List(); + this.PriorityChangePoints = new SortedSet(); + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + next = null; + return this.GetNextHelper(ref next, ops, current); + } + + /// + /// Returns the next boolean choice. + /// + public bool GetNextBooleanChoice(int maxValue, out bool next) + { + next = false; + if (this.RandomNumberGenerator.Next(maxValue) == 0) + { + next = true; + } + + this.ScheduledSteps++; + + return true; + } + + /// + /// Returns the next integer choice. + /// + public bool GetNextIntegerChoice(int maxValue, out int next) + { + next = this.RandomNumberGenerator.Next(maxValue); + this.ScheduledSteps++; + return true; + } + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + this.GetNextHelper(ref next, ops, current); + } + + /// + /// Returns or forces the next asynchronous operation to schedule. + /// + private bool GetNextHelper(ref IAsyncOperation next, List ops, IAsyncOperation current) + { + var enabledOperations = ops.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + if (enabledOperations.Count == 0) + { + return false; + } + + IAsyncOperation highestEnabledOp = this.GetPrioritizedOperation(enabledOperations, current); + if (next is null) + { + next = highestEnabledOp; + } + + this.ScheduledSteps++; + + return true; + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + this.ScheduledSteps++; + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + this.ScheduledSteps++; + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public bool PrepareForNextIteration() + { + this.ScheduleLength = Math.Max(this.ScheduleLength, this.ScheduledSteps); + this.ScheduledSteps = 0; + + this.PrioritizedOperations.Clear(); + this.PriorityChangePoints.Clear(); + + var range = new List(); + for (int idx = 0; idx < this.ScheduleLength; idx++) + { + range.Add(idx); + } + + foreach (int point in this.Shuffle(range).Take(this.MaxPrioritySwitchPoints)) + { + this.PriorityChangePoints.Add(point); + } + + return true; + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public void Reset() + { + this.ScheduleLength = 0; + this.ScheduledSteps = 0; + this.PrioritizedOperations.Clear(); + this.PriorityChangePoints.Clear(); + } + + /// + /// Returns the scheduled steps. + /// + public int GetScheduledSteps() => this.ScheduledSteps; + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public bool HasReachedMaxSchedulingSteps() + { + if (this.MaxScheduledSteps == 0) + { + return false; + } + + return this.ScheduledSteps >= this.MaxScheduledSteps; + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public bool IsFair() => false; + + /// + /// Returns a textual description of the scheduling strategy. + /// + public string GetDescription() + { + var text = $"PCT[priority change points '{this.MaxPrioritySwitchPoints}' ["; + + int idx = 0; + foreach (var points in this.PriorityChangePoints) + { + text += points; + if (idx < this.PriorityChangePoints.Count - 1) + { + text += ", "; + } + + idx++; + } + + text += "], seed '" + this.RandomNumberGenerator.Seed + "']"; + return text; + } + + /// + /// Returns the prioritized operation. + /// + private IAsyncOperation GetPrioritizedOperation(List ops, IAsyncOperation current) + { + if (this.PrioritizedOperations.Count == 0) + { + this.PrioritizedOperations.Add(current); + } + + foreach (var op in ops.Where(op => !this.PrioritizedOperations.Contains(op))) + { + var mIndex = this.RandomNumberGenerator.Next(this.PrioritizedOperations.Count) + 1; + this.PrioritizedOperations.Insert(mIndex, op); + Debug.WriteLine($" Detected new operation from '{op.SourceName}' at index '{mIndex}'."); + } + + if (this.PriorityChangePoints.Contains(this.ScheduledSteps)) + { + if (ops.Count == 1) + { + this.MovePriorityChangePointForward(); + } + else + { + var priority = this.GetHighestPriorityEnabledOperation(ops); + this.PrioritizedOperations.Remove(priority); + this.PrioritizedOperations.Add(priority); + Debug.WriteLine($" Operation '{priority}' changes to lowest priority."); + } + } + + var prioritizedSchedulable = this.GetHighestPriorityEnabledOperation(ops); + Debug.WriteLine($" Prioritized schedulable '{prioritizedSchedulable}'."); + Debug.Write(" Priority list: "); + for (int idx = 0; idx < this.PrioritizedOperations.Count; idx++) + { + if (idx < this.PrioritizedOperations.Count - 1) + { + Debug.Write($"'{this.PrioritizedOperations[idx]}', "); + } + else + { + Debug.WriteLine($"'{this.PrioritizedOperations[idx]}({1})'."); + } + } + + return ops.First(op => op.Equals(prioritizedSchedulable)); + } + + /// + /// Returns the highest-priority enabled operation. + /// + private IAsyncOperation GetHighestPriorityEnabledOperation(IEnumerable choices) + { + IAsyncOperation prioritizedOp = null; + foreach (var entity in this.PrioritizedOperations) + { + if (choices.Any(m => m == entity)) + { + prioritizedOp = entity; + break; + } + } + + return prioritizedOp; + } + + /// + /// Shuffles the specified list using the Fisher-Yates algorithm. + /// + private IList Shuffle(IList list) + { + var result = new List(list); + for (int idx = result.Count - 1; idx >= 1; idx--) + { + int point = this.RandomNumberGenerator.Next(this.ScheduleLength); + int temp = result[idx]; + result[idx] = result[point]; + result[point] = temp; + } + + return result; + } + + /// + /// Moves the current priority change point forward. This is a useful + /// optimization when a priority change point is assigned in either a + /// sequential execution or a nondeterministic choice. + /// + private void MovePriorityChangePointForward() + { + this.PriorityChangePoints.Remove(this.ScheduledSteps); + var newPriorityChangePoint = this.ScheduledSteps + 1; + while (this.PriorityChangePoints.Contains(newPriorityChangePoint)) + { + newPriorityChangePoint++; + } + + this.PriorityChangePoints.Add(newPriorityChangePoint); + Debug.WriteLine($" Moving priority change to '{newPriorityChangePoint}'."); + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Bounded/RandomDelayBoundingStrategy.cs b/Source/TestingServices/Exploration/Strategies/Bounded/RandomDelayBoundingStrategy.cs new file mode 100644 index 000000000..35758e4ac --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Bounded/RandomDelayBoundingStrategy.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// A randomized delay-bounding scheduling strategy. + /// + public sealed class RandomDelayBoundingStrategy : DelayBoundingStrategy, ISchedulingStrategy + { + /// + /// Delays during this iteration. + /// + private readonly List CurrentIterationDelays; + + /// + /// Initializes a new instance of the class. + /// It uses the default random number generator (seed is based on current time). + /// + public RandomDelayBoundingStrategy(int maxSteps, int maxDelays) + : this(maxSteps, maxDelays, new DefaultRandomNumberGenerator(DateTime.Now.Millisecond)) + { + } + + /// + /// Initializes a new instance of the class. + /// It uses the specified random number generator. + /// + public RandomDelayBoundingStrategy(int maxSteps, int maxDelays, IRandomNumberGenerator random) + : base(maxSteps, maxDelays, random) + { + this.CurrentIterationDelays = new List(); + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + /// True to start the next iteration. + public override bool PrepareForNextIteration() + { + this.ScheduleLength = Math.Max(this.ScheduleLength, this.ScheduledSteps); + this.ScheduledSteps = 0; + + this.RemainingDelays.Clear(); + for (int idx = 0; idx < this.MaxDelays; idx++) + { + this.RemainingDelays.Add(this.RandomNumberGenerator.Next(this.ScheduleLength)); + } + + this.RemainingDelays.Sort(); + + this.CurrentIterationDelays.Clear(); + this.CurrentIterationDelays.AddRange(this.RemainingDelays); + + return true; + } + + /// + /// Returns a textual description of the scheduling strategy. + /// + public override string GetDescription() + { + var text = "Random seed '" + this.RandomNumberGenerator.Seed + "', '" + this.MaxDelays + "' delays, delays '["; + for (int idx = 0; idx < this.CurrentIterationDelays.Count; idx++) + { + text += this.CurrentIterationDelays[idx]; + if (idx < this.CurrentIterationDelays.Count - 1) + { + text += ", "; + } + } + + text += "]'"; + return text; + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Exhaustive/DFSStrategy.cs b/Source/TestingServices/Exploration/Strategies/Exhaustive/DFSStrategy.cs new file mode 100644 index 000000000..34b2a8c9c --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Exhaustive/DFSStrategy.cs @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// A depth-first search scheduling strategy. + /// + public class DFSStrategy : ISchedulingStrategy + { + /// + /// The maximum number of steps to schedule. + /// + protected int MaxScheduledSteps; + + /// + /// The number of scheduled steps. + /// + protected int ScheduledSteps; + + /// + /// Stack of scheduling choices. + /// + private readonly List> ScheduleStack; + + /// + /// Stack of nondeterministic choices. + /// + private readonly List> BoolNondetStack; + + /// + /// Stack of nondeterministic choices. + /// + private readonly List> IntNondetStack; + + /// + /// Current schedule index. + /// + private int SchIndex; + + /// + /// Current nondeterministic index. + /// + private int NondetIndex; + + /// + /// Initializes a new instance of the class. + /// + public DFSStrategy(int maxSteps) + { + this.MaxScheduledSteps = maxSteps; + this.ScheduledSteps = 0; + this.SchIndex = 0; + this.NondetIndex = 0; + this.ScheduleStack = new List>(); + this.BoolNondetStack = new List>(); + this.IntNondetStack = new List>(); + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + var enabledOperations = ops.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + if (enabledOperations.Count == 0) + { + next = null; + return false; + } + + SChoice nextChoice = null; + List scs = null; + + if (this.SchIndex < this.ScheduleStack.Count) + { + scs = this.ScheduleStack[this.SchIndex]; + } + else + { + scs = new List(); + foreach (var task in enabledOperations) + { + scs.Add(new SChoice(task.SourceId)); + } + + this.ScheduleStack.Add(scs); + } + + nextChoice = scs.FirstOrDefault(val => !val.IsDone); + if (nextChoice is null) + { + next = null; + return false; + } + + if (this.SchIndex > 0) + { + var previousChoice = this.ScheduleStack[this.SchIndex - 1].Last(val => val.IsDone); + previousChoice.IsDone = false; + } + + next = enabledOperations.Find(task => task.SourceId == nextChoice.Id); + nextChoice.IsDone = true; + this.SchIndex++; + + if (next is null) + { + return false; + } + + this.ScheduledSteps++; + + return true; + } + + /// + /// Returns the next boolean choice. + /// + public bool GetNextBooleanChoice(int maxValue, out bool next) + { + NondetBooleanChoice nextChoice = null; + List ncs = null; + + if (this.NondetIndex < this.BoolNondetStack.Count) + { + ncs = this.BoolNondetStack[this.NondetIndex]; + } + else + { + ncs = new List + { + new NondetBooleanChoice(false), + new NondetBooleanChoice(true) + }; + + this.BoolNondetStack.Add(ncs); + } + + nextChoice = ncs.FirstOrDefault(val => !val.IsDone); + if (nextChoice is null) + { + next = false; + return false; + } + + if (this.NondetIndex > 0) + { + var previousChoice = this.BoolNondetStack[this.NondetIndex - 1].Last(val => val.IsDone); + previousChoice.IsDone = false; + } + + next = nextChoice.Value; + nextChoice.IsDone = true; + this.NondetIndex++; + + this.ScheduledSteps++; + + return true; + } + + /// + /// Returns the next integer choice. + /// + public bool GetNextIntegerChoice(int maxValue, out int next) + { + NondetIntegerChoice nextChoice = null; + List ncs = null; + + if (this.NondetIndex < this.IntNondetStack.Count) + { + ncs = this.IntNondetStack[this.NondetIndex]; + } + else + { + ncs = new List(); + for (int value = 0; value < maxValue; value++) + { + ncs.Add(new NondetIntegerChoice(value)); + } + + this.IntNondetStack.Add(ncs); + } + + nextChoice = ncs.FirstOrDefault(val => !val.IsDone); + if (nextChoice is null) + { + next = 0; + return false; + } + + if (this.NondetIndex > 0) + { + var previousChoice = this.IntNondetStack[this.NondetIndex - 1].Last(val => val.IsDone); + previousChoice.IsDone = false; + } + + next = nextChoice.Value; + nextChoice.IsDone = true; + this.NondetIndex++; + + this.ScheduledSteps++; + + return true; + } + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + this.ScheduledSteps++; + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + this.ScheduledSteps++; + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + this.ScheduledSteps++; + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public virtual bool PrepareForNextIteration() + { + if (this.ScheduleStack.All(scs => scs.All(val => val.IsDone))) + { + return false; + } + + // PrintSchedule(); + this.ScheduledSteps = 0; + + this.SchIndex = 0; + this.NondetIndex = 0; + + for (int idx = this.BoolNondetStack.Count - 1; idx > 0; idx--) + { + if (!this.BoolNondetStack[idx].All(val => val.IsDone)) + { + break; + } + + var previousChoice = this.BoolNondetStack[idx - 1].First(val => !val.IsDone); + previousChoice.IsDone = true; + + this.BoolNondetStack.RemoveAt(idx); + } + + for (int idx = this.IntNondetStack.Count - 1; idx > 0; idx--) + { + if (!this.IntNondetStack[idx].All(val => val.IsDone)) + { + break; + } + + var previousChoice = this.IntNondetStack[idx - 1].First(val => !val.IsDone); + previousChoice.IsDone = true; + + this.IntNondetStack.RemoveAt(idx); + } + + if (this.BoolNondetStack.Count > 0 && + this.BoolNondetStack.All(ns => ns.All(nsc => nsc.IsDone))) + { + this.BoolNondetStack.Clear(); + } + + if (this.IntNondetStack.Count > 0 && + this.IntNondetStack.All(ns => ns.All(nsc => nsc.IsDone))) + { + this.IntNondetStack.Clear(); + } + + if (this.BoolNondetStack.Count == 0 && + this.IntNondetStack.Count == 0) + { + for (int idx = this.ScheduleStack.Count - 1; idx > 0; idx--) + { + if (!this.ScheduleStack[idx].All(val => val.IsDone)) + { + break; + } + + var previousChoice = this.ScheduleStack[idx - 1].First(val => !val.IsDone); + previousChoice.IsDone = true; + + this.ScheduleStack.RemoveAt(idx); + } + } + else + { + var previousChoice = this.ScheduleStack.Last().LastOrDefault(val => val.IsDone); + if (previousChoice != null) + { + previousChoice.IsDone = false; + } + } + + return true; + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public void Reset() + { + this.ScheduleStack.Clear(); + this.BoolNondetStack.Clear(); + this.IntNondetStack.Clear(); + this.SchIndex = 0; + this.NondetIndex = 0; + this.ScheduledSteps = 0; + } + + /// + /// Returns the scheduled steps. + /// + public int GetScheduledSteps() => this.ScheduledSteps; + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public bool HasReachedMaxSchedulingSteps() + { + if (this.MaxScheduledSteps == 0) + { + return false; + } + + return this.ScheduledSteps >= this.MaxScheduledSteps; + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public bool IsFair() => false; + + /// + /// Returns a textual description of the scheduling strategy. + /// + public string GetDescription() => "DFS"; + + /// + /// Prints the schedule. + /// + private void PrintSchedule() + { + Debug.WriteLine("*******************"); + Debug.WriteLine("Schedule stack size: " + this.ScheduleStack.Count); + for (int idx = 0; idx < this.ScheduleStack.Count; idx++) + { + Debug.WriteLine("Index: " + idx); + foreach (var sc in this.ScheduleStack[idx]) + { + Debug.Write(sc.Id + " [" + sc.IsDone + "], "); + } + + Debug.WriteLine(string.Empty); + } + + Debug.WriteLine("*******************"); + Debug.WriteLine("Random bool stack size: " + this.BoolNondetStack.Count); + for (int idx = 0; idx < this.BoolNondetStack.Count; idx++) + { + Debug.WriteLine("Index: " + idx); + foreach (var nc in this.BoolNondetStack[idx]) + { + Debug.Write(nc.Value + " [" + nc.IsDone + "], "); + } + + Debug.WriteLine(string.Empty); + } + + Debug.WriteLine("*******************"); + Debug.WriteLine("Random int stack size: " + this.IntNondetStack.Count); + for (int idx = 0; idx < this.IntNondetStack.Count; idx++) + { + Debug.WriteLine("Index: " + idx); + foreach (var nc in this.IntNondetStack[idx]) + { + Debug.Write(nc.Value + " [" + nc.IsDone + "], "); + } + + Debug.WriteLine(string.Empty); + } + + Debug.WriteLine("*******************"); + } + + /// + /// A scheduling choice. Contains an id and a boolean that is + /// true if the choice has been previously explored. + /// + private class SChoice + { + internal ulong Id; + internal bool IsDone; + + /// + /// Initializes a new instance of the class. + /// + internal SChoice(ulong id) + { + this.Id = id; + this.IsDone = false; + } + } + + /// + /// A nondeterministic choice. Contains a boolean value that + /// corresponds to the choice and a boolean that is true if + /// the choice has been previously explored. + /// + private class NondetBooleanChoice + { + internal bool Value; + internal bool IsDone; + + /// + /// Initializes a new instance of the class. + /// + internal NondetBooleanChoice(bool value) + { + this.Value = value; + this.IsDone = false; + } + } + + /// + /// A nondeterministic choice. Contains an integer value that + /// corresponds to the choice and a boolean that is true if + /// the choice has been previously explored. + /// + private class NondetIntegerChoice + { + internal int Value; + internal bool IsDone; + + /// + /// Initializes a new instance of the class. + /// + internal NondetIntegerChoice(int value) + { + this.Value = value; + this.IsDone = false; + } + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Exhaustive/IterativeDeepeningDFSStrategy.cs b/Source/TestingServices/Exploration/Strategies/Exhaustive/IterativeDeepeningDFSStrategy.cs new file mode 100644 index 000000000..e2ff4e102 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Exhaustive/IterativeDeepeningDFSStrategy.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// A depth-first search scheduling strategy that uses iterative deepening. + /// + public sealed class IterativeDeepeningDFSStrategy : DFSStrategy, ISchedulingStrategy + { + /// + /// The max depth. + /// + private readonly int MaxDepth; + + /// + /// The current depth. + /// + private int CurrentDepth; + + /// + /// Initializes a new instance of the class. + /// + public IterativeDeepeningDFSStrategy(int maxSteps) + : base(maxSteps) + { + this.MaxDepth = maxSteps; + this.CurrentDepth = 1; + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public override bool PrepareForNextIteration() + { + bool doNext = this.PrepareForNextIteration(); + if (!doNext) + { + this.Reset(); + this.CurrentDepth++; + if (this.CurrentDepth <= this.MaxDepth) + { + Debug.WriteLine($" Depth bound increased to {this.CurrentDepth} (max is {this.MaxDepth})."); + doNext = true; + } + } + + return doNext; + } + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public new bool HasReachedMaxSchedulingSteps() => this.ScheduledSteps == this.CurrentDepth; + + /// + /// Returns a textual description of the scheduling strategy. + /// + public new string GetDescription() => $"DFS with iterative deepening (max depth is {this.MaxDepth})"; + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Liveness/CycleDetectionStrategy.cs b/Source/TestingServices/Exploration/Strategies/Liveness/CycleDetectionStrategy.cs new file mode 100644 index 000000000..86c2c449b --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Liveness/CycleDetectionStrategy.cs @@ -0,0 +1,682 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices.StateCaching; +using Microsoft.Coyote.TestingServices.Tracing.Schedule; + +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// Strategy for detecting liveness property violations using partial state-caching + /// and cycle-replaying. It contains a nested that + /// is used for scheduling decisions. Note that liveness property violations are + /// checked only if the nested strategy is fair. + /// + internal sealed class CycleDetectionStrategy : LivenessCheckingStrategy + { + /// + /// The state cache of the program. + /// + private readonly StateCache StateCache; + + /// + /// The schedule trace of the program. + /// + private readonly ScheduleTrace ScheduleTrace; + + /// + /// Monitors that are stuck in the hot state + /// for the duration of the latest found + /// potential cycle. + /// + private HashSet HotMonitors; + + /// + /// The latest found potential cycle. + /// + private readonly List PotentialCycle; + + /// + /// Fingerprints captured in the latest potential cycle. + /// + private readonly HashSet PotentialCycleFingerprints; + + /// + /// Is strategy trying to replay a potential cycle. + /// + private bool IsReplayingCycle; + + /// + /// A counter that increases in each step of the execution, + /// as long as the Coyote program remains in the same cycle, + /// with the liveness monitors at the hot state. + /// + private int LivenessTemperature; + + /// + /// The index of the last scheduling step in + /// the currently detected cycle. + /// + private int EndOfCycleIndex; + + /// + /// The current cycle index. + /// + private int CurrentCycleIndex; + + /// + /// Nondeterminitic seed. + /// + private readonly int Seed; + + /// + /// Randomizer. + /// + private readonly IRandomNumberGenerator Random; + + /// + /// Map of fingerprints to schedule step indexes. + /// + private readonly Dictionary> FingerprintIndexMap; + + /// + /// Initializes a new instance of the class. + /// + internal CycleDetectionStrategy(Configuration configuration, StateCache cache, ScheduleTrace trace, + List monitors, ISchedulingStrategy strategy) + : base(configuration, monitors, strategy) + { + this.StateCache = cache; + this.ScheduleTrace = trace; + + this.HotMonitors = new HashSet(); + this.PotentialCycle = new List(); + this.PotentialCycleFingerprints = new HashSet(); + + this.LivenessTemperature = 0; + this.EndOfCycleIndex = 0; + this.CurrentCycleIndex = 0; + + this.Seed = this.Configuration.RandomSchedulingSeed ?? DateTime.Now.Millisecond; + this.Random = new DefaultRandomNumberGenerator(this.Seed); + + this.FingerprintIndexMap = new Dictionary>(); + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public override bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + this.CaptureAndCheckProgramState(); + + if (this.IsReplayingCycle) + { + var enabledOperations = ops.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + if (enabledOperations.Count == 0) + { + next = null; + return false; + } + + ScheduleStep nextStep = this.PotentialCycle[this.CurrentCycleIndex]; + if (nextStep.Type != ScheduleStepType.SchedulingChoice) + { + Debug.WriteLine(" Trace is not reproducible: next step is not an operation."); + this.EscapeUnfairCycle(); + return this.SchedulingStrategy.GetNext(out next, ops, current); + } + + Debug.WriteLine(" Replaying '{0}' '{1}'.", nextStep.Index, nextStep.ScheduledOperationId); + + next = enabledOperations.FirstOrDefault(choice => choice.SourceId == nextStep.ScheduledOperationId); + if (next is null) + { + Debug.WriteLine(" Trace is not reproducible: cannot detect machine with id '{0}'.", nextStep.ScheduledOperationId); + this.EscapeUnfairCycle(); + return this.SchedulingStrategy.GetNext(out next, ops, current); + } + + this.SchedulingStrategy.ForceNext(next, ops, current); + + this.CurrentCycleIndex++; + if (this.CurrentCycleIndex == this.PotentialCycle.Count) + { + this.CurrentCycleIndex = 0; + } + + return true; + } + else + { + return this.SchedulingStrategy.GetNext(out next, ops, current); + } + } + + /// + /// Returns the next boolean choice. + /// + public override bool GetNextBooleanChoice(int maxValue, out bool next) + { + this.CaptureAndCheckProgramState(); + + if (this.IsReplayingCycle) + { + ScheduleStep nextStep = this.PotentialCycle[this.CurrentCycleIndex]; + if ((nextStep.Type == ScheduleStepType.SchedulingChoice) || nextStep.BooleanChoice is null) + { + Debug.WriteLine(" Trace is not reproducible: next step is not a nondeterministic boolean choice."); + this.EscapeUnfairCycle(); + return this.SchedulingStrategy.GetNextBooleanChoice(maxValue, out next); + } + + Debug.WriteLine(" Replaying '{0}' '{1}'.", nextStep.Index, nextStep.BooleanChoice.Value); + + next = nextStep.BooleanChoice.Value; + + this.SchedulingStrategy.ForceNextBooleanChoice(maxValue, next); + + this.CurrentCycleIndex++; + if (this.CurrentCycleIndex == this.PotentialCycle.Count) + { + this.CurrentCycleIndex = 0; + } + + return true; + } + else + { + return this.SchedulingStrategy.GetNextBooleanChoice(maxValue, out next); + } + } + + /// + /// Returns the next integer choice. + /// + public override bool GetNextIntegerChoice(int maxValue, out int next) + { + this.CaptureAndCheckProgramState(); + + if (this.IsReplayingCycle) + { + ScheduleStep nextStep = this.PotentialCycle[this.CurrentCycleIndex]; + if (nextStep.Type != ScheduleStepType.NondeterministicChoice || + nextStep.IntegerChoice is null) + { + Debug.WriteLine(" Trace is not reproducible: next step is not a nondeterministic integer choice."); + this.EscapeUnfairCycle(); + return this.SchedulingStrategy.GetNextIntegerChoice(maxValue, out next); + } + + Debug.WriteLine(" Replaying '{0}' '{1}'.", nextStep.Index, nextStep.IntegerChoice.Value); + + next = nextStep.IntegerChoice.Value; + + this.SchedulingStrategy.ForceNextIntegerChoice(maxValue, next); + + this.CurrentCycleIndex++; + if (this.CurrentCycleIndex == this.PotentialCycle.Count) + { + this.CurrentCycleIndex = 0; + } + + return true; + } + else + { + return this.SchedulingStrategy.GetNextIntegerChoice(maxValue, out next); + } + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + /// True to start the next iteration. + public override bool PrepareForNextIteration() + { + if (this.IsReplayingCycle) + { + this.CurrentCycleIndex = 0; + return true; + } + else + { + return base.PrepareForNextIteration(); + } + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public override void Reset() + { + if (this.IsReplayingCycle) + { + this.CurrentCycleIndex = 0; + } + else + { + base.Reset(); + } + } + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public override bool HasReachedMaxSchedulingSteps() + { + if (this.IsReplayingCycle) + { + return false; + } + else + { + return base.HasReachedMaxSchedulingSteps(); + } + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public override bool IsFair() + { + if (this.IsReplayingCycle) + { + return true; + } + else + { + return base.IsFair(); + } + } + + /// + /// Captures the program state and checks for liveness violations. + /// + private void CaptureAndCheckProgramState() + { + if (this.ScheduleTrace.Count == 0) + { + return; + } + + if (this.Configuration.SafetyPrefixBound <= this.GetScheduledSteps()) + { + bool stateExists = this.StateCache.CaptureState(out State _, out Fingerprint fingerprint, + this.FingerprintIndexMap, this.ScheduleTrace.Peek(), this.Monitors); + if (stateExists) + { + Debug.WriteLine(" Detected potential infinite execution."); + this.CheckLivenessAtTraceCycle(this.FingerprintIndexMap[fingerprint]); + } + } + + if (this.PotentialCycle.Count > 0) + { + // Only check for a liveness property violation + // if there is a potential cycle. + this.CheckLivenessTemperature(); + } + } + + /// + /// Checks the liveness temperature of each monitor, and + /// reports an error if one of the liveness monitors has + /// passed the temperature threshold. + /// + private void CheckLivenessTemperature() + { + var coldMonitors = this.HotMonitors.Where(m => m.IsInColdState()).ToList(); + if (coldMonitors.Count > 0) + { + if (Debug.IsEnabled) + { + foreach (var coldMonitor in coldMonitors) + { + Debug.WriteLine( + " Trace is not reproducible: monitor {0} transitioned to a cold state.", + coldMonitor.Id); + } + } + + this.EscapeUnfairCycle(); + return; + } + + var randomWalkScheduleTrace = this.ScheduleTrace.Where(val => val.Index > this.EndOfCycleIndex); + foreach (var step in randomWalkScheduleTrace) + { + State state = step.State; + if (!this.PotentialCycleFingerprints.Contains(state.Fingerprint)) + { + if (Debug.IsEnabled) + { + state.PrettyPrint(); + Debug.WriteLine(" Detected a state that does not belong to the potential cycle."); + } + + this.EscapeUnfairCycle(); + return; + } + } + + // Increments the temperature of each monitor. + // foreach (var monitor in HotMonitors) + // { + // string message = IO.Utilities.Format("Monitor '{0}' detected infinite execution that " + + // "violates a liveness property.", monitor.GetType().Name); + // Runtime.Scheduler.NotifyAssertionFailure(message, false); + // } + this.LivenessTemperature++; + if (this.LivenessTemperature > this.Configuration.LivenessTemperatureThreshold) + { + foreach (var monitor in this.HotMonitors) + { + monitor.CheckLivenessTemperature(this.LivenessTemperature); + } + + // foreach (var monitor in HotMonitors) + // { + // string message = IO.Utilities.Format("Monitor '{0}' detected infinite execution that " + + // "violates a liveness property.", monitor.GetType().Name); + // Runtime.Scheduler.NotifyAssertionFailure(message, false); + // } + + // Runtime.Scheduler.Stop(); + } + } + + /// + /// Checks liveness at a schedule trace cycle. + /// + /// Indices corresponding to the fingerprint of root. + private void CheckLivenessAtTraceCycle(List indices) + { + // If there is a potential cycle found, do not create a new one until the + // liveness checker has finished exploring the current cycle. + if (this.PotentialCycle.Count > 0) + { + return; + } + + var checkIndexRand = indices[indices.Count - 2]; + var index = this.ScheduleTrace.Count - 1; + + for (int i = checkIndexRand + 1; i <= index; i++) + { + var scheduleStep = this.ScheduleTrace[i]; + this.PotentialCycle.Add(scheduleStep); + this.PotentialCycleFingerprints.Add(scheduleStep.State.Fingerprint); + Debug.WriteLine( + " Cycle contains {0} with {1}.", + scheduleStep.Type, scheduleStep.State.Fingerprint.ToString()); + } + + this.DebugPrintScheduleTrace(); + this.DebugPrintPotentialCycle(); + + if (!IsSchedulingFair(this.PotentialCycle)) + { + Debug.WriteLine(" Scheduling in cycle is unfair."); + this.PotentialCycle.Clear(); + this.PotentialCycleFingerprints.Clear(); + } + else if (!IsNondeterminismFair(this.PotentialCycle)) + { + Debug.WriteLine(" Nondeterminism in cycle is unfair."); + this.PotentialCycle.Clear(); + this.PotentialCycleFingerprints.Clear(); + } + + if (this.PotentialCycle.Count == 0) + { + bool isFairCycleFound = false; + int counter = Math.Min(indices.Count - 1, 3); + while (counter > 0) + { + var randInd = this.Random.Next(indices.Count - 2); + checkIndexRand = indices[randInd]; + + index = this.ScheduleTrace.Count - 1; + for (int i = checkIndexRand + 1; i <= index; i++) + { + var scheduleStep = this.ScheduleTrace[i]; + this.PotentialCycle.Add(scheduleStep); + this.PotentialCycleFingerprints.Add(scheduleStep.State.Fingerprint); + Debug.WriteLine( + " Cycle contains {0} with {1}.", + scheduleStep.Type, scheduleStep.State.Fingerprint.ToString()); + } + + if (IsSchedulingFair(this.PotentialCycle) && IsNondeterminismFair(this.PotentialCycle)) + { + isFairCycleFound = true; + break; + } + else + { + this.PotentialCycle.Clear(); + this.PotentialCycleFingerprints.Clear(); + } + + counter--; + } + + if (!isFairCycleFound) + { + this.PotentialCycle.Clear(); + this.PotentialCycleFingerprints.Clear(); + return; + } + } + + Debug.WriteLine(" Cycle execution is fair."); + + this.HotMonitors = GetHotMonitors(this.PotentialCycle); + if (this.HotMonitors.Count > 0) + { + this.EndOfCycleIndex = this.PotentialCycle.Select(val => val).Min(val => val.Index); + this.Configuration.LivenessTemperatureThreshold = 10 * this.PotentialCycle.Count; + this.IsReplayingCycle = true; + } + else + { + this.PotentialCycle.Clear(); + this.PotentialCycleFingerprints.Clear(); + } + } + + /// + /// Checks if the scheduling is fair in a schedule trace cycle. + /// + /// Cycle of states. + private static bool IsSchedulingFair(List cycle) + { + var result = false; + + var enabledMachines = new HashSet(); + var scheduledMachines = new HashSet(); + + var schedulingChoiceSteps = cycle.Where( + val => val.Type == ScheduleStepType.SchedulingChoice); + foreach (var step in schedulingChoiceSteps) + { + scheduledMachines.Add(step.ScheduledOperationId); + } + + foreach (var step in cycle) + { + enabledMachines.UnionWith(step.State.EnabledMachineIds); + } + + if (Debug.IsEnabled) + { + foreach (var m in enabledMachines) + { + Debug.WriteLine(" Enabled machine {0}.", m); + } + + foreach (var m in scheduledMachines) + { + Debug.WriteLine(" Scheduled machine {0}.", m); + } + } + + if (enabledMachines.Count == scheduledMachines.Count) + { + result = true; + } + + return result; + } + + private static bool IsNondeterminismFair(List cycle) + { + var fairNondeterministicChoiceSteps = cycle.Where( + val => val.Type == ScheduleStepType.FairNondeterministicChoice && + val.BooleanChoice != null).ToList(); + foreach (var step in fairNondeterministicChoiceSteps) + { + var choices = fairNondeterministicChoiceSteps.Where(c => c.NondetId.Equals(step.NondetId)).ToList(); + var falseChoices = choices.Count(c => c.BooleanChoice == false); + var trueChoices = choices.Count(c => c.BooleanChoice == true); + if (trueChoices == 0 || falseChoices == 0) + { + return false; + } + } + + return true; + } + + /// + /// Gets all monitors that are in hot state, but not in cold + /// state during the schedule trace cycle. + /// + /// Cycle of states. + private static HashSet GetHotMonitors(List cycle) + { + var hotMonitors = new HashSet(); + + foreach (var step in cycle) + { + foreach (var kvp in step.State.MonitorStatus) + { + if (kvp.Value == MonitorStatus.Hot) + { + hotMonitors.Add(kvp.Key); + } + } + } + + if (hotMonitors.Count > 0) + { + foreach (var step in cycle) + { + foreach (var kvp in step.State.MonitorStatus) + { + if (kvp.Value == MonitorStatus.Cold && + hotMonitors.Contains(kvp.Key)) + { + hotMonitors.Remove(kvp.Key); + } + } + } + } + + return hotMonitors; + } + + /// + /// Escapes the unfair cycle and continues to explore the + /// schedule with the original scheduling strategy. + /// + private void EscapeUnfairCycle() + { + Debug.WriteLine(" Escaped from unfair cycle."); + + this.HotMonitors.Clear(); + this.PotentialCycle.Clear(); + this.PotentialCycleFingerprints.Clear(); + + this.LivenessTemperature = 0; + this.EndOfCycleIndex = 0; + this.CurrentCycleIndex = 0; + + this.IsReplayingCycle = false; + } + + /// + /// Prints the program schedule trace. Works only + /// if debug mode is enabled. + /// + private void DebugPrintScheduleTrace() + { + if (Debug.IsEnabled) + { + Debug.WriteLine(" ------------ SCHEDULE ------------."); + + foreach (var step in this.ScheduleTrace) + { + if (step.Type == ScheduleStepType.SchedulingChoice) + { + Debug.WriteLine($"{step.Index} :: {step.Type} :: {step.ScheduledOperationId} :: {step.State.Fingerprint}"); + } + else if (step.BooleanChoice != null) + { + Debug.WriteLine($"{step.Index} :: {step.Type} :: {step.BooleanChoice.Value} :: {step.State.Fingerprint}"); + } + else + { + Debug.WriteLine($"{step.Index} :: {step.Type} :: {step.IntegerChoice.Value} :: {step.State.Fingerprint}"); + } + } + + Debug.WriteLine(" ----------------------------------."); + } + } + + /// + /// Prints the potential cycle. Works only if + /// debug mode is enabled. + /// + private void DebugPrintPotentialCycle() + { + if (Debug.IsEnabled) + { + Debug.WriteLine(" ------------- CYCLE --------------."); + + foreach (var step in this.PotentialCycle) + { + if (step.Type == ScheduleStepType.SchedulingChoice) + { + Debug.WriteLine($"{step.Index} :: {step.Type} :: {step.ScheduledOperationId}"); + } + else if (step.BooleanChoice != null) + { + Debug.WriteLine($"{step.Index} :: {step.Type} :: {step.BooleanChoice.Value}"); + } + else + { + Debug.WriteLine($"{step.Index} :: {step.Type} :: {step.IntegerChoice.Value}"); + } + + step.State.PrettyPrint(); + } + + Debug.WriteLine(" ----------------------------------."); + } + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Liveness/LivenessCheckingStrategy.cs b/Source/TestingServices/Exploration/Strategies/Liveness/LivenessCheckingStrategy.cs new file mode 100644 index 000000000..9c94288d3 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Liveness/LivenessCheckingStrategy.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// Abstract strategy for detecting liveness property violations. It + /// contains a nested that is used + /// for scheduling decisions. + /// + internal abstract class LivenessCheckingStrategy : ISchedulingStrategy + { + /// + /// The configuration. + /// + protected Configuration Configuration; + + /// + /// List of monitors in the program. + /// + protected List Monitors; + + /// + /// Strategy used for scheduling decisions. + /// + protected ISchedulingStrategy SchedulingStrategy; + + /// + /// Initializes a new instance of the class. + /// + internal LivenessCheckingStrategy(Configuration configuration, List monitors, ISchedulingStrategy strategy) + { + this.Configuration = configuration; + this.Monitors = monitors; + this.SchedulingStrategy = strategy; + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public abstract bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current); + + /// + /// Returns the next boolean choice. + /// + public abstract bool GetNextBooleanChoice(int maxValue, out bool next); + + /// + /// Returns the next integer choice. + /// + public abstract bool GetNextIntegerChoice(int maxValue, out int next); + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + throw new NotImplementedException(); + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + throw new NotImplementedException(); + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + throw new NotImplementedException(); + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public virtual bool PrepareForNextIteration() + { + return this.SchedulingStrategy.PrepareForNextIteration(); + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public virtual void Reset() + { + this.SchedulingStrategy.Reset(); + } + + /// + /// Returns the scheduled steps. + /// + public virtual int GetScheduledSteps() + { + return this.SchedulingStrategy.GetScheduledSteps(); + } + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public virtual bool HasReachedMaxSchedulingSteps() + { + return this.SchedulingStrategy.HasReachedMaxSchedulingSteps(); + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public virtual bool IsFair() + { + return this.SchedulingStrategy.IsFair(); + } + + /// + /// Returns a textual description of the scheduling strategy. + /// + public virtual string GetDescription() + { + return this.SchedulingStrategy.GetDescription(); + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Liveness/TemperatureCheckingStrategy.cs b/Source/TestingServices/Exploration/Strategies/Liveness/TemperatureCheckingStrategy.cs new file mode 100644 index 000000000..68d4592e2 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Liveness/TemperatureCheckingStrategy.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// Strategy for detecting liveness property violations using the "temperature" + /// method. It contains a nested that is used + /// for scheduling decisions. Note that liveness property violations are checked + /// only if the nested strategy is fair. + /// + internal sealed class TemperatureCheckingStrategy : LivenessCheckingStrategy + { + /// + /// Initializes a new instance of the class. + /// + internal TemperatureCheckingStrategy(Configuration configuration, List monitors, ISchedulingStrategy strategy) + : base(configuration, monitors, strategy) + { + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public override bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + this.CheckLivenessTemperature(); + return this.SchedulingStrategy.GetNext(out next, ops, current); + } + + /// + /// Returns the next boolean choice. + /// + public override bool GetNextBooleanChoice(int maxValue, out bool next) + { + this.CheckLivenessTemperature(); + return this.SchedulingStrategy.GetNextBooleanChoice(maxValue, out next); + } + + /// + /// Returns the next integer choice. + /// + public override bool GetNextIntegerChoice(int maxValue, out int next) + { + this.CheckLivenessTemperature(); + return this.SchedulingStrategy.GetNextIntegerChoice(maxValue, out next); + } + + /// + /// Checks the liveness temperature of each monitor, and + /// reports an error if one of the liveness monitors has + /// passed the temperature threshold. + /// + private void CheckLivenessTemperature() + { + if (this.IsFair()) + { + foreach (var monitor in this.Monitors) + { + monitor.CheckLivenessTemperature(); + } + } + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Probabilistic/ProbabilisticRandomStrategy.cs b/Source/TestingServices/Exploration/Strategies/Probabilistic/ProbabilisticRandomStrategy.cs new file mode 100644 index 000000000..f518a63cb --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Probabilistic/ProbabilisticRandomStrategy.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// A randomized scheduling strategy with increased probability + /// to remain in the same scheduling choice. + /// + public sealed class ProbabilisticRandomStrategy : RandomStrategy + { + /// + /// Number of coin flips. + /// + private readonly int NumberOfCoinFlips; + + /// + /// Initializes a new instance of the class. + /// It uses the default random number generator (seed is based on current time). + /// + public ProbabilisticRandomStrategy(int maxSteps, int numberOfCoinFlips) + : base(maxSteps) + { + this.NumberOfCoinFlips = numberOfCoinFlips; + } + + /// + /// Initializes a new instance of the class. + /// It uses the specified random number generator. + /// + public ProbabilisticRandomStrategy(int maxSteps, int numberOfCoinFlips, IRandomNumberGenerator random) + : base(maxSteps, random) + { + this.NumberOfCoinFlips = numberOfCoinFlips; + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public override bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + var enabledOperations = ops.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + if (enabledOperations.Count == 0) + { + next = null; + return false; + } + + this.ScheduledSteps++; + + if (enabledOperations.Count > 1) + { + if (!this.ShouldCurrentMachineChange() && current.Status is AsyncOperationStatus.Enabled) + { + next = current; + return true; + } + } + + int idx = this.RandomNumberGenerator.Next(enabledOperations.Count); + next = enabledOperations[idx]; + + return true; + } + + /// + /// Returns a textual description of the scheduling strategy. + /// + public override string GetDescription() => + $"ProbabilisticRandom[seed '{this.RandomNumberGenerator.Seed}', coin flips '{this.NumberOfCoinFlips}']"; + + /// + /// Flip the coin a specified number of times. + /// + private bool ShouldCurrentMachineChange() + { + for (int idx = 0; idx < this.NumberOfCoinFlips; idx++) + { + if (this.RandomNumberGenerator.Next(2) == 1) + { + return false; + } + } + + return true; + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Probabilistic/RandomStrategy.cs b/Source/TestingServices/Exploration/Strategies/Probabilistic/RandomStrategy.cs new file mode 100644 index 000000000..cf507e80e --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Probabilistic/RandomStrategy.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// A simple (but effective) randomized scheduling strategy. + /// + public class RandomStrategy : ISchedulingStrategy + { + /// + /// Random number generator. + /// + protected IRandomNumberGenerator RandomNumberGenerator; + + /// + /// The maximum number of steps to schedule. + /// + protected int MaxScheduledSteps; + + /// + /// The number of scheduled steps. + /// + protected int ScheduledSteps; + + /// + /// Initializes a new instance of the class. + /// It uses the default random number generator (seed is based on current time). + /// + public RandomStrategy(int maxSteps) + : this(maxSteps, new DefaultRandomNumberGenerator(DateTime.Now.Millisecond)) + { + } + + /// + /// Initializes a new instance of the class. + /// It uses the specified random number generator. + /// + public RandomStrategy(int maxSteps, IRandomNumberGenerator random) + { + this.RandomNumberGenerator = random; + this.MaxScheduledSteps = maxSteps; + this.ScheduledSteps = 0; + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public virtual bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + var enabledOperations = ops.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + if (enabledOperations.Count == 0) + { + next = null; + return false; + } + + int idx = this.RandomNumberGenerator.Next(enabledOperations.Count); + next = enabledOperations[idx]; + + this.ScheduledSteps++; + + return true; + } + + /// + /// Returns the next boolean choice. + /// + public virtual bool GetNextBooleanChoice(int maxValue, out bool next) + { + next = false; + if (this.RandomNumberGenerator.Next(maxValue) == 0) + { + next = true; + } + + this.ScheduledSteps++; + + return true; + } + + /// + /// Returns the next integer choice. + /// + public virtual bool GetNextIntegerChoice(int maxValue, out int next) + { + next = this.RandomNumberGenerator.Next(maxValue); + this.ScheduledSteps++; + return true; + } + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + this.ScheduledSteps++; + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + this.ScheduledSteps++; + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + this.ScheduledSteps++; + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public virtual bool PrepareForNextIteration() + { + this.ScheduledSteps = 0; + return true; + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public virtual void Reset() + { + this.ScheduledSteps = 0; + } + + /// + /// Returns the scheduled steps. + /// + public int GetScheduledSteps() => this.ScheduledSteps; + + /// + /// True if the scheduling strategy has reached the depth + /// bound for the given scheduling iteration. + /// + public bool HasReachedMaxSchedulingSteps() + { + if (this.MaxScheduledSteps == 0) + { + return false; + } + + return this.ScheduledSteps >= this.MaxScheduledSteps; + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public bool IsFair() => true; + + /// + /// Returns a textual description of the scheduling strategy. + /// + public virtual string GetDescription() => $"Random[seed '{this.RandomNumberGenerator.Seed}']"; + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Special/ComboStrategy.cs b/Source/TestingServices/Exploration/Strategies/Special/ComboStrategy.cs new file mode 100644 index 000000000..47cb18d0f --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Special/ComboStrategy.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// This strategy combines two given strategies, using them to schedule + /// the prefix and suffix of an execution. + /// + public sealed class ComboStrategy : ISchedulingStrategy + { + /// + /// The prefix strategy. + /// + private readonly ISchedulingStrategy PrefixStrategy; + + /// + /// The suffix strategy. + /// + private readonly ISchedulingStrategy SuffixStrategy; + + /// + /// Initializes a new instance of the class. + /// + public ComboStrategy(ISchedulingStrategy prefixStrategy, ISchedulingStrategy suffixStrategy) + { + this.PrefixStrategy = prefixStrategy; + this.SuffixStrategy = suffixStrategy; + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + if (this.PrefixStrategy.HasReachedMaxSchedulingSteps()) + { + return this.SuffixStrategy.GetNext(out next, ops, current); + } + else + { + return this.PrefixStrategy.GetNext(out next, ops, current); + } + } + + /// + /// Returns the next boolean choice. + /// + public bool GetNextBooleanChoice(int maxValue, out bool next) + { + if (this.PrefixStrategy.HasReachedMaxSchedulingSteps()) + { + return this.SuffixStrategy.GetNextBooleanChoice(maxValue, out next); + } + else + { + return this.PrefixStrategy.GetNextBooleanChoice(maxValue, out next); + } + } + + /// + /// Returns the next integer choice. + /// + public bool GetNextIntegerChoice(int maxValue, out int next) + { + if (this.PrefixStrategy.HasReachedMaxSchedulingSteps()) + { + return this.SuffixStrategy.GetNextIntegerChoice(maxValue, out next); + } + else + { + return this.PrefixStrategy.GetNextIntegerChoice(maxValue, out next); + } + } + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + if (this.PrefixStrategy.HasReachedMaxSchedulingSteps()) + { + this.SuffixStrategy.ForceNext(next, ops, current); + } + else + { + this.PrefixStrategy.ForceNext(next, ops, current); + } + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + if (this.PrefixStrategy.HasReachedMaxSchedulingSteps()) + { + this.SuffixStrategy.ForceNextBooleanChoice(maxValue, next); + } + else + { + this.PrefixStrategy.ForceNextBooleanChoice(maxValue, next); + } + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + if (this.PrefixStrategy.HasReachedMaxSchedulingSteps()) + { + this.SuffixStrategy.ForceNextIntegerChoice(maxValue, next); + } + else + { + this.PrefixStrategy.ForceNextIntegerChoice(maxValue, next); + } + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public bool PrepareForNextIteration() + { + bool doNext = this.PrefixStrategy.PrepareForNextIteration(); + doNext |= this.SuffixStrategy.PrepareForNextIteration(); + return doNext; + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public void Reset() + { + this.PrefixStrategy.Reset(); + this.SuffixStrategy.Reset(); + } + + /// + /// Returns the scheduled steps. + /// + public int GetScheduledSteps() + { + if (this.PrefixStrategy.HasReachedMaxSchedulingSteps()) + { + return this.SuffixStrategy.GetScheduledSteps() + this.PrefixStrategy.GetScheduledSteps(); + } + else + { + return this.PrefixStrategy.GetScheduledSteps(); + } + } + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public bool HasReachedMaxSchedulingSteps() => this.SuffixStrategy.HasReachedMaxSchedulingSteps(); + + /// + /// Checks if this is a fair scheduling strategy. + /// + public bool IsFair() => this.SuffixStrategy.IsFair(); + + /// + /// Returns a textual description of the scheduling strategy. + /// + public string GetDescription() => + string.Format("Combo[{0},{1}]", this.PrefixStrategy.GetDescription(), this.SuffixStrategy.GetDescription()); + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Special/InteractiveStrategy.cs b/Source/TestingServices/Exploration/Strategies/Special/InteractiveStrategy.cs new file mode 100644 index 000000000..b0a148ee6 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Special/InteractiveStrategy.cs @@ -0,0 +1,463 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// Class representing an interactive scheduling strategy. + /// + internal sealed class InteractiveStrategy : ISchedulingStrategy + { + /// + /// The configuration. + /// + private readonly Configuration Configuration; + + /// + /// The installed logger. + /// + private readonly IO.ILogger Logger; + + /// + /// The input cache. + /// + private readonly List InputCache; + + /// + /// The number of explored steps. + /// + private int ExploredSteps; + + /// + /// Initializes a new instance of the class. + /// + public InteractiveStrategy(Configuration configuration, IO.ILogger logger) + { + this.Logger = logger ?? new ConsoleLogger(); + this.Configuration = configuration; + this.InputCache = new List(); + this.ExploredSteps = 0; + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + next = null; + + var enabledOperations = ops.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + + if (enabledOperations.Count == 0) + { + this.Logger.WriteLine(">> No available machines to schedule ..."); + return false; + } + + this.ExploredSteps++; + + var parsed = false; + while (!parsed) + { + if (this.InputCache.Count >= this.ExploredSteps) + { + var step = this.InputCache[this.ExploredSteps - 1]; + int idx = 0; + if (step.Length > 0) + { + idx = Convert.ToInt32(step); + } + else + { + this.InputCache[this.ExploredSteps - 1] = "0"; + } + + next = enabledOperations[idx]; + parsed = true; + break; + } + + this.Logger.WriteLine(">> Available machines to schedule ..."); + for (int idx = 0; idx < enabledOperations.Count; idx++) + { + var op = enabledOperations[idx]; + this.Logger.WriteLine($">> [{idx}] '{op.SourceName}'"); + } + + this.Logger.WriteLine($">> Choose machine to schedule [step '{this.ExploredSteps}']"); + + var input = Console.ReadLine(); + if (input.Equals("replay")) + { + if (!this.Replay()) + { + continue; + } + + this.Configuration.SchedulingIterations++; + this.PrepareForNextIteration(); + return false; + } + else if (input.Equals("jump")) + { + this.Jump(); + continue; + } + else if (input.Equals("reset")) + { + this.Configuration.SchedulingIterations++; + this.Reset(); + return false; + } + else if (input.Length > 0) + { + try + { + var idx = Convert.ToInt32(input); + if (idx < 0) + { + this.Logger.WriteLine(">> Expected positive integer, please retry ..."); + continue; + } + + next = enabledOperations[idx]; + if (next is null) + { + this.Logger.WriteLine(">> Unexpected id, please retry ..."); + continue; + } + } + catch (FormatException) + { + this.Logger.WriteLine(">> Wrong format, please retry ..."); + continue; + } + } + else + { + next = enabledOperations[0]; + } + + this.InputCache.Add(input); + parsed = true; + } + + return true; + } + + /// + /// Returns the next boolean choice. + /// + public bool GetNextBooleanChoice(int maxValue, out bool next) + { + next = false; + this.ExploredSteps++; + + var parsed = false; + while (!parsed) + { + if (this.InputCache.Count >= this.ExploredSteps) + { + var step = this.InputCache[this.ExploredSteps - 1]; + if (step.Length > 0) + { + next = Convert.ToBoolean(this.InputCache[this.ExploredSteps - 1]); + } + else + { + this.InputCache[this.ExploredSteps - 1] = "false"; + } + + break; + } + + this.Logger.WriteLine($">> Choose true or false [step '{this.ExploredSteps}']"); + + var input = Console.ReadLine(); + if (input.Equals("replay")) + { + if (!this.Replay()) + { + continue; + } + + this.Configuration.SchedulingIterations++; + this.PrepareForNextIteration(); + return false; + } + else if (input.Equals("jump")) + { + this.Jump(); + continue; + } + else if (input.Equals("reset")) + { + this.Configuration.SchedulingIterations++; + this.Reset(); + return false; + } + else if (input.Length > 0) + { + try + { + next = Convert.ToBoolean(input); + } + catch (FormatException) + { + this.Logger.WriteLine(">> Wrong format, please retry ..."); + continue; + } + } + + this.InputCache.Add(input); + parsed = true; + } + + return true; + } + + /// + /// Returns the next integer choice. + /// + public bool GetNextIntegerChoice(int maxValue, out int next) + { + next = 0; + this.ExploredSteps++; + + var parsed = false; + while (!parsed) + { + if (this.InputCache.Count >= this.ExploredSteps) + { + var step = this.InputCache[this.ExploredSteps - 1]; + if (step.Length > 0) + { + next = Convert.ToInt32(this.InputCache[this.ExploredSteps - 1]); + } + else + { + this.InputCache[this.ExploredSteps - 1] = "0"; + } + + break; + } + + this.Logger.WriteLine($">> Choose an integer (< {maxValue}) [step '{this.ExploredSteps}']"); + + var input = Console.ReadLine(); + if (input.Equals("replay")) + { + if (!this.Replay()) + { + continue; + } + + this.Configuration.SchedulingIterations++; + this.PrepareForNextIteration(); + return false; + } + else if (input.Equals("jump")) + { + this.Jump(); + continue; + } + else if (input.Equals("reset")) + { + this.Configuration.SchedulingIterations++; + this.Reset(); + return false; + } + else if (input.Length > 0) + { + try + { + next = Convert.ToInt32(input); + } + catch (FormatException) + { + this.Logger.WriteLine(">> Wrong format, please retry ..."); + continue; + } + } + + if (next >= maxValue) + { + this.Logger.WriteLine($">> {next} is >= {maxValue}, please retry ..."); + } + + this.InputCache.Add(input); + parsed = true; + } + + return true; + } + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public bool PrepareForNextIteration() + { + this.ExploredSteps = 0; + return true; + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public void Reset() + { + this.InputCache.Clear(); + this.ExploredSteps = 0; + } + + /// + /// Returns the scheduled steps. + /// + public int GetScheduledSteps() + { + return this.ExploredSteps; + } + + /// + /// True if the scheduling strategy has reached the max + /// scheduling steps for the given scheduling iteration. + /// + public bool HasReachedMaxSchedulingSteps() + { + var bound = this.IsFair() ? this.Configuration.MaxFairSchedulingSteps : + this.Configuration.MaxUnfairSchedulingSteps; + + if (bound == 0) + { + return false; + } + + return this.ExploredSteps >= bound; + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public bool IsFair() => false; + + /// + /// Returns a textual description of the scheduling strategy. + /// + public string GetDescription() => string.Empty; + + /// + /// Replays an earlier point of the execution. + /// + private bool Replay() + { + var result = true; + + this.Logger.WriteLine($">> Replay up to first ?? steps [step '{this.ExploredSteps}']"); + + try + { + var steps = Convert.ToInt32(Console.ReadLine()); + if (steps < 0) + { + this.Logger.WriteLine(">> Expected positive integer, please retry ..."); + result = false; + } + + this.RemoveFromInputCache(steps); + } + catch (FormatException) + { + this.Logger.WriteLine(">> Wrong format, please retry ..."); + result = false; + } + + return result; + } + + /// + /// Jumps to a later point in the execution. + /// + private bool Jump() + { + var result = true; + + this.Logger.WriteLine($">> Jump to ?? step [step '{this.ExploredSteps}']"); + + try + { + var steps = Convert.ToInt32(Console.ReadLine()); + if (steps < this.ExploredSteps) + { + this.Logger.WriteLine(">> Expected integer greater than " + + $"{this.ExploredSteps}, please retry ..."); + result = false; + } + + this.AddInInputCache(steps); + } + catch (FormatException) + { + this.Logger.WriteLine(">> Wrong format, please retry ..."); + result = false; + } + + return result; + } + + /// + /// Adds in the input cache. + /// + private void AddInInputCache(int steps) + { + if (steps > this.InputCache.Count) + { + this.InputCache.AddRange(Enumerable.Repeat(string.Empty, steps - this.InputCache.Count)); + } + } + + /// + /// Removes from the input cache. + /// + private void RemoveFromInputCache(int steps) + { + if (steps > 0 && steps < this.InputCache.Count) + { + this.InputCache.RemoveRange(steps, this.InputCache.Count - steps); + } + else if (steps == 0) + { + this.InputCache.Clear(); + } + } + } +} diff --git a/Source/TestingServices/Exploration/Strategies/Special/ReplayStrategy.cs b/Source/TestingServices/Exploration/Strategies/Special/ReplayStrategy.cs new file mode 100644 index 000000000..18d02a3b9 --- /dev/null +++ b/Source/TestingServices/Exploration/Strategies/Special/ReplayStrategy.cs @@ -0,0 +1,365 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices.Tracing.Schedule; + +namespace Microsoft.Coyote.TestingServices.Scheduling.Strategies +{ + /// + /// Class representing a replaying scheduling strategy. + /// + internal sealed class ReplayStrategy : ISchedulingStrategy + { + /// + /// The configuration. + /// + private readonly Configuration Configuration; + + /// + /// The Coyote program schedule trace. + /// + private readonly ScheduleTrace ScheduleTrace; + + /// + /// The suffix strategy. + /// + private readonly ISchedulingStrategy SuffixStrategy; + + /// + /// Is the scheduler that produced the + /// schedule trace fair? + /// + private readonly bool IsSchedulerFair; + + /// + /// Is the scheduler replaying the trace? + /// + private bool IsReplaying; + + /// + /// The number of scheduled steps. + /// + private int ScheduledSteps; + + /// + /// Text describing a replay error. + /// + internal string ErrorText { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + public ReplayStrategy(Configuration configuration, ScheduleTrace trace, bool isFair) + : this(configuration, trace, isFair, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + public ReplayStrategy(Configuration configuration, ScheduleTrace trace, bool isFair, ISchedulingStrategy suffixStrategy) + { + this.Configuration = configuration; + this.ScheduleTrace = trace; + this.ScheduledSteps = 0; + this.IsSchedulerFair = isFair; + this.IsReplaying = true; + this.SuffixStrategy = suffixStrategy; + this.ErrorText = string.Empty; + } + + /// + /// Returns the next asynchronous operation to schedule. + /// + public bool GetNext(out IAsyncOperation next, List ops, IAsyncOperation current) + { + if (this.IsReplaying) + { + var enabledOperations = ops.Where(op => op.Status is AsyncOperationStatus.Enabled).ToList(); + if (enabledOperations.Count == 0) + { + next = null; + return false; + } + + try + { + if (this.ScheduledSteps >= this.ScheduleTrace.Count) + { + this.ErrorText = "Trace is not reproducible: execution is longer than trace."; + throw new InvalidOperationException(this.ErrorText); + } + + ScheduleStep nextStep = this.ScheduleTrace[this.ScheduledSteps]; + if (nextStep.Type != ScheduleStepType.SchedulingChoice) + { + this.ErrorText = "Trace is not reproducible: next step is not a scheduling choice."; + throw new InvalidOperationException(this.ErrorText); + } + + next = enabledOperations.FirstOrDefault(op => op.SourceId == nextStep.ScheduledOperationId); + if (next is null) + { + this.ErrorText = $"Trace is not reproducible: cannot detect id '{nextStep.ScheduledOperationId}'."; + throw new InvalidOperationException(this.ErrorText); + } + } + catch (InvalidOperationException ex) + { + if (this.SuffixStrategy is null) + { + if (!this.Configuration.DisableEnvironmentExit) + { + Error.ReportAndExit(ex.Message); + } + + next = null; + return false; + } + else + { + this.IsReplaying = false; + return this.SuffixStrategy.GetNext(out next, ops, current); + } + } + + this.ScheduledSteps++; + return true; + } + + return this.SuffixStrategy.GetNext(out next, ops, current); + } + + /// + /// Returns the next boolean choice. + /// + public bool GetNextBooleanChoice(int maxValue, out bool next) + { + if (this.IsReplaying) + { + ScheduleStep nextStep; + + try + { + if (this.ScheduledSteps >= this.ScheduleTrace.Count) + { + this.ErrorText = "Trace is not reproducible: execution is longer than trace."; + throw new InvalidOperationException(this.ErrorText); + } + + nextStep = this.ScheduleTrace[this.ScheduledSteps]; + if (nextStep.Type != ScheduleStepType.NondeterministicChoice) + { + this.ErrorText = "Trace is not reproducible: next step is not a nondeterministic choice."; + throw new InvalidOperationException(this.ErrorText); + } + + if (nextStep.BooleanChoice is null) + { + this.ErrorText = "Trace is not reproducible: next step is not a nondeterministic boolean choice."; + throw new InvalidOperationException(this.ErrorText); + } + } + catch (InvalidOperationException ex) + { + if (this.SuffixStrategy is null) + { + if (!this.Configuration.DisableEnvironmentExit) + { + Error.ReportAndExit(ex.Message); + } + + next = false; + return false; + } + else + { + this.IsReplaying = false; + return this.SuffixStrategy.GetNextBooleanChoice(maxValue, out next); + } + } + + next = nextStep.BooleanChoice.Value; + this.ScheduledSteps++; + return true; + } + + return this.SuffixStrategy.GetNextBooleanChoice(maxValue, out next); + } + + /// + /// Returns the next integer choice. + /// + public bool GetNextIntegerChoice(int maxValue, out int next) + { + if (this.IsReplaying) + { + ScheduleStep nextStep; + + try + { + if (this.ScheduledSteps >= this.ScheduleTrace.Count) + { + this.ErrorText = "Trace is not reproducible: execution is longer than trace."; + throw new InvalidOperationException(this.ErrorText); + } + + nextStep = this.ScheduleTrace[this.ScheduledSteps]; + if (nextStep.Type != ScheduleStepType.NondeterministicChoice) + { + this.ErrorText = "Trace is not reproducible: next step is not a nondeterministic choice."; + throw new InvalidOperationException(this.ErrorText); + } + + if (nextStep.IntegerChoice is null) + { + this.ErrorText = "Trace is not reproducible: next step is not a nondeterministic integer choice."; + throw new InvalidOperationException(this.ErrorText); + } + } + catch (InvalidOperationException ex) + { + if (this.SuffixStrategy is null) + { + if (!this.Configuration.DisableEnvironmentExit) + { + Error.ReportAndExit(ex.Message); + } + + next = 0; + return false; + } + else + { + this.IsReplaying = false; + return this.SuffixStrategy.GetNextIntegerChoice(maxValue, out next); + } + } + + next = nextStep.IntegerChoice.Value; + this.ScheduledSteps++; + return true; + } + + return this.SuffixStrategy.GetNextIntegerChoice(maxValue, out next); + } + + /// + /// Forces the next asynchronous operation to be scheduled. + /// + public void ForceNext(IAsyncOperation next, List ops, IAsyncOperation current) + { + throw new NotImplementedException(); + } + + /// + /// Forces the next boolean choice. + /// + public void ForceNextBooleanChoice(int maxValue, bool next) + { + throw new NotImplementedException(); + } + + /// + /// Forces the next integer choice. + /// + public void ForceNextIntegerChoice(int maxValue, int next) + { + throw new NotImplementedException(); + } + + /// + /// Prepares for the next scheduling iteration. This is invoked + /// at the end of a scheduling iteration. It must return false + /// if the scheduling strategy should stop exploring. + /// + public bool PrepareForNextIteration() + { + this.ScheduledSteps = 0; + if (this.SuffixStrategy != null) + { + return this.SuffixStrategy.PrepareForNextIteration(); + } + else + { + return false; + } + } + + /// + /// Resets the scheduling strategy. This is typically invoked by + /// parent strategies to reset child strategies. + /// + public void Reset() + { + this.ScheduledSteps = 0; + this.SuffixStrategy?.Reset(); + } + + /// + /// Returns the scheduled steps. + /// + public int GetScheduledSteps() + { + if (this.SuffixStrategy != null) + { + return this.ScheduledSteps + this.SuffixStrategy.GetScheduledSteps(); + } + else + { + return this.ScheduledSteps; + } + } + + /// + /// True if the scheduling strategy has reached the depth + /// bound for the given scheduling iteration. + /// + public bool HasReachedMaxSchedulingSteps() + { + if (this.SuffixStrategy != null) + { + return this.SuffixStrategy.HasReachedMaxSchedulingSteps(); + } + else + { + return false; + } + } + + /// + /// Checks if this is a fair scheduling strategy. + /// + public bool IsFair() + { + if (this.SuffixStrategy != null) + { + return this.SuffixStrategy.IsFair(); + } + else + { + return this.IsSchedulerFair; + } + } + + /// + /// Returns a textual description of the scheduling strategy. + /// + public string GetDescription() + { + if (this.SuffixStrategy != null) + { + return "Replay(" + this.SuffixStrategy.GetDescription() + ")"; + } + else + { + return "Replay"; + } + } + } +} diff --git a/Source/TestingServices/Machines/EventQueues/SerializedMachineEventQueue.cs b/Source/TestingServices/Machines/EventQueues/SerializedMachineEventQueue.cs new file mode 100644 index 000000000..9036073d1 --- /dev/null +++ b/Source/TestingServices/Machines/EventQueues/SerializedMachineEventQueue.cs @@ -0,0 +1,375 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices.Runtime +{ + /// + /// Implements a queue of events that is used by a serialized machine during testing. + /// + internal sealed class SerializedMachineEventQueue : IEventQueue + { + /// + /// Manages the state of the machine that owns this queue. + /// + private readonly IMachineStateManager MachineStateManager; + + /// + /// The machine that owns this queue. + /// + private readonly Machine Machine; + + /// + /// The internal queue that contains events with their metadata. + /// + private readonly LinkedList<(Event e, Guid opGroupId, EventInfo info)> Queue; + + /// + /// The raised event and its metadata, or null if no event has been raised. + /// + private (Event e, Guid opGroupId, EventInfo info) RaisedEvent; + + /// + /// Map from the types of events that the owner of the queue is waiting to receive + /// to an optional predicate. If an event of one of these types is enqueued, then + /// if there is no predicate, or if there is a predicate and evaluates to true, then + /// the event is received, else the event is deferred. + /// + private Dictionary> EventWaitTypes; + + /// + /// Task completion source that contains the event obtained using an explicit receive. + /// + private TaskCompletionSource ReceiveCompletionSource; + + /// + /// Checks if the queue is accepting new events. + /// + private bool IsClosed; + + /// + /// The size of the queue. + /// + public int Size => this.Queue.Count; + + /// + /// Checks if an event has been raised. + /// + public bool IsEventRaised => this.RaisedEvent != default; + + /// + /// Initializes a new instance of the class. + /// + internal SerializedMachineEventQueue(IMachineStateManager machineStateManager, Machine machine) + { + this.MachineStateManager = machineStateManager; + this.Machine = machine; + this.Queue = new LinkedList<(Event, Guid, EventInfo)>(); + this.EventWaitTypes = new Dictionary>(); + this.IsClosed = false; + } + + /// + /// Enqueues the specified event and its optional metadata. + /// + public EnqueueStatus Enqueue(Event e, Guid opGroupId, EventInfo info) + { + if (this.IsClosed) + { + return EnqueueStatus.Dropped; + } + + if (this.EventWaitTypes.TryGetValue(e.GetType(), out Func predicate) && + (predicate is null || predicate(e))) + { + this.EventWaitTypes.Clear(); + this.MachineStateManager.OnReceiveEvent(e, opGroupId, info); + this.ReceiveCompletionSource.SetResult(e); + return EnqueueStatus.EventHandlerRunning; + } + + this.MachineStateManager.OnEnqueueEvent(e, opGroupId, info); + this.Queue.AddLast((e, opGroupId, info)); + + if (info.Assert >= 0) + { + var eventCount = this.Queue.Count(val => val.e.GetType().Equals(e.GetType())); + this.MachineStateManager.Assert(eventCount <= info.Assert, + "There are more than {0} instances of '{1}' in the input queue of machine '{2}'.", + info.Assert, info.EventName, this.Machine.Id); + } + + if (info.Assume >= 0) + { + var eventCount = this.Queue.Count(val => val.e.GetType().Equals(e.GetType())); + this.MachineStateManager.Assert(eventCount <= info.Assume, + "There are more than {0} instances of '{1}' in the input queue of machine '{2}'.", + info.Assume, info.EventName, this.Machine.Id); + } + + if (!this.MachineStateManager.IsEventHandlerRunning) + { + if (this.TryDequeueEvent(true).e is null) + { + return EnqueueStatus.NextEventUnavailable; + } + else + { + this.MachineStateManager.IsEventHandlerRunning = true; + return EnqueueStatus.EventHandlerNotRunning; + } + } + + return EnqueueStatus.EventHandlerRunning; + } + + /// + /// Dequeues the next event, if there is one available. + /// + public (DequeueStatus status, Event e, Guid opGroupId, EventInfo info) Dequeue() + { + // Try to get the raised event, if there is one. Raised events + // have priority over the events in the inbox. + if (this.RaisedEvent != default) + { + if (this.MachineStateManager.IsEventIgnoredInCurrentState(this.RaisedEvent.e, this.RaisedEvent.opGroupId, this.RaisedEvent.info)) + { + // TODO: should the user be able to raise an ignored event? + // The raised event is ignored in the current state. + this.RaisedEvent = default; + } + else + { + (Event e, Guid opGroupId, EventInfo info) raisedEvent = this.RaisedEvent; + this.RaisedEvent = default; + return (DequeueStatus.Raised, raisedEvent.e, raisedEvent.opGroupId, raisedEvent.info); + } + } + + var hasDefaultHandler = this.MachineStateManager.IsDefaultHandlerInstalledInCurrentState(); + if (hasDefaultHandler) + { + this.Machine.Runtime.NotifyDefaultEventHandlerCheck(this.Machine); + } + + // Try to dequeue the next event, if there is one. + var (e, opGroupId, info) = this.TryDequeueEvent(); + if (e != null) + { + // Found next event that can be dequeued. + return (DequeueStatus.Success, e, opGroupId, info); + } + + // No event can be dequeued, so check if there is a default event handler. + if (!hasDefaultHandler) + { + // There is no default event handler installed, so do not return an event. + this.MachineStateManager.IsEventHandlerRunning = false; + return (DequeueStatus.NotAvailable, null, Guid.Empty, null); + } + + // TODO: check op-id of default event. + // A default event handler exists. + var eventOrigin = new EventOriginInfo(this.Machine.Id, this.Machine.GetType().FullName, + NameResolver.GetStateNameForLogging(this.Machine.CurrentState)); + return (DequeueStatus.Default, Default.Event, Guid.Empty, new EventInfo(Default.Event, eventOrigin)); + } + + /// + /// Dequeues the next event and its metadata, if there is one available, else returns null. + /// + private (Event e, Guid opGroupId, EventInfo info) TryDequeueEvent(bool checkOnly = false) + { + (Event, Guid, EventInfo) nextAvailableEvent = default; + + // Iterates through the events and metadata in the inbox. + var node = this.Queue.First; + while (node != null) + { + var nextNode = node.Next; + var currentEvent = node.Value; + + if (this.MachineStateManager.IsEventIgnoredInCurrentState(currentEvent.e, currentEvent.opGroupId, currentEvent.info)) + { + if (!checkOnly) + { + // Removes an ignored event. + this.Queue.Remove(node); + } + + node = nextNode; + continue; + } + + // Skips a deferred event. + if (!this.MachineStateManager.IsEventDeferredInCurrentState(currentEvent.e, currentEvent.opGroupId, currentEvent.info)) + { + nextAvailableEvent = currentEvent; + if (!checkOnly) + { + this.Queue.Remove(node); + } + + break; + } + + node = nextNode; + } + + return nextAvailableEvent; + } + + /// + /// Enqueues the specified raised event. + /// + public void Raise(Event e, Guid opGroupId) + { + var eventOrigin = new EventOriginInfo(this.Machine.Id, this.Machine.GetType().FullName, + NameResolver.GetStateNameForLogging(this.Machine.CurrentState)); + var info = new EventInfo(e, eventOrigin); + this.RaisedEvent = (e, opGroupId, info); + this.MachineStateManager.OnRaiseEvent(e, opGroupId, info); + } + + /// + /// Waits to receive an event of the specified type that satisfies an optional predicate. + /// + public Task ReceiveAsync(Type eventType, Func predicate = null) + { + var eventWaitTypes = new Dictionary> + { + { eventType, predicate } + }; + + return this.ReceiveAsync(eventWaitTypes); + } + + /// + /// Waits to receive an event of the specified types. + /// + public Task ReceiveAsync(params Type[] eventTypes) + { + var eventWaitTypes = new Dictionary>(); + foreach (var type in eventTypes) + { + eventWaitTypes.Add(type, null); + } + + return this.ReceiveAsync(eventWaitTypes); + } + + /// + /// Waits to receive an event of the specified types that satisfy the specified predicates. + /// + public Task ReceiveAsync(params Tuple>[] events) + { + var eventWaitTypes = new Dictionary>(); + foreach (var e in events) + { + eventWaitTypes.Add(e.Item1, e.Item2); + } + + return this.ReceiveAsync(eventWaitTypes); + } + + /// + /// Waits for an event to be enqueued. + /// + private Task ReceiveAsync(Dictionary> eventWaitTypes) + { + this.Machine.Runtime.NotifyReceiveCalled(this.Machine); + + (Event e, Guid opGroupId, EventInfo info) receivedEvent = default; + var node = this.Queue.First; + while (node != null) + { + // Dequeue the first event that the caller waits to receive, if there is one in the queue. + if (eventWaitTypes.TryGetValue(node.Value.e.GetType(), out Func predicate) && + (predicate is null || predicate(node.Value.e))) + { + receivedEvent = node.Value; + this.Queue.Remove(node); + break; + } + + node = node.Next; + } + + if (receivedEvent == default) + { + this.ReceiveCompletionSource = new TaskCompletionSource(); + this.EventWaitTypes = eventWaitTypes; + this.MachineStateManager.OnWaitEvent(this.EventWaitTypes.Keys); + return this.ReceiveCompletionSource.Task; + } + + this.MachineStateManager.OnReceiveEventWithoutWaiting(receivedEvent.e, receivedEvent.opGroupId, receivedEvent.info); + return Task.FromResult(receivedEvent.e); + } + + /// + /// Returns the cached state of the queue. + /// + public int GetCachedState() + { + unchecked + { + var hash = 19; + foreach (var (_, _, info) in this.Queue) + { + hash = (hash * 31) + info.EventName.GetHashCode(); + if (info.HashedState != 0) + { + // Adds the user-defined hashed event state. + hash = (hash * 31) + info.HashedState; + } + } + + return hash; + } + } + + /// + /// Closes the queue, which stops any further event enqueues. + /// + public void Close() + { + this.IsClosed = true; + } + + /// + /// Disposes the queue resources. + /// + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + foreach (var (e, opGroupId, info) in this.Queue) + { + this.MachineStateManager.OnDropEvent(e, opGroupId, info); + } + + this.Queue.Clear(); + } + + /// + /// Disposes the queue resources. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Source/TestingServices/Machines/MachineTestKit.cs b/Source/TestingServices/Machines/MachineTestKit.cs new file mode 100644 index 000000000..f7e19ffd6 --- /dev/null +++ b/Source/TestingServices/Machines/MachineTestKit.cs @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices.Runtime; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Provides methods for testing a machine of type in isolation. + /// + /// The machine type to test. + public sealed class MachineTestKit + where T : Machine + { + /// + /// The machine testing runtime. + /// + private readonly MachineTestingRuntime Runtime; + + /// + /// The instance of the machine being tested. + /// + public readonly T Machine; + + /// + /// True if the machine has started its execution, else false. + /// + private bool IsRunning; + + /// + /// Initializes a new instance of the class. + /// + /// The runtime configuration to use. + public MachineTestKit(Configuration configuration) + { + configuration = configuration ?? Configuration.Create(); + this.Runtime = new MachineTestingRuntime(typeof(T), configuration); + this.Machine = this.Runtime.Machine as T; + this.IsRunning = false; + this.Runtime.OnFailure += ex => + { + this.Runtime.Logger.WriteLine(ex.ToString()); + }; + } + + /// + /// Transitions the machine to its start state, passes the optional specified event + /// and invokes its on-entry handler, if there is one available. This method returns + /// a task that completes when the machine reaches quiescence (typically when the + /// event handler finishes executing because there are not more events to dequeue, + /// or when the machine asynchronously waits to receive an event). + /// + /// Optional event used during initialization. + /// Task that represents the asynchronous operation. + public Task StartMachineAsync(Event initialEvent = null) + { + this.Runtime.Assert(!this.IsRunning, + string.Format("Machine '{0}' is already running.", this.Machine.Id)); + this.IsRunning = true; + return this.Runtime.StartAsync(initialEvent); + } + + /// + /// Sends an event to the machine and starts its event handler. This method returns + /// a task that completes when the machine reaches quiescence (typically when the + /// event handler finishes executing because there are not more events to dequeue, + /// or when the machine asynchronously waits to receive an event). + /// + /// Task that represents the asynchronous operation. + public Task SendEventAsync(Event e) + { + this.Runtime.Assert(this.IsRunning, + string.Format("Machine '{0}' is not running.", this.Machine.Id)); + return this.Runtime.SendEventAndExecuteAsync(this.Runtime.Machine.Id, e, null, Guid.Empty, null); + } + + /// + /// Invokes the machine method with the specified name, and passing the specified + /// optional parameters. Use this method to invoke private methods of the machine. + /// + /// The name of the machine method. + /// The parameters to the method. + public object Invoke(string methodName, params object[] parameters) + { + MethodInfo method = this.GetMethod(methodName, false, null); + return method.Invoke(this.Machine, parameters); + } + + /// + /// Invokes the machine method with the specified name and parameter types, and passing the + /// specified optional parameters. Use this method to invoke private methods of the machine. + /// + /// The name of the machine method. + /// The parameter types of the method. + /// The parameters to the method. + public object Invoke(string methodName, Type[] parameterTypes, params object[] parameters) + { + MethodInfo method = this.GetMethod(methodName, false, parameterTypes); + return method.Invoke(this.Machine, parameters); + } + + /// + /// Invokes the asynchronous machine method with the specified name, and passing the specified + /// optional parameters. Use this method to invoke private methods of the machine. + /// + /// The name of the machine method. + /// The parameters to the method. + public async Task InvokeAsync(string methodName, params object[] parameters) + { + MethodInfo method = this.GetMethod(methodName, true, null); + var task = (Task)method.Invoke(this.Machine, parameters); + await task.ConfigureAwait(false); + var resultProperty = task.GetType().GetProperty("Result"); + return resultProperty.GetValue(task); + } + + /// + /// Invokes the asynchronous machine method with the specified name and parameter types, and passing + /// the specified optional parameters. Use this method to invoke private methods of the machine. + /// + /// The name of the machine method. + /// The parameter types of the method. + /// The parameters to the method. + public async Task InvokeAsync(string methodName, Type[] parameterTypes, params object[] parameters) + { + MethodInfo method = this.GetMethod(methodName, true, parameterTypes); + var task = (Task)method.Invoke(this.Machine, parameters); + await task.ConfigureAwait(false); + var resultProperty = task.GetType().GetProperty("Result"); + return resultProperty.GetValue(task); + } + + /// + /// Uses reflection to get the machine method with the specified name and parameter types. + /// + /// The name of the machine method. + /// True if the method is async, else false. + /// The parameter types of the method. + private MethodInfo GetMethod(string methodName, bool isAsync, Type[] parameterTypes) + { + var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + MethodInfo method; + if (parameterTypes is null) + { + method = this.Machine.GetType().GetMethod(methodName, bindingFlags); + } + else + { + method = this.Machine.GetType().GetMethod(methodName, bindingFlags, + Type.DefaultBinder, parameterTypes, null); + } + + this.Runtime.Assert(method != null, + string.Format("Unable to invoke method '{0}' in machine '{1}'.", + methodName, this.Machine.Id)); + this.Runtime.Assert(method.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) is null != isAsync, + string.Format("Must invoke {0}method '{1}' of machine '{2}' using '{3}'.", + isAsync ? string.Empty : "async ", methodName, this.Machine.Id, isAsync ? "Invoke" : "InvokeAsync")); + + return method; + } + + /// + /// Asserts if the specified condition holds. + /// + public void Assert(bool predicate) + { + this.Runtime.Assert(predicate); + } + + /// + /// Asserts if the specified condition holds. + /// + public void Assert(bool predicate, string s, object arg0) + { + this.Runtime.Assert(predicate, s, arg0); + } + + /// + /// Asserts if the specified condition holds. + /// + public void Assert(bool predicate, string s, object arg0, object arg1) + { + this.Runtime.Assert(predicate, s, arg0, arg1); + } + + /// + /// Asserts if the specified condition holds. + /// + public void Assert(bool predicate, string s, object arg0, object arg1, object arg2) + { + this.Runtime.Assert(predicate, s, arg0, arg1, arg2); + } + + /// + /// Asserts if the specified condition holds. + /// + public void Assert(bool predicate, string s, params object[] args) + { + this.Runtime.Assert(predicate, s, args); + } + + /// + /// Asserts that the machine has transitioned to the state with the specified type . + /// + /// The type of the machine state. + public void AssertStateTransition() + where S : MachineState + { + this.AssertStateTransition(typeof(S).FullName); + } + + /// + /// Asserts that the machine has transitioned to the state with the specified name + /// (either or ). + /// + /// The name of the machine state. + public void AssertStateTransition(string machineStateName) + { + bool predicate = this.Machine.CurrentState.FullName.Equals(machineStateName) || + this.Machine.CurrentState.FullName.Equals( + this.Machine.CurrentState.DeclaringType.FullName + "+" + machineStateName); + this.Runtime.Assert(predicate, string.Format("Machine '{0}' is in state '{1}', not in '{2}'.", + this.Machine.Id, this.Machine.CurrentState.FullName, machineStateName)); + } + + /// + /// Asserts that the machine is waiting (or not) to receive an event. + /// + public void AssertIsWaitingToReceiveEvent(bool isWaiting) + { + this.Runtime.Assert(this.Runtime.IsMachineWaitingToReceiveEvent == isWaiting, + "Machine '{0}' is {1}waiting to receive an event.", + this.Machine.Id, this.Runtime.IsMachineWaitingToReceiveEvent ? string.Empty : "not "); + } + + /// + /// Asserts that the machine inbox contains the specified number of events. + /// + /// The number of events in the inbox. + public void AssertInboxSize(int numEvents) + { + this.Runtime.Assert(this.Runtime.MachineInbox.Size == numEvents, + "Machine '{0}' contains '{1}' events in its inbox.", + this.Machine.Id, this.Runtime.MachineInbox.Size); + } + } +} diff --git a/Source/TestingServices/Machines/MachineTestingRuntime.cs b/Source/TestingServices/Machines/MachineTestingRuntime.cs new file mode 100644 index 000000000..5d6adc050 --- /dev/null +++ b/Source/TestingServices/Machines/MachineTestingRuntime.cs @@ -0,0 +1,785 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.TestingServices.Timers; +using Microsoft.Coyote.Threading; +using Microsoft.Coyote.Threading.Tasks; + +using EventInfo = Microsoft.Coyote.Runtime.EventInfo; +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.TestingServices.Runtime +{ + /// + /// Runtime for testing a machine in isolation. + /// + internal sealed class MachineTestingRuntime : CoyoteRuntime + { + /// + /// The machine being tested. + /// + internal readonly Machine Machine; + + /// + /// The inbox of the machine being tested. + /// + internal readonly EventQueue MachineInbox; + + /// + /// Task completion source that completes when the machine being tested reaches quiescence. + /// + private TaskCompletionSource QuiescenceCompletionSource; + + /// + /// True if the machine is waiting to receive and event, else false. + /// + internal bool IsMachineWaitingToReceiveEvent { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + internal MachineTestingRuntime(Type machineType, Configuration configuration) + : base(configuration) + { + if (!machineType.IsSubclassOf(typeof(Machine))) + { + this.Assert(false, "Type '{0}' is not a machine.", machineType.FullName); + } + + var mid = new MachineId(machineType, null, this); + + this.Machine = MachineFactory.Create(machineType); + IMachineStateManager stateManager = new MachineStateManager(this, this.Machine, Guid.Empty); + this.MachineInbox = new EventQueue(stateManager); + + this.Machine.Initialize(this, mid, stateManager, this.MachineInbox); + this.Machine.InitializeStateInformation(); + + this.LogWriter.OnCreateMachine(this.Machine.Id, null); + + this.MachineMap.TryAdd(mid, this.Machine); + + this.IsMachineWaitingToReceiveEvent = false; + } + + /// + /// Starts executing the machine-under-test by transitioning it to its initial state + /// and passing an optional initialization event. + /// + internal Task StartAsync(Event initialEvent) + { + this.RunMachineEventHandler(this.Machine, initialEvent, true); + return this.QuiescenceCompletionSource.Task; + } + + /// + /// Creates a machine id that is uniquely tied to the specified unique name. The + /// returned machine id can either be a fresh id (not yet bound to any machine), + /// or it can be bound to a previously created machine. In the second case, this + /// machine id can be directly used to communicate with the corresponding machine. + /// + public override MachineId CreateMachineIdFromName(Type type, string machineName) => new MachineId(type, machineName, this, true); + + /// + /// Creates a new machine of the specified and with + /// the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(Type type, Event e = null, Guid opGroupId = default) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new machine of the specified and name, and + /// with the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(Type type, string machineName, Event e = null, Guid opGroupId = default) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new machine of the specified type, using the specified . + /// This method optionally passes an to the new machine, which can only + /// be used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(MachineId mid, Type type, Event e = null, Guid opGroupId = default) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new machine of the specified and with the + /// specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when + /// the machine is initialized and the (if any) is handled. + /// + public override Task CreateMachineAndExecuteAsync(Type type, Event e = null, Guid opGroupId = default) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new machine of the specified and name, and with + /// the specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when the + /// machine is initialized and the (if any) is handled. + /// + public override Task CreateMachineAndExecuteAsync(Type type, string machineName, Event e = null, Guid opGroupId = default) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new machine of the specified , using the specified + /// unbound machine id, and passes the specified optional . This + /// event can only be used to access its payload, and cannot be handled. The method + /// returns only when the machine is initialized and the (if any) + /// is handled. + /// + public override Task CreateMachineAndExecuteAsync(MachineId mid, Type type, Event e = null, Guid opGroupId = default) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Sends an asynchronous to a machine. + /// + public override void SendEvent(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Sends an to a machine. Returns immediately if the target machine was already + /// running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + public override Task SendEventAndExecuteAsync(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Returns the operation group id of the specified machine. Returns + /// if the id is not set, or if the is not associated with this runtime. + /// During testing, the runtime asserts that the specified machine is currently executing. + /// + public override Guid GetCurrentOperationGroupId(MachineId currentMachine) => Guid.Empty; + + /// + /// Creates a new of the specified . + /// + internal override MachineId CreateMachine(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId) + { + mid = mid ?? new MachineId(type, null, this); + this.LogWriter.OnCreateMachine(mid, creator?.Id); + return mid; + } + + /// + /// Creates a new of the specified . The + /// method returns only when the created machine reaches quiescence. + /// + internal override Task CreateMachineAndExecuteAsync(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId) + { + mid = mid ?? new MachineId(type, null, this); + this.LogWriter.OnCreateMachine(mid, creator?.Id); + return Task.FromResult(mid); + } + + /// + /// Sends an asynchronous to a machine. + /// + internal override void SendEvent(MachineId target, Event e, AsyncMachine sender, Guid opGroupId, SendOptions options) + { + this.Assert(sender is null || this.Machine.Id.Equals(sender.Id), + string.Format("Only machine '{0}' can send an event during this test.", this.Machine.Id.ToString())); + this.Assert(target != null, string.Format("Machine '{0}' is sending to a null machine.", this.Machine.Id.ToString())); + this.Assert(e != null, string.Format("Machine '{0}' is sending a null event.", this.Machine.Id.ToString())); + + // The operation group id of this operation is set using the following precedence: + // (1) To the specified send operation group id, if it is non-empty. + // (2) To the operation group id of the sender machine, if it exists and is non-empty. + // (3) To the empty operation group id. + if (opGroupId == Guid.Empty && sender != null) + { + opGroupId = sender.OperationGroupId; + } + + if (this.Machine.IsHalted) + { + this.LogWriter.OnSend(target, sender?.Id, (sender as Machine)?.CurrentStateName ?? string.Empty, + e.GetType().FullName, opGroupId, isTargetHalted: true); + return; + } + + this.LogWriter.OnSend(target, sender?.Id, (sender as Machine)?.CurrentStateName ?? string.Empty, + e.GetType().FullName, opGroupId, isTargetHalted: false); + + if (!target.Equals(this.Machine.Id)) + { + // Drop all events sent to a machine other than the machine-under-test. + return; + } + + EnqueueStatus enqueueStatus = this.Machine.Enqueue(e, opGroupId, null); + if (enqueueStatus == EnqueueStatus.EventHandlerNotRunning) + { + this.RunMachineEventHandler(this.Machine, null, false); + } + } + + /// + /// Sends an asynchronous to a machine. Returns immediately if the target machine was + /// already running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + internal override Task SendEventAndExecuteAsync(MachineId target, Event e, AsyncMachine sender, + Guid opGroupId, SendOptions options) + { + this.SendEvent(target, e, sender, opGroupId, options); + return this.QuiescenceCompletionSource.Task; + } + + /// + /// Runs a new asynchronous machine event handler. + /// + private Task RunMachineEventHandler(Machine machine, Event initialEvent, bool isFresh) + { + this.QuiescenceCompletionSource = new TaskCompletionSource(); + + return Task.Run(async () => + { + try + { + if (isFresh) + { + await machine.GotoStartState(initialEvent); + } + + await machine.RunEventHandlerAsync(); + this.QuiescenceCompletionSource.SetResult(true); + } + catch (Exception ex) + { + this.IsRunning = false; + this.RaiseOnFailureEvent(ex); + this.QuiescenceCompletionSource.SetException(ex); + } + }); + } + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal override ControlledTask CreateControlledTask(Action action, CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal override ControlledTask CreateControlledTask(Func function, CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal override ControlledTask CreateControlledTask(Func function, + CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new to execute the specified asynchronous work. + /// + internal override ControlledTask CreateControlledTask(Func> function, + CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + internal override ControlledTask CreateControlledTaskDelay(int millisecondsDelay, CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + internal override ControlledTask CreateControlledTaskDelay(TimeSpan delay, CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a associated with a completion source. + /// + internal override ControlledTask CreateControlledTaskCompletionSource(Task task) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a associated with a completion source. + /// + internal override ControlledTask CreateControlledTaskCompletionSource(Task task) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal override ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal override ControlledTask WaitAllTasksAsync(params Task[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask WaitAllTasksAsync(IEnumerable tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask WaitAllTasksAsync(IEnumerable tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal override ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + internal override ControlledTask WaitAllTasksAsync(params Task[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask WaitAllTasksAsync(IEnumerable> tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask WaitAllTasksAsync(IEnumerable> tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal override ControlledTask WaitAnyTaskAsync(params ControlledTask[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal override ControlledTask WaitAnyTaskAsync(params Task[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask WaitAnyTaskAsync(IEnumerable tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask WaitAnyTaskAsync(IEnumerable tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal override ControlledTask> WaitAnyTaskAsync(params ControlledTask[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + internal override ControlledTask> WaitAnyTaskAsync(params Task[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + internal override ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Waits for any of the provided objects to complete execution. + /// + internal override int WaitAnyTask(params ControlledTask[] tasks) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds. + /// + internal override int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds or until a cancellation + /// token is cancelled. + /// + internal override int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout, CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Waits for any of the provided objects to complete + /// execution unless the wait is cancelled. + /// + internal override int WaitAnyTask(ControlledTask[] tasks, CancellationToken cancellationToken) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified time interval. + /// + internal override int WaitAnyTask(ControlledTask[] tasks, TimeSpan timeout) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a controlled awaiter that switches into a target environment. + /// + internal override ControlledYieldAwaitable.ControlledYieldAwaiter CreateControlledYieldAwaiter() => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Ends the wait for the completion of the yield operation. + /// + internal override void OnGetYieldResult(YieldAwaitable.YieldAwaiter awaiter) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Sets the action to perform when the yield operation completes. + /// + internal override void OnYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Schedules the continuation action that is invoked when the yield operation completes. + /// + internal override void OnUnsafeYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter) => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a mutual exclusion lock that is compatible with objects. + /// + internal override ControlledLock CreateControlledLock() => + throw new NotSupportedException("Invoking this method is not supported in machine unit testing mode."); + + /// + /// Creates a new timer that sends a to its owner machine. + /// + internal override IMachineTimer CreateMachineTimer(TimerInfo info, Machine owner) + { + var mid = this.CreateMachineId(typeof(MockMachineTimer)); + this.CreateMachine(mid, typeof(MockMachineTimer), new TimerSetupEvent(info, owner, this.Configuration.TimeoutDelay)); + return this.GetMachineFromId(mid); + } + + /// + /// Tries to create a new of the specified . + /// + internal override void TryCreateMonitor(Type type) + { + // No-op in this runtime mode. + } + + /// + /// Invokes the specified with the specified . + /// + internal override void Monitor(Type type, AsyncMachine sender, Event e) + { + // No-op in this runtime mode. + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public override void Assert(bool predicate) + { + if (!predicate) + { + throw new AssertionFailureException("Detected an assertion failure."); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public override void Assert(bool predicate, string s, object arg0) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public override void Assert(bool predicate, string s, object arg0, object arg1) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString(), arg1.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public override void Assert(bool predicate, string s, object arg0, object arg1, object arg2) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString(), arg1.ToString(), arg2.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + public override void Assert(bool predicate, string s, params object[] args) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(CultureInfo.InvariantCulture, s, args)); + } + } + + /// + /// Returns a nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal override bool GetNondeterministicBooleanChoice(AsyncMachine machine, int maxValue) + { + Random random = new Random(DateTime.Now.Millisecond); + + bool result = false; + if (random.Next(maxValue) == 0) + { + result = true; + } + + this.LogWriter.OnRandom(machine?.Id, result); + + return result; + } + + /// + /// Returns a fair nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal override bool GetFairNondeterministicBooleanChoice(AsyncMachine machine, string uniqueId) + { + return this.GetNondeterministicBooleanChoice(machine, 2); + } + + /// + /// Returns a nondeterministic integer choice, that can be + /// controlled during analysis or testing. + /// + internal override int GetNondeterministicIntegerChoice(AsyncMachine machine, int maxValue) + { + Random random = new Random(DateTime.Now.Millisecond); + var result = random.Next(maxValue); + + this.LogWriter.OnRandom(machine?.Id, result); + + return result; + } + + /// + /// Notifies that a machine entered a state. + /// + internal override void NotifyEnteredState(Machine machine) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineState(machine.Id, machine.CurrentStateName, isEntry: true); + } + } + + /// + /// Notifies that a monitor entered a state. + /// + internal override void NotifyEnteredState(Monitor monitor) + { + if (this.Configuration.IsVerbose) + { + string monitorState = monitor.CurrentStateNameWithTemperature; + this.LogWriter.OnMonitorState(monitor.GetType().FullName, monitor.Id, monitorState, true, monitor.GetHotState()); + } + } + + /// + /// Notifies that a machine exited a state. + /// + internal override void NotifyExitedState(Machine machine) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineState(machine.Id, machine.CurrentStateName, isEntry: false); + } + } + + /// + /// Notifies that a monitor exited a state. + /// + internal override void NotifyExitedState(Monitor monitor) + { + if (this.Configuration.IsVerbose) + { + string monitorState = monitor.CurrentStateNameWithTemperature; + this.LogWriter.OnMonitorState(monitor.GetType().FullName, monitor.Id, monitorState, false, monitor.GetHotState()); + } + } + + /// + /// Notifies that a machine invoked an action. + /// + internal override void NotifyInvokedAction(Machine machine, MethodInfo action, Event receivedEvent) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineAction(machine.Id, machine.CurrentStateName, action.Name); + } + } + + /// + /// Notifies that a monitor invoked an action. + /// + internal override void NotifyInvokedAction(Monitor monitor, MethodInfo action, Event receivedEvent) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMonitorAction(monitor.GetType().FullName, monitor.Id, action.Name, monitor.CurrentStateName); + } + } + + /// + /// Notifies that a machine raised an . + /// + internal override void NotifyRaisedEvent(Machine machine, Event e, EventInfo eventInfo) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMachineEvent(machine.Id, machine.CurrentStateName, e.GetType().FullName); + } + } + + /// + /// Notifies that a monitor raised an . + /// + internal override void NotifyRaisedEvent(Monitor monitor, Event e, EventInfo eventInfo) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnMonitorEvent(monitor.GetType().FullName, monitor.Id, monitor.CurrentStateName, + e.GetType().FullName, isProcessing: false); + } + } + + /// + /// Notifies that a machine dequeued an . + /// + internal override void NotifyDequeuedEvent(Machine machine, Event e, EventInfo eventInfo) + { + if (this.Configuration.IsVerbose) + { + this.LogWriter.OnDequeue(machine.Id, machine.CurrentStateName, e.GetType().FullName); + } + } + + /// + /// Notifies that a machine is waiting to receive an event of one of the specified types. + /// + internal override void NotifyWaitEvent(Machine machine, IEnumerable eventTypes) + { + if (this.Configuration.IsVerbose) + { + var eventWaitTypesArray = eventTypes.ToArray(); + if (eventWaitTypesArray.Length == 1) + { + this.LogWriter.OnWait(this.Machine.Id, this.Machine.CurrentStateName, eventWaitTypesArray[0]); + } + else + { + this.LogWriter.OnWait(this.Machine.Id, this.Machine.CurrentStateName, eventWaitTypesArray); + } + } + + this.IsMachineWaitingToReceiveEvent = true; + this.QuiescenceCompletionSource.SetResult(true); + } + + /// + /// Notifies that a machine received an event that it was waiting for. + /// + internal override void NotifyReceivedEvent(Machine machine, Event e, EventInfo eventInfo) + { + this.LogWriter.OnReceive(machine.Id, machine.CurrentStateName, e.GetType().FullName, wasBlocked: true); + this.IsMachineWaitingToReceiveEvent = false; + this.QuiescenceCompletionSource = new TaskCompletionSource(); + } + + /// + /// Notifies that a machine received an event without waiting because the event + /// was already in the inbox when the machine invoked the receive statement. + /// + internal override void NotifyReceivedEventWithoutWaiting(Machine machine, Event e, EventInfo eventInfo) + { + this.LogWriter.OnReceive(machine.Id, machine.CurrentStateName, e.GetType().FullName, wasBlocked: false); + } + + /// + /// Notifies that a machine has halted. + /// + internal override void NotifyHalted(Machine machine) + { + this.MachineMap.TryRemove(machine.Id, out AsyncMachine _); + } + + /// + /// Disposes runtime resources. + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.MachineMap.Clear(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Source/TestingServices/Machines/SerializedMachineStateManager.cs b/Source/TestingServices/Machines/SerializedMachineStateManager.cs new file mode 100644 index 000000000..ab4656501 --- /dev/null +++ b/Source/TestingServices/Machines/SerializedMachineStateManager.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Runtime +{ + /// + /// Implements a state manager that is used by a serialized machine during testing. + /// + internal sealed class SerializedMachineStateManager : IMachineStateManager + { + /// + /// The runtime that executes the machine being managed. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// The machine being managed. + /// + private readonly Machine Machine; + + /// + /// True if the event handler of the machine is running, else false. + /// + public bool IsEventHandlerRunning { get; set; } + + /// + /// Id used to identify subsequent operations performed by the machine. + /// + public Guid OperationGroupId { get; set; } + + /// + /// Program counter used for state-caching. Distinguishes + /// scheduling from non-deterministic choices. + /// + internal int ProgramCounter; + + /// + /// True if a transition statement was called in the current action, else false. + /// + internal bool IsTransitionStatementCalledInCurrentAction; + + /// + /// True if the machine is currently executing an asynchronous handler + /// that returns a , else false. + /// + internal bool IsInsideControlledTaskHandler; + + /// + /// True if the machine is executing an on exit action, else false. + /// + internal bool IsInsideOnExit; + + /// + /// Initializes a new instance of the class. + /// + internal SerializedMachineStateManager(SystematicTestingRuntime runtime, Machine machine, Guid operationGroupId) + { + this.Runtime = runtime; + this.Machine = machine; + this.IsEventHandlerRunning = true; + this.OperationGroupId = operationGroupId; + this.ProgramCounter = 0; + this.IsTransitionStatementCalledInCurrentAction = false; + this.IsInsideControlledTaskHandler = false; + this.IsInsideOnExit = false; + } + + /// + /// Returns the cached state of the machine. + /// + public int GetCachedState() + { + unchecked + { + var hash = 19; + hash = (hash * 31) + this.IsEventHandlerRunning.GetHashCode(); + hash = (hash * 31) + this.ProgramCounter; + return hash; + } + } + + /// + /// Checks if the specified event is ignored in the current machine state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEventIgnoredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Machine.IsEventIgnoredInCurrentState(e); + + /// + /// Checks if the specified event is deferred in the current machine state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsEventDeferredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Machine.IsEventDeferredInCurrentState(e); + + /// + /// Checks if a default handler is installed in the current machine state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool IsDefaultHandlerInstalledInCurrentState() => this.Machine.IsDefaultHandlerInstalledInCurrentState(); + + /// + /// Notifies the machine that an event has been enqueued. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnEnqueueEvent(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Runtime.LogWriter.OnEnqueue(this.Machine.Id, e.GetType().FullName); + + /// + /// Notifies the machine that an event has been raised. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnRaiseEvent(Event e, Guid opGroupId, EventInfo eventInfo) => + this.Runtime.NotifyRaisedEvent(this.Machine, e, eventInfo); + + /// + /// Notifies the machine that it is waiting to receive an event of one of the specified types. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnWaitEvent(IEnumerable eventTypes) => + this.Runtime.NotifyWaitEvent(this.Machine, eventTypes); + + /// + /// Notifies the machine that an event it was waiting to receive has been enqueued. + /// + public void OnReceiveEvent(Event e, Guid opGroupId, EventInfo eventInfo) + { + if (opGroupId != Guid.Empty) + { + // Inherit the operation group id of the receive operation, if it is non-empty. + this.OperationGroupId = opGroupId; + } + + this.Runtime.NotifyReceivedEvent(this.Machine, e, eventInfo); + } + + /// + /// Notifies the machine that an event it was waiting to receive was already in the + /// event queue when the machine invoked the receive statement. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnReceiveEventWithoutWaiting(Event e, Guid opGroupId, EventInfo eventInfo) + { + if (opGroupId != Guid.Empty) + { + // Inherit the operation group id of the receive operation, if it is non-empty. + this.OperationGroupId = opGroupId; + } + + this.Runtime.NotifyReceivedEventWithoutWaiting(this.Machine, e, eventInfo); + } + + /// + /// Notifies the machine that an event has been dropped. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void OnDropEvent(Event e, Guid opGroupId, EventInfo eventInfo) + { + this.Runtime.Assert(!eventInfo.MustHandle, "Machine '{0}' halted before dequeueing must-handle event '{1}'.", + this.Machine.Id, e.GetType().FullName); + this.Runtime.TryHandleDroppedEvent(e, this.Machine.Id); + } + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, object arg0) => this.Runtime.Assert(predicate, s, arg0); + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, object arg0, object arg1) => this.Runtime.Assert(predicate, s, arg0, arg1); + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, object arg0, object arg1, object arg2) => + this.Runtime.Assert(predicate, s, arg0, arg1, arg2); + + /// + /// Asserts if the specified condition holds. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Assert(bool predicate, string s, params object[] args) => this.Runtime.Assert(predicate, s, args); + } +} diff --git a/Source/TestingServices/Machines/Timers/MockMachineTimer.cs b/Source/TestingServices/Machines/Timers/MockMachineTimer.cs new file mode 100644 index 000000000..ee5d6f7e9 --- /dev/null +++ b/Source/TestingServices/Machines/Timers/MockMachineTimer.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; + +namespace Microsoft.Coyote.TestingServices.Timers +{ + /// + /// A mock timer that replaces during testing. + /// It is implemented as a machine. + /// + internal class MockMachineTimer : Machine, IMachineTimer + { + /// + /// Stores information about this timer. + /// + private TimerInfo TimerInfo; + + /// + /// Stores information about this timer. + /// + TimerInfo IMachineTimer.Info => this.TimerInfo; + + /// + /// The machine that owns this timer. + /// + private Machine Owner; + + /// + /// The timeout event. + /// + private TimerElapsedEvent TimeoutEvent; + + /// + /// Adjusts the probability of firing a timeout event. + /// + private uint Delay; + + [Start] + [OnEntry(nameof(Setup))] + [OnEventDoAction(typeof(Default), nameof(HandleTimeout))] + private class Active : MachineState + { + } + + /// + /// Initializes the timer with the specified configuration. + /// + private void Setup() + { + this.TimerInfo = (this.ReceivedEvent as TimerSetupEvent).Info; + this.Owner = (this.ReceivedEvent as TimerSetupEvent).Owner; + this.Delay = (this.ReceivedEvent as TimerSetupEvent).Delay; + this.TimeoutEvent = new TimerElapsedEvent(this.TimerInfo); + } + + /// + /// Handles the timeout. + /// + private void HandleTimeout() + { + // Try to send the next timeout event. + bool isTimeoutSent = false; + int delay = (int)this.Delay > 0 ? (int)this.Delay : 1; + if ((this.RandomInteger(delay) == 0) && this.FairRandom()) + { + // The probability of sending a timeout event is atmost 1/N. + this.Send(this.Owner.Id, this.TimeoutEvent); + isTimeoutSent = true; + } + + // If non-periodic, and a timeout was successfully sent, then become + // inactive until disposal. Else retry. + if (isTimeoutSent && this.TimerInfo.Period.TotalMilliseconds < 0) + { + this.Goto(); + } + } + + private class Inactive : MachineState + { + } + + /// + /// Determines whether the specified System.Object is equal + /// to the current System.Object. + /// + public override bool Equals(object obj) + { + if (obj is MockMachineTimer timer) + { + return this.Id == timer.Id; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// Returns a string that represents the current instance. + /// + public override string ToString() => this.Id.Name; + + /// + /// Indicates whether the specified is equal + /// to the current . + /// + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; otherwise, false. + public bool Equals(MachineTimer other) + { + return this.Equals((object)other); + } + + /// + /// Disposes the resources held by this timer. + /// + public void Dispose() + { + this.Runtime.SendEvent(this.Id, new Halt()); + } + } +} diff --git a/Source/TestingServices/Machines/Timers/TimerSetupEvent.cs b/Source/TestingServices/Machines/Timers/TimerSetupEvent.cs new file mode 100644 index 000000000..52fc9e7ff --- /dev/null +++ b/Source/TestingServices/Machines/Timers/TimerSetupEvent.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; + +namespace Microsoft.Coyote.TestingServices.Timers +{ + /// + /// Defines a timer elapsed event that is sent from a timer to the machine that owns the timer. + /// + internal class TimerSetupEvent : Event + { + /// + /// Stores information about the timer. + /// + internal readonly TimerInfo Info; + + /// + /// The machine that owns the timer. + /// + internal readonly Machine Owner; + + /// + /// Adjusts the probability of firing a timeout event. + /// + internal readonly uint Delay; + + /// + /// Initializes a new instance of the class. + /// + /// Stores information about the timer. + /// The machine that owns the timer. + /// Adjusts the probability of firing a timeout event. + internal TimerSetupEvent(TimerInfo info, Machine owner, uint delay) + { + this.Info = info; + this.Owner = owner; + this.Delay = delay; + } + } +} diff --git a/Source/TestingServices/Operations/AsyncOperationStatus.cs b/Source/TestingServices/Operations/AsyncOperationStatus.cs new file mode 100644 index 000000000..fd405552c --- /dev/null +++ b/Source/TestingServices/Operations/AsyncOperationStatus.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.TestingServices.Scheduling +{ + /// + /// The status of an asynchronous operation. + /// + public enum AsyncOperationStatus + { + /// + /// The operation does not have a status yet. + /// + None = 0, + + /// + /// The operation is enabled. + /// + Enabled, + + /// + /// The operation is waiting for all of its dependencies to complete. + /// + BlockedOnWaitAll, + + /// + /// The operation is waiting for any of its dependencies to complete. + /// + BlockedOnWaitAny, + + /// + /// The operation is waiting to receive an event. + /// + BlockedOnReceive, + + /// + /// The operation is waiting to acquire a resource. + /// + BlockedOnResource, + + /// + /// The operation is completed. + /// + Completed + } +} diff --git a/Source/TestingServices/Operations/IAsyncOperation.cs b/Source/TestingServices/Operations/IAsyncOperation.cs new file mode 100644 index 000000000..92f127fae --- /dev/null +++ b/Source/TestingServices/Operations/IAsyncOperation.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.TestingServices.Scheduling +{ + /// + /// Interface of an asynchronous operation that can be controlled during testing. + /// + public interface IAsyncOperation + { + /// + /// Unique id of the source of the operation. + /// + ulong SourceId { get; } + + /// + /// Unique name of the source of the operation. + /// + string SourceName { get; } + + /// + /// The status of the operation. An operation can be scheduled only + /// if it is . + /// + AsyncOperationStatus Status { get; set; } + } +} diff --git a/Source/TestingServices/Operations/MachineOperation.cs b/Source/TestingServices/Operations/MachineOperation.cs new file mode 100644 index 000000000..c41dc3cdd --- /dev/null +++ b/Source/TestingServices/Operations/MachineOperation.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.TestingServices.Scheduling +{ + /// + /// Contains information about a machine operation that can be scheduled. + /// + [DebuggerStepThrough] + internal sealed class MachineOperation : IAsyncOperation + { + /// + /// Stores the unique scheduler id that executes this operation. + /// + internal static readonly AsyncLocal SchedulerId = new AsyncLocal(); + + /// + /// The scheduler executing this operation. + /// + internal readonly OperationScheduler Scheduler; + + /// + /// The machine that owns this operation. + /// + internal readonly AsyncMachine Machine; + + /// + /// Unique id of the source of the operation. + /// + public ulong SourceId => this.Machine.Id.Value; + + /// + /// Unique name of the source of the operation. + /// + public string SourceName => this.Machine.Id.Name; + + /// + /// The status of the operation. An operation can be scheduled only + /// if it is . + /// + public AsyncOperationStatus Status { get; set; } + + /// + /// Set of tasks that this operation is waiting to join. All tasks + /// in the set must complete before this operation can resume. + /// + private readonly HashSet JoinDependencies; + + /// + /// Set of events that this operation is waiting to receive. Receiving any + /// event in the set allows this operation to resume. + /// + private readonly HashSet EventDependencies; + + /// + /// Is the source of the operation active. + /// + internal bool IsActive; + + /// + /// True if the handler of the source of the operation is running, else false. + /// + internal bool IsHandlerRunning; + + /// + /// True if the next awaiter is controlled, else false. + /// + internal bool IsAwaiterControlled; + + /// + /// True if it should skip the next receive scheduling point, + /// because it was already called in the end of the previous + /// event handler. + /// + internal bool SkipNextReceiveSchedulingPoint; + + /// + /// Initializes a new instance of the class. + /// + internal MachineOperation(AsyncMachine machine, OperationScheduler scheduler) + { + this.Scheduler = scheduler; + this.Machine = machine; + this.Status = AsyncOperationStatus.None; + this.JoinDependencies = new HashSet(); + this.EventDependencies = new HashSet(); + this.IsActive = false; + this.IsHandlerRunning = false; + this.IsAwaiterControlled = false; + this.SkipNextReceiveSchedulingPoint = false; + } + + /// + /// Invoked when the operation has been created. + /// + internal void OnCreated() + { + this.Status = AsyncOperationStatus.Enabled; + this.IsActive = false; + this.IsHandlerRunning = false; + } + + internal void OnGetControlledAwaiter() + { + IO.Debug.WriteLine(" Operation '{0}' received a controlled awaiter.", this.SourceId); + this.IsAwaiterControlled = true; + } + + /// + /// Invoked when the operation is waiting to join the specified task. + /// + internal void OnWaitTask(Task task) + { + IO.Debug.WriteLine(" Operation '{0}' is waiting for task '{1}'.", this.SourceId, task.Id); + this.JoinDependencies.Add(task); + this.Status = AsyncOperationStatus.BlockedOnWaitAll; + this.Scheduler.ScheduleNextEnabledOperation(); + this.IsAwaiterControlled = false; + } + + /// + /// Invoked when the operation is waiting to join the specified tasks. + /// + internal void OnWaitTasks(IEnumerable tasks, bool waitAll) + { + foreach (var task in tasks) + { + if (!task.IsCompleted) + { + IO.Debug.WriteLine(" Operation '{0}' is waiting for task '{1}'.", this.SourceId, task.Id); + this.JoinDependencies.Add(task); + } + } + + if (this.JoinDependencies.Count > 0) + { + this.Status = waitAll ? AsyncOperationStatus.BlockedOnWaitAll : AsyncOperationStatus.BlockedOnWaitAny; + this.Scheduler.ScheduleNextEnabledOperation(); + } + + this.IsAwaiterControlled = false; + } + + /// + /// Invoked when the operation is waiting to receive an event of the specified type or types. + /// + internal void OnWaitEvent(IEnumerable eventTypes) + { + this.EventDependencies.UnionWith(eventTypes); + this.Status = AsyncOperationStatus.BlockedOnReceive; + } + + /// + /// Invoked when the operation received an event from the specified operation. + /// + internal void OnReceivedEvent() + { + this.EventDependencies.Clear(); + this.Status = AsyncOperationStatus.Enabled; + } + + /// + /// Invoked when the operation completes. + /// + internal void OnCompleted() + { + this.Status = AsyncOperationStatus.Completed; + this.IsHandlerRunning = false; + this.SkipNextReceiveSchedulingPoint = true; + this.Scheduler.ScheduleNextEnabledOperation(); + } + + /// + /// Tries to enable the operation, if it was not already enabled. + /// + internal void TryEnable() + { + if (this.Status == AsyncOperationStatus.BlockedOnWaitAll) + { + IO.Debug.WriteLine(" Try enable operation '{0}'.", this.SourceId); + if (!this.JoinDependencies.All(task => task.IsCompleted)) + { + IO.Debug.WriteLine(" Operation '{0}' is waiting for all join tasks to complete.", this.SourceId); + return; + } + + this.JoinDependencies.Clear(); + this.Status = AsyncOperationStatus.Enabled; + } + else if (this.Status == AsyncOperationStatus.BlockedOnWaitAny) + { + IO.Debug.WriteLine(" Try enable operation '{0}'.", this.SourceId); + if (!this.JoinDependencies.Any(task => task.IsCompleted)) + { + IO.Debug.WriteLine(" Operation '{0}' is waiting for any join task to complete.", this.SourceId); + return; + } + + this.JoinDependencies.Clear(); + this.Status = AsyncOperationStatus.Enabled; + } + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + public override bool Equals(object obj) + { + if (obj is MachineOperation op) + { + return this.SourceId == op.SourceId; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => (int)this.SourceId; + } +} diff --git a/Source/TestingServices/Properties/InternalsVisibleTo.cs b/Source/TestingServices/Properties/InternalsVisibleTo.cs new file mode 100644 index 000000000..297f0d205 --- /dev/null +++ b/Source/TestingServices/Properties/InternalsVisibleTo.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +// Libraries +[assembly: InternalsVisibleTo("Microsoft.Coyote.SharedObjects,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] + +// Tools +[assembly: InternalsVisibleTo("CoyoteTester,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("CoyoteReplayer,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("CoyoteCoverageReportMerger,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("CoyoteTestLauncher,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] + +// Tests +[assembly: InternalsVisibleTo("Microsoft.Coyote.SharedObjects.Tests,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("Microsoft.Coyote.TestingServices.Tests,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] +[assembly: InternalsVisibleTo("Microsoft.Coyote.TestingServices.Tests,PublicKey=" + + "0024000004800000940000000602000000240000525341310004000001000100d7971281941569" + + "53fd8af100ac5ecaf1d96fab578562b91133663d6ccbf0b313d037a830a20d7af1ce02a6641d71" + + "d7bc9fd67a08d3fa122120a469158da22a652af4508571ac9b16c6a05d2b3b6d7004ac76be85c3" + + "ca3d55f6ae823cd287a2810243f2bd6be5f4ba7b016c80da954371e591b10c97b0938f721c7149" + + "3bc97f9e")] diff --git a/Source/TestingServices/StateCaching/Fingerprint.cs b/Source/TestingServices/StateCaching/Fingerprint.cs new file mode 100644 index 000000000..7c2449fc7 --- /dev/null +++ b/Source/TestingServices/StateCaching/Fingerprint.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.TestingServices.StateCaching +{ + /// + /// Class implementing a program state fingerprint. + /// + internal sealed class Fingerprint + { + /// + /// The hash value of the fingerprint. + /// + private readonly int HashValue; + + /// + /// Initializes a new instance of the class. + /// + internal Fingerprint(int hash) + { + this.HashValue = hash; + } + + /// + /// Returns true if the fingerprint is equal to + /// the given object. + /// + public override bool Equals(object obj) + { + var fingerprint = obj as Fingerprint; + return fingerprint != null && this.HashValue == fingerprint.HashValue; + } + + /// + /// Returns the hashcode of the fingerprint. + /// + public override int GetHashCode() => this.HashValue; + + /// + /// Returns a string representation of the fingerprint. + /// + public override string ToString() => $"fingerprint['{this.HashValue}']"; + } +} diff --git a/Source/TestingServices/StateCaching/MonitorStatus.cs b/Source/TestingServices/StateCaching/MonitorStatus.cs new file mode 100644 index 000000000..adce82356 --- /dev/null +++ b/Source/TestingServices/StateCaching/MonitorStatus.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.TestingServices.StateCaching +{ + /// + /// Monitor status. + /// + internal enum MonitorStatus + { + None = 0, + Hot, + Cold + } +} diff --git a/Source/TestingServices/StateCaching/State.cs b/Source/TestingServices/StateCaching/State.cs new file mode 100644 index 000000000..657a59734 --- /dev/null +++ b/Source/TestingServices/StateCaching/State.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Coyote.IO; + +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.TestingServices.StateCaching +{ + /// + /// Represents a snapshot of the program state. + /// + internal sealed class State + { + /// + /// The fingerprint of the trace step. + /// + internal Fingerprint Fingerprint { get; private set; } + + /// + /// Map from monitors to their liveness status. + /// + internal readonly Dictionary MonitorStatus; + + /// + /// Ids of the enabled machines. Only relevant + /// if this is a scheduling trace step. + /// + internal readonly HashSet EnabledMachineIds; + + /// + /// Initializes a new instance of the class. + /// + internal State(Fingerprint fingerprint, HashSet enabledMachineIds, Dictionary monitorStatus) + { + this.Fingerprint = fingerprint; + this.EnabledMachineIds = enabledMachineIds; + this.MonitorStatus = monitorStatus; + } + + /// + /// Pretty prints the state. + /// + internal void PrettyPrint() + { + Debug.WriteLine($"Fingerprint: {this.Fingerprint}"); + foreach (var id in this.EnabledMachineIds) + { + Debug.WriteLine($" Enabled machine id: {id}"); + } + + foreach (var m in this.MonitorStatus) + { + Debug.WriteLine($" Monitor status: {m.Key.Id} is {m.Value}"); + } + } + } +} diff --git a/Source/TestingServices/StateCaching/StateCache.cs b/Source/TestingServices/StateCaching/StateCache.cs new file mode 100644 index 000000000..2e7977ed9 --- /dev/null +++ b/Source/TestingServices/StateCaching/StateCache.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.TestingServices.Tracing.Schedule; + +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.TestingServices.StateCaching +{ + /// + /// Class implementing a state cache. + /// + internal sealed class StateCache + { + /// + /// The testing runtime. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// Set of fingerprints. + /// + private readonly HashSet Fingerprints; + + /// + /// Initializes a new instance of the class. + /// + internal StateCache(SystematicTestingRuntime runtime) + { + this.Runtime = runtime; + this.Fingerprints = new HashSet(); + } + + /// + /// Captures a snapshot of the program state. + /// + internal bool CaptureState(out State state, out Fingerprint fingerprint, Dictionary> fingerprintIndexMap, + ScheduleStep scheduleStep, List monitors) + { + fingerprint = this.Runtime.GetProgramState(); + var enabledMachineIds = this.Runtime.Scheduler.GetEnabledSchedulableIds(); + state = new State(fingerprint, enabledMachineIds, GetMonitorStatus(monitors)); + + if (Debug.IsEnabled) + { + if (scheduleStep.Type == ScheduleStepType.SchedulingChoice) + { + Debug.WriteLine( + " Captured program state '{0}' at scheduling choice.", fingerprint.GetHashCode()); + } + else if (scheduleStep.Type == ScheduleStepType.NondeterministicChoice && + scheduleStep.BooleanChoice != null) + { + Debug.WriteLine( + " Captured program state '{0}' at nondeterministic choice '{1}'.", + fingerprint.GetHashCode(), scheduleStep.BooleanChoice.Value); + } + else if (scheduleStep.Type == ScheduleStepType.FairNondeterministicChoice && + scheduleStep.BooleanChoice != null) + { + Debug.WriteLine( + " Captured program state '{0}' at fair nondeterministic choice '{1}-{2}'.", + fingerprint.GetHashCode(), scheduleStep.NondetId, scheduleStep.BooleanChoice.Value); + } + else if (scheduleStep.Type == ScheduleStepType.NondeterministicChoice && + scheduleStep.IntegerChoice != null) + { + Debug.WriteLine( + " Captured program state '{0}' at nondeterministic choice '{1}'.", + fingerprint.GetHashCode(), scheduleStep.IntegerChoice.Value); + } + } + + var stateExists = this.Fingerprints.Contains(fingerprint); + this.Fingerprints.Add(fingerprint); + scheduleStep.State = state; + + if (!fingerprintIndexMap.ContainsKey(fingerprint)) + { + var hs = new List { scheduleStep.Index }; + fingerprintIndexMap.Add(fingerprint, hs); + } + else + { + fingerprintIndexMap[fingerprint].Add(scheduleStep.Index); + } + + return stateExists; + } + + /// + /// Returns the monitor status. + /// + private static Dictionary GetMonitorStatus(List monitors) + { + var monitorStatus = new Dictionary(); + foreach (var monitor in monitors) + { + MonitorStatus status = MonitorStatus.None; + if (monitor.IsInHotState()) + { + status = MonitorStatus.Hot; + } + else if (monitor.IsInColdState()) + { + status = MonitorStatus.Cold; + } + + monitorStatus.Add(monitor, status); + } + + return monitorStatus; + } + } +} diff --git a/Source/TestingServices/Statistics/Coverage/ActivityCoverageReporter.cs b/Source/TestingServices/Statistics/Coverage/ActivityCoverageReporter.cs new file mode 100644 index 000000000..359f5d564 --- /dev/null +++ b/Source/TestingServices/Statistics/Coverage/ActivityCoverageReporter.cs @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; + +namespace Microsoft.Coyote.TestingServices.Coverage +{ + /// + /// The Coyote code coverage reporter. + /// + public class ActivityCoverageReporter + { + /// + /// Data structure containing information + /// regarding testing coverage. + /// + private readonly CoverageInfo CoverageInfo; + + /// + /// Initializes a new instance of the class. + /// + public ActivityCoverageReporter(CoverageInfo coverageInfo) + { + this.CoverageInfo = coverageInfo; + } + + /// + /// Emits the visualization graph. + /// + public void EmitVisualizationGraph(string graphFile) + { + using (var writer = new XmlTextWriter(graphFile, Encoding.UTF8)) + { + this.WriteVisualizationGraph(writer); + } + } + + /// + /// Emits the code coverage report. + /// + public void EmitCoverageReport(string coverageFile) + { + using (var writer = new StreamWriter(coverageFile)) + { + this.WriteCoverageText(writer); + } + } + + /// + /// Writes the visualization graph. + /// + private void WriteVisualizationGraph(XmlTextWriter writer) + { + // Starts document. + writer.WriteStartDocument(true); + writer.Formatting = Formatting.Indented; + writer.Indentation = 2; + + // Starts DirectedGraph element. + writer.WriteStartElement("DirectedGraph", @"http://schemas.microsoft.com/vs/2009/dgml"); + + // Starts Nodes element. + writer.WriteStartElement("Nodes"); + + // Iterates machines. + foreach (var machine in this.CoverageInfo.MachinesToStates.Keys) + { + writer.WriteStartElement("Node"); + writer.WriteAttributeString("Id", machine); + writer.WriteAttributeString("Group", "Expanded"); + writer.WriteEndElement(); + } + + // Iterates states. + foreach (var tup in this.CoverageInfo.MachinesToStates) + { + var machine = tup.Key; + foreach (var state in tup.Value) + { + writer.WriteStartElement("Node"); + writer.WriteAttributeString("Id", GetStateId(machine, state)); + writer.WriteAttributeString("Label", state); + writer.WriteEndElement(); + } + } + + // Ends Nodes element. + writer.WriteEndElement(); + + // Starts Links element. + writer.WriteStartElement("Links"); + + // Iterates states. + foreach (var tup in this.CoverageInfo.MachinesToStates) + { + var machine = tup.Key; + foreach (var state in tup.Value) + { + writer.WriteStartElement("Link"); + writer.WriteAttributeString("Source", machine); + writer.WriteAttributeString("Target", GetStateId(machine, state)); + writer.WriteAttributeString("Category", "Contains"); + writer.WriteEndElement(); + } + } + + var parallelEdgeCounter = new Dictionary, int>(); + + // Iterates transitions. + foreach (var transition in this.CoverageInfo.Transitions) + { + var source = GetStateId(transition.MachineOrigin, transition.StateOrigin); + var target = GetStateId(transition.MachineTarget, transition.StateTarget); + var counter = 0; + if (parallelEdgeCounter.ContainsKey(Tuple.Create(source, target))) + { + counter = parallelEdgeCounter[Tuple.Create(source, target)]; + parallelEdgeCounter[Tuple.Create(source, target)] = counter + 1; + } + else + { + parallelEdgeCounter[Tuple.Create(source, target)] = 1; + } + + writer.WriteStartElement("Link"); + writer.WriteAttributeString("Source", source); + writer.WriteAttributeString("Target", target); + writer.WriteAttributeString("Label", transition.EdgeLabel); + if (counter != 0) + { + writer.WriteAttributeString("Index", counter.ToString()); + } + + writer.WriteEndElement(); + } + + // Ends Links element. + writer.WriteEndElement(); + + // Ends DirectedGraph element. + writer.WriteEndElement(); + + // Ends document. + writer.WriteEndDocument(); + } + + /// + /// Writes the visualization text. + /// + internal void WriteCoverageText(TextWriter writer) + { + var machines = new List(this.CoverageInfo.MachinesToStates.Keys); + + var uncoveredEvents = new HashSet>(this.CoverageInfo.RegisteredEvents); + foreach (var transition in this.CoverageInfo.Transitions) + { + if (transition.MachineOrigin == transition.MachineTarget) + { + uncoveredEvents.Remove(Tuple.Create(transition.MachineOrigin, transition.StateOrigin, transition.EdgeLabel)); + } + else + { + uncoveredEvents.Remove(Tuple.Create(transition.MachineTarget, transition.StateTarget, transition.EdgeLabel)); + } + } + + string eventCoverage = this.CoverageInfo.RegisteredEvents.Count == 0 ? "100.0" : + ((this.CoverageInfo.RegisteredEvents.Count - uncoveredEvents.Count) * 100.0 / this.CoverageInfo.RegisteredEvents.Count).ToString("F1"); + writer.WriteLine("Total event coverage: {0}%", eventCoverage); + + // Map from machines to states to registered events. + var machineToStatesToEvents = new Dictionary>>(); + machines.ForEach(m => machineToStatesToEvents.Add(m, new Dictionary>())); + machines.ForEach(m => + { + foreach (var state in this.CoverageInfo.MachinesToStates[m]) + { + machineToStatesToEvents[m].Add(state, new HashSet()); + } + }); + + foreach (var ev in this.CoverageInfo.RegisteredEvents) + { + machineToStatesToEvents[ev.Item1][ev.Item2].Add(ev.Item3); + } + + // Maps from machines to transitions. + var machineToOutgoingTransitions = new Dictionary>(); + var machineToIncomingTransitions = new Dictionary>(); + var machineToIntraTransitions = new Dictionary>(); + + machines.ForEach(m => machineToIncomingTransitions.Add(m, new List())); + machines.ForEach(m => machineToOutgoingTransitions.Add(m, new List())); + machines.ForEach(m => machineToIntraTransitions.Add(m, new List())); + + foreach (var tr in this.CoverageInfo.Transitions) + { + if (tr.MachineOrigin == tr.MachineTarget) + { + machineToIntraTransitions[tr.MachineOrigin].Add(tr); + } + else + { + machineToIncomingTransitions[tr.MachineTarget].Add(tr); + machineToOutgoingTransitions[tr.MachineOrigin].Add(tr); + } + } + + // Per-machine data. + foreach (var machine in machines) + { + writer.WriteLine("Machine: {0}", machine); + writer.WriteLine("***************"); + + var machineUncoveredEvents = new Dictionary>(); + foreach (var state in this.CoverageInfo.MachinesToStates[machine]) + { + machineUncoveredEvents.Add(state, new HashSet(machineToStatesToEvents[machine][state])); + } + + foreach (var tr in machineToIncomingTransitions[machine]) + { + machineUncoveredEvents[tr.StateTarget].Remove(tr.EdgeLabel); + } + + foreach (var tr in machineToIntraTransitions[machine]) + { + machineUncoveredEvents[tr.StateOrigin].Remove(tr.EdgeLabel); + } + + var numTotalEvents = 0; + foreach (var tup in machineToStatesToEvents[machine]) + { + numTotalEvents += tup.Value.Count; + } + + var numUncoveredEvents = 0; + foreach (var tup in machineUncoveredEvents) + { + numUncoveredEvents += tup.Value.Count; + } + + eventCoverage = numTotalEvents == 0 ? "100.0" : ((numTotalEvents - numUncoveredEvents) * 100.0 / numTotalEvents).ToString("F1"); + writer.WriteLine("Machine event coverage: {0}%", eventCoverage); + + // Find uncovered states. + var uncoveredStates = new HashSet(this.CoverageInfo.MachinesToStates[machine]); + foreach (var tr in machineToIntraTransitions[machine]) + { + uncoveredStates.Remove(tr.StateOrigin); + uncoveredStates.Remove(tr.StateTarget); + } + + foreach (var tr in machineToIncomingTransitions[machine]) + { + uncoveredStates.Remove(tr.StateTarget); + } + + foreach (var tr in machineToOutgoingTransitions[machine]) + { + uncoveredStates.Remove(tr.StateOrigin); + } + + // State maps. + var stateToIncomingEvents = new Dictionary>(); + foreach (var tr in machineToIncomingTransitions[machine]) + { + if (!stateToIncomingEvents.ContainsKey(tr.StateTarget)) + { + stateToIncomingEvents.Add(tr.StateTarget, new HashSet()); + } + + stateToIncomingEvents[tr.StateTarget].Add(tr.EdgeLabel); + } + + var stateToOutgoingEvents = new Dictionary>(); + foreach (var tr in machineToOutgoingTransitions[machine]) + { + if (!stateToOutgoingEvents.ContainsKey(tr.StateOrigin)) + { + stateToOutgoingEvents.Add(tr.StateOrigin, new HashSet()); + } + + stateToOutgoingEvents[tr.StateOrigin].Add(tr.EdgeLabel); + } + + var stateToOutgoingStates = new Dictionary>(); + var stateToIncomingStates = new Dictionary>(); + foreach (var tr in machineToIntraTransitions[machine]) + { + if (!stateToOutgoingStates.ContainsKey(tr.StateOrigin)) + { + stateToOutgoingStates.Add(tr.StateOrigin, new HashSet()); + } + + stateToOutgoingStates[tr.StateOrigin].Add(tr.StateTarget); + + if (!stateToIncomingStates.ContainsKey(tr.StateTarget)) + { + stateToIncomingStates.Add(tr.StateTarget, new HashSet()); + } + + stateToIncomingStates[tr.StateTarget].Add(tr.StateOrigin); + } + + // Per-state data. + foreach (var state in this.CoverageInfo.MachinesToStates[machine]) + { + writer.WriteLine(); + writer.WriteLine("\tState: {0}{1}", state, uncoveredStates.Contains(state) ? " is uncovered" : string.Empty); + if (!uncoveredStates.Contains(state)) + { + eventCoverage = machineToStatesToEvents[machine][state].Count == 0 ? "100.0" : + ((machineToStatesToEvents[machine][state].Count - machineUncoveredEvents[state].Count) * 100.0 / + machineToStatesToEvents[machine][state].Count).ToString("F1"); + writer.WriteLine("\t\tState event coverage: {0}%", eventCoverage); + } + + if (stateToIncomingEvents.ContainsKey(state) && stateToIncomingEvents[state].Count > 0) + { + writer.Write("\t\tEvents received: "); + foreach (var e in stateToIncomingEvents[state]) + { + writer.Write("{0} ", e); + } + + writer.WriteLine(); + } + + if (stateToOutgoingEvents.ContainsKey(state) && stateToOutgoingEvents[state].Count > 0) + { + writer.Write("\t\tEvents sent: "); + foreach (var e in stateToOutgoingEvents[state]) + { + writer.Write("{0} ", e); + } + + writer.WriteLine(); + } + + if (machineUncoveredEvents.ContainsKey(state) && machineUncoveredEvents[state].Count > 0) + { + writer.Write("\t\tEvents not covered: "); + foreach (var e in machineUncoveredEvents[state]) + { + writer.Write("{0} ", e); + } + + writer.WriteLine(); + } + + if (stateToIncomingStates.ContainsKey(state) && stateToIncomingStates[state].Count > 0) + { + writer.Write("\t\tPrevious states: "); + foreach (var s in stateToIncomingStates[state]) + { + writer.Write("{0} ", s); + } + + writer.WriteLine(); + } + + if (stateToOutgoingStates.ContainsKey(state) && stateToOutgoingStates[state].Count > 0) + { + writer.Write("\t\tNext states: "); + foreach (var s in stateToOutgoingStates[state]) + { + writer.Write("{0} ", s); + } + + writer.WriteLine(); + } + } + + writer.WriteLine(); + } + } + + private static string GetStateId(string machineName, string stateName) => + string.Format("{0}::{1}", stateName, machineName); + } +} diff --git a/Source/TestingServices/Statistics/Coverage/CoverageInfo.cs b/Source/TestingServices/Statistics/Coverage/CoverageInfo.cs new file mode 100644 index 000000000..6cc78d97d --- /dev/null +++ b/Source/TestingServices/Statistics/Coverage/CoverageInfo.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.TestingServices.Coverage +{ + /// + /// Class for storing coverage-specific data + /// across multiple testing iterations. + /// + [DataContract] + public class CoverageInfo + { + /// + /// Map from machines to states. + /// + [DataMember] + public Dictionary> MachinesToStates { get; private set; } + + /// + /// Set of (machines, states, registered events). + /// + [DataMember] + public HashSet> RegisteredEvents { get; private set; } + + /// + /// Set of machine transitions. + /// + [DataMember] + public HashSet Transitions { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + public CoverageInfo() + { + this.MachinesToStates = new Dictionary>(); + this.RegisteredEvents = new HashSet>(); + this.Transitions = new HashSet(); + } + + /// + /// Checks if the machine type has already been registered for coverage. + /// + public bool IsMachineDeclared(string machineName) => this.MachinesToStates.ContainsKey(machineName); + + /// + /// Adds a new transition. + /// + public void AddTransition(string machineOrigin, string stateOrigin, string edgeLabel, + string machineTarget, string stateTarget) + { + this.AddState(machineOrigin, stateOrigin); + this.AddState(machineTarget, stateTarget); + this.Transitions.Add(new Transition(machineOrigin, stateOrigin, + edgeLabel, machineTarget, stateTarget)); + } + + /// + /// Declares a state. + /// + public void DeclareMachineState(string machine, string state) => this.AddState(machine, state); + + /// + /// Declares a registered state, event pair. + /// + public void DeclareStateEvent(string machine, string state, string eventName) + { + this.AddState(machine, state); + this.RegisteredEvents.Add(Tuple.Create(machine, state, eventName)); + } + + /// + /// Merges the information from the specified + /// coverage info. This is not thread-safe. + /// + public void Merge(CoverageInfo coverageInfo) + { + foreach (var machine in coverageInfo.MachinesToStates) + { + foreach (var state in machine.Value) + { + this.DeclareMachineState(machine.Key, state); + } + } + + foreach (var tup in coverageInfo.RegisteredEvents) + { + this.DeclareStateEvent(tup.Item1, tup.Item2, tup.Item3); + } + + foreach (var transition in coverageInfo.Transitions) + { + this.AddTransition(transition.MachineOrigin, transition.StateOrigin, + transition.EdgeLabel, transition.MachineTarget, transition.StateTarget); + } + } + + /// + /// Adds a new state. + /// + private void AddState(string machineName, string stateName) + { + if (!this.MachinesToStates.ContainsKey(machineName)) + { + this.MachinesToStates.Add(machineName, new HashSet()); + } + + this.MachinesToStates[machineName].Add(stateName); + } + } +} diff --git a/Source/TestingServices/Statistics/Coverage/Transition.cs b/Source/TestingServices/Statistics/Coverage/Transition.cs new file mode 100644 index 000000000..523f346fa --- /dev/null +++ b/Source/TestingServices/Statistics/Coverage/Transition.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.TestingServices.Coverage +{ + /// + /// Specifies a program transition. + /// + [DataContract] + public struct Transition + { + /// + /// The origin machine. + /// + [DataMember] + public readonly string MachineOrigin; + + /// + /// The origin state. + /// + [DataMember] + public readonly string StateOrigin; + + /// + /// The edge label. + /// + [DataMember] + public readonly string EdgeLabel; + + /// + /// The target machine. + /// + [DataMember] + public readonly string MachineTarget; + + /// + /// The target state. + /// + [DataMember] + public readonly string StateTarget; + + /// + /// Initializes a new instance of the struct. + /// + public Transition(string machineOrigin, string stateOrigin, string edgeLabel, + string machineTarget, string stateTarget) + { + this.MachineOrigin = machineOrigin; + this.StateOrigin = stateOrigin; + this.EdgeLabel = edgeLabel; + this.MachineTarget = machineTarget; + this.StateTarget = stateTarget; + } + + /// + /// Pretty print. + /// + public override string ToString() + { + if (this.MachineOrigin == this.MachineTarget) + { + return string.Format("{0}: {1} --{2}--> {3}", this.MachineOrigin, this.StateOrigin, this.EdgeLabel, this.StateTarget); + } + + return string.Format("({0}, {1}) --{2}--> ({3}, {4})", this.MachineOrigin, this.StateOrigin, this.EdgeLabel, this.MachineTarget, this.StateTarget); + } + } +} diff --git a/Source/TestingServices/Statistics/TestReport.cs b/Source/TestingServices/Statistics/TestReport.cs new file mode 100644 index 000000000..7b924a5f4 --- /dev/null +++ b/Source/TestingServices/Statistics/TestReport.cs @@ -0,0 +1,295 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; + +using Microsoft.Coyote.TestingServices.Coverage; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Class implementing the Coyote test report. + /// + [DataContract] + public class TestReport + { + /// + /// Configuration of the program-under-test. + /// + [DataMember] + public Configuration Configuration { get; private set; } + + /// + /// Information regarding code coverage. + /// + [DataMember] + public CoverageInfo CoverageInfo { get; private set; } + + /// + /// Number of explored fair schedules. + /// + [DataMember] + public int NumOfExploredFairSchedules { get; internal set; } + + /// + /// Number of explored unfair schedules. + /// + [DataMember] + public int NumOfExploredUnfairSchedules { get; internal set; } + + /// + /// Number of found bugs. + /// + [DataMember] + public int NumOfFoundBugs { get; internal set; } + + /// + /// Set of unique bug reports. + /// + [DataMember] + public HashSet BugReports { get; internal set; } + + /// + /// The min explored scheduling steps in average, + /// in fair tests. + /// + [DataMember] + public int MinExploredFairSteps { get; internal set; } + + /// + /// The max explored scheduling steps in average, + /// in fair tests. + /// + [DataMember] + public int MaxExploredFairSteps { get; internal set; } + + /// + /// The total explored scheduling steps (across + /// all testing iterations), in fair tests. + /// + [DataMember] + public int TotalExploredFairSteps { get; internal set; } + + /// + /// Number of times the fair max steps bound was hit, + /// in fair tests. + /// + [DataMember] + public int MaxFairStepsHitInFairTests { get; internal set; } + + /// + /// Number of times the unfair max steps bound was hit, + /// in fair tests. + /// + [DataMember] + public int MaxUnfairStepsHitInFairTests { get; internal set; } + + /// + /// Number of times the unfair max steps bound was hit, + /// in unfair tests. + /// + [DataMember] + public int MaxUnfairStepsHitInUnfairTests { get; internal set; } + + /// + /// Set of internal errors. If no internal errors + /// occurred, then this set is empty. + /// + [DataMember] + public HashSet InternalErrors { get; internal set; } + + /// + /// Lock for the test report. + /// + private readonly object Lock; + + /// + /// Initializes a new instance of the class. + /// + public TestReport(Configuration configuration) + { + this.Configuration = configuration; + + this.CoverageInfo = new CoverageInfo(); + + this.NumOfExploredFairSchedules = 0; + this.NumOfExploredUnfairSchedules = 0; + this.NumOfFoundBugs = 0; + this.BugReports = new HashSet(); + + this.MinExploredFairSteps = -1; + this.MaxExploredFairSteps = -1; + this.TotalExploredFairSteps = 0; + this.MaxFairStepsHitInFairTests = 0; + this.MaxUnfairStepsHitInFairTests = 0; + this.MaxUnfairStepsHitInUnfairTests = 0; + + this.InternalErrors = new HashSet(); + + this.Lock = new object(); + } + + /// + /// Merges the information from the specified test report. + /// + /// True if merged successfully. + public bool Merge(TestReport testReport) + { + if (!this.Configuration.AssemblyToBeAnalyzed.Equals(testReport.Configuration.AssemblyToBeAnalyzed)) + { + // Only merge test reports that have the same program name. + return false; + } + + lock (this.Lock) + { + this.CoverageInfo.Merge(testReport.CoverageInfo); + + this.NumOfFoundBugs += testReport.NumOfFoundBugs; + + this.BugReports.UnionWith(testReport.BugReports); + + this.NumOfExploredFairSchedules += testReport.NumOfExploredFairSchedules; + this.NumOfExploredUnfairSchedules += testReport.NumOfExploredUnfairSchedules; + + if (testReport.MinExploredFairSteps >= 0 && + (this.MinExploredFairSteps < 0 || + this.MinExploredFairSteps > testReport.MinExploredFairSteps)) + { + this.MinExploredFairSteps = testReport.MinExploredFairSteps; + } + + if (this.MaxExploredFairSteps < testReport.MaxExploredFairSteps) + { + this.MaxExploredFairSteps = testReport.MaxExploredFairSteps; + } + + this.TotalExploredFairSteps += testReport.TotalExploredFairSteps; + + this.MaxFairStepsHitInFairTests += testReport.MaxFairStepsHitInFairTests; + this.MaxUnfairStepsHitInFairTests += testReport.MaxUnfairStepsHitInFairTests; + this.MaxUnfairStepsHitInUnfairTests += testReport.MaxUnfairStepsHitInUnfairTests; + + this.InternalErrors.UnionWith(testReport.InternalErrors); + } + + return true; + } + + /// + /// Returns the testing report as a string, given a configuration and an optional prefix. + /// + public string GetText(Configuration configuration, string prefix = "") + { + StringBuilder report = new StringBuilder(); + + report.AppendFormat("{0} Testing statistics:", prefix); + + report.AppendLine(); + report.AppendFormat( + "{0} Found {1} bug{2}.", + prefix.Equals("...") ? "....." : prefix, + this.NumOfFoundBugs, + this.NumOfFoundBugs == 1 ? string.Empty : "s"); + + report.AppendLine(); + report.AppendFormat("{0} Scheduling statistics:", prefix); + + int totalExploredSchedules = this.NumOfExploredFairSchedules + + this.NumOfExploredUnfairSchedules; + + report.AppendLine(); + report.AppendFormat( + "{0} Explored {1} schedule{2}: {3} fair and {4} unfair.", + prefix.Equals("...") ? "....." : prefix, + totalExploredSchedules, + totalExploredSchedules == 1 ? string.Empty : "s", + this.NumOfExploredFairSchedules, + this.NumOfExploredUnfairSchedules); + + if (totalExploredSchedules > 0 && + this.NumOfFoundBugs > 0) + { + report.AppendLine(); + report.AppendFormat( + "{0} Found {1:F2}% buggy schedules.", + prefix.Equals("...") ? "....." : prefix, + ((double)this.NumOfFoundBugs / totalExploredSchedules) * 100); + } + + if (this.NumOfExploredFairSchedules > 0) + { + int averageExploredFairSteps = this.TotalExploredFairSteps / + this.NumOfExploredFairSchedules; + + report.AppendLine(); + report.AppendFormat( + "{0} Number of scheduling points in fair terminating schedules: {1} (min), {2} (avg), {3} (max).", + prefix.Equals("...") ? "....." : prefix, + this.MinExploredFairSteps < 0 ? 0 : this.MinExploredFairSteps, + averageExploredFairSteps, + this.MaxExploredFairSteps < 0 ? 0 : this.MaxExploredFairSteps); + + if (configuration.MaxUnfairSchedulingSteps > 0 && + this.MaxUnfairStepsHitInFairTests > 0) + { + report.AppendLine(); + report.AppendFormat( + "{0} Exceeded the max-steps bound of '{1}' in {2:F2}% of the fair schedules.", + prefix.Equals("...") ? "....." : prefix, + configuration.MaxUnfairSchedulingSteps, + ((double)this.MaxUnfairStepsHitInFairTests / this.NumOfExploredFairSchedules) * 100); + } + + if (configuration.UserExplicitlySetMaxFairSchedulingSteps && + configuration.MaxFairSchedulingSteps > 0 && + this.MaxFairStepsHitInFairTests > 0) + { + report.AppendLine(); + report.AppendFormat( + "{0} Hit the max-steps bound of '{1}' in {2:F2}% of the fair schedules.", + prefix.Equals("...") ? "....." : prefix, + configuration.MaxFairSchedulingSteps, + ((double)this.MaxFairStepsHitInFairTests / this.NumOfExploredFairSchedules) * 100); + } + } + + if (this.NumOfExploredUnfairSchedules > 0) + { + if (configuration.MaxUnfairSchedulingSteps > 0 && + this.MaxUnfairStepsHitInUnfairTests > 0) + { + report.AppendLine(); + report.AppendFormat( + "{0} Hit the max-steps bound of '{1}' in {2:F2}% of the unfair schedules.", + prefix.Equals("...") ? "....." : prefix, + configuration.MaxUnfairSchedulingSteps, + ((double)this.MaxUnfairStepsHitInUnfairTests / this.NumOfExploredUnfairSchedules) * 100); + } + } + + return report.ToString(); + } + + /// + /// Clones the test report. + /// + public TestReport Clone() + { + var serializerSettings = new DataContractSerializerSettings(); + serializerSettings.PreserveObjectReferences = true; + var serializer = new DataContractSerializer(typeof(TestReport), serializerSettings); + using (var ms = new System.IO.MemoryStream()) + { + lock (this.Lock) + { + serializer.WriteObject(ms, this); + ms.Position = 0; + return (TestReport)serializer.ReadObject(ms); + } + } + } + } +} diff --git a/Source/TestingServices/SystematicTestingRuntime.cs b/Source/TestingServices/SystematicTestingRuntime.cs new file mode 100644 index 000000000..d2be86c43 --- /dev/null +++ b/Source/TestingServices/SystematicTestingRuntime.cs @@ -0,0 +1,2075 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.TestingServices.Coverage; +using Microsoft.Coyote.TestingServices.Scheduling; +using Microsoft.Coyote.TestingServices.Scheduling.Strategies; +using Microsoft.Coyote.TestingServices.StateCaching; +using Microsoft.Coyote.TestingServices.Threading; +using Microsoft.Coyote.TestingServices.Threading.Tasks; +using Microsoft.Coyote.TestingServices.Timers; +using Microsoft.Coyote.TestingServices.Tracing.Error; +using Microsoft.Coyote.TestingServices.Tracing.Schedule; +using Microsoft.Coyote.Threading; +using Microsoft.Coyote.Threading.Tasks; +using Microsoft.Coyote.Utilities; + +using EventInfo = Microsoft.Coyote.Runtime.EventInfo; +using Monitor = Microsoft.Coyote.Specifications.Monitor; + +namespace Microsoft.Coyote.TestingServices.Runtime +{ + /// + /// Runtime for systematically testing machines by controlling the scheduler. + /// + internal sealed class SystematicTestingRuntime : CoyoteRuntime + { + /// + /// The asynchronous operation scheduler. + /// + internal OperationScheduler Scheduler; + + /// + /// The intercepting task scheduler. + /// + private readonly InterceptingTaskScheduler TaskScheduler; + + /// + /// The bug trace. + /// + internal BugTrace BugTrace; + + /// + /// Data structure containing information + /// regarding testing coverage. + /// + internal CoverageInfo CoverageInfo; + + /// + /// The program state cache. + /// + internal StateCache StateCache; + + /// + /// List of monitors in the program. + /// + private readonly List Monitors; + + /// + /// Map from unique ids to operations. + /// + private readonly ConcurrentDictionary MachineOperations; + + /// + /// Map that stores all unique names and their corresponding machine ids. + /// + internal readonly ConcurrentDictionary NameValueToMachineId; + + /// + /// Set of all machine Ids created by this runtime. + /// + internal HashSet CreatedMachineIds; + + /// + /// The root task id. + /// + internal readonly int? RootTaskId; + + /// + /// Initializes a new instance of the class. + /// + internal SystematicTestingRuntime(Configuration configuration, ISchedulingStrategy strategy) + : base(configuration) + { + this.Monitors = new List(); + this.MachineOperations = new ConcurrentDictionary(); + this.RootTaskId = Task.CurrentId; + this.CreatedMachineIds = new HashSet(); + this.NameValueToMachineId = new ConcurrentDictionary(); + + this.BugTrace = new BugTrace(); + this.StateCache = new StateCache(this); + this.CoverageInfo = new CoverageInfo(); + + var scheduleTrace = new ScheduleTrace(); + if (configuration.EnableLivenessChecking && configuration.EnableCycleDetection) + { + strategy = new CycleDetectionStrategy(configuration, this.StateCache, scheduleTrace, this.Monitors, strategy); + } + else if (configuration.EnableLivenessChecking) + { + strategy = new TemperatureCheckingStrategy(configuration, this.Monitors, strategy); + } + + this.Scheduler = new OperationScheduler(this, strategy, scheduleTrace, this.Configuration); + this.TaskScheduler = new InterceptingTaskScheduler(this.Scheduler.ControlledTaskMap); + + // Set a provider to the runtime in each asynchronous control flow. + Provider = new AsyncLocalRuntimeProvider(this); + } + + /// + /// Creates a machine id that is uniquely tied to the specified unique name. The + /// returned machine id can either be a fresh id (not yet bound to any machine), + /// or it can be bound to a previously created machine. In the second case, this + /// machine id can be directly used to communicate with the corresponding machine. + /// + public override MachineId CreateMachineIdFromName(Type type, string machineName) + { + // It is important that all machine ids use the monotonically incrementing + // value as the id during testing, and not the unique name. + var mid = new MachineId(type, machineName, this); + return this.NameValueToMachineId.GetOrAdd(machineName, mid); + } + + /// + /// Creates a new machine of the specified and with + /// the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(Type type, Event e = null, Guid opGroupId = default) => + this.CreateMachine(null, type, null, e, opGroupId); + + /// + /// Creates a new machine of the specified and name, and + /// with the specified optional . This event can only be + /// used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(Type type, string machineName, Event e = null, Guid opGroupId = default) => + this.CreateMachine(null, type, machineName, e, opGroupId); + + /// + /// Creates a new machine of the specified type, using the specified . + /// This method optionally passes an to the new machine, which can only + /// be used to access its payload, and cannot be handled. + /// + public override MachineId CreateMachine(MachineId mid, Type type, Event e = null, Guid opGroupId = default) + { + this.Assert(mid != null, "Cannot create a machine using a null machine id."); + return this.CreateMachine(mid, type, null, e, opGroupId); + } + + /// + /// Creates a new machine of the specified and with the + /// specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when + /// the machine is initialized and the (if any) is handled. + /// + public override Task CreateMachineAndExecuteAsync(Type type, Event e = null, Guid opGroupId = default) => + this.CreateMachineAndExecuteAsync(null, type, null, e, opGroupId); + + /// + /// Creates a new machine of the specified and name, and with + /// the specified optional . This event can only be used to + /// access its payload, and cannot be handled. The method returns only when the + /// machine is initialized and the (if any) is handled. + /// + public override Task CreateMachineAndExecuteAsync(Type type, string machineName, Event e = null, Guid opGroupId = default) => + this.CreateMachineAndExecuteAsync(null, type, machineName, e, opGroupId); + + /// + /// Creates a new machine of the specified , using the specified + /// unbound machine id, and passes the specified optional . This + /// event can only be used to access its payload, and cannot be handled. The method + /// returns only when the machine is initialized and the (if any) + /// is handled. + /// + public override Task CreateMachineAndExecuteAsync(MachineId mid, Type type, Event e = null, Guid opGroupId = default) + { + this.Assert(mid != null, "Cannot create a machine using a null machine id."); + return this.CreateMachineAndExecuteAsync(mid, type, null, e, opGroupId); + } + + /// + /// Sends an asynchronous to a machine. + /// + public override void SendEvent(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null) + { + this.SendEvent(target, e, this.GetExecutingMachine(), opGroupId, options); + } + + /// + /// Sends an to a machine. Returns immediately if the target machine was already + /// running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + public override Task SendEventAndExecuteAsync(MachineId target, Event e, Guid opGroupId = default, SendOptions options = null) => + this.SendEventAndExecuteAsync(target, e, this.GetExecutingMachine(), opGroupId, options); + + /// + /// Returns the operation group id of the specified machine. Returns + /// if the id is not set, or if the is not associated with this runtime. + /// During testing, the runtime asserts that the specified machine is currently executing. + /// + public override Guid GetCurrentOperationGroupId(MachineId currentMachine) + { + this.Assert(currentMachine == this.GetCurrentMachineId(), + "Trying to access the operation group id of '{0}', which is not the currently executing machine.", + currentMachine); + + Machine machine = this.GetMachineFromId(currentMachine); + return machine is null ? Guid.Empty : machine.OperationGroupId; + } + + /// + /// Runs the specified test method. + /// + internal void RunTest(Delegate testMethod, string testName) + { + this.Assert(testMethod != null, "Unable to run a null test method."); + this.Assert(Task.CurrentId != null, "The test must execute inside a controlled task."); + + testName = string.IsNullOrEmpty(testName) ? string.Empty : $" '{testName}'"; + this.Logger.WriteLine($" Running test{testName}."); + + var machine = new TestExecutionMachine(this, testMethod); + this.DispatchWork(machine, null); + } + + /// + /// Creates a new machine of the specified and name, using the specified + /// unbound machine id, and passes the specified optional . This event + /// can only be used to access its payload, and cannot be handled. + /// + internal MachineId CreateMachine(MachineId mid, Type type, string machineName, Event e = null, Guid opGroupId = default) + { + Machine creator = this.GetExecutingMachine(); + return this.CreateMachine(mid, type, machineName, e, creator, opGroupId); + } + + /// + /// Creates a new of the specified . + /// + internal override MachineId CreateMachine(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId) + { + this.AssertCorrectCallerMachine(creator, "CreateMachine"); + if (creator != null) + { + this.AssertNoPendingTransitionStatement(creator, "create a machine"); + } + + Machine machine = this.CreateMachine(mid, type, machineName, creator, opGroupId); + + this.BugTrace.AddCreateMachineStep(creator, machine.Id, e is null ? null : new EventInfo(e)); + this.RunMachineEventHandler(machine, e, true, null); + + return machine.Id; + } + + /// + /// Creates a new machine of the specified and name, using the specified + /// unbound machine id, and passes the specified optional . This event + /// can only be used to access its payload, and cannot be handled. The method returns only + /// when the machine is initialized and the (if any) is handled. + /// + internal Task CreateMachineAndExecuteAsync(MachineId mid, Type type, string machineName, Event e = null, + Guid opGroupId = default) + { + Machine creator = this.GetExecutingMachine(); + return this.CreateMachineAndExecuteAsync(mid, type, machineName, e, creator, opGroupId); + } + + /// + /// Creates a new of the specified . The + /// method returns only when the machine is initialized and the + /// (if any) is handled. + /// + internal override async Task CreateMachineAndExecuteAsync(MachineId mid, Type type, string machineName, Event e, + Machine creator, Guid opGroupId) + { + this.AssertCorrectCallerMachine(creator, "CreateMachineAndExecute"); + this.Assert(creator != null, + "Only a machine can call 'CreateMachineAndExecute': avoid calling it directly from the 'Test' method; instead call it through a 'harness' machine."); + this.AssertNoPendingTransitionStatement(creator, "create a machine"); + + Machine machine = this.CreateMachine(mid, type, machineName, creator, opGroupId); + + this.BugTrace.AddCreateMachineStep(creator, machine.Id, e is null ? null : new EventInfo(e)); + this.RunMachineEventHandler(machine, e, true, creator); + + // Wait until the machine reaches quiescence. + await creator.Receive(typeof(QuiescentEvent), rev => (rev as QuiescentEvent).MachineId == machine.Id); + + return await Task.FromResult(machine.Id); + } + + /// + /// Creates a new of the specified . + /// + private Machine CreateMachine(MachineId mid, Type type, string machineName, Machine creator, Guid opGroupId) + { + this.Assert(type.IsSubclassOf(typeof(Machine)), "Type '{0}' is not a machine.", type.FullName); + + // Using ulong.MaxValue because a 'Create' operation cannot specify + // the id of its target, because the id does not exist yet. + this.Scheduler.ScheduleNextEnabledOperation(); + ResetProgramCounter(creator); + + if (mid is null) + { + mid = new MachineId(type, machineName, this); + } + else + { + this.Assert(mid.Runtime is null || mid.Runtime == this, "Unbound machine id '{0}' was created by another runtime.", mid.Value); + this.Assert(mid.Type == type.FullName, "Cannot bind machine id '{0}' of type '{1}' to a machine of type '{2}'.", + mid.Value, mid.Type, type.FullName); + mid.Bind(this); + } + + // The operation group id of the machine is set using the following precedence: + // (1) To the specified machine creation operation group id, if it is non-empty. + // (2) To the operation group id of the creator machine, if it exists and is non-empty. + // (3) To the empty operation group id. + if (opGroupId == Guid.Empty && creator != null) + { + opGroupId = creator.OperationGroupId; + } + + Machine machine = MachineFactory.Create(type); + IMachineStateManager stateManager = new SerializedMachineStateManager(this, machine, opGroupId); + IEventQueue eventQueue = new SerializedMachineEventQueue(stateManager, machine); + machine.Initialize(this, mid, stateManager, eventQueue); + machine.InitializeStateInformation(); + + if (this.Configuration.ReportActivityCoverage) + { + this.ReportActivityCoverageOfMachine(machine); + } + + bool result = this.MachineMap.TryAdd(mid, machine); + this.Assert(result, "Machine id '{0}' is used by an existing machine.", mid.Value); + + this.Assert(!this.CreatedMachineIds.Contains(mid), + "Machine id '{0}' of a previously halted machine cannot be reused to create a new machine of type '{1}'", + mid.Value, type.FullName); + this.CreatedMachineIds.Add(mid); + this.MachineOperations.GetOrAdd(mid.Value, new MachineOperation(machine, this.Scheduler)); + + this.LogWriter.OnCreateMachine(mid, creator?.Id); + + return machine; + } + + /// + /// Sends an asynchronous to a machine. + /// + internal override void SendEvent(MachineId target, Event e, AsyncMachine sender, Guid opGroupId, SendOptions options) + { + if (sender != null) + { + this.Assert(target != null, "Machine '{0}' is sending to a null machine.", sender.Id); + this.Assert(e != null, "Machine '{0}' is sending a null event.", sender.Id); + } + else + { + this.Assert(target != null, "Cannot send to a null machine."); + this.Assert(e != null, "Cannot send a null event."); + } + + this.AssertCorrectCallerMachine(sender as Machine, "SendEvent"); + + EnqueueStatus enqueueStatus = this.EnqueueEvent(target, e, sender, opGroupId, options, out Machine targetMachine); + if (enqueueStatus is EnqueueStatus.EventHandlerNotRunning) + { + this.RunMachineEventHandler(targetMachine, null, false, null); + } + } + + /// + /// Sends an asynchronous to a machine. Returns immediately if the target machine was + /// already running. Otherwise blocks until the machine handles the event and reaches quiescense. + /// + internal override async Task SendEventAndExecuteAsync(MachineId target, Event e, AsyncMachine sender, + Guid opGroupId, SendOptions options) + { + this.Assert(sender is Machine, + "Only a machine can call 'SendEventAndExecute': avoid calling it directly from the 'Test' method; instead call it through a 'harness' machine."); + this.Assert(target != null, "Machine '{0}' is sending to a null machine.", sender.Id); + this.Assert(e != null, "Machine '{0}' is sending a null event.", sender.Id); + this.AssertCorrectCallerMachine(sender as Machine, "SendEventAndExecute"); + + EnqueueStatus enqueueStatus = this.EnqueueEvent(target, e, sender, opGroupId, options, out Machine targetMachine); + if (enqueueStatus is EnqueueStatus.EventHandlerNotRunning) + { + this.RunMachineEventHandler(targetMachine, null, false, sender as Machine); + + // Wait until the machine reaches quiescence. + await (sender as Machine).Receive(typeof(QuiescentEvent), rev => (rev as QuiescentEvent).MachineId == target); + return true; + } + + // 'EnqueueStatus.EventHandlerNotRunning' is not returned by 'EnqueueEvent' (even when + // the machine was previously inactive) when the event 'e' requires no action by the + // machine (i.e., it implicitly handles the event). + return enqueueStatus is EnqueueStatus.Dropped || enqueueStatus is EnqueueStatus.NextEventUnavailable; + } + + /// + /// Enqueues an event to the machine with the specified id. + /// + private EnqueueStatus EnqueueEvent(MachineId target, Event e, AsyncMachine sender, Guid opGroupId, + SendOptions options, out Machine targetMachine) + { + this.Assert(this.CreatedMachineIds.Contains(target), + "Cannot send event '{0}' to machine id '{1}' that was never previously bound to a machine of type '{2}'", + e.GetType().FullName, target.Value, target.Type); + + this.Scheduler.ScheduleNextEnabledOperation(); + ResetProgramCounter(sender as Machine); + + // The operation group id of this operation is set using the following precedence: + // (1) To the specified send operation group id, if it is non-empty. + // (2) To the operation group id of the sender machine, if it exists and is non-empty. + // (3) To the empty operation group id. + if (opGroupId == Guid.Empty && sender != null) + { + opGroupId = sender.OperationGroupId; + } + + targetMachine = this.GetMachineFromId(target); + if (targetMachine is null) + { + this.LogWriter.OnSend(target, sender?.Id, (sender as Machine)?.CurrentStateName ?? string.Empty, + e.GetType().FullName, opGroupId, isTargetHalted: true); + this.Assert(options is null || !options.MustHandle, + "A must-handle event '{0}' was sent to the halted machine '{1}'.", e.GetType().FullName, target); + this.TryHandleDroppedEvent(e, target); + return EnqueueStatus.Dropped; + } + + if (sender is Machine) + { + this.AssertNoPendingTransitionStatement(sender as Machine, "send an event"); + } + + EnqueueStatus enqueueStatus = this.EnqueueEvent(targetMachine, e, sender, opGroupId, options); + if (enqueueStatus == EnqueueStatus.Dropped) + { + this.TryHandleDroppedEvent(e, target); + } + + return enqueueStatus; + } + + /// + /// Enqueues an event to the machine with the specified id. + /// + private EnqueueStatus EnqueueEvent(Machine machine, Event e, AsyncMachine sender, Guid opGroupId, SendOptions options) + { + EventOriginInfo originInfo; + if (sender is Machine senderMachine) + { + originInfo = new EventOriginInfo(sender.Id, senderMachine.GetType().FullName, + NameResolver.GetStateNameForLogging(senderMachine.CurrentState)); + } + else + { + // Message comes from the environment. + originInfo = new EventOriginInfo(null, "Env", "Env"); + } + + EventInfo eventInfo = new EventInfo(e, originInfo) + { + MustHandle = options?.MustHandle ?? false, + Assert = options?.Assert ?? -1, + Assume = options?.Assume ?? -1, + SendStep = this.Scheduler.ScheduledSteps + }; + + this.LogWriter.OnSend(machine.Id, sender?.Id, (sender as Machine)?.CurrentStateName ?? string.Empty, + e.GetType().FullName, opGroupId, isTargetHalted: false); + + if (sender != null) + { + var stateName = sender is Machine ? (sender as Machine).CurrentStateName : string.Empty; + this.BugTrace.AddSendEventStep(sender.Id, stateName, eventInfo, machine.Id); + } + + return machine.Enqueue(e, opGroupId, eventInfo); + } + + /// + /// Runs a new asynchronous event handler for the specified machine. + /// This is a fire and forget invocation. + /// + /// Machine that executes this event handler. + /// Event for initializing the machine. + /// If true, then this is a new machine. + /// Caller machine that is blocked for quiscence. + private void RunMachineEventHandler(Machine machine, Event initialEvent, bool isFresh, Machine syncCaller) + { + MachineOperation op = this.GetAsynchronousOperation(machine.Id.Value); + + Task task = new Task(async () => + { + try + { + // Set the runtime in the async control flow runtime provider, allowing + // future retrieval in the same asynchronous call stack. + Provider.SetCurrentRuntime(this); + + OperationScheduler.NotifyOperationStarted(op); + + if (isFresh) + { + await machine.GotoStartState(initialEvent); + } + + await machine.RunEventHandlerAsync(); + + if (syncCaller != null) + { + this.EnqueueEvent(syncCaller, new QuiescentEvent(machine.Id), machine, machine.OperationGroupId, null); + } + + IO.Debug.WriteLine($" Completed event handler of '{machine.Id}' on task '{Task.CurrentId}'."); + op.OnCompleted(); + IO.Debug.WriteLine($" Terminated event handler of '{machine.Id}' on task '{Task.CurrentId}'."); + ResetProgramCounter(machine); + } + catch (Exception ex) + { + Exception innerException = ex; + while (innerException is TargetInvocationException) + { + innerException = innerException.InnerException; + } + + if (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + if (innerException is ExecutionCanceledException) + { + IO.Debug.WriteLine($" ExecutionCanceledException was thrown from machine '{machine.Id}'."); + } + else if (innerException is TaskSchedulerException) + { + IO.Debug.WriteLine($" TaskSchedulerException was thrown from machine '{machine.Id}'."); + } + else if (innerException is ObjectDisposedException) + { + IO.Debug.WriteLine($" ObjectDisposedException was thrown from machine '{machine.Id}' with reason '{ex.Message}'."); + } + else + { + // Reports the unhandled exception. + string message = string.Format(CultureInfo.InvariantCulture, + $"Exception '{ex.GetType()}' was thrown in machine '{machine.Id}', " + + $"'{ex.Source}':\n" + + $" {ex.Message}\n" + + $"The stack trace is:\n{ex.StackTrace}"); + this.Scheduler.NotifyAssertionFailure(message, killTasks: true, cancelExecution: false); + } + } + finally + { + if (machine.IsHalted) + { + this.MachineMap.TryRemove(machine.Id, out AsyncMachine _); + } + } + }); + + op.OnCreated(); + this.Scheduler.NotifyOperationCreated(op, task); + + task.Start(this.TaskScheduler); + this.Scheduler.WaitForOperationToStart(op); + } + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTask(Action action, CancellationToken cancellationToken) + { + this.Assert(action != null, "The task cannot execute a null action."); + var machine = new ActionMachine(this, action); + this.DispatchWork(machine, null); + return new MachineTask(this, machine.AwaiterTask, MachineTaskType.ExplicitTask); + } + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTask(Func function, CancellationToken cancellationToken) + { + this.Assert(function != null, "The task cannot execute a null function."); + var machine = new FuncMachine(this, function); + this.DispatchWork(machine, null); + return new MachineTask(this, machine.AwaiterTask, MachineTaskType.ExplicitTask); + } + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTask(Func function, + CancellationToken cancellationToken) + { + this.Assert(function != null, "The task cannot execute a null function."); + var machine = new FuncMachine(this, function); + this.DispatchWork(machine, null); + return new MachineTask(this, machine.AwaiterTask, MachineTaskType.ExplicitTask); + } + + /// + /// Creates a new to execute the specified asynchronous work. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTask(Func> function, + CancellationToken cancellationToken) + { + this.Assert(function != null, "The task cannot execute a null function."); + var machine = new FuncTaskMachine(this, function); + this.DispatchWork(machine, null); + return new MachineTask(this, machine.AwaiterTask, MachineTaskType.ExplicitTask); + } + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTaskDelay(int millisecondsDelay, CancellationToken cancellationToken) => + this.CreateControlledTaskDelay(TimeSpan.FromMilliseconds(millisecondsDelay), cancellationToken); + + /// + /// Creates a new to execute the specified asynchronous delay. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTaskDelay(TimeSpan delay, CancellationToken cancellationToken) + { + if (delay.TotalMilliseconds == 0) + { + // If the delay is 0, then complete synchronously. + return ControlledTask.CompletedTask; + } + + var machine = new DelayMachine(this); + this.DispatchWork(machine, null); + return new MachineTask(this, machine.AwaiterTask, MachineTaskType.ExplicitTask); + } + + /// + /// Creates a associated with a completion source. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTaskCompletionSource(Task task) + { + this.Scheduler.CheckNoExternalConcurrencyUsed(); + return new MachineTask(this, task, MachineTaskType.CompletionSourceTask); + } + + /// + /// Creates a associated with a completion source. + /// + [DebuggerStepThrough] + internal override ControlledTask CreateControlledTaskCompletionSource(Task task) + { + this.Scheduler.CheckNoExternalConcurrencyUsed(); + return new MachineTask(this, task, MachineTaskType.CompletionSourceTask); + } + + /// + /// Schedules the specified to be executed asynchronously. + /// This is a fire and forget invocation. + /// + [DebuggerStepThrough] + internal void DispatchWork(ControlledTaskMachine machine, Task parentTask) + { + MachineOperation op = new MachineOperation(machine, this.Scheduler); + + this.MachineOperations.GetOrAdd(machine.Id.Value, op); + this.MachineMap.TryAdd(machine.Id, machine); + this.CreatedMachineIds.Add(machine.Id); + + Task task = new Task(async () => + { + try + { + // Set the runtime in the async control flow runtime provider, allowing + // future retrieval in the same asynchronous call stack. + Provider.SetCurrentRuntime(this); + + OperationScheduler.NotifyOperationStarted(op); + if (parentTask != null) + { + op.OnWaitTask(parentTask); + } + + try + { + await machine.ExecuteAsync(); + } + catch (Exception ex) + { + // Reports the unhandled exception. + string message = string.Format(CultureInfo.InvariantCulture, + $"Exception '{ex.GetType()}' was thrown in task {Task.CurrentId}, " + + $"'{ex.Source}':\n" + + $" {ex.Message}\n" + + $"The stack trace is:\n{ex.StackTrace}"); + IO.Debug.WriteLine($" {message}"); + machine.TryCompleteWithException(ex); + } + + IO.Debug.WriteLine($" Completed '{machine.Id}' on task '{Task.CurrentId}'."); + op.OnCompleted(); + IO.Debug.WriteLine($" Terminated '{machine.Id}' on task '{Task.CurrentId}'."); + } + catch (Exception ex) + { + Exception innerException = ex; + while (innerException is TargetInvocationException) + { + innerException = innerException.InnerException; + } + + if (innerException is AggregateException) + { + innerException = innerException.InnerException; + } + + if (innerException is ExecutionCanceledException) + { + IO.Debug.WriteLine($" ExecutionCanceledException was thrown from task '{Task.CurrentId}'."); + } + else if (innerException is TaskSchedulerException) + { + IO.Debug.WriteLine($" TaskSchedulerException was thrown from task '{Task.CurrentId}'."); + } + else + { + // Reports the unhandled exception. + string message = string.Format(CultureInfo.InvariantCulture, + $"Exception '{ex.GetType()}' was thrown in task {Task.CurrentId}, " + + $"'{ex.Source}':\n" + + $" {ex.Message}\n" + + $"The stack trace is:\n{ex.StackTrace}"); + this.Scheduler.NotifyAssertionFailure(message, killTasks: true, cancelExecution: false); + } + } + finally + { + // TODO: properly cleanup controlled tasks. + this.MachineMap.TryRemove(machine.Id, out AsyncMachine _); + } + }); + + IO.Debug.WriteLine($" Machine '{machine.Id}' was created to execute task '{task.Id}'."); + + op.OnCreated(); + this.Scheduler.NotifyOperationCreated(op, task); + + task.Start(); + this.Scheduler.WaitForOperationToStart(op); + this.Scheduler.ScheduleNextEnabledOperation(); + } + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks) => + this.WaitAllTasksAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(params Task[] tasks) => + this.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(IEnumerable tasks) => + this.WaitAllTasksAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(IEnumerable tasks) + { + this.Assert(tasks != null, "Cannot wait for a null array of tasks to complete."); + this.Assert(tasks.Count() > 0, "Cannot wait for zero tasks to complete."); + + AsyncMachine caller = this.GetExecutingMachine(); + if (caller is null) + { + // TODO: throw an error, as a non-controlled task is awaiting? + return new ControlledTask(Task.WhenAll(tasks)); + } + + MachineOperation callerOp = this.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTasks(tasks, waitAll: true); + return ControlledTask.CompletedTask; + } + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(params ControlledTask[] tasks) => + this.WaitAllTasksAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when all tasks + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(params Task[] tasks) => + this.WaitAllTasksAsync(tasks); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(IEnumerable> tasks) => + this.WaitAllTasksAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when all tasks + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAllTasksAsync(IEnumerable> tasks) + { + this.Assert(tasks != null, "Cannot wait for a null array of tasks to complete."); + this.Assert(tasks.Count() > 0, "Cannot wait for zero tasks to complete."); + + AsyncMachine caller = this.GetExecutingMachine(); + if (caller is null) + { + // TODO: throw an error, as a non-controlled task is awaiting? + return new ControlledTask(Task.WhenAll(tasks)); + } + + MachineOperation callerOp = this.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTasks(tasks, waitAll: true); + + int idx = 0; + TResult[] result = new TResult[tasks.Count()]; + foreach (var task in tasks) + { + result[idx] = task.Result; + idx++; + } + + return ControlledTask.FromResult(result); + } + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAnyTaskAsync(params ControlledTask[] tasks) => + this.WaitAnyTaskAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAnyTaskAsync(params Task[] tasks) => + this.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAnyTaskAsync(IEnumerable tasks) => + this.WaitAnyTaskAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask WaitAnyTaskAsync(IEnumerable tasks) + { + this.Assert(tasks != null, "Cannot wait for a null array of tasks to complete."); + this.Assert(tasks.Count() > 0, "Cannot wait for zero tasks to complete."); + + AsyncMachine caller = this.GetExecutingMachine(); + if (caller is null) + { + // TODO: throw an error, as a non-controlled task is awaiting? + return new ControlledTask(Task.WhenAny(tasks)); + } + + MachineOperation callerOp = this.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTasks(tasks, waitAll: false); + + Task result = null; + foreach (var task in tasks) + { + if (task.IsCompleted) + { + result = task; + break; + } + } + + return ControlledTask.FromResult(result); + } + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask> WaitAnyTaskAsync(params ControlledTask[] tasks) => + this.WaitAnyTaskAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when any task + /// in the specified array have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask> WaitAnyTaskAsync(params Task[] tasks) => + this.WaitAnyTaskAsync(tasks); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks) => + this.WaitAnyTaskAsync(tasks.Select(t => t.AwaiterTask)); + + /// + /// Creates a that will complete when any task + /// in the specified enumerable collection have completed. + /// + [DebuggerStepThrough] + internal override ControlledTask> WaitAnyTaskAsync(IEnumerable> tasks) + { + this.Assert(tasks != null, "Cannot wait for a null array of tasks to complete."); + this.Assert(tasks.Count() > 0, "Cannot wait for zero tasks to complete."); + + AsyncMachine caller = this.GetExecutingMachine(); + if (caller is null) + { + // TODO: throw an error, as a non-controlled task is awaiting? + return new ControlledTask>(Task.WhenAny(tasks)); + } + + MachineOperation callerOp = this.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTasks(tasks, waitAll: false); + + Task result = null; + foreach (var task in tasks) + { + if (task.IsCompleted) + { + result = task; + break; + } + } + + return ControlledTask.FromResult(result); + } + + /// + /// Waits for any of the provided objects to complete execution. + /// + [DebuggerStepThrough] + internal override int WaitAnyTask(params ControlledTask[] tasks) => + this.WaitAnyTask(tasks.Select(t => t.AwaiterTask).ToArray()); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds. + /// + [DebuggerStepThrough] + internal override int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout) => + this.WaitAnyTask(tasks.Select(t => t.AwaiterTask).ToArray()); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified number of milliseconds or until a cancellation + /// token is cancelled. + /// + [DebuggerStepThrough] + internal override int WaitAnyTask(ControlledTask[] tasks, int millisecondsTimeout, CancellationToken cancellationToken) => + this.WaitAnyTask(tasks.Select(t => t.AwaiterTask).ToArray()); + + /// + /// Waits for any of the provided objects to complete + /// execution unless the wait is cancelled. + /// + [DebuggerStepThrough] + internal override int WaitAnyTask(ControlledTask[] tasks, CancellationToken cancellationToken) => + this.WaitAnyTask(tasks.Select(t => t.AwaiterTask).ToArray()); + + /// + /// Waits for any of the provided objects to complete + /// execution within a specified time interval. + /// + [DebuggerStepThrough] + internal override int WaitAnyTask(ControlledTask[] tasks, TimeSpan timeout) => + this.WaitAnyTask(tasks.Select(t => t.AwaiterTask).ToArray()); + + /// + /// Waits for any of the specified tasks to complete. + /// + [DebuggerStepThrough] + private int WaitAnyTask(Task[] tasks) + { + this.Assert(tasks != null, "Cannot wait for a null array of tasks to complete."); + this.Assert(tasks.Count() > 0, "Cannot wait for zero tasks to complete."); + + AsyncMachine caller = this.GetExecutingMachine(); + if (caller is null) + { + // TODO: throw an error, as a non-controlled task is awaiting? + return Task.WaitAny(tasks.ToArray()); + } + + MachineOperation callerOp = this.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTasks(tasks, waitAll: false); + + int result = -1; + for (int i = 0; i < tasks.Length; i++) + { + if (tasks[i].IsCompleted) + { + result = i; + break; + } + } + + return result; + } + + /// + /// Creates a controlled awaiter that switches into a target environment. + /// + [DebuggerStepThrough] + internal override ControlledYieldAwaitable.ControlledYieldAwaiter CreateControlledYieldAwaiter() + { + AsyncMachine caller = this.GetExecutingMachine(); + MachineOperation callerOp = this.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnGetControlledAwaiter(); + return new ControlledYieldAwaitable.ControlledYieldAwaiter(this, default); + } + + /// + /// Ends the wait for the completion of the yield operation. + /// + [DebuggerStepThrough] + internal override void OnGetYieldResult(YieldAwaitable.YieldAwaiter awaiter) + { + this.Scheduler.ScheduleNextEnabledOperation(); + awaiter.GetResult(); + } + + /// + /// Sets the action to perform when the yield operation completes. + /// + [DebuggerHidden] + internal override void OnYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter) => + this.DispatchYield(continuation); + + /// + /// Schedules the continuation action that is invoked when the yield operation completes. + /// + [DebuggerHidden] + internal override void OnUnsafeYieldCompleted(Action continuation, YieldAwaitable.YieldAwaiter awaiter) => + this.DispatchYield(continuation); + + /// + /// Dispatches the work. + /// + [DebuggerHidden] + private void DispatchYield(Action continuation) + { + try + { + AsyncMachine caller = this.GetExecutingMachine(); + this.Assert(caller != null, + "Task with id '{0}' that is not controlled by the Coyote runtime invoked yield operation.", + Task.CurrentId.HasValue ? Task.CurrentId.Value.ToString() : ""); + + if (caller is Machine machine) + { + this.Assert((machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler, + "Machine '{0}' is executing a yield operation inside a handler that does not return a 'ControlledTask'.", caller.Id); + } + + IO.Debug.WriteLine(" Machine '{0}' is executing a yield operation.", caller.Id); + this.DispatchWork(new ActionMachine(this, continuation), null); + IO.Debug.WriteLine(" Machine '{0}' is executing a yield operation.", caller.Id); + } + catch (ExecutionCanceledException) + { + IO.Debug.WriteLine($" ExecutionCanceledException was thrown from task '{Task.CurrentId}'."); + } + } + + /// + /// Creates a mutual exclusion lock that is compatible with objects. + /// + internal override ControlledLock CreateControlledLock() + { + var id = (ulong)Interlocked.Increment(ref this.LockIdCounter) - 1; + return new MachineLock(this, id); + } + + /// + /// Creates a new timer that sends a to its owner machine. + /// + internal override IMachineTimer CreateMachineTimer(TimerInfo info, Machine owner) + { + var mid = this.CreateMachineId(typeof(MockMachineTimer)); + this.CreateMachine(mid, typeof(MockMachineTimer), new TimerSetupEvent(info, owner, this.Configuration.TimeoutDelay)); + return this.GetMachineFromId(mid); + } + + /// + /// Tries to create a new monitor of the given type. + /// + internal override void TryCreateMonitor(Type type) + { + if (this.Monitors.Any(m => m.GetType() == type)) + { + // Idempotence: only one monitor per type can exist. + return; + } + + this.Assert(type.IsSubclassOf(typeof(Monitor)), "Type '{0}' is not a subclass of Monitor.", type.FullName); + + MachineId mid = new MachineId(type, null, this); + + Monitor monitor = Activator.CreateInstance(type) as Monitor; + monitor.Initialize(this, mid); + monitor.InitializeStateInformation(); + + this.LogWriter.OnCreateMonitor(type.FullName, monitor.Id); + + this.ReportActivityCoverageOfMonitor(monitor); + this.BugTrace.AddCreateMonitorStep(mid); + + this.Monitors.Add(monitor); + + monitor.GotoStartState(); + } + + /// + /// Invokes the specified monitor with the given event. + /// + internal override void Monitor(Type type, AsyncMachine sender, Event e) + { + this.AssertCorrectCallerMachine(sender as Machine, "Monitor"); + foreach (var m in this.Monitors) + { + if (m.GetType() == type) + { + if (this.Configuration.ReportActivityCoverage) + { + this.ReportActivityCoverageOfMonitorEvent(sender, m, e); + this.ReportActivityCoverageOfMonitorTransition(m, e); + } + + m.MonitorEvent(e); + break; + } + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + [DebuggerHidden] + public override void Assert(bool predicate) + { + if (!predicate) + { + this.Scheduler.NotifyAssertionFailure("Detected an assertion failure."); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + [DebuggerHidden] + public override void Assert(bool predicate, string s, object arg0) + { + if (!predicate) + { + this.Scheduler.NotifyAssertionFailure(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + [DebuggerHidden] + public override void Assert(bool predicate, string s, object arg0, object arg1) + { + if (!predicate) + { + this.Scheduler.NotifyAssertionFailure(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString(), arg1.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + [DebuggerHidden] + public override void Assert(bool predicate, string s, object arg0, object arg1, object arg2) + { + if (!predicate) + { + this.Scheduler.NotifyAssertionFailure(string.Format(CultureInfo.InvariantCulture, s, arg0.ToString(), arg1.ToString(), arg2.ToString())); + } + } + + /// + /// Checks if the assertion holds, and if not, throws an exception. + /// + [DebuggerHidden] + public override void Assert(bool predicate, string s, params object[] args) + { + if (!predicate) + { + this.Scheduler.NotifyAssertionFailure(string.Format(CultureInfo.InvariantCulture, s, args)); + } + } + + /// + /// Asserts that a transition statement (raise, goto or pop) has not + /// already been called. Records that RGP has been called. + /// + [DebuggerHidden] + internal void AssertTransitionStatement(Machine machine) + { + var stateManager = machine.StateManager as SerializedMachineStateManager; + this.Assert(!stateManager.IsInsideOnExit, + "Machine '{0}' has called raise, goto, push or pop inside an OnExit method.", + machine.Id.Name); + this.Assert(!stateManager.IsTransitionStatementCalledInCurrentAction, + "Machine '{0}' has called multiple raise, goto, push or pop in the same action.", + machine.Id.Name); + stateManager.IsTransitionStatementCalledInCurrentAction = true; + } + + /// + /// Asserts that a transition statement (raise, goto or pop) has not already been called. + /// + [DebuggerHidden] + private void AssertNoPendingTransitionStatement(Machine machine, string action) + { + if (!this.Configuration.EnableNoApiCallAfterTransitionStmtAssertion) + { + // The check is disabled. + return; + } + + var stateManager = machine.StateManager as SerializedMachineStateManager; + this.Assert(!stateManager.IsTransitionStatementCalledInCurrentAction, + "Machine '{0}' cannot {1} after calling raise, goto, push or pop in the same action.", + machine.Id.Name, action); + } + + /// + /// Asserts that the machine calling a machine method is also + /// the machine that is currently executing. + /// + [DebuggerHidden] + private void AssertCorrectCallerMachine(Machine callerMachine, string calledAPI) + { + if (callerMachine is null) + { + return; + } + + var executingMachine = this.GetExecutingMachine(); + if (executingMachine is null) + { + return; + } + + this.Assert(executingMachine.Equals(callerMachine), "Machine '{0}' invoked {1} on behalf of machine '{2}'.", + executingMachine.Id, calledAPI, callerMachine.Id); + } + + /// + /// Asserts that the currently executing controlled task is awaiting a controlled awaiter. + /// + [DebuggerHidden] + internal override void AssertAwaitingControlledAwaiter(ref TAwaiter awaiter) + { + this.AssertAwaitingControlledAwaiter(awaiter.GetType()); + } + + /// + /// Asserts that the currently executing controlled task is awaiting a controlled awaiter. + /// + [DebuggerHidden] + internal override void AssertAwaitingUnsafeControlledAwaiter(ref TAwaiter awaiter) + { + this.AssertAwaitingControlledAwaiter(awaiter.GetType()); + } + + [DebuggerHidden] + private void AssertAwaitingControlledAwaiter(Type awaiterType) + { + AsyncMachine caller = this.GetExecutingMachine(); + MachineOperation callerOp = this.GetAsynchronousOperation(caller.Id.Value); + this.Assert(callerOp.IsAwaiterControlled, "Controlled task '{0}' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency (e.g. ControlledTask instead of Task).", Task.CurrentId); + this.Assert(awaiterType.Namespace == typeof(ControlledTask).Namespace, + "Controlled task '{0}' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency (e.g. ControlledTask instead of Task).", Task.CurrentId); + } + + /// + /// Checks that no monitor is in a hot state upon program termination. + /// If the program is still running, then this method returns without + /// performing a check. + /// + [DebuggerHidden] + internal void CheckNoMonitorInHotStateAtTermination() + { + if (!this.Scheduler.HasFullyExploredSchedule) + { + return; + } + + foreach (var monitor in this.Monitors) + { + if (monitor.IsInHotState(out string stateName)) + { + string message = string.Format(CultureInfo.InvariantCulture, + "Monitor '{0}' detected liveness bug in hot state '{1}' at the end of program execution.", + monitor.GetType().FullName, stateName); + this.Scheduler.NotifyAssertionFailure(message, killTasks: false, cancelExecution: false); + } + } + } + + /// + /// Returns a nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal override bool GetNondeterministicBooleanChoice(AsyncMachine caller, int maxValue) + { + caller = caller ?? this.GetExecutingMachine(); + this.AssertCorrectCallerMachine(caller as Machine, "Random"); + if (caller is Machine machine) + { + this.AssertNoPendingTransitionStatement(caller as Machine, "invoke 'Random'"); + (machine.StateManager as SerializedMachineStateManager).ProgramCounter++; + } + + var choice = this.Scheduler.GetNextNondeterministicBooleanChoice(maxValue); + this.LogWriter.OnRandom(caller?.Id, choice); + + var stateName = caller is Machine ? (caller as Machine).CurrentStateName : string.Empty; + this.BugTrace.AddRandomChoiceStep(caller?.Id, stateName, choice); + + return choice; + } + + /// + /// Returns a fair nondeterministic boolean choice, that can be + /// controlled during analysis or testing. + /// + internal override bool GetFairNondeterministicBooleanChoice(AsyncMachine caller, string uniqueId) + { + caller = caller ?? this.GetExecutingMachine(); + this.AssertCorrectCallerMachine(caller as Machine, "FairRandom"); + if (caller is Machine machine) + { + this.AssertNoPendingTransitionStatement(caller as Machine, "invoke 'FairRandom'"); + (machine.StateManager as SerializedMachineStateManager).ProgramCounter++; + } + + var choice = this.Scheduler.GetNextNondeterministicBooleanChoice(2, uniqueId); + this.LogWriter.OnRandom(caller?.Id, choice); + + var stateName = caller is Machine ? (caller as Machine).CurrentStateName : string.Empty; + this.BugTrace.AddRandomChoiceStep(caller?.Id, stateName, choice); + + return choice; + } + + /// + /// Returns a nondeterministic integer, that can be + /// controlled during analysis or testing. + /// + internal override int GetNondeterministicIntegerChoice(AsyncMachine caller, int maxValue) + { + caller = caller ?? this.GetExecutingMachine(); + this.AssertCorrectCallerMachine(caller as Machine, "RandomInteger"); + if (caller is Machine) + { + this.AssertNoPendingTransitionStatement(caller as Machine, "invoke 'RandomInteger'"); + } + + var choice = this.Scheduler.GetNextNondeterministicIntegerChoice(maxValue); + this.LogWriter.OnRandom(caller?.Id, choice); + + var stateName = caller is Machine ? (caller as Machine).CurrentStateName : string.Empty; + this.BugTrace.AddRandomChoiceStep(caller?.Id, stateName, choice); + + return choice; + } + + /// + /// Injects a context switch point that can be systematically explored during testing. + /// + internal override void ExploreContextSwitch() + { + AsyncMachine caller = this.GetExecutingMachine(); + if (caller != null) + { + this.Scheduler.ScheduleNextEnabledOperation(); + } + } + + /// + /// Notifies that a machine entered a state. + /// + internal override void NotifyEnteredState(Machine machine) + { + string machineState = machine.CurrentStateName; + this.BugTrace.AddGotoStateStep(machine.Id, machineState); + + this.LogWriter.OnMachineState(machine.Id, machineState, isEntry: true); + } + + /// + /// Notifies that a monitor entered a state. + /// + internal override void NotifyEnteredState(Monitor monitor) + { + string monitorState = monitor.CurrentStateNameWithTemperature; + this.BugTrace.AddGotoStateStep(monitor.Id, monitorState); + + this.LogWriter.OnMonitorState(monitor.GetType().FullName, monitor.Id, monitorState, true, monitor.GetHotState()); + } + + /// + /// Notifies that a machine exited a state. + /// + internal override void NotifyExitedState(Machine machine) + { + this.LogWriter.OnMachineState(machine.Id, machine.CurrentStateName, isEntry: false); + } + + /// + /// Notifies that a monitor exited a state. + /// + internal override void NotifyExitedState(Monitor monitor) + { + string monitorState = monitor.CurrentStateNameWithTemperature; + this.LogWriter.OnMonitorState(monitor.GetType().FullName, monitor.Id, monitorState, false, monitor.GetHotState()); + } + + /// + /// Notifies that a machine invoked an action. + /// + internal override void NotifyInvokedAction(Machine machine, MethodInfo action, Event receivedEvent) + { + (machine.StateManager as SerializedMachineStateManager).IsTransitionStatementCalledInCurrentAction = false; + if (action.ReturnType == typeof(ControlledTask)) + { + (machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler = true; + } + + string machineState = machine.CurrentStateName; + this.BugTrace.AddInvokeActionStep(machine.Id, machineState, action); + this.LogWriter.OnMachineAction(machine.Id, machineState, action.Name); + } + + /// + /// Notifies that a machine completed an action. + /// + internal override void NotifyCompletedAction(Machine machine, MethodInfo action, Event receivedEvent) + { + (machine.StateManager as SerializedMachineStateManager).IsTransitionStatementCalledInCurrentAction = false; + (machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler = false; + } + + /// + /// Notifies that a machine invoked an action. + /// + internal override void NotifyInvokedOnEntryAction(Machine machine, MethodInfo action, Event receivedEvent) + { + (machine.StateManager as SerializedMachineStateManager).IsTransitionStatementCalledInCurrentAction = false; + if (action.ReturnType == typeof(ControlledTask)) + { + (machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler = true; + } + + string machineState = machine.CurrentStateName; + this.BugTrace.AddInvokeActionStep(machine.Id, machineState, action); + this.LogWriter.OnMachineAction(machine.Id, machineState, action.Name); + } + + /// + /// Notifies that a machine completed invoking an action. + /// + internal override void NotifyCompletedOnEntryAction(Machine machine, MethodInfo action, Event receivedEvent) + { + (machine.StateManager as SerializedMachineStateManager).IsTransitionStatementCalledInCurrentAction = false; + (machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler = false; + } + + /// + /// Notifies that a machine invoked an action. + /// + internal override void NotifyInvokedOnExitAction(Machine machine, MethodInfo action, Event receivedEvent) + { + (machine.StateManager as SerializedMachineStateManager).IsInsideOnExit = true; + (machine.StateManager as SerializedMachineStateManager).IsTransitionStatementCalledInCurrentAction = false; + if (action.ReturnType == typeof(ControlledTask)) + { + (machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler = true; + } + + string machineState = machine.CurrentStateName; + this.BugTrace.AddInvokeActionStep(machine.Id, machineState, action); + this.LogWriter.OnMachineAction(machine.Id, machineState, action.Name); + } + + /// + /// Notifies that a machine completed invoking an action. + /// + internal override void NotifyCompletedOnExitAction(Machine machine, MethodInfo action, Event receivedEvent) + { + (machine.StateManager as SerializedMachineStateManager).IsInsideOnExit = false; + (machine.StateManager as SerializedMachineStateManager).IsTransitionStatementCalledInCurrentAction = false; + (machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler = false; + } + + /// + /// Notifies that a monitor invoked an action. + /// + internal override void NotifyInvokedAction(Monitor monitor, MethodInfo action, Event receivedEvent) + { + string monitorState = monitor.CurrentStateName; + this.BugTrace.AddInvokeActionStep(monitor.Id, monitorState, action); + this.LogWriter.OnMonitorAction(monitor.GetType().FullName, monitor.Id, action.Name, monitorState); + } + + /// + /// Notifies that a machine raised an . + /// + internal override void NotifyRaisedEvent(Machine machine, Event e, EventInfo eventInfo) + { + this.AssertTransitionStatement(machine); + string machineState = machine.CurrentStateName; + this.BugTrace.AddRaiseEventStep(machine.Id, machineState, eventInfo); + this.LogWriter.OnMachineEvent(machine.Id, machineState, eventInfo.EventName); + } + + /// + /// Notifies that a monitor raised an . + /// + internal override void NotifyRaisedEvent(Monitor monitor, Event e, EventInfo eventInfo) + { + string monitorState = monitor.CurrentStateName; + this.BugTrace.AddRaiseEventStep(monitor.Id, monitorState, eventInfo); + this.LogWriter.OnMonitorEvent(monitor.GetType().FullName, monitor.Id, monitor.CurrentStateName, + eventInfo.EventName, isProcessing: false); + } + + /// + /// Notifies that a machine dequeued an . + /// + internal override void NotifyDequeuedEvent(Machine machine, Event e, EventInfo eventInfo) + { + MachineOperation op = this.GetAsynchronousOperation(machine.Id.Value); + + // Skip `Receive` if the last operation exited the previous event handler, + // to avoid scheduling duplicate `Receive` operations. + if (op.SkipNextReceiveSchedulingPoint) + { + op.SkipNextReceiveSchedulingPoint = false; + } + else + { + this.Scheduler.ScheduleNextEnabledOperation(); + ResetProgramCounter(machine); + } + + this.LogWriter.OnDequeue(machine.Id, machine.CurrentStateName, eventInfo.EventName); + this.BugTrace.AddDequeueEventStep(machine.Id, machine.CurrentStateName, eventInfo); + + if (this.Configuration.ReportActivityCoverage) + { + this.ReportActivityCoverageOfReceivedEvent(machine, eventInfo); + this.ReportActivityCoverageOfStateTransition(machine, e); + } + } + + /// + /// Notifies that a machine invoked pop. + /// + internal override void NotifyPop(Machine machine) + { + this.AssertCorrectCallerMachine(machine, "Pop"); + this.AssertTransitionStatement(machine); + + this.LogWriter.OnPop(machine.Id, string.Empty, machine.CurrentStateName); + + if (this.Configuration.ReportActivityCoverage) + { + this.ReportActivityCoverageOfPopTransition(machine, machine.CurrentState, machine.GetStateTypeAtStackIndex(1)); + } + } + + /// + /// Notifies that a machine called Receive. + /// + internal override void NotifyReceiveCalled(Machine machine) + { + this.AssertCorrectCallerMachine(machine, "Receive"); + this.AssertNoPendingTransitionStatement(machine, "invoke 'Receive'"); + } + + /// + /// Notifies that a machine is handling a raised event. + /// + internal override void NotifyHandleRaisedEvent(Machine machine, Event e) + { + if (this.Configuration.ReportActivityCoverage) + { + this.ReportActivityCoverageOfStateTransition(machine, e); + } + } + + /// + /// Notifies that a machine is waiting for the specified task to complete. + /// + internal override void NotifyWaitTask(Machine machine, Task task) + { + this.Assert(task != null, "Machine '{0}' is waiting for a null task to complete.", machine.Id); + this.Assert(task.IsCompleted || task.IsCanceled || task.IsFaulted, + "Machine '{0}' is trying to wait for an uncontrolled task or awaiter to complete. Please make sure to avoid " + + "using concurrency APIs such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers. If you are " + + "using external libraries that are executing concurrently, you will need to mock them during testing.", + Task.CurrentId); + } + + /// + /// Notifies that a is waiting for the specified task to complete. + /// + internal override void NotifyWaitTask(ControlledTaskMachine machine, Task task) + { + this.Assert(task != null, "Controlled task '{0}' is waiting for a null task to complete.", Task.CurrentId); + MachineOperation callerOp = this.GetAsynchronousOperation(machine.Id.Value); + if (!task.IsCompleted) + { + callerOp.OnWaitTask(task); + } + } + + /// + /// Notifies that a machine is waiting to receive an event of one of the specified types. + /// + internal override void NotifyWaitEvent(Machine machine, IEnumerable eventTypes) + { + MachineOperation op = this.GetAsynchronousOperation(machine.Id.Value); + op.OnWaitEvent(eventTypes); + + string eventNames; + var eventWaitTypesArray = eventTypes.ToArray(); + if (eventWaitTypesArray.Length == 1) + { + this.LogWriter.OnWait(machine.Id, machine.CurrentStateName, eventWaitTypesArray[0]); + eventNames = eventWaitTypesArray[0].FullName; + } + else + { + this.LogWriter.OnWait(machine.Id, machine.CurrentStateName, eventWaitTypesArray); + if (eventWaitTypesArray.Length > 0) + { + string[] eventNameArray = new string[eventWaitTypesArray.Length - 1]; + for (int i = 0; i < eventWaitTypesArray.Length - 2; i++) + { + eventNameArray[i] = eventWaitTypesArray[i].FullName; + } + + eventNames = string.Join(", ", eventNameArray) + " or " + eventWaitTypesArray[eventWaitTypesArray.Length - 1].FullName; + } + else + { + eventNames = string.Empty; + } + } + + this.BugTrace.AddWaitToReceiveStep(machine.Id, machine.CurrentStateName, eventNames); + this.Scheduler.ScheduleNextEnabledOperation(); + ResetProgramCounter(machine); + } + + /// + /// Notifies that a machine enqueued an event that it was waiting to receive. + /// + internal override void NotifyReceivedEvent(Machine machine, Event e, EventInfo eventInfo) + { + this.LogWriter.OnReceive(machine.Id, machine.CurrentStateName, e.GetType().FullName, wasBlocked: true); + this.BugTrace.AddReceivedEventStep(machine.Id, machine.CurrentStateName, eventInfo); + + MachineOperation op = this.GetAsynchronousOperation(machine.Id.Value); + op.OnReceivedEvent(); + + if (this.Configuration.ReportActivityCoverage) + { + this.ReportActivityCoverageOfReceivedEvent(machine, eventInfo); + } + } + + /// + /// Notifies that a machine received an event without waiting because the event + /// was already in the inbox when the machine invoked the receive statement. + /// + internal override void NotifyReceivedEventWithoutWaiting(Machine machine, Event e, EventInfo eventInfo) + { + this.LogWriter.OnReceive(machine.Id, machine.CurrentStateName, e.GetType().FullName, wasBlocked: false); + this.Scheduler.ScheduleNextEnabledOperation(); + ResetProgramCounter(machine); + } + + /// + /// Notifies that a machine has halted. + /// + internal override void NotifyHalted(Machine machine) + { + this.BugTrace.AddHaltStep(machine.Id, null); + } + + /// + /// Notifies that the inbox of the specified machine is about to be + /// checked to see if the default event handler should fire. + /// + internal override void NotifyDefaultEventHandlerCheck(Machine machine) + { + this.Scheduler.ScheduleNextEnabledOperation(); + } + + /// + /// Notifies that the default handler of the specified machine has been fired. + /// + internal override void NotifyDefaultHandlerFired(Machine machine) + { + this.Scheduler.ScheduleNextEnabledOperation(); + ResetProgramCounter(machine); + } + + /// + /// Reports coverage for the specified received event. + /// + private void ReportActivityCoverageOfReceivedEvent(Machine machine, EventInfo eventInfo) + { + string originMachine = eventInfo.OriginInfo.SenderMachineName; + string originState = eventInfo.OriginInfo.SenderStateName; + string edgeLabel = eventInfo.EventName; + string destMachine = machine.GetType().FullName; + string destState = NameResolver.GetStateNameForLogging(machine.CurrentState); + + this.CoverageInfo.AddTransition(originMachine, originState, edgeLabel, destMachine, destState); + } + + /// + /// Reports coverage for the specified monitor event. + /// + private void ReportActivityCoverageOfMonitorEvent(AsyncMachine sender, Monitor monitor, Event e) + { + string originMachine = sender is null ? "Env" : sender.GetType().FullName; + string originState = sender is null ? "Env" : + (sender is Machine) ? NameResolver.GetStateNameForLogging((sender as Machine).CurrentState) : "Env"; + + string edgeLabel = e.GetType().FullName; + string destMachine = monitor.GetType().FullName; + string destState = NameResolver.GetStateNameForLogging(monitor.CurrentState); + + this.CoverageInfo.AddTransition(originMachine, originState, edgeLabel, destMachine, destState); + } + + /// + /// Reports coverage for the specified machine. + /// + private void ReportActivityCoverageOfMachine(Machine machine) + { + var machineName = machine.GetType().FullName; + if (this.CoverageInfo.IsMachineDeclared(machineName)) + { + return; + } + + // Fetch states. + var states = machine.GetAllStates(); + foreach (var state in states) + { + this.CoverageInfo.DeclareMachineState(machineName, state); + } + + // Fetch registered events. + var pairs = machine.GetAllStateEventPairs(); + foreach (var tup in pairs) + { + this.CoverageInfo.DeclareStateEvent(machineName, tup.Item1, tup.Item2); + } + } + + /// + /// Reports coverage for the specified monitor. + /// + private void ReportActivityCoverageOfMonitor(Monitor monitor) + { + var monitorName = monitor.GetType().FullName; + + // Fetch states. + var states = monitor.GetAllStates(); + + foreach (var state in states) + { + this.CoverageInfo.DeclareMachineState(monitorName, state); + } + + // Fetch registered events. + var pairs = monitor.GetAllStateEventPairs(); + + foreach (var tup in pairs) + { + this.CoverageInfo.DeclareStateEvent(monitorName, tup.Item1, tup.Item2); + } + } + + /// + /// Reports coverage for the specified state transition. + /// + private void ReportActivityCoverageOfStateTransition(Machine machine, Event e) + { + string originMachine = machine.GetType().FullName; + string originState = NameResolver.GetStateNameForLogging(machine.CurrentState); + string destMachine = machine.GetType().FullName; + + string edgeLabel; + string destState; + if (e is GotoStateEvent gotoStateEvent) + { + edgeLabel = "goto"; + destState = NameResolver.GetStateNameForLogging(gotoStateEvent.State); + } + else if (e is PushStateEvent pushStateEvent) + { + edgeLabel = "push"; + destState = NameResolver.GetStateNameForLogging(pushStateEvent.State); + } + else if (machine.GotoTransitions.ContainsKey(e.GetType())) + { + edgeLabel = e.GetType().FullName; + destState = NameResolver.GetStateNameForLogging( + machine.GotoTransitions[e.GetType()].TargetState); + } + else if (machine.PushTransitions.ContainsKey(e.GetType())) + { + edgeLabel = e.GetType().FullName; + destState = NameResolver.GetStateNameForLogging( + machine.PushTransitions[e.GetType()].TargetState); + } + else + { + return; + } + + this.CoverageInfo.AddTransition(originMachine, originState, edgeLabel, destMachine, destState); + } + + /// + /// Reports coverage for a pop transition. + /// + private void ReportActivityCoverageOfPopTransition(Machine machine, Type fromState, Type toState) + { + string originMachine = machine.GetType().FullName; + string originState = NameResolver.GetStateNameForLogging(fromState); + string destMachine = machine.GetType().FullName; + string edgeLabel = "pop"; + string destState = NameResolver.GetStateNameForLogging(toState); + + this.CoverageInfo.AddTransition(originMachine, originState, edgeLabel, destMachine, destState); + } + + /// + /// Reports coverage for the specified state transition. + /// + private void ReportActivityCoverageOfMonitorTransition(Monitor monitor, Event e) + { + string originMachine = monitor.GetType().FullName; + string originState = NameResolver.GetStateNameForLogging(monitor.CurrentState); + string destMachine = originMachine; + + string edgeLabel; + string destState; + if (e is GotoStateEvent) + { + edgeLabel = "goto"; + destState = NameResolver.GetStateNameForLogging((e as GotoStateEvent).State); + } + else if (monitor.GotoTransitions.ContainsKey(e.GetType())) + { + edgeLabel = e.GetType().FullName; + destState = NameResolver.GetStateNameForLogging( + monitor.GotoTransitions[e.GetType()].TargetState); + } + else + { + return; + } + + this.CoverageInfo.AddTransition(originMachine, originState, edgeLabel, destMachine, destState); + } + + /// + /// Resets the program counter of the specified machine. + /// + private static void ResetProgramCounter(Machine machine) + { + if (machine != null) + { + (machine.StateManager as SerializedMachineStateManager).ProgramCounter = 0; + } + } + + /// + /// Gets the currently executing machine of type , + /// or null if no such machine is currently executing. + /// + [DebuggerStepThrough] + internal TMachine GetExecutingMachine() + where TMachine : AsyncMachine + { + if (Task.CurrentId.HasValue && + this.Scheduler.ControlledTaskMap.TryGetValue(Task.CurrentId.Value, out MachineOperation op) && + op?.Machine is TMachine machine) + { + return machine; + } + + return null; + } + + /// + /// Gets the id of the currently executing machine. + /// + internal MachineId GetCurrentMachineId() => this.GetExecutingMachine()?.Id; + + /// + /// Gets the asynchronous operation associated with the specified id. + /// + [DebuggerStepThrough] + internal MachineOperation GetAsynchronousOperation(ulong id) + { + if (!this.IsRunning) + { + throw new ExecutionCanceledException(); + } + + this.MachineOperations.TryGetValue(id, out MachineOperation op); + return op; + } + + /// + /// Returns the fingerprint of the current program state. + /// + [DebuggerStepThrough] + internal Fingerprint GetProgramState() + { + Fingerprint fingerprint = null; + + unchecked + { + int hash = 19; + + foreach (var machine in this.MachineMap.Values.OrderBy(mi => mi.Id.Value)) + { + if (machine is Machine m) + { + hash = (hash * 31) + m.GetCachedState(); + } + } + + foreach (var monitor in this.Monitors) + { + hash = (hash * 31) + monitor.GetCachedState(); + } + + fingerprint = new Fingerprint(hash); + } + + return fingerprint; + } + + /// + /// Throws an exception + /// containing the specified exception. + /// + [DebuggerStepThrough] + internal override void WrapAndThrowException(Exception exception, string s, params object[] args) + { + string message = string.Format(CultureInfo.InvariantCulture, s, args); + this.Scheduler.NotifyAssertionFailure(message); + } + + /// + /// Waits until all machines have finished execution. + /// + [DebuggerStepThrough] + internal async Task WaitAsync() + { + await this.Scheduler.WaitAsync(); + this.IsRunning = false; + } + + /// + /// Disposes runtime resources. + /// + [DebuggerStepThrough] + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.Monitors.Clear(); + this.MachineMap.Clear(); + this.MachineOperations.Clear(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Source/TestingServices/TestingServices.csproj b/Source/TestingServices/TestingServices.csproj new file mode 100644 index 000000000..9765433a2 --- /dev/null +++ b/Source/TestingServices/TestingServices.csproj @@ -0,0 +1,24 @@ + + + + + The Coyote framework testing services. + Microsoft.Coyote.TestingServices + Microsoft.Coyote.TestingServices + true + coyote;state-machines;testing + ..\..\bin\ + + + netstandard2.0;net46;net47 + + + netstandard2.0 + + + + + + + + \ No newline at end of file diff --git a/Source/TestingServices/Threading/InterceptingTaskScheduler.cs b/Source/TestingServices/Threading/InterceptingTaskScheduler.cs new file mode 100644 index 000000000..65ebf5d8f --- /dev/null +++ b/Source/TestingServices/Threading/InterceptingTaskScheduler.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices.Scheduling; + +namespace Microsoft.Coyote.TestingServices.Threading +{ + /// + /// A task scheduler that intercepts (non-controlled) tasks during testing. + /// This is currently used only by handlers. + /// + /// TODO: figure out if this is still needed. + /// + internal sealed class InterceptingTaskScheduler : TaskScheduler + { + /// + /// Map from ids of tasks that are controlled by the runtime to operations. + /// + private readonly ConcurrentDictionary ControlledTaskMap; + + /// + /// Initializes a new instance of the class. + /// + internal InterceptingTaskScheduler(ConcurrentDictionary controlledTaskMap) + { + this.ControlledTaskMap = controlledTaskMap; + } + + /// + /// Enqueues the given task. + /// + protected override void QueueTask(Task task) + { + if (Task.CurrentId.HasValue && + this.ControlledTaskMap.TryGetValue(Task.CurrentId.Value, out MachineOperation op) && + !this.ControlledTaskMap.ContainsKey(task.Id)) + { + // If the task does not correspond to a machine operation, then associate + // it with the currently executing machine operation and schedule it. + this.ControlledTaskMap.TryAdd(task.Id, op); + IO.Debug.WriteLine($" Operation '{op.SourceId}' is associated with task '{task.Id}'."); + } + + this.Execute(task); + } + + /// + /// Tries to execute the task inline. + /// + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + return false; + } + + /// + /// Returns the wrapped in a machine scheduled tasks. + /// + protected override IEnumerable GetScheduledTasks() + { + throw new InvalidOperationException("The controlled task scheduler does not provide access to the scheduled tasks."); + } + + /// + /// Executes the given scheduled task on the thread pool. + /// + private void Execute(Task task) + { + ThreadPool.UnsafeQueueUserWorkItem( + _ => + { + this.TryExecuteTask(task); + }, null); + } + } +} diff --git a/Source/TestingServices/Threading/MachineLock.cs b/Source/TestingServices/Threading/MachineLock.cs new file mode 100644 index 000000000..d1ad27f06 --- /dev/null +++ b/Source/TestingServices/Threading/MachineLock.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.TestingServices.Scheduling; +using Microsoft.Coyote.Threading; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Threading +{ + /// + /// A that is controlled by the runtime scheduler. + /// + internal sealed class MachineLock : ControlledLock + { + /// + /// The testing runtime controlling this lock. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// Queue of operations awaiting to acquire the lock. + /// + private readonly Queue Awaiters; + + /// + /// Initializes a new instance of the class. + /// + internal MachineLock(SystematicTestingRuntime runtime, ulong id) + : base(id) + { + this.Runtime = runtime; + this.Awaiters = new Queue(); + } + + /// + /// Tries to acquire the lock asynchronously, and returns a task that completes + /// when the lock has been acquired. The returned task contains a releaser that + /// releases the lock when disposed. + /// + public override ControlledTask AcquireAsync() + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + + if (this.IsAcquired) + { + this.Runtime.Logger.WriteLine(" Machine '{0}' is waiting to acquire lock '{1}'.", + caller.Id, this.Id); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + this.Awaiters.Enqueue(callerOp); + callerOp.Status = AsyncOperationStatus.BlockedOnResource; + } + + this.IsAcquired = true; + this.Runtime.Scheduler.ScheduleNextEnabledOperation(); + this.Runtime.Logger.WriteLine(" Machine '{0}' is acquiring lock '{1}'.", caller.Id, this.Id); + + return ControlledTask.FromResult(new Releaser(this)); + } + + /// + /// Releases the lock. + /// + protected override void Release() + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + this.IsAcquired = false; + if (this.Awaiters.Count > 0) + { + MachineOperation awaiterOp = this.Awaiters.Dequeue(); + awaiterOp.Status = AsyncOperationStatus.Enabled; + } + + this.Runtime.Logger.WriteLine(" Machine '{0}' is releasing lock '{1}'.", caller.Id, this.Id); + this.Runtime.Scheduler.ScheduleNextEnabledOperation(); + } + } +} diff --git a/Source/TestingServices/Threading/Tasks/ActionMachine.cs b/Source/TestingServices/Threading/Tasks/ActionMachine.cs new file mode 100644 index 000000000..9dc730d14 --- /dev/null +++ b/Source/TestingServices/Threading/Tasks/ActionMachine.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Threading.Tasks +{ + /// + /// Implements a machine that can execute an asynchronously. + /// + internal sealed class ActionMachine : ControlledTaskMachine + { + /// + /// Work to be executed asynchronously. + /// + private readonly Action Work; + + /// + /// Provides the capability to await for work completion. + /// + private readonly TaskCompletionSource Awaiter; + + /// + /// Task that provides access to the completed work. + /// + internal Task AwaiterTask => this.Awaiter.Task; + + /// + /// The id of the task that provides access to the completed work. + /// + internal override int AwaiterTaskId => this.AwaiterTask.Id; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal ActionMachine(SystematicTestingRuntime runtime, Action work) + : base(runtime) + { + this.Work = work; + this.Awaiter = new TaskCompletionSource(); + } + + /// + /// Executes the work asynchronously. + /// + [DebuggerHidden] + internal override Task ExecuteAsync() + { + IO.Debug.WriteLine($"Machine '{this.Id}' is executing action on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Work(); + IO.Debug.WriteLine($"Machine '{this.Id}' executed action on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Awaiter.SetResult(default); + IO.Debug.WriteLine($"Machine '{this.Id}' completed action on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + return Task.CompletedTask; + } + + /// + /// Tries to complete the machine with the specified exception. + /// + [DebuggerStepThrough] + internal override void TryCompleteWithException(Exception exception) + { + this.Awaiter.SetException(exception); + } + } +} diff --git a/Source/TestingServices/Threading/Tasks/DelayMachine.cs b/Source/TestingServices/Threading/Tasks/DelayMachine.cs new file mode 100644 index 000000000..f946e95d2 --- /dev/null +++ b/Source/TestingServices/Threading/Tasks/DelayMachine.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Threading.Tasks +{ + /// + /// Implements a machine that can execute a delay asynchronously. + /// + internal sealed class DelayMachine : ControlledTaskMachine + { + /// + /// Provides the capability to await for work completion. + /// + private readonly TaskCompletionSource Awaiter; + + /// + /// Task that provides access to the completed work. + /// + internal Task AwaiterTask => this.Awaiter.Task; + + /// + /// The id of the task that provides access to the completed work. + /// + internal override int AwaiterTaskId => this.AwaiterTask.Id; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal DelayMachine(SystematicTestingRuntime runtime) + : base(runtime) + { + this.Awaiter = new TaskCompletionSource(); + } + + /// + /// Executes the work asynchronously. + /// + [DebuggerHidden] + internal override Task ExecuteAsync() + { + IO.Debug.WriteLine($"Machine '{this.Id}' is performing a delay on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Awaiter.SetResult(default); + IO.Debug.WriteLine($"Machine '{this.Id}' completed a delay on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + return Task.CompletedTask; + } + + /// + /// Tries to complete the machine with the specified exception. + /// + [DebuggerStepThrough] + internal override void TryCompleteWithException(Exception exception) + { + this.Awaiter.SetException(exception); + } + } +} diff --git a/Source/TestingServices/Threading/Tasks/FuncMachine.cs b/Source/TestingServices/Threading/Tasks/FuncMachine.cs new file mode 100644 index 000000000..b75907d94 --- /dev/null +++ b/Source/TestingServices/Threading/Tasks/FuncMachine.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Threading.Tasks +{ + /// + /// Implements a machine that can execute a asynchronously. + /// + internal sealed class FuncMachine : ControlledTaskMachine + { + /// + /// Work to be executed asynchronously. + /// + private readonly Func Work; + + /// + /// Provides the capability to await for work completion. + /// + private readonly TaskCompletionSource Awaiter; + + /// + /// Task that provides access to the completed work. + /// + internal Task AwaiterTask => this.Awaiter.Task; + + /// + /// The id of the task that provides access to the completed work. + /// + internal override int AwaiterTaskId => this.AwaiterTask.Id; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal FuncMachine(SystematicTestingRuntime runtime, Func work) + : base(runtime) + { + this.Work = work; + this.Awaiter = new TaskCompletionSource(); + } + + /// + /// Executes the work asynchronously. + /// + [DebuggerHidden] + internal override async Task ExecuteAsync() + { + IO.Debug.WriteLine($"Machine '{this.Id}' is executing function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + ControlledTask task = this.Work(); + this.Runtime.NotifyWaitTask(this, task.AwaiterTask); + await task; + IO.Debug.WriteLine($"Machine '{this.Id}' executed function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Awaiter.SetResult(default); + IO.Debug.WriteLine($"Machine '{this.Id}' completed function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + } + + /// + /// Tries to complete the machine with the specified exception. + /// + [DebuggerStepThrough] + internal override void TryCompleteWithException(Exception exception) + { + this.Awaiter.SetException(exception); + } + } + + /// + /// Implements a machine that can execute a asynchronously. + /// + internal sealed class FuncMachine : ControlledTaskMachine + { + /// + /// Work to be executed asynchronously. + /// + private readonly Func Work; + + /// + /// Provides the capability to await for work completion. + /// + private readonly TaskCompletionSource Awaiter; + + /// + /// Task that provides access to the completed work. + /// + internal Task AwaiterTask => this.Awaiter.Task; + + /// + /// The id of the task that provides access to the completed work. + /// + internal override int AwaiterTaskId => this.AwaiterTask.Id; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal FuncMachine(SystematicTestingRuntime runtime, Func work) + : base(runtime) + { + this.Work = work; + this.Awaiter = new TaskCompletionSource(); + } + + /// + /// Executes the work asynchronously. + /// + [DebuggerHidden] + internal override async Task ExecuteAsync() + { + IO.Debug.WriteLine($"Machine '{this.Id}' is executing function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + + TResult result = this.Work(); + if (this.Work is Func taskFunc) + { + var task = taskFunc(); + this.Runtime.NotifyWaitTask(this, task); + await task; + if (task is TResult resultTask) + { + result = resultTask; + } + } + else + { + result = this.Work(); + } + + IO.Debug.WriteLine($"Machine '{this.Id}' executed function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Awaiter.SetResult(result); + IO.Debug.WriteLine($"Machine '{this.Id}' completed function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + } + + /// + /// Tries to complete the machine with the specified exception. + /// + [DebuggerStepThrough] + internal override void TryCompleteWithException(Exception exception) + { + this.Awaiter.SetException(exception); + } + } + + /// + /// Implements a machine that can execute a asynchronously. + /// + internal sealed class FuncTaskMachine : ControlledTaskMachine + { + /// + /// Work to be executed asynchronously. + /// + private readonly Func> Work; + + /// + /// Provides the capability to await for work completion. + /// + private readonly TaskCompletionSource Awaiter; + + /// + /// Task that provides access to the completed work. + /// + internal Task AwaiterTask => this.Awaiter.Task; + + /// + /// The id of the task that provides access to the completed work. + /// + internal override int AwaiterTaskId => this.AwaiterTask.Id; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal FuncTaskMachine(SystematicTestingRuntime runtime, Func> work) + : base(runtime) + { + this.Work = work; + this.Awaiter = new TaskCompletionSource(); + } + + /// + /// Executes the work asynchronously. + /// + [DebuggerHidden] + internal override async Task ExecuteAsync() + { + IO.Debug.WriteLine($"Machine '{this.Id}' is executing function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + ControlledTask task = this.Work(); + IO.Debug.WriteLine($"Machine '{this.Id}' is getting result on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Runtime.NotifyWaitTask(this, task.AwaiterTask); + TResult result = await task; + IO.Debug.WriteLine($"Machine '{this.Id}' executed function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Awaiter.SetResult(result); + IO.Debug.WriteLine($"Machine '{this.Id}' completed function on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + } + + /// + /// Tries to complete the machine with the specified exception. + /// + [DebuggerStepThrough] + internal override void TryCompleteWithException(Exception exception) + { + this.Awaiter.SetException(exception); + } + } +} diff --git a/Source/TestingServices/Threading/Tasks/MachineTask.cs b/Source/TestingServices/Threading/Tasks/MachineTask.cs new file mode 100644 index 000000000..2b48a5ff3 --- /dev/null +++ b/Source/TestingServices/Threading/Tasks/MachineTask.cs @@ -0,0 +1,319 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.TestingServices.Scheduling; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Threading.Tasks +{ + /// + /// A that is controlled by the runtime scheduler. + /// + internal sealed class MachineTask : ControlledTask + { + /// + /// The testing runtime executing this task. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// The type of the task. + /// + private readonly MachineTaskType Type; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal MachineTask(SystematicTestingRuntime runtime, Task task, MachineTaskType taskType) + : base(task) + { + IO.Debug.WriteLine(" Creating task '{0}' from task '{1}' (option: {2}).", + task.Id, Task.CurrentId, taskType); + this.Runtime = runtime; + this.Type = taskType; + } + + /// + /// Gets an awaiter for this awaitable. + /// + [DebuggerHidden] + public override ControlledTaskAwaiter GetAwaiter() + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnGetControlledAwaiter(); + return new ControlledTaskAwaiter(this, this.AwaiterTask); + } + + /// + /// Ends the wait for the completion of the task. + /// + [DebuggerHidden] + internal override void GetResult(TaskAwaiter awaiter) + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTask(this.AwaiterTask); + awaiter.GetResult(); + } + + /// + /// Sets the action to perform when the task completes. + /// + [DebuggerHidden] + internal override void OnCompleted(Action continuation, TaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [DebuggerHidden] + internal override void UnsafeOnCompleted(Action continuation, TaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Configures an awaiter used to await this task. + /// + /// + /// True to attempt to marshal the continuation back to the original context captured; otherwise, false. + /// + [DebuggerHidden] + public override ConfiguredControlledTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnGetControlledAwaiter(); + return new ConfiguredControlledTaskAwaitable(this, this.AwaiterTask, continueOnCapturedContext); + } + + /// + /// Ends the wait for the completion of the task. + /// + [DebuggerHidden] + internal override void GetResult(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + IO.Debug.WriteLine(" Machine '{0}' is waiting task '{1}' to complete from task '{2}'.", + caller.Id, this.Id, Task.CurrentId); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTask(this.AwaiterTask); + awaiter.GetResult(); + } + + /// + /// Sets the action to perform when the task completes. + /// + [DebuggerHidden] + internal override void OnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [DebuggerHidden] + internal override void UnsafeOnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Dispatches the work. + /// + [DebuggerHidden] + private void DispatchWork(Action continuation) + { + try + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + this.Runtime.Assert(caller != null, + "Task with id '{0}' that is not controlled by the Coyote runtime is executing controlled task '{1}'.", + Task.CurrentId.HasValue ? Task.CurrentId.Value.ToString() : "", this.Id); + + if (caller is Machine machine) + { + this.Runtime.Assert((machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler, + "Machine '{0}' is executing controlled task '{1}' inside a handler that does not return a 'ControlledTask'.", + caller.Id, this.Id); + } + + if (this.Type is MachineTaskType.CompletionSourceTask) + { + IO.Debug.WriteLine(" Machine '{0}' is executing continuation of task '{1}' on task '{2}'.", + caller.Id, this.Id, Task.CurrentId); + continuation(); + IO.Debug.WriteLine(" Machine '{0}' resumed after continuation of task '{1}' on task '{2}'.", + caller.Id, this.Id, Task.CurrentId); + } + else if (this.Type is MachineTaskType.ExplicitTask) + { + IO.Debug.WriteLine(" Machine '{0}' is dispatching continuation of task '{1}'.", caller.Id, this.Id); + this.Runtime.DispatchWork(new ActionMachine(this.Runtime, continuation), this.AwaiterTask); + IO.Debug.WriteLine(" Machine '{0}' dispatched continuation of task '{1}'.", caller.Id, this.Id); + } + } + catch (ExecutionCanceledException) + { + IO.Debug.WriteLine($" ExecutionCanceledException was thrown from task '{Task.CurrentId}'."); + } + } + } + + /// + /// A that is controlled by the runtime scheduler. + /// + internal sealed class MachineTask : ControlledTask + { + /// + /// The testing runtime executing this task. + /// + private readonly SystematicTestingRuntime Runtime; + + /// + /// The type of the task. + /// + private readonly MachineTaskType Type; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal MachineTask(SystematicTestingRuntime runtime, Task task, MachineTaskType taskType) + : base(task) + { + IO.Debug.WriteLine(" Creating task '{0}' with result type '{1}' from task '{2}' (option: {3}).", + task.Id, typeof(TResult), Task.CurrentId, taskType); + this.Runtime = runtime; + this.Type = taskType; + } + + /// + /// Gets an awaiter for this awaitable. + /// + [DebuggerHidden] + public override ControlledTaskAwaiter GetAwaiter() + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnGetControlledAwaiter(); + return new ControlledTaskAwaiter(this, this.AwaiterTask); + } + + /// + /// Ends the wait for the completion of the task. + /// + [DebuggerHidden] + internal override TResult GetResult(TaskAwaiter awaiter) + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTask(this.AwaiterTask); + return awaiter.GetResult(); + } + + /// + /// Sets the action to perform when the task completes. + /// + [DebuggerHidden] + internal override void OnCompleted(Action continuation, TaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [DebuggerHidden] + internal override void UnsafeOnCompleted(Action continuation, TaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Configures an awaiter used to await this task. + /// + /// + /// True to attempt to marshal the continuation back to the original context captured; otherwise, false. + /// + [DebuggerHidden] + public override ConfiguredControlledTaskAwaitable ConfigureAwait(bool continueOnCapturedContext) + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnGetControlledAwaiter(); + return new ConfiguredControlledTaskAwaitable(this, this.AwaiterTask, continueOnCapturedContext); + } + + /// + /// Ends the wait for the completion of the task. + /// + [DebuggerHidden] + internal override TResult GetResult(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + IO.Debug.WriteLine(" Machine '{0}' is waiting task '{1}' with result type '{2}' to complete from task '{3}'.", + caller.Id, this.Id, typeof(TResult), Task.CurrentId); + MachineOperation callerOp = this.Runtime.GetAsynchronousOperation(caller.Id.Value); + callerOp.OnWaitTask(this.AwaiterTask); + return awaiter.GetResult(); + } + + /// + /// Sets the action to perform when the task completes. + /// + [DebuggerHidden] + internal override void OnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Schedules the continuation action that is invoked when the task completes. + /// + [DebuggerHidden] + internal override void UnsafeOnCompleted(Action continuation, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter) => + this.DispatchWork(continuation); + + /// + /// Dispatches the work. + /// + [DebuggerHidden] + private void DispatchWork(Action continuation) + { + try + { + AsyncMachine caller = this.Runtime.GetExecutingMachine(); + this.Runtime.Assert(caller != null, + "Task with id '{0}' that is not controlled by the Coyote runtime is executing controlled task '{1}'.", + Task.CurrentId.HasValue ? Task.CurrentId.Value.ToString() : "", this.Id); + + if (caller is Machine machine) + { + this.Runtime.Assert((machine.StateManager as SerializedMachineStateManager).IsInsideControlledTaskHandler, + "Machine '{0}' is executing controlled task '{1}' inside a handler that does not return a 'ControlledTask'.", + caller.Id, this.Id); + } + + if (this.Type is MachineTaskType.CompletionSourceTask) + { + IO.Debug.WriteLine(" Machine '{0}' is executing continuation of task '{1}' with result type '{2}' on task '{3}'.", + caller.Id, this.Id, typeof(TResult), Task.CurrentId); + continuation(); + IO.Debug.WriteLine(" Machine '{0}' resumed after continuation of task '{1}' with result type '{2}' on task '{3}'.", + caller.Id, this.Id, typeof(TResult), Task.CurrentId); + } + else if (this.Type is MachineTaskType.ExplicitTask) + { + IO.Debug.WriteLine(" Machine '{0}' is dispatching continuation of task '{1}' with result type '{2}'.", + caller.Id, this.Id, typeof(TResult)); + this.Runtime.DispatchWork(new ActionMachine(this.Runtime, continuation), this.AwaiterTask); + IO.Debug.WriteLine(" Machine '{0}' dispatched continuation of task '{1}' with result type '{2}'.", + caller.Id, this.Id, typeof(TResult)); + } + } + catch (ExecutionCanceledException) + { + IO.Debug.WriteLine($" ExecutionCanceledException was thrown from task '{Task.CurrentId}'."); + } + } + } +} diff --git a/Source/TestingServices/Threading/Tasks/MachineTaskType.cs b/Source/TestingServices/Threading/Tasks/MachineTaskType.cs new file mode 100644 index 000000000..d31686975 --- /dev/null +++ b/Source/TestingServices/Threading/Tasks/MachineTaskType.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Threading.Tasks +{ + /// + /// Specifies the type of a . + /// + internal enum MachineTaskType + { + /// + /// Specifies that the task was explicitly created. + /// + ExplicitTask = 0, + + /// + /// Specifies that the task was created by a completion source. + /// + CompletionSourceTask + } +} diff --git a/Source/TestingServices/Threading/Tasks/TestExecutionMachine.cs b/Source/TestingServices/Threading/Tasks/TestExecutionMachine.cs new file mode 100644 index 000000000..e04365214 --- /dev/null +++ b/Source/TestingServices/Threading/Tasks/TestExecutionMachine.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Coyote.TestingServices.Runtime; +using Microsoft.Coyote.Threading.Tasks; + +namespace Microsoft.Coyote.TestingServices.Threading.Tasks +{ + /// + /// Implements a machine that can execute a test asynchronously. + /// + internal sealed class TestExecutionMachine : ControlledTaskMachine + { + /// + /// Test to be executed asynchronously. + /// + private readonly Delegate Test; + + /// + /// Provides the capability to await for work completion. + /// + private readonly TaskCompletionSource Awaiter; + + /// + /// Task that provides access to the completed work. + /// + internal Task AwaiterTask => this.Awaiter.Task; + + /// + /// The id of the task that provides access to the completed work. + /// + internal override int AwaiterTaskId => this.AwaiterTask.Id; + + /// + /// Initializes a new instance of the class. + /// + [DebuggerStepThrough] + internal TestExecutionMachine(SystematicTestingRuntime runtime, Delegate test) + : base(runtime) + { + this.Test = test; + this.Awaiter = new TaskCompletionSource(); + } + + /// + /// Executes the work asynchronously. + /// + [DebuggerHidden] + internal override async Task ExecuteAsync() + { + IO.Debug.WriteLine($"Machine '{this.Id}' is executing test on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + + if (this.Test is Action actionWithRuntime) + { + actionWithRuntime(this.Runtime); + } + else if (this.Test is Action action) + { + action(); + } + else if (this.Test is Func functionWithRuntime) + { + await functionWithRuntime(this.Runtime); + } + else if (this.Test is Func function) + { + await function(); + } + else + { + throw new InvalidOperationException($"Unsupported test delegate of type '{this.Test?.GetType()}'."); + } + + IO.Debug.WriteLine($"Machine '{this.Id}' executed test on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + this.Awaiter.SetResult(default); + IO.Debug.WriteLine($"Machine '{this.Id}' completed test on task '{ControlledTask.CurrentId}' (tcs: {this.Awaiter.Task.Id})"); + } + + /// + /// Tries to complete the machine with the specified exception. + /// + [DebuggerStepThrough] + internal override void TryCompleteWithException(Exception exception) + { + // The entry point of a test should always report + // an unhandled exception as an error. + throw exception; + } + } +} diff --git a/Source/TestingServices/Tracing/Error/BugTrace.cs b/Source/TestingServices/Tracing/Error/BugTrace.cs new file mode 100644 index 000000000..46b4b11a0 --- /dev/null +++ b/Source/TestingServices/Tracing/Error/BugTrace.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.Serialization; +using Microsoft.Coyote.Machines; + +using EventInfo = Microsoft.Coyote.Runtime.EventInfo; + +namespace Microsoft.Coyote.TestingServices.Tracing.Error +{ + /// + /// Class implementing a bug trace. A trace is a series of transitions + /// from some initial state to some end state. + /// + [DataContract] + internal sealed class BugTrace : IEnumerable, IEnumerable + { + /// + /// The steps of the bug trace. + /// + [DataMember] + private readonly List Steps; + + /// + /// The number of steps in the bug trace. + /// + internal int Count + { + get { return this.Steps.Count; } + } + + /// + /// Index for the bug trace. + /// + internal BugTraceStep this[int index] + { + get { return this.Steps[index]; } + set { this.Steps[index] = value; } + } + + /// + /// Initializes a new instance of the class. + /// + internal BugTrace() + { + this.Steps = new List(); + } + + /// + /// Adds a bug trace step. + /// + internal void AddCreateMachineStep(Machine machine, MachineId targetMachine, EventInfo eventInfo) + { + MachineId mid = null; + string machineState = null; + if (machine != null) + { + mid = machine.Id; + machineState = machine.CurrentStateName; + } + + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.CreateMachine, + mid, machineState, eventInfo, null, targetMachine, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddCreateMonitorStep(MachineId monitor) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.CreateMonitor, + null, null, null, null, monitor, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddSendEventStep(MachineId machine, string machineState, + EventInfo eventInfo, MachineId targetMachine) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.SendEvent, + machine, machineState, eventInfo, null, targetMachine, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddDequeueEventStep(MachineId machine, string machineState, EventInfo eventInfo) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.DequeueEvent, + machine, machineState, eventInfo, null, null, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddRaiseEventStep(MachineId machine, string machineState, EventInfo eventInfo) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.RaiseEvent, + machine, machineState, eventInfo, null, null, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddGotoStateStep(MachineId machine, string machineState) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.GotoState, + machine, machineState, null, null, null, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddInvokeActionStep(MachineId machine, string machineState, MethodInfo action) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.InvokeAction, + machine, machineState, null, action, null, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddWaitToReceiveStep(MachineId machine, string machineState, string eventNames) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.WaitToReceive, + machine, machineState, null, null, null, null, null, eventNames); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddReceivedEventStep(MachineId machine, string machineState, EventInfo eventInfo) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.ReceiveEvent, + machine, machineState, eventInfo, null, null, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddRandomChoiceStep(MachineId machine, string machineState, bool choice) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.RandomChoice, + machine, machineState, null, null, null, choice, null, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddRandomChoiceStep(MachineId machine, string machineState, int choice) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.RandomChoice, + machine, machineState, null, null, null, null, choice, null); + this.Push(scheduleStep); + } + + /// + /// Adds a bug trace step. + /// + internal void AddHaltStep(MachineId machine, string machineState) + { + var scheduleStep = BugTraceStep.Create(this.Count, BugTraceStepType.Halt, + machine, machineState, null, null, null, null, null, null); + this.Push(scheduleStep); + } + + /// + /// Returns the latest bug trace step and removes it from the trace. + /// + internal BugTraceStep Pop() + { + if (this.Count > 0) + { + this.Steps[this.Count - 1].Next = null; + } + + var step = this.Steps[this.Count - 1]; + this.Steps.RemoveAt(this.Count - 1); + + return step; + } + + /// + /// Returns the latest bug trace step without removing it. + /// + internal BugTraceStep Peek() + { + BugTraceStep step = null; + + if (this.Steps.Count > 0) + { + step = this.Steps[this.Count - 1]; + } + + return step; + } + + /// + /// Returns an enumerator. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.Steps.GetEnumerator(); + } + + /// + /// Returns an enumerator. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.Steps.GetEnumerator(); + } + + /// + /// Pushes a new step to the trace. + /// + private void Push(BugTraceStep step) + { + if (this.Count > 0) + { + this.Steps[this.Count - 1].Next = step; + step.Previous = this.Steps[this.Count - 1]; + } + + this.Steps.Add(step); + } + } +} diff --git a/Source/TestingServices/Tracing/Error/BugTraceStep.cs b/Source/TestingServices/Tracing/Error/BugTraceStep.cs new file mode 100644 index 000000000..9fd858475 --- /dev/null +++ b/Source/TestingServices/Tracing/Error/BugTraceStep.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; +using System.Runtime.Serialization; +using Microsoft.Coyote.Machines; + +using EventInfo = Microsoft.Coyote.Runtime.EventInfo; + +namespace Microsoft.Coyote.TestingServices.Tracing.Error +{ + /// + /// Class implementing a bug trace step. + /// + [DataContract(IsReference = true)] + internal sealed class BugTraceStep + { + /// + /// The unique index of this bug trace step. + /// + internal int Index; + + /// + /// The type of this bug trace step. + /// + [DataMember] + internal BugTraceStepType Type { get; private set; } + + /// + /// The machine initiating the action. + /// + [DataMember] + internal MachineId Machine; + + /// + /// The machine state. + /// + [DataMember] + internal string MachineState; + + /// + /// Information about the event being sent. + /// + [DataMember] + internal EventInfo EventInfo; + + /// + /// The invoked action. + /// + [DataMember] + internal string InvokedAction; + + /// + /// The target machine. + /// + [DataMember] + internal MachineId TargetMachine; + + /// + /// The taken nondeterministic boolean choice. + /// + [DataMember] + internal bool? RandomBooleanChoice; + + /// + /// The taken nondeterministic integer choice. + /// + [DataMember] + internal int? RandomIntegerChoice; + + /// + /// Extra information that can be used to + /// enhance the trace reported to the user. + /// + [DataMember] + internal string ExtraInfo; + + /// + /// Previous bug trace step. + /// + internal BugTraceStep Previous; + + /// + /// Next bug trace step. + /// + internal BugTraceStep Next; + + /// + /// Creates a bug trace step. + /// + internal static BugTraceStep Create(int index, BugTraceStepType type, MachineId machine, + string machineState, EventInfo eventInfo, MethodInfo action, MachineId targetMachine, + bool? boolChoice, int? intChoice, string extraInfo) + { + var traceStep = new BugTraceStep(); + + traceStep.Index = index; + traceStep.Type = type; + + traceStep.Machine = machine; + traceStep.MachineState = machineState; + + traceStep.EventInfo = eventInfo; + + if (action != null) + { + traceStep.InvokedAction = action.Name; + } + + traceStep.TargetMachine = targetMachine; + traceStep.RandomBooleanChoice = boolChoice; + traceStep.RandomIntegerChoice = intChoice; + traceStep.ExtraInfo = extraInfo; + + traceStep.Previous = null; + traceStep.Next = null; + + return traceStep; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + public override bool Equals(object obj) + { + if (obj is BugTraceStep traceStep) + { + return this.Index == traceStep.Index; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => this.Index.GetHashCode(); + } +} diff --git a/Source/TestingServices/Tracing/Error/BugTraceStepType.cs b/Source/TestingServices/Tracing/Error/BugTraceStepType.cs new file mode 100644 index 000000000..6782fab4e --- /dev/null +++ b/Source/TestingServices/Tracing/Error/BugTraceStepType.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace Microsoft.Coyote.TestingServices.Tracing.Error +{ + /// + /// The bug trace step type. + /// + [DataContract] + internal enum BugTraceStepType + { + [EnumMember(Value = "CreateMachine")] + CreateMachine = 0, + [EnumMember(Value = "CreateMonitor")] + CreateMonitor, + [EnumMember(Value = "SendEvent")] + SendEvent, + [EnumMember(Value = "DequeueEvent")] + DequeueEvent, + [EnumMember(Value = "RaiseEvent")] + RaiseEvent, + [EnumMember(Value = "GotoState")] + GotoState, + [EnumMember(Value = "InvokeAction")] + InvokeAction, + [EnumMember(Value = "WaitToReceive")] + WaitToReceive, + [EnumMember(Value = "ReceiveEvent")] + ReceiveEvent, + [EnumMember(Value = "RandomChoice")] + RandomChoice, + [EnumMember(Value = "Halt")] + Halt + } +} diff --git a/Source/TestingServices/Tracing/Schedules/ScheduleStep.cs b/Source/TestingServices/Tracing/Schedules/ScheduleStep.cs new file mode 100644 index 000000000..ab589ed53 --- /dev/null +++ b/Source/TestingServices/Tracing/Schedules/ScheduleStep.cs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.TestingServices.StateCaching; + +namespace Microsoft.Coyote.TestingServices.Tracing.Schedule +{ + /// + /// Class implementing a program schedule step. + /// + internal sealed class ScheduleStep + { + /// + /// The unique index of this schedule step. + /// + internal int Index; + + /// + /// The type of this schedule step. + /// + internal ScheduleStepType Type { get; private set; } + + /// + /// The id of the scheduled operation. Only relevant if this is + /// a regular schedule step. + /// + internal ulong ScheduledOperationId; + + /// + /// The non-deterministic choice id. Only relevant if + /// this is a choice schedule step. + /// + internal string NondetId; + + /// + /// The non-deterministic boolean choice value. Only relevant if + /// this is a choice schedule step. + /// + internal bool? BooleanChoice; + + /// + /// The non-deterministic integer choice value. Only relevant if + /// this is a choice schedule step. + /// + internal int? IntegerChoice; + + /// + /// Previous schedule step. + /// + internal ScheduleStep Previous; + + /// + /// Next schedule step. + /// + internal ScheduleStep Next; + + /// + /// Snapshot of the program state in this schedule step. + /// + internal State State; + + /// + /// Creates a schedule step. + /// + internal static ScheduleStep CreateSchedulingChoice(int index, ulong scheduledMachineId) + { + var scheduleStep = new ScheduleStep(); + + scheduleStep.Index = index; + scheduleStep.Type = ScheduleStepType.SchedulingChoice; + + scheduleStep.ScheduledOperationId = scheduledMachineId; + + scheduleStep.BooleanChoice = null; + scheduleStep.IntegerChoice = null; + + scheduleStep.Previous = null; + scheduleStep.Next = null; + + return scheduleStep; + } + + /// + /// Creates a nondeterministic boolean choice schedule step. + /// + internal static ScheduleStep CreateNondeterministicBooleanChoice(int index, bool choice) + { + var scheduleStep = new ScheduleStep(); + + scheduleStep.Index = index; + scheduleStep.Type = ScheduleStepType.NondeterministicChoice; + + scheduleStep.BooleanChoice = choice; + scheduleStep.IntegerChoice = null; + + scheduleStep.Previous = null; + scheduleStep.Next = null; + + return scheduleStep; + } + + /// + /// Creates a fair nondeterministic boolean choice schedule step. + /// + internal static ScheduleStep CreateFairNondeterministicBooleanChoice( + int index, string uniqueId, bool choice) + { + var scheduleStep = new ScheduleStep(); + + scheduleStep.Index = index; + scheduleStep.Type = ScheduleStepType.FairNondeterministicChoice; + + scheduleStep.NondetId = uniqueId; + scheduleStep.BooleanChoice = choice; + scheduleStep.IntegerChoice = null; + + scheduleStep.Previous = null; + scheduleStep.Next = null; + + return scheduleStep; + } + + /// + /// Creates a nondeterministic integer choice schedule step. + /// + internal static ScheduleStep CreateNondeterministicIntegerChoice(int index, int choice) + { + var scheduleStep = new ScheduleStep(); + + scheduleStep.Index = index; + scheduleStep.Type = ScheduleStepType.NondeterministicChoice; + + scheduleStep.BooleanChoice = null; + scheduleStep.IntegerChoice = choice; + + scheduleStep.Previous = null; + scheduleStep.Next = null; + + return scheduleStep; + } + + /// + /// Determines whether the specified System.Object is equal + /// to the current System.Object. + /// + public override bool Equals(object obj) + { + if (obj is ScheduleStep step) + { + return this.Index == step.Index; + } + + return false; + } + + /// + /// Returns the hash code for this instance. + /// + public override int GetHashCode() => this.Index.GetHashCode(); + } +} diff --git a/Source/TestingServices/Tracing/Schedules/ScheduleStepType.cs b/Source/TestingServices/Tracing/Schedules/ScheduleStepType.cs new file mode 100644 index 000000000..cf3609cef --- /dev/null +++ b/Source/TestingServices/Tracing/Schedules/ScheduleStepType.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.TestingServices.Tracing.Schedule +{ + /// + /// The schedule step type. + /// + internal enum ScheduleStepType + { + SchedulingChoice = 0, + NondeterministicChoice, + FairNondeterministicChoice + } +} diff --git a/Source/TestingServices/Tracing/Schedules/ScheduleTrace.cs b/Source/TestingServices/Tracing/Schedules/ScheduleTrace.cs new file mode 100644 index 000000000..b29ec7037 --- /dev/null +++ b/Source/TestingServices/Tracing/Schedules/ScheduleTrace.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Coyote.TestingServices.Tracing.Schedule +{ + /// + /// Class implementing a program schedule trace. + /// A trace is a series of transitions from some + /// initial state to some end state. + /// + internal sealed class ScheduleTrace : IEnumerable, IEnumerable + { + /// + /// The steps of the schedule trace. + /// + private readonly List Steps; + + /// + /// The number of steps in the schedule trace. + /// + internal int Count + { + get { return this.Steps.Count; } + } + + /// + /// Index for the schedule trace. + /// + internal ScheduleStep this[int index] + { + get { return this.Steps[index]; } + set { this.Steps[index] = value; } + } + + /// + /// Initializes a new instance of the class. + /// + internal ScheduleTrace() + { + this.Steps = new List(); + } + + /// + /// Initializes a new instance of the class. + /// + internal ScheduleTrace(string[] traceDump) + { + this.Steps = new List(); + + foreach (var step in traceDump) + { + int intChoice; + if (step.StartsWith("--") || step.Length == 0) + { + continue; + } + else if (step.Equals("True")) + { + this.AddNondeterministicBooleanChoice(true); + } + else if (step.Equals("False")) + { + this.AddNondeterministicBooleanChoice(false); + } + else if (int.TryParse(step, out intChoice)) + { + this.AddNondeterministicIntegerChoice(intChoice); + } + else + { + string id = step.TrimStart('(').TrimEnd(')'); + this.AddSchedulingChoice(ulong.Parse(id)); + } + } + } + + /// + /// Adds a scheduling choice. + /// + internal void AddSchedulingChoice(ulong scheduledMachineId) + { + var scheduleStep = ScheduleStep.CreateSchedulingChoice(this.Count, scheduledMachineId); + this.Push(scheduleStep); + } + + /// + /// Adds a nondeterministic boolean choice. + /// + internal void AddNondeterministicBooleanChoice(bool choice) + { + var scheduleStep = ScheduleStep.CreateNondeterministicBooleanChoice( + this.Count, choice); + this.Push(scheduleStep); + } + + /// + /// Adds a fair nondeterministic boolean choice. + /// + internal void AddFairNondeterministicBooleanChoice(string uniqueId, bool choice) + { + var scheduleStep = ScheduleStep.CreateFairNondeterministicBooleanChoice( + this.Count, uniqueId, choice); + this.Push(scheduleStep); + } + + /// + /// Adds a nondeterministic integer choice. + /// + internal void AddNondeterministicIntegerChoice(int choice) + { + var scheduleStep = ScheduleStep.CreateNondeterministicIntegerChoice( + this.Count, choice); + this.Push(scheduleStep); + } + + /// + /// Returns the latest schedule step and removes + /// it from the trace. + /// + internal ScheduleStep Pop() + { + if (this.Count > 0) + { + this.Steps[this.Count - 1].Next = null; + } + + var step = this.Steps[this.Count - 1]; + this.Steps.RemoveAt(this.Count - 1); + + return step; + } + + /// + /// Returns the latest schedule step without removing it. + /// + internal ScheduleStep Peek() + { + ScheduleStep step = null; + + if (this.Steps.Count > 0) + { + step = this.Steps[this.Count - 1]; + } + + return step; + } + + /// + /// Returns an enumerator. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.Steps.GetEnumerator(); + } + + /// + /// Returns an enumerator. + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.Steps.GetEnumerator(); + } + + /// + /// Pushes a new step to the trace. + /// + private void Push(ScheduleStep step) + { + if (this.Count > 0) + { + this.Steps[this.Count - 1].Next = step; + step.Previous = this.Steps[this.Count - 1]; + } + + this.Steps.Add(step); + } + } +} diff --git a/Tests/Core.Tests/BaseTest.cs b/Tests/Core.Tests/BaseTest.cs new file mode 100644 index 000000000..e976ee253 --- /dev/null +++ b/Tests/Core.Tests/BaseTest.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Tests.Common; +using Xunit; +using Xunit.Abstractions; + +using Common = Microsoft.Coyote.Tests.Common; + +namespace Microsoft.Coyote.Core.Tests +{ + public abstract class BaseTest : Common.BaseTest + { + public BaseTest(ITestOutputHelper output) + : base(output) + { + } + + protected void Run(Action test, Configuration configuration = null) + { + configuration = configuration ?? GetConfiguration(); + + ILogger logger; + if (configuration.IsVerbose) + { + logger = new TestOutputLogger(this.TestOutput, true); + } + else + { + logger = new NulLogger(); + } + + try + { + var runtime = MachineRuntimeFactory.Create(configuration); + runtime.SetLogger(logger); + test(runtime); + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + } + + protected async Task RunAsync(Func test, Configuration configuration = null) + { + configuration = configuration ?? GetConfiguration(); + + ILogger logger; + if (configuration.IsVerbose) + { + logger = new TestOutputLogger(this.TestOutput, true); + } + else + { + logger = new NulLogger(); + } + + try + { + var runtime = MachineRuntimeFactory.Create(configuration); + runtime.SetLogger(logger); + await test(runtime); + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + } + + protected static async Task WaitAsync(Task task, int millisecondsDelay = 5000) + { + await Task.WhenAny(task, Task.Delay(millisecondsDelay)); + Assert.True(task.IsCompleted); + } + + protected static async Task GetResultAsync(Task task, int millisecondsDelay = 5000) + { + await Task.WhenAny(task, Task.Delay(millisecondsDelay)); + Assert.True(task.IsCompleted); + return await task; + } + + protected static Configuration GetConfiguration() + { + return Configuration.Create(); + } + } +} diff --git a/Tests/Core.Tests/Core.Tests.csproj b/Tests/Core.Tests/Core.Tests.csproj new file mode 100644 index 000000000..b9c2b4ac6 --- /dev/null +++ b/Tests/Core.Tests/Core.Tests.csproj @@ -0,0 +1,34 @@ + + + + + Tests for the Coyote core library. + Microsoft.Coyote.Core.Tests + Microsoft.Coyote.Core.Tests + ..\bin\ + + + netcoreapp2.1;net46;net47 + + + netcoreapp2.1 + + + + + + + + + + + + + + PreserveNewest + + + + + + \ No newline at end of file diff --git a/Tests/Core.Tests/EventQueues/EventQueueStressTest.cs b/Tests/Core.Tests/EventQueues/EventQueueStressTest.cs new file mode 100644 index 000000000..2e05e1f4b --- /dev/null +++ b/Tests/Core.Tests/EventQueues/EventQueueStressTest.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Tests.Common; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class EventQueueStressTest : BaseTest + { + public EventQueueStressTest(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + [Fact(Timeout = 5000)] + public async Task TestEnqueueDequeueEvents() + { + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => { }); + + var queue = new EventQueue(machineStateManager); + int numMessages = 10000; + + var enqueueTask = Task.Run(() => + { + for (int i = 0; i < numMessages; i++) + { + queue.Enqueue(new E1(), Guid.Empty, null); + } + }); + + var dequeueTask = Task.Run(() => + { + for (int i = 0; i < numMessages; i++) + { + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + if (deqeueStatus is DequeueStatus.Success) + { + Assert.IsType(e); + } + } + }); + + await Task.WhenAny(Task.WhenAll(enqueueTask, dequeueTask), Task.Delay(3000)); + Assert.True(enqueueTask.IsCompleted); + Assert.True(dequeueTask.IsCompleted); + } + + [Fact(Timeout = 5000)] + public async Task TestEnqueueReceiveEvents() + { + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => { }); + + var queue = new EventQueue(machineStateManager); + int numMessages = 10000; + + var enqueueTask = Task.Run(() => + { + for (int i = 0; i < numMessages; i++) + { + queue.Enqueue(new E1(), Guid.Empty, null); + } + }); + + var receiveTask = Task.Run(async () => + { + for (int i = 0; i < numMessages; i++) + { + await queue.ReceiveAsync(typeof(E1)); + } + }); + + await Task.WhenAny(Task.WhenAll(enqueueTask, receiveTask), Task.Delay(3000)); + Assert.True(enqueueTask.IsCompleted); + Assert.True(receiveTask.IsCompleted); + } + + [Fact(Timeout = 5000)] + public async Task TestEnqueueReceiveEventsAlternateType() + { + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => { }); + + var queue = new EventQueue(machineStateManager); + int numMessages = 10000; + + var enqueueTask = Task.Run(() => + { + for (int i = 0; i < numMessages; i++) + { + if (i % 2 == 0) + { + queue.Enqueue(new E1(), Guid.Empty, null); + } + else + { + queue.Enqueue(new E2(), Guid.Empty, null); + } + } + }); + + var receiveTask = Task.Run(async () => + { + for (int i = 0; i < numMessages; i++) + { + if (i % 2 == 0) + { + var e = await queue.ReceiveAsync(typeof(E1)); + Assert.IsType(e); + } + else + { + var e = await queue.ReceiveAsync(typeof(E2)); + Assert.IsType(e); + } + } + }); + + await Task.WhenAny(Task.WhenAll(enqueueTask, receiveTask), Task.Delay(3000)); + Assert.True(enqueueTask.IsCompleted); + Assert.True(receiveTask.IsCompleted); + } + } +} diff --git a/Tests/Core.Tests/EventQueues/EventQueueTest.cs b/Tests/Core.Tests/EventQueues/EventQueueTest.cs new file mode 100644 index 000000000..cc0e3b226 --- /dev/null +++ b/Tests/Core.Tests/EventQueues/EventQueueTest.cs @@ -0,0 +1,465 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Tests.Common; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class EventQueueTest : BaseTest + { + public EventQueueTest(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class E4 : Event + { + public bool Value; + + public E4(bool value) + { + this.Value = value; + } + } + + [Fact(Timeout = 5000)] + public void TestEnqueueEvent() + { + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => { }); + + using (var queue = new EventQueue(machineStateManager)) + { + Assert.Equal(0, queue.Size); + + var enqueueStatus = queue.Enqueue(new E1(), Guid.Empty, null); + Assert.Equal(1, queue.Size); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + + enqueueStatus = queue.Enqueue(new E2(), Guid.Empty, null); + Assert.Equal(2, queue.Size); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + + enqueueStatus = queue.Enqueue(new E3(), Guid.Empty, null); + Assert.Equal(3, queue.Size); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + } + } + + [Fact(Timeout = 5000)] + public void TestDequeueEvent() + { + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => { }); + + using (var queue = new EventQueue(machineStateManager)) + { + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + queue.Enqueue(new E1(), Guid.Empty, null); + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(0, queue.Size); + + queue.Enqueue(new E3(), Guid.Empty, null); + queue.Enqueue(new E2(), Guid.Empty, null); + queue.Enqueue(new E1(), Guid.Empty, null); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(2, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(1, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(0, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + } + } + + [Fact(Timeout = 5000)] + public void TestEnqueueEventWithHandlerNotRunning() + { + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => { }); + + using (var queue = new EventQueue(machineStateManager)) + { + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + var enqueueStatus = queue.Enqueue(new E1(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerNotRunning, enqueueStatus); + Assert.Equal(1, queue.Size); + } + } + + [Fact(Timeout = 5000)] + public void TestRaiseEvent() + { + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => { }); + + using (var queue = new EventQueue(machineStateManager)) + { + queue.Raise(new E1(), Guid.Empty); + Assert.True(queue.IsEventRaised); + Assert.Equal(0, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Raised, deqeueStatus); + Assert.False(queue.IsEventRaised); + Assert.Equal(0, queue.Size); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEvent() + { + int notificationCount = 0; + var tcs = new TaskCompletionSource(); + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => + { + notificationCount++; + if (notificationCount == 2) + { + Assert.Equal(MockMachineStateManager.Notification.ReceiveEvent, notification); + tcs.SetResult(true); + } + }); + + using (var queue = new EventQueue(machineStateManager)) + { + var receivedEventTask = queue.ReceiveAsync(typeof(E1)); + + var enqueueStatus = queue.Enqueue(new E1(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.Received, enqueueStatus); + Assert.Equal(0, queue.Size); + + var receivedEvent = await receivedEventTask; + Assert.IsType(receivedEvent); + Assert.Equal(0, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + await Task.WhenAny(tcs.Task, Task.Delay(500)); + Assert.True(tcs.Task.IsCompleted); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventWithPredicate() + { + int notificationCount = 0; + var tcs = new TaskCompletionSource(); + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => + { + notificationCount++; + if (notificationCount == 3) + { + Assert.Equal(MockMachineStateManager.Notification.ReceiveEvent, notification); + tcs.SetResult(true); + } + }); + + using (var queue = new EventQueue(machineStateManager)) + { + var receivedEventTask = queue.ReceiveAsync(typeof(E4), evt => (evt as E4).Value); + + var enqueueStatus = queue.Enqueue(new E4(false), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(1, queue.Size); + + enqueueStatus = queue.Enqueue(new E4(true), Guid.Empty, null); + Assert.Equal(EnqueueStatus.Received, enqueueStatus); + Assert.Equal(1, queue.Size); + + var receivedEvent = await receivedEventTask; + Assert.IsType(receivedEvent); + Assert.True((receivedEvent as E4).Value); + Assert.Equal(1, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.False((e as E4).Value); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(0, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + await Task.WhenAny(tcs.Task, Task.Delay(500)); + Assert.True(tcs.Task.IsCompleted); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventWithoutWaiting() + { + int notificationCount = 0; + var tcs = new TaskCompletionSource(); + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => + { + notificationCount++; + if (notificationCount == 3) + { + Assert.Equal(MockMachineStateManager.Notification.ReceiveEventWithoutWaiting, notification); + tcs.SetResult(true); + } + }); + + using (var queue = new EventQueue(machineStateManager)) + { + var enqueueStatus = queue.Enqueue(new E4(false), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(1, queue.Size); + + enqueueStatus = queue.Enqueue(new E4(true), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(2, queue.Size); + + var receivedEvent = await queue.ReceiveAsync(typeof(E4), evt => (evt as E4).Value); + Assert.IsType(receivedEvent); + Assert.True((receivedEvent as E4).Value); + Assert.Equal(1, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.False((e as E4).Value); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(0, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + await Task.WhenAny(tcs.Task, Task.Delay(500)); + Assert.True(tcs.Task.IsCompleted); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventWithPredicateWithoutWaiting() + { + int notificationCount = 0; + var tcs = new TaskCompletionSource(); + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => + { + notificationCount++; + if (notificationCount == 2) + { + Assert.Equal(MockMachineStateManager.Notification.ReceiveEventWithoutWaiting, notification); + tcs.SetResult(true); + } + }); + + using (var queue = new EventQueue(machineStateManager)) + { + var enqueueStatus = queue.Enqueue(new E1(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(1, queue.Size); + + var receivedEvent = await queue.ReceiveAsync(typeof(E1)); + Assert.IsType(receivedEvent); + Assert.Equal(0, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + await Task.WhenAny(tcs.Task, Task.Delay(500)); + Assert.True(tcs.Task.IsCompleted); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventMultipleTypes() + { + int notificationCount = 0; + var tcs = new TaskCompletionSource(); + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => + { + notificationCount++; + if (notificationCount == 2) + { + Assert.Equal(MockMachineStateManager.Notification.ReceiveEvent, notification); + tcs.SetResult(true); + } + }); + + using (var queue = new EventQueue(machineStateManager)) + { + var receivedEventTask = queue.ReceiveAsync(typeof(E1), typeof(E2)); + + var enqueueStatus = queue.Enqueue(new E2(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.Received, enqueueStatus); + Assert.Equal(0, queue.Size); + + var receivedEvent = await receivedEventTask; + Assert.IsType(receivedEvent); + Assert.Equal(0, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + await Task.WhenAny(tcs.Task, Task.Delay(500)); + Assert.True(tcs.Task.IsCompleted); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventAfterMultipleEnqueues() + { + int notificationCount = 0; + var tcs = new TaskCompletionSource(); + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => + { + notificationCount++; + if (notificationCount == 4) + { + Assert.Equal(MockMachineStateManager.Notification.ReceiveEvent, notification); + tcs.SetResult(true); + } + }); + + using (var queue = new EventQueue(machineStateManager)) + { + var receivedEventTask = queue.ReceiveAsync(typeof(E1)); + + var enqueueStatus = queue.Enqueue(new E2(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(1, queue.Size); + + enqueueStatus = queue.Enqueue(new E3(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(2, queue.Size); + + enqueueStatus = queue.Enqueue(new E1(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.Received, enqueueStatus); + Assert.Equal(2, queue.Size); + + var receivedEvent = await receivedEventTask; + Assert.IsType(receivedEvent); + Assert.Equal(2, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(1, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(0, queue.Size); + + await Task.WhenAny(tcs.Task, Task.Delay(500)); + Assert.True(tcs.Task.IsCompleted); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventWithoutWaitingAndWithMultipleEventsInQueue() + { + int notificationCount = 0; + var tcs = new TaskCompletionSource(); + var logger = new TestOutputLogger(this.TestOutput, false); + var machineStateManager = new MockMachineStateManager(logger, + (notification, evt, _) => + { + notificationCount++; + if (notificationCount == 4) + { + Assert.Equal(MockMachineStateManager.Notification.ReceiveEventWithoutWaiting, notification); + tcs.SetResult(true); + } + }); + + using (var queue = new EventQueue(machineStateManager)) + { + var enqueueStatus = queue.Enqueue(new E2(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(1, queue.Size); + + enqueueStatus = queue.Enqueue(new E1(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(2, queue.Size); + + enqueueStatus = queue.Enqueue(new E3(), Guid.Empty, null); + Assert.Equal(EnqueueStatus.EventHandlerRunning, enqueueStatus); + Assert.Equal(3, queue.Size); + + var receivedEvent = await queue.ReceiveAsync(typeof(E1)); + Assert.IsType(receivedEvent); + Assert.Equal(2, queue.Size); + + var (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(1, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.IsType(e); + Assert.Equal(DequeueStatus.Success, deqeueStatus); + Assert.Equal(0, queue.Size); + + (deqeueStatus, e, opGroupId, info) = queue.Dequeue(); + Assert.Equal(DequeueStatus.NotAvailable, deqeueStatus); + Assert.Equal(0, queue.Size); + + await Task.WhenAny(tcs.Task, Task.Delay(500)); + Assert.True(tcs.Task.IsCompleted); + } + } + } +} diff --git a/Tests/Core.Tests/EventQueues/MockMachineStateManager.cs b/Tests/Core.Tests/EventQueues/MockMachineStateManager.cs new file mode 100644 index 000000000..9ca9962af --- /dev/null +++ b/Tests/Core.Tests/EventQueues/MockMachineStateManager.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Core.Tests +{ + internal class MockMachineStateManager : IMachineStateManager + { + internal enum Notification + { + EnqueueEvent = 0, + RaiseEvent, + WaitEvent, + ReceiveEvent, + ReceiveEventWithoutWaiting, + DropEvent + } + + private readonly ILogger Logger; + private readonly Action Notify; + private readonly Type[] IgnoredEvents; + private readonly Type[] DeferredEvents; + private readonly bool IsDefaultHandlerInstalled; + + public bool IsEventHandlerRunning { get; set; } + + public Guid OperationGroupId { get; set; } + + internal MockMachineStateManager(ILogger logger, Action notify, + Type[] ignoredEvents = null, Type[] deferredEvents = null, bool isDefaultHandlerInstalled = false) + { + this.Logger = logger; + this.Notify = notify; + this.IgnoredEvents = ignoredEvents ?? Array.Empty(); + this.DeferredEvents = deferredEvents ?? Array.Empty(); + this.IsDefaultHandlerInstalled = isDefaultHandlerInstalled; + this.IsEventHandlerRunning = true; + } + + public int GetCachedState() => 0; + + public bool IsEventIgnoredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo) => + this.IgnoredEvents.Contains(e.GetType()); + + public bool IsEventDeferredInCurrentState(Event e, Guid opGroupId, EventInfo eventInfo) => + this.DeferredEvents.Contains(e.GetType()); + + public bool IsDefaultHandlerInstalledInCurrentState() => this.IsDefaultHandlerInstalled; + + public void OnEnqueueEvent(Event e, Guid opGroupId, EventInfo eventInfo) + { + this.Logger.WriteLine("Enqueued event of type '{0}'.", e.GetType().FullName); + this.Notify(Notification.EnqueueEvent, e, eventInfo); + } + + public void OnRaiseEvent(Event e, Guid opGroupId, EventInfo eventInfo) + { + this.Logger.WriteLine("Raised event of type '{0}'.", e.GetType().FullName); + this.Notify(Notification.RaiseEvent, e, eventInfo); + } + + public void OnWaitEvent(IEnumerable eventTypes) + { + foreach (var type in eventTypes) + { + this.Logger.WriteLine("Waits to receive event of type '{0}'.", type.FullName); + } + + this.Notify(Notification.WaitEvent, null, null); + } + + public void OnReceiveEvent(Event e, Guid opGroupId, EventInfo eventInfo) + { + if (opGroupId != Guid.Empty) + { + // Inherit the operation group id of the receive operation, if it is non-empty. + this.OperationGroupId = opGroupId; + } + + this.Logger.WriteLine("Received event of type '{0}'.", e.GetType().FullName); + this.Notify(Notification.ReceiveEvent, e, eventInfo); + } + + public void OnReceiveEventWithoutWaiting(Event e, Guid opGroupId, EventInfo eventInfo) + { + if (opGroupId != Guid.Empty) + { + // Inherit the operation group id of the receive operation, if it is non-empty. + this.OperationGroupId = opGroupId; + } + + this.Logger.WriteLine("Received event of type '{0}' without waiting.", e.GetType().FullName); + this.Notify(Notification.ReceiveEventWithoutWaiting, e, eventInfo); + } + + public void OnDropEvent(Event e, Guid opGroupId, EventInfo eventInfo) + { + this.Logger.WriteLine("Dropped event of type '{0}'.", e.GetType().FullName); + this.Notify(Notification.DropEvent, e, eventInfo); + } + + public void Assert(bool predicate, string s, object arg0) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(s, arg0)); + } + } + + public void Assert(bool predicate, string s, object arg0, object arg1) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(s, arg0, arg1)); + } + } + + public void Assert(bool predicate, string s, object arg0, object arg1, object arg2) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(s, arg0, arg1, arg2)); + } + } + + public void Assert(bool predicate, string s, params object[] args) + { + if (!predicate) + { + throw new AssertionFailureException(string.Format(s, args)); + } + } + } +} diff --git a/Tests/Core.Tests/ExceptionPropagation/ExceptionPropagationTest.cs b/Tests/Core.Tests/ExceptionPropagation/ExceptionPropagationTest.cs new file mode 100644 index 000000000..6cfcc9ee6 --- /dev/null +++ b/Tests/Core.Tests/ExceptionPropagation/ExceptionPropagationTest.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class ExceptionPropagationTest : BaseTest + { + public ExceptionPropagationTest(ITestOutputHelper output) + : base(output) + { + } + + private class Configure : Event + { + public TaskCompletionSource Tcs; + + public Configure(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as Configure).Tcs; + try + { + this.Assert(false); + } + finally + { + tcs.SetResult(true); + } + } + } + + private class N : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as Configure).Tcs; + try + { + throw new InvalidOperationException(); + } + finally + { + tcs.SetResult(true); + } + } + } + + [Fact(Timeout=5000)] + public async Task TestAssertFailureNoEventHandler() + { + var runtime = MachineRuntimeFactory.Create(); + var tcs = new TaskCompletionSource(); + runtime.CreateMachine(typeof(M), new Configure(tcs)); + await tcs.Task; + } + + [Fact(Timeout=5000)] + public async Task TestAssertFailureEventHandler() + { + await this.RunAsync(async r => + { + var tcsFail = new TaskCompletionSource(); + int count = 0; + + r.OnFailure += (exception) => + { + if (!(exception is MachineActionExceptionFilterException)) + { + count++; + tcsFail.SetException(exception); + } + }; + + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M), new Configure(tcs)); + + await WaitAsync(tcs.Task); + + AssertionFailureException ex = await Assert.ThrowsAsync(async () => await tcsFail.Task); + Assert.Equal(1, count); + }); + } + + [Fact(Timeout=5000)] + public async Task TestUnhandledExceptionEventHandler() + { + await this.RunAsync(async r => + { + var tcsFail = new TaskCompletionSource(); + int count = 0; + bool sawFilterException = false; + + r.OnFailure += (exception) => + { + // This test throws an exception that we should receive a filter call for + if (exception is MachineActionExceptionFilterException) + { + sawFilterException = true; + return; + } + + count++; + tcsFail.SetException(exception); + }; + + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(N), new Configure(tcs)); + + await WaitAsync(tcs.Task); + + AssertionFailureException ex = await Assert.ThrowsAsync(async () => await tcsFail.Task); + Assert.IsType(ex.InnerException); + Assert.Equal(1, count); + Assert.True(sawFilterException); + }); + } + } +} diff --git a/Tests/Core.Tests/ExceptionPropagation/OnExceptionTest.cs b/Tests/Core.Tests/ExceptionPropagation/OnExceptionTest.cs new file mode 100644 index 000000000..c8eb71b89 --- /dev/null +++ b/Tests/Core.Tests/ExceptionPropagation/OnExceptionTest.cs @@ -0,0 +1,337 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class OnExceptionTest : BaseTest + { + public OnExceptionTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public int X; + public TaskCompletionSource Tcs; + + public E(TaskCompletionSource tcs) + { + this.X = 0; + this.Tcs = tcs; + } + } + + private class F : Event + { + } + + private class M1a : Machine + { + private E e; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(F), nameof(OnF))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.e = this.ReceivedEvent as E; + throw new NotImplementedException(); + } + + private void OnF() + { + this.e.Tcs.SetResult(true); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.e.X++; + return OnExceptionOutcome.HandledException; + } + } + + private class M1b : Machine + { + private E e; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.e = this.ReceivedEvent as E; + throw new NotImplementedException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.e.X++; + return OnExceptionOutcome.ThrowException; + } + } + + private class M2a : Machine + { + private E e; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(F), nameof(OnF))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await Task.CompletedTask; + this.e = this.ReceivedEvent as E; + throw new NotImplementedException(); + } + + private void OnF() + { + this.e.Tcs.SetResult(true); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.e.X++; + return OnExceptionOutcome.HandledException; + } + } + + private class M2b : Machine + { + private E e; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await Task.CompletedTask; + this.e = this.ReceivedEvent as E; + throw new NotImplementedException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.e.X++; + return OnExceptionOutcome.ThrowException; + } + } + + private class M3 : Machine + { + private E e; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.e = this.ReceivedEvent as E; + throw new NotImplementedException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HaltMachine; + } + + protected override void OnHalt() + { + this.e.Tcs.TrySetResult(true); + } + } + + private class M4 : Machine + { + private E e; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.e = this.ReceivedEvent as E; + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + if (ex is UnhandledEventException) + { + return OnExceptionOutcome.HaltMachine; + } + + return OnExceptionOutcome.ThrowException; + } + + protected override void OnHalt() + { + this.e.Tcs.TrySetResult(true); + } + } + + [Fact(Timeout=5000)] + public async Task TestOnExceptionCalledOnce1() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + Assert.True(false); + failed = true; + tcs.SetResult(true); + }; + + var e = new E(tcs); + var m = r.CreateMachine(typeof(M1a), e); + r.SendEvent(m, new F()); + + await WaitAsync(tcs.Task); + Assert.False(failed); + Assert.True(e.X == 1); + }); + } + + [Fact(Timeout=5000)] + public async Task TestOnExceptionCalledOnce2() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(true); + }; + + var e = new E(tcs); + r.CreateMachine(typeof(M1b), e); + + await WaitAsync(tcs.Task); + Assert.True(failed); + Assert.True(e.X == 1); + }); + } + + [Fact(Timeout=5000)] + public async Task TestOnExceptionCalledOnceAsync1() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + Assert.True(false); + failed = true; + tcs.SetResult(true); + }; + + var e = new E(tcs); + var m = r.CreateMachine(typeof(M2a), e); + r.SendEvent(m, new F()); + + await WaitAsync(tcs.Task); + Assert.False(failed); + Assert.True(e.X == 1); + }); + } + + [Fact(Timeout=5000)] + public async Task TestOnExceptionCalledOnceAsync2() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(true); + }; + + var e = new E(tcs); + r.CreateMachine(typeof(M2b), e); + + await WaitAsync(tcs.Task); + Assert.True(failed); + Assert.True(e.X == 1); + }); + } + + [Fact(Timeout=5000)] + public async Task TestOnExceptionCanHalt() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.TrySetResult(false); + }; + + var e = new E(tcs); + r.CreateMachine(typeof(M3), e); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + Assert.False(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestUnHandledEventCanHalt() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.TrySetResult(false); + }; + + var e = new E(tcs); + var m = r.CreateMachine(typeof(M4), e); + r.SendEvent(m, new F()); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + Assert.False(failed); + }); + } + } +} diff --git a/Tests/Core.Tests/Features/GetOperationGroupIdTest.cs b/Tests/Core.Tests/Features/GetOperationGroupIdTest.cs new file mode 100644 index 000000000..9d01a0fac --- /dev/null +++ b/Tests/Core.Tests/Features/GetOperationGroupIdTest.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class GetOperationGroupIdTest : BaseTest + { + public GetOperationGroupIdTest(ITestOutputHelper output) + : base(output) + { + } + + private static Guid OperationGroup = Guid.NewGuid(); + + private class SetupEvent : Event + { + public TaskCompletionSource Tcs; + + public SetupEvent(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class E : Event + { + public MachineId Id; + + public E(MachineId id) + { + this.Id = id; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + tcs.SetResult(this.OperationGroupId == Guid.Empty); + } + } + + [Fact(Timeout = 5000)] + public async Task TestGetOperationGroupIdNotSet() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M1), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + private class M2 : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Runtime.SendEvent(this.Id, new E(this.Id), OperationGroup); + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup); + } + } + + [Fact(Timeout=5000)] + public async Task TestGetOperationGroupIdSet() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M2), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + } +} diff --git a/Tests/Core.Tests/Features/OnEventDroppedTest.cs b/Tests/Core.Tests/Features/OnEventDroppedTest.cs new file mode 100644 index 000000000..780546cef --- /dev/null +++ b/Tests/Core.Tests/Features/OnEventDroppedTest.cs @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class OnEventDroppedTest : BaseTest + { + public OnEventDroppedTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public MachineId Id; + public TaskCompletionSource Tcs; + + public E() + { + } + + public E(MachineId id) + { + this.Id = id; + } + + public E(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class M1 : Machine + { + [Start] + private class Init : MachineState + { + } + + protected override void OnHalt() + { + this.Send(this.Id, new E()); + } + } + + [Fact(Timeout=5000)] + public async Task TestOnDroppedCalled1() + { + await this.RunAsync(async r => + { + var called = false; + var tcs = new TaskCompletionSource(); + + r.OnEventDropped += (e, target) => + { + called = true; + tcs.SetResult(true); + }; + + var m = r.CreateMachine(typeof(M1)); + r.SendEvent(m, new Halt()); + + await WaitAsync(tcs.Task); + Assert.True(called); + }); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new Halt()); + this.Send(this.Id, new E()); + } + } + + [Fact(Timeout=5000)] + public async Task TestOnDroppedCalled2() + { + await this.RunAsync(async r => + { + var called = false; + var tcs = new TaskCompletionSource(); + + r.OnEventDropped += (e, target) => + { + called = true; + tcs.SetResult(true); + }; + + var m = r.CreateMachine(typeof(M2)); + + await WaitAsync(tcs.Task); + Assert.True(called); + }); + } + + [Fact(Timeout=5000)] + public async Task TestOnDroppedParams() + { + await this.RunAsync(async r => + { + var called = false; + var tcs = new TaskCompletionSource(); + + var m = r.CreateMachine(typeof(M1)); + + r.OnEventDropped += (e, target) => + { + Assert.True(e is E); + Assert.True(target == m); + called = true; + tcs.SetResult(true); + }; + + r.SendEvent(m, new Halt()); + + await WaitAsync(tcs.Task); + Assert.True(called); + }); + } + + private class EventProcessed : Event + { + } + + private class EventDropped : Event + { + } + + private class Monitor3 : Monitor + { + private TaskCompletionSource Tcs; + + [Start] + [OnEventDoAction(typeof(E), nameof(InitOnEntry))] + private class S0 : MonitorState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as E).Tcs; + this.Goto(); + } + + [OnEventGotoState(typeof(EventProcessed), typeof(S2))] + [OnEventGotoState(typeof(EventDropped), typeof(S2))] + private class S1 : MonitorState + { + } + + [OnEntry(nameof(Done))] + private class S2 : MonitorState + { + } + + private void Done() + { + this.Tcs.SetResult(true); + } + } + + private class M3a : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send((this.ReceivedEvent as E).Id, new Halt()); + } + } + + private class M3b : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send((this.ReceivedEvent as E).Id, new E()); + } + } + + private class M3c : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Processed))] + private class Init : MachineState + { + } + + private void Processed() + { + this.Monitor(new EventProcessed()); + } + } + + [Fact(Timeout=5000)] + public async Task TestProcessedOrDropped() + { + var config = GetConfiguration(); + config.EnableMonitorsInProduction = true; + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + + r.RegisterMonitor(typeof(Monitor3)); + r.InvokeMonitor(typeof(Monitor3), new E(tcs)); + + r.OnFailure += (ex) => + { + Assert.True(false); + tcs.SetResult(false); + }; + + r.OnEventDropped += (e, target) => + { + r.InvokeMonitor(typeof(Monitor3), new EventDropped()); + }; + + var m = r.CreateMachine(typeof(M3c)); + r.CreateMachine(typeof(M3a), new E(m)); + r.CreateMachine(typeof(M3b), new E(m)); + + await WaitAsync(tcs.Task); + }, config); + } + } +} diff --git a/Tests/Core.Tests/Features/OnHaltTest.cs b/Tests/Core.Tests/Features/OnHaltTest.cs new file mode 100644 index 000000000..564d98bd5 --- /dev/null +++ b/Tests/Core.Tests/Features/OnHaltTest.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class OnHaltTest : BaseTest + { + public OnHaltTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public MachineId Id; + public TaskCompletionSource Tcs; + + public E() + { + } + + public E(MachineId id) + { + this.Id = id; + } + + public E(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Assert(false); + } + } + + private class M2a : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Receive(typeof(Event)).Wait(); + } + } + + private class M2b : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Raise(new E()); + } + } + + private class M2c : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Goto(); + } + } + + private class Dummy : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + } + + private class M3 : Machine + { + private TaskCompletionSource tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.tcs = (this.ReceivedEvent as E).Tcs; + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + // no-ops but no failure + this.Send(this.Id, new E()); + this.Random(); + this.Assert(true); + this.CreateMachine(typeof(Dummy)); + + this.tcs.TrySetResult(true); + } + } + + [Fact(Timeout=5000)] + public async Task TestHaltCalled() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(true); + }; + + r.CreateMachine(typeof(M1)); + + await WaitAsync(tcs.Task); + Assert.True(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestReceiveOnHalt() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(true); + }; + + r.CreateMachine(typeof(M2a)); + + await WaitAsync(tcs.Task); + Assert.True(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestRaiseOnHalt() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(true); + }; + + r.CreateMachine(typeof(M2b)); + + await WaitAsync(tcs.Task); + Assert.True(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestGotoOnHalt() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(true); + }; + + r.CreateMachine(typeof(M2c)); + + await WaitAsync(tcs.Task); + Assert.True(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestAPIsOnHalt() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.TrySetResult(true); + }; + + r.CreateMachine(typeof(M3), new E(tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }); + } + } +} diff --git a/Tests/Core.Tests/Features/OperationGroupingTest.cs b/Tests/Core.Tests/Features/OperationGroupingTest.cs new file mode 100644 index 000000000..01e3a5c6f --- /dev/null +++ b/Tests/Core.Tests/Features/OperationGroupingTest.cs @@ -0,0 +1,519 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class OperationGroupingTest : BaseTest + { + public OperationGroupingTest(ITestOutputHelper output) + : base(output) + { + } + + private static Guid OperationGroup1 = Guid.NewGuid(); + private static Guid OperationGroup2 = Guid.NewGuid(); + + private class SetupEvent : Event + { + public TaskCompletionSource Tcs; + + public SetupEvent(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class SetupMultipleEvent : Event + { + public TaskCompletionSource[] Tcss; + + public SetupMultipleEvent(params TaskCompletionSource[] tcss) + { + this.Tcss = tcss; + } + } + + private class E : Event + { + public MachineId Id; + + public E() + { + } + + public E(MachineId id) + { + this.Id = id; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + tcs.SetResult(this.OperationGroupId == Guid.Empty); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingSingleMachineNoSend() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M1), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + private class M2 : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Send(this.Id, new E()); + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == Guid.Empty); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingSingleMachineSend() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M2), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + private class M3 : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Runtime.SendEvent(this.Id, new E(), OperationGroup1); + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup1); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingSingleMachineSendStarter() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M3), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + private class M4A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.CreateMachine(typeof(M4B), new SetupEvent(tcs)); + } + } + + private class M4B : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + tcs.SetResult(this.OperationGroupId == Guid.Empty); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingTwoMachinesCreate() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M4A), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + private class M5A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + var target = this.CreateMachine(typeof(M5B), new SetupEvent(tcs)); + this.Send(target, new E()); + } + } + + private class M5B : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == Guid.Empty); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingTwoMachinesSend() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M5A), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + private class M6A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + var target = this.CreateMachine(typeof(M6B), new SetupEvent(tcs)); + this.Runtime.SendEvent(target, new E(), OperationGroup1); + } + } + + private class M6B : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup1); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingTwoMachinesSendStarter() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M6A), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + private class M7A : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcss = (this.ReceivedEvent as SetupMultipleEvent).Tcss; + this.Tcs = tcss[0]; + var target = this.CreateMachine(typeof(M7B), new SetupEvent(tcss[1])); + this.Runtime.SendEvent(target, new E(this.Id), OperationGroup1); + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup1); + } + } + + private class M7B : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup1); + this.Send((this.ReceivedEvent as E).Id, new E()); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingTwoMachinesSendBack() + { + await this.RunAsync(async r => + { + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + r.CreateMachine(typeof(M7A), new SetupMultipleEvent(tcs1, tcs2)); + + var result = await GetResultAsync(tcs1.Task); + Assert.True(result); + + result = await GetResultAsync(tcs2.Task); + Assert.True(result); + }); + } + + private class M8A : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcss = (this.ReceivedEvent as SetupMultipleEvent).Tcss; + this.Tcs = tcss[0]; + var target = this.CreateMachine(typeof(M8B), new SetupEvent(tcss[1])); + this.Runtime.SendEvent(target, new E(this.Id), OperationGroup1); + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup2); + } + } + + private class M8B : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup1); + this.Runtime.SendEvent((this.ReceivedEvent as E).Id, new E(), OperationGroup2); + } + } + + [Fact(Timeout = 5000)] + public async Task TestOperationGroupingTwoMachinesSendBackStarter() + { + await this.RunAsync(async r => + { + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + r.CreateMachine(typeof(M8A), new SetupMultipleEvent(tcs1, tcs2)); + + var result = await GetResultAsync(tcs1.Task); + Assert.True(result); + + result = await GetResultAsync(tcs2.Task); + Assert.True(result); + }); + } + + private class M9A : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcss = (this.ReceivedEvent as SetupMultipleEvent).Tcss; + this.Tcs = tcss[0]; + var target = this.CreateMachine(typeof(M9B), new SetupMultipleEvent(tcss[1], tcss[2])); + this.Runtime.SendEvent(target, new E(this.Id), OperationGroup1); + } + + private void CheckEvent() + { + this.Tcs.SetResult(this.OperationGroupId == OperationGroup2); + } + } + + private class M9B : Machine + { + private TaskCompletionSource Tcs; + private TaskCompletionSource TargetTcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcss = (this.ReceivedEvent as SetupMultipleEvent).Tcss; + this.Tcs = tcss[0]; + this.TargetTcs = tcss[1]; + } + + private void CheckEvent() + { + this.CreateMachine(typeof(M9C), new SetupEvent(this.TargetTcs)); + this.Tcs.SetResult(this.OperationGroupId == OperationGroup1); + this.Runtime.SendEvent((this.ReceivedEvent as E).Id, new E(), OperationGroup2); + } + } + + private class M9C : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + tcs.SetResult(this.OperationGroupId == OperationGroup1); + } + } + + [Fact(Timeout=5000)] + public async Task TestOperationGroupingThreeMachinesSendStarter() + { + await this.RunAsync(async r => + { + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + var tcs3 = new TaskCompletionSource(); + r.CreateMachine(typeof(M9A), new SetupMultipleEvent(tcs1, tcs2, tcs3)); + + var result = await GetResultAsync(tcs1.Task); + Assert.True(result); + + result = await GetResultAsync(tcs2.Task); + Assert.True(result); + + result = await GetResultAsync(tcs3.Task); + Assert.True(result); + }); + } + } +} diff --git a/Tests/Core.Tests/LogMessages/Common/CustomLogWriter.cs b/Tests/Core.Tests/LogMessages/Common/CustomLogWriter.cs new file mode 100644 index 000000000..4dd7327f6 --- /dev/null +++ b/Tests/Core.Tests/LogMessages/Common/CustomLogWriter.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.Core.Tests.LogMessages +{ + internal class CustomLogWriter : RuntimeLogWriter + { + public override void OnEnqueue(MachineId machineId, string eventName) + { + } + + public override void OnSend(MachineId targetMachineId, MachineId senderId, string senderStateName, string eventName, + Guid opGroupId, bool isTargetHalted) + { + } + + protected override string FormatOnCreateMachineLogMessage(MachineId machineId, MachineId creator) => $"."; + + protected override string FormatOnMachineStateLogMessage(MachineId machineId, string stateName, bool isEntry) => $"."; + } +} diff --git a/Tests/Core.Tests/LogMessages/Common/CustomLogger.cs b/Tests/Core.Tests/LogMessages/Common/CustomLogger.cs new file mode 100644 index 000000000..91cd99a03 --- /dev/null +++ b/Tests/Core.Tests/LogMessages/Common/CustomLogger.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text; +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.Core.Tests.LogMessages +{ + internal class CustomLogger : ILogger + { + private StringBuilder StringBuilder; + + public bool IsVerbose { get; set; } = false; + + public CustomLogger(bool isVerbose) + { + this.StringBuilder = new StringBuilder(); + this.IsVerbose = isVerbose; + } + + public void Write(string value) + { + this.StringBuilder.Append(value); + } + + public void Write(string format, object arg0) + { + this.StringBuilder.AppendFormat(format, arg0.ToString()); + } + + public void Write(string format, object arg0, object arg1) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString()); + } + + public void Write(string format, object arg0, object arg1, object arg2) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + + public void Write(string format, params object[] args) + { + this.StringBuilder.AppendFormat(format, args); + } + + public void WriteLine(string value) + { + this.StringBuilder.AppendLine(value); + } + + public void WriteLine(string format, object arg0) + { + this.StringBuilder.AppendFormat(format, arg0.ToString()); + this.StringBuilder.AppendLine(); + } + + public void WriteLine(string format, object arg0, object arg1) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString()); + this.StringBuilder.AppendLine(); + } + + public void WriteLine(string format, object arg0, object arg1, object arg2) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + this.StringBuilder.AppendLine(); + } + + public void WriteLine(string format, params object[] args) + { + this.StringBuilder.AppendFormat(format, args); + this.StringBuilder.AppendLine(); + } + + public override string ToString() + { + return this.StringBuilder.ToString(); + } + + public void Dispose() + { + this.StringBuilder.Clear(); + this.StringBuilder = null; + } + } +} diff --git a/Tests/Core.Tests/LogMessages/Common/Machines.cs b/Tests/Core.Tests/LogMessages/Common/Machines.cs new file mode 100644 index 000000000..2b96d11c0 --- /dev/null +++ b/Tests/Core.Tests/LogMessages/Common/Machines.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.Core.Tests.LogMessages +{ + internal class Configure : Event + { + public TaskCompletionSource Tcs; + + public Configure(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + internal class E : Event + { + public MachineId Id; + + public E(MachineId id) + { + this.Id = id; + } + } + + internal class Unit : Event + { + } + + internal class M : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as Configure).Tcs; + var nTcs = new TaskCompletionSource(); + var n = this.CreateMachine(typeof(N), new Configure(nTcs)); + await nTcs.Task; + this.Send(n, new E(this.Id)); + } + + private void Act() + { + this.Tcs.SetResult(true); + } + } + + internal class N : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as Configure).Tcs; + tcs.SetResult(true); + } + + private void Act() + { + MachineId m = (this.ReceivedEvent as E).Id; + this.Send(m, new E(this.Id)); + } + } +} diff --git a/Tests/Core.Tests/LogMessages/CustomLogWriterTest.cs b/Tests/Core.Tests/LogMessages/CustomLogWriterTest.cs new file mode 100644 index 000000000..cb87216a8 --- /dev/null +++ b/Tests/Core.Tests/LogMessages/CustomLogWriterTest.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests.LogMessages +{ + public class CustomLogWriterTest : BaseTest + { + public CustomLogWriterTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout=5000)] + public async Task TestCustomLogWriter() + { + CustomLogger logger = new CustomLogger(true); + + Configuration config = Configuration.Create().WithVerbosityEnabled(); + var runtime = MachineRuntimeFactory.Create(config); + runtime.SetLogger(logger); + runtime.SetLogWriter(new CustomLogWriter()); + + var tcs = new TaskCompletionSource(); + runtime.CreateMachine(typeof(M), new Configure(tcs)); + + await WaitAsync(tcs.Task); + await Task.Delay(200); + + string expected = @". +. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' in state 'Init' invoked action 'InitOnEntry'. +. +. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' in state 'Init' invoked action 'InitOnEntry'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' in state 'Init' dequeued event 'Microsoft.Coyote.Core.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' in state 'Init' invoked action 'Act'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' in state 'Init' dequeued event 'Microsoft.Coyote.Core.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' in state 'Init' invoked action 'Act'. +"; + string actual = Regex.Replace(logger.ToString(), "[0-9]", string.Empty); + Assert.Equal(expected, actual); + + logger.Dispose(); + } + } +} diff --git a/Tests/Core.Tests/LogMessages/CustomLoggerTest.cs b/Tests/Core.Tests/LogMessages/CustomLoggerTest.cs new file mode 100644 index 000000000..b499b547e --- /dev/null +++ b/Tests/Core.Tests/LogMessages/CustomLoggerTest.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests.LogMessages +{ + public class CustomLoggerTest : BaseTest + { + public CustomLoggerTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout=5000)] + public async Task TestCustomLogger() + { + CustomLogger logger = new CustomLogger(true); + + Configuration config = Configuration.Create().WithVerbosityEnabled(); + var runtime = MachineRuntimeFactory.Create(config); + runtime.SetLogger(logger); + + var tcs = new TaskCompletionSource(); + runtime.CreateMachine(typeof(M), new Configure(tcs)); + + await WaitAsync(tcs.Task); + await Task.Delay(200); + + string expected = @" Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' was created by the runtime. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' enters state 'Init'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' in state 'Init' invoked action 'InitOnEntry'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' was created by machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' enters state 'Init'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' in state 'Init' invoked action 'InitOnEntry'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' in state 'Init' sent event 'Microsoft.Coyote.Core.Tests.LogMessages.E' to machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' enqueued event 'Microsoft.Coyote.Core.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' in state 'Init' dequeued event 'Microsoft.Coyote.Core.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' in state 'Init' invoked action 'Act'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.N()' in state 'Init' sent event 'Microsoft.Coyote.Core.Tests.LogMessages.E' to machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' enqueued event 'Microsoft.Coyote.Core.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' in state 'Init' dequeued event 'Microsoft.Coyote.Core.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.Core.Tests.LogMessages.M()' in state 'Init' invoked action 'Act'. +"; + string actual = Regex.Replace(logger.ToString(), "[0-9]", string.Empty); + Assert.Equal(expected, actual); + + logger.Dispose(); + } + + [Fact(Timeout=5000)] + public async Task TestCustomLoggerNoVerbosity() + { + CustomLogger logger = new CustomLogger(false); + + var runtime = MachineRuntimeFactory.Create(); + runtime.SetLogger(logger); + + var tcs = new TaskCompletionSource(); + runtime.CreateMachine(typeof(M), new Configure(tcs)); + + await WaitAsync(tcs.Task); + + Assert.Equal(string.Empty, logger.ToString()); + + logger.Dispose(); + } + + [Fact(Timeout=5000)] + public void TestNullCustomLoggerFail() + { + this.Run(r => + { + InvalidOperationException ex = Assert.Throws(() => r.SetLogger(null)); + Assert.Equal("Cannot install a null logger.", ex.Message); + }); + } + } +} diff --git a/Tests/Core.Tests/Machines/GotoStateTransitionTest.cs b/Tests/Core.Tests/Machines/GotoStateTransitionTest.cs new file mode 100644 index 000000000..1ca735943 --- /dev/null +++ b/Tests/Core.Tests/Machines/GotoStateTransitionTest.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class GotoStateTransitionTest : BaseTest + { + public GotoStateTransitionTest(ITestOutputHelper output) + : base(output) + { + } + + private class Message : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Goto(); + } + + private class Final : MachineState + { + } + } + + private class M2 : Machine + { + [Start] + [OnEventGotoState(typeof(Message), typeof(Final))] + private class Init : MachineState + { + } + + private class Final : MachineState + { + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Message), typeof(Final))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Message()); + } + + private class Final : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public async Task TestGotoStateTransition() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + await test.StartMachineAsync(); + test.AssertStateTransition("Final"); + } + + [Fact(Timeout = 5000)] + public async Task TestGotoStateTransitionAfterSend() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + await test.StartMachineAsync(); + test.AssertStateTransition("Init"); + + await test.SendEventAsync(new Message()); + test.AssertStateTransition("Final"); + } + + [Fact(Timeout = 5000)] + public async Task TestGotoStateTransitionAfterRaise() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + await test.StartMachineAsync(); + test.AssertStateTransition("Final"); + } + } +} diff --git a/Tests/Core.Tests/Machines/HandleEventTest.cs b/Tests/Core.Tests/Machines/HandleEventTest.cs new file mode 100644 index 000000000..1551fb57c --- /dev/null +++ b/Tests/Core.Tests/Machines/HandleEventTest.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class HandleEventTest : BaseTest + { + public HandleEventTest(ITestOutputHelper output) + : base(output) + { + } + + private class Result + { + public int Value = 0; + } + + private class SetupEvent : Event + { + public Result Result; + + public SetupEvent(Result result) + { + this.Result = result; + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class M1 : Machine + { + private Result Result; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(HandleE1))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Result = (this.ReceivedEvent as SetupEvent).Result; + } + + private void HandleE1() + { + this.Result.Value += 1; + } + } + + private class M2 : Machine + { + private Result Result; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(HandleE1))] + [OnEventDoAction(typeof(E2), nameof(HandleE2))] + [OnEventDoAction(typeof(E3), nameof(HandleE3))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Result = (this.ReceivedEvent as SetupEvent).Result; + } + + private void HandleE1() + { + this.Result.Value += 1; + } + + private void HandleE2() + { + this.Result.Value += 2; + } + + private void HandleE3() + { + this.Result.Value += 3; + } + } + + [Fact(Timeout = 5000)] + public async Task TestHandleEvent() + { + var result = new Result(); + + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + await test.StartMachineAsync(new SetupEvent(result)); + + await test.SendEventAsync(new E1()); + + test.AssertInboxSize(0); + test.Assert(result.Value == 1, $"Incorrect result '{result.Value}'"); + } + + [Fact(Timeout = 5000)] + public async Task TestHandleMultipleEvents() + { + var result = new Result(); + + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + await test.StartMachineAsync(new SetupEvent(result)); + + await test.SendEventAsync(new E1()); + await test.SendEventAsync(new E2()); + await test.SendEventAsync(new E3()); + + test.AssertInboxSize(0); + test.Assert(result.Value == 6, $"Incorrect result '{result.Value}'"); + } + } +} diff --git a/Tests/Core.Tests/Machines/InvokeMethodTest.cs b/Tests/Core.Tests/Machines/InvokeMethodTest.cs new file mode 100644 index 000000000..0c0c3a593 --- /dev/null +++ b/Tests/Core.Tests/Machines/InvokeMethodTest.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class InvokeMethodTest : BaseTest + { + public InvokeMethodTest(ITestOutputHelper output) + : base(output) + { + } + + private class M1 : Machine + { + [Start] + private class Init : MachineState + { + } + + internal int Add(int m, int k) + { + return m + k; + } + } + + [Fact(Timeout = 5000)] + public void TestInvokeInternalMethod() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + int result = test.Machine.Add(3, 4); + test.Assert(result == 7, $"Incorrect result '{result}'"); + } + + private class M2 : Machine + { + [Start] + private class Init : MachineState + { + } + + internal async Task AddAsync(int m, int k) + { + await Task.CompletedTask; + return m + k; + } + } + + [Fact(Timeout = 5000)] + public async Task TestInvokeInternalAsyncMethod() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + int result = await test.Machine.AddAsync(3, 4); + test.Assert(result == 7, $"Incorrect result '{result}'"); + } + + private class M3 : Machine + { + [Start] + private class Init : MachineState + { + } + +#pragma warning disable IDE0051 // Remove unused private members + private int Add(int m, int k) + { + return m + k; + } +#pragma warning restore IDE0051 // Remove unused private members + } + + [Fact(Timeout = 5000)] + public void TestInvokePrivateMethod() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + int result = (int)test.Invoke("Add", 3, 4); + test.Assert(result == 7, $"Incorrect result '{result}'"); + + result = (int)test.Invoke("Add", new Type[] { typeof(int), typeof(int) }, 3, 4); + test.Assert(result == 7, $"Incorrect result '{result}'"); + } + + private class M4 : Machine + { + [Start] + private class Init : MachineState + { + } + +#pragma warning disable IDE0051 // Remove unused private members + private async Task AddAsync(int m, int k) + { + await Task.CompletedTask; + return m + k; + } +#pragma warning restore IDE0051 // Remove unused private members + } + + [Fact(Timeout = 5000)] + public async Task TestInvokePrivateAsyncMethod() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + int result = (int)await test.InvokeAsync("AddAsync", 3, 4); + test.Assert(result == 7, $"Incorrect result '{result}'"); + + result = (int)await test.InvokeAsync("AddAsync", new Type[] { typeof(int), typeof(int) }, 3, 4); + test.Assert(result == 7, $"Incorrect result '{result}'"); + } + } +} diff --git a/Tests/Core.Tests/Machines/ReceiveEventIntegrationTest.cs b/Tests/Core.Tests/Machines/ReceiveEventIntegrationTest.cs new file mode 100644 index 000000000..6910a7038 --- /dev/null +++ b/Tests/Core.Tests/Machines/ReceiveEventIntegrationTest.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class ReceiveEventIntegrationTest : BaseTest + { + public ReceiveEventIntegrationTest(ITestOutputHelper output) + : base(output) + { + } + + internal class SetupEvent : Event + { + public TaskCompletionSource Tcs; + + public SetupEvent(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + public MachineId Id; + + public E2(MachineId id) + { + this.Id = id; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Send(this.Id, new E1()); + await this.Receive(typeof(E1)); + tcs.SetResult(true); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Send(this.Id, new E1()); + await this.Receive(typeof(E1), e => e is E1); + tcs.SetResult(true); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Send(this.Id, new E1()); + await this.Receive(typeof(E1), typeof(E2)); + tcs.SetResult(true); + } + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + var mid = this.CreateMachine(typeof(M5), new E2(this.Id)); + this.Send(mid, new E2(this.Id)); + await this.Receive(typeof(E2)); + this.Send(mid, new E2(this.Id)); + this.Send(mid, new E2(this.Id)); + await this.Receive(typeof(E2)); + tcs.SetResult(true); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E2), nameof(Handle))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var mid = (this.ReceivedEvent as E2).Id; + var e = (E2)await this.Receive(typeof(E2)); + this.Send(e.Id, new E2(this.Id)); + } + + private async Task Handle() + { + var mid = (this.ReceivedEvent as E2).Id; + var e = (E2)await this.Receive(typeof(E2)); + this.Send(e.Id, new E2(this.Id)); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventOneMachine() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M1), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventWithPredicateOneMachine() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M2), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventMultipleTypesOneMachine() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M3), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventTwoMachines() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M4), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + } +} diff --git a/Tests/Core.Tests/Machines/ReceiveEventStressTest.cs b/Tests/Core.Tests/Machines/ReceiveEventStressTest.cs new file mode 100644 index 000000000..09c1d561f --- /dev/null +++ b/Tests/Core.Tests/Machines/ReceiveEventStressTest.cs @@ -0,0 +1,258 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class ReceiveEventStressTest : BaseTest + { + public ReceiveEventStressTest(ITestOutputHelper output) + : base(output) + { + } + + internal class SetupTcsEvent : Event + { + public TaskCompletionSource Tcs; + + public int NumMessages; + + public SetupTcsEvent(TaskCompletionSource tcs, int numMessages) + { + this.Tcs = tcs; + this.NumMessages = numMessages; + } + } + + internal class SetupIdEvent : Event + { + public MachineId Id; + + public int NumMessages; + + public SetupIdEvent(MachineId id, int numMessages) + { + this.Id = id; + this.NumMessages = numMessages; + } + } + + private class Message : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupTcsEvent).Tcs; + var numMessages = (this.ReceivedEvent as SetupTcsEvent).NumMessages; + + var mid = this.CreateMachine(typeof(M2), new SetupTcsEvent(tcs, numMessages)); + + var counter = 0; + while (counter < numMessages) + { + counter++; + this.Send(mid, new Message()); + } + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupTcsEvent).Tcs; + var numMessages = (this.ReceivedEvent as SetupTcsEvent).NumMessages; + + var counter = 0; + while (counter < numMessages) + { + counter++; + await this.Receive(typeof(Message)); + } + + tcs.SetResult(true); + } + } + + [Fact(Timeout = 20000)] + public async Task TestReceiveEvent() + { + for (int i = 0; i < 100; i++) + { + await this.RunAsync(async r => + { + r.Logger.WriteLine($"Iteration #{i}"); + + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M1), new SetupTcsEvent(tcs, 18000)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupTcsEvent).Tcs; + var numMessages = (this.ReceivedEvent as SetupTcsEvent).NumMessages; + + var mid = this.CreateMachine(typeof(M4), new SetupTcsEvent(tcs, numMessages)); + + var counter = 0; + while (counter < numMessages) + { + counter++; + this.Send(mid, new Message()); + } + } + } + + private class M4 : Machine + { + private TaskCompletionSource Tcs; + + private int NumMessages; + + private int Counter; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(Message), nameof(HandleMessage))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupTcsEvent).Tcs; + this.NumMessages = (this.ReceivedEvent as SetupTcsEvent).NumMessages; + this.Counter = 0; + } + + private async Task HandleMessage() + { + await this.Receive(typeof(Message)); + this.Counter += 2; + + if (this.Counter == this.NumMessages) + { + this.Tcs.SetResult(true); + } + } + } + + [Fact(Timeout = 20000)] + public async Task TestReceiveEventAlternate() + { + for (int i = 0; i < 100; i++) + { + await this.RunAsync(async r => + { + r.Logger.WriteLine($"Iteration #{i}"); + + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M3), new SetupTcsEvent(tcs, 18000)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupTcsEvent).Tcs; + var numMessages = (this.ReceivedEvent as SetupTcsEvent).NumMessages; + + var mid = this.CreateMachine(typeof(M6), new SetupIdEvent(this.Id, numMessages)); + + var counter = 0; + while (counter < numMessages) + { + counter++; + this.Send(mid, new Message()); + await this.Receive(typeof(Message)); + } + + tcs.SetResult(true); + } + } + + private class M6 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var mid = (this.ReceivedEvent as SetupIdEvent).Id; + var numMessages = (this.ReceivedEvent as SetupIdEvent).NumMessages; + + var counter = 0; + while (counter < numMessages) + { + counter++; + await this.Receive(typeof(Message)); + this.Send(mid, new Message()); + } + } + } + + [Fact(Timeout = 20000)] + public async Task TestReceiveEventExchange() + { + for (int i = 0; i < 100; i++) + { + await this.RunAsync(async r => + { + r.Logger.WriteLine($"Iteration #{i}"); + + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M5), new SetupTcsEvent(tcs, 18000)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + } + } +} diff --git a/Tests/Core.Tests/Machines/ReceiveEventTest.cs b/Tests/Core.Tests/Machines/ReceiveEventTest.cs new file mode 100644 index 000000000..4a9442e01 --- /dev/null +++ b/Tests/Core.Tests/Machines/ReceiveEventTest.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class ReceiveEventTest : BaseTest + { + public ReceiveEventTest(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Receive(typeof(E1)); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Receive(typeof(E1)); + await this.Receive(typeof(E2)); + await this.Receive(typeof(E3)); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Receive(typeof(E1), typeof(E2), typeof(E3)); + } + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Receive(typeof(E1), typeof(E2), typeof(E3)); + await this.Receive(typeof(E1), typeof(E2), typeof(E3)); + await this.Receive(typeof(E1), typeof(E2), typeof(E3)); + } + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventStatement() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + await test.StartMachineAsync(); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E1()); + test.AssertIsWaitingToReceiveEvent(false); + test.AssertInboxSize(0); + } + + [Fact(Timeout = 5000)] + public async Task TestMultipleReceiveEventStatements() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + await test.StartMachineAsync(); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E1()); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E2()); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E3()); + test.AssertIsWaitingToReceiveEvent(false); + test.AssertInboxSize(0); + } + + [Fact(Timeout = 5000)] + public async Task TestMultipleReceiveEventStatementsUnordered() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + await test.StartMachineAsync(); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E2()); + test.AssertIsWaitingToReceiveEvent(true); + test.AssertInboxSize(1); + + await test.SendEventAsync(new E3()); + test.AssertIsWaitingToReceiveEvent(true); + test.AssertInboxSize(2); + + await test.SendEventAsync(new E1()); + test.AssertIsWaitingToReceiveEvent(false); + test.AssertInboxSize(0); + } + + [Fact(Timeout = 5000)] + public async Task TestReceiveEventStatementWithMultipleTypes() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + await test.StartMachineAsync(); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E1()); + test.AssertIsWaitingToReceiveEvent(false); + test.AssertInboxSize(0); + } + + [Fact(Timeout = 5000)] + public async Task TestMultipleReceiveEventStatementsWithMultipleTypes() + { + var configuration = GetConfiguration(); + var test = new MachineTestKit(configuration: configuration); + + await test.StartMachineAsync(); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E1()); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E2()); + test.AssertIsWaitingToReceiveEvent(true); + + await test.SendEventAsync(new E3()); + test.AssertIsWaitingToReceiveEvent(false); + test.AssertInboxSize(0); + } + } +} diff --git a/Tests/Core.Tests/Machines/Timers/TimerStressTest.cs b/Tests/Core.Tests/Machines/Timers/TimerStressTest.cs new file mode 100644 index 000000000..5a7c33dfb --- /dev/null +++ b/Tests/Core.Tests/Machines/Timers/TimerStressTest.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TimerStressTest : BaseTest + { + public TimerStressTest(ITestOutputHelper output) + : base(output) + { + } + + private class SetupEvent : Event + { + public TaskCompletionSource Tcs; + + public SetupEvent(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class T1 : Machine + { + private TaskCompletionSource Tcs; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + + // Start a regular timer. + this.StartTimer(TimeSpan.FromTicks(1)); + } + + private void HandleTimeout() + { + this.Tcs.SetResult(true); + this.Raise(new Halt()); + } + } + + [Fact(Timeout= 6000)] + public async Task TestTimerLifetime() + { + await this.RunAsync(async r => + { + int numTimers = 1000; + var awaiters = new Task[numTimers]; + for (int i = 0; i < numTimers; i++) + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(T1), new SetupEvent(tcs)); + awaiters[i] = tcs.Task; + } + + Task task = Task.WhenAll(awaiters); + await WaitAsync(task); + }); + } + + private class T2 : Machine + { + private TaskCompletionSource Tcs; + private int Counter; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Counter = 0; + + // Start a periodic timer. + this.StartPeriodicTimer(TimeSpan.FromTicks(1), TimeSpan.FromTicks(1)); + } + + private void HandleTimeout() + { + this.Counter++; + if (this.Counter == 10) + { + this.Tcs.SetResult(true); + this.Raise(new Halt()); + } + } + } + + [Fact(Timeout = 6000)] + public async Task TestPeriodicTimerLifetime() + { + await this.RunAsync(async r => + { + int numTimers = 1000; + var awaiters = new Task[numTimers]; + for (int i = 0; i < numTimers; i++) + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(T2), new SetupEvent(tcs)); + awaiters[i] = tcs.Task; + } + + Task task = Task.WhenAll(awaiters); + await WaitAsync(task); + }); + } + } +} diff --git a/Tests/Core.Tests/Machines/Timers/TimerTest.cs b/Tests/Core.Tests/Machines/Timers/TimerTest.cs new file mode 100644 index 000000000..27770f50a --- /dev/null +++ b/Tests/Core.Tests/Machines/Timers/TimerTest.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TimerTest : BaseTest + { + public TimerTest(ITestOutputHelper output) + : base(output) + { + } + + private class SetupEvent : Event + { + public TaskCompletionSource Tcs; + + public SetupEvent(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class TransferTimerEvent : Event + { + public TaskCompletionSource Tcs; + public TimerInfo Timer; + + public TransferTimerEvent(TaskCompletionSource tcs, TimerInfo timer) + { + this.Tcs = tcs; + this.Timer = timer; + } + } + + private class T1 : Machine + { + private TaskCompletionSource Tcs; + + private int Count; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Count = 0; + + // Start a regular timer. + this.StartTimer(TimeSpan.FromMilliseconds(10)); + } + + private void HandleTimeout() + { + this.Count++; + if (this.Count == 1) + { + this.Tcs.SetResult(true); + this.Raise(new Halt()); + return; + } + + this.Tcs.SetResult(false); + this.Raise(new Halt()); + } + } + + private class T2 : Machine + { + private TaskCompletionSource Tcs; + + private TimerInfo Timer; + private int Count; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + this.Count = 0; + + // Start a periodic timer. + this.Timer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(10)); + } + + private void HandleTimeout() + { + this.Count++; + if (this.Count == 10) + { + this.StopTimer(this.Timer); + this.Tcs.SetResult(true); + this.Raise(new Halt()); + } + } + } + + private class T3 : Machine + { + private TaskCompletionSource Tcs; + + private TimerInfo PingTimer; + private TimerInfo PongTimer; + + /// + /// Start the PingTimer and start handling the timeout events from it. + /// After handling 10 events, stop the timer and move to the Pong state. + /// + [Start] + [OnEntry(nameof(DoPing))] + [IgnoreEvents(typeof(TimerElapsedEvent))] + private class Ping : MachineState + { + } + + /// + /// Start the PongTimer and start handling the timeout events from it. + /// After handling 10 events, stop the timer and move to the Ping state. + /// + [OnEntry(nameof(DoPong))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Pong : MachineState + { + } + + private async Task DoPing() + { + this.Tcs = (this.ReceivedEvent as SetupEvent).Tcs; + + this.PingTimer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(5), TimeSpan.FromMilliseconds(5)); + await Task.Delay(100); + this.StopTimer(this.PingTimer); + + this.Goto(); + } + + private void DoPong() + { + this.PongTimer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(50)); + } + + private void HandleTimeout() + { + var timeout = this.ReceivedEvent as TimerElapsedEvent; + if (timeout.Info == this.PongTimer) + { + this.Tcs.SetResult(true); + this.Raise(new Halt()); + } + else + { + this.Tcs.SetResult(false); + this.Raise(new Halt()); + } + } + } + + private class T4 : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + private class Init : MachineState + { + } + + private void Initialize() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + + try + { + this.StartTimer(TimeSpan.FromSeconds(-1)); + } + catch (AssertionFailureException ex) + { + this.Logger.WriteLine(ex.Message); + tcs.SetResult(true); + this.Raise(new Halt()); + return; + } + + tcs.SetResult(false); + this.Raise(new Halt()); + } + } + + private class T5 : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + private class Init : MachineState + { + } + + private void Initialize() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + + try + { + this.StartPeriodicTimer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(-1)); + } + catch (AssertionFailureException ex) + { + this.Logger.WriteLine(ex.Message); + tcs.SetResult(true); + this.Raise(new Halt()); + return; + } + + tcs.SetResult(false); + this.Raise(new Halt()); + } + } + + [Fact(Timeout=10000)] + public async Task TestBasicTimerOperation() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(T1), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + [Fact(Timeout=10000)] + public async Task TestBasicPeriodicTimerOperation() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(T2), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + [Fact(Timeout=10000)] + public async Task TestDropTimeoutsAfterTimerDisposal() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(T3), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + [Fact(Timeout=10000)] + public async Task TestIllegalDueTimeSpecification() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(T4), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + + [Fact(Timeout=10000)] + public async Task TestIllegalPeriodSpecification() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(T5), new SetupEvent(tcs)); + + var result = await GetResultAsync(tcs.Task); + Assert.True(result); + }); + } + } +} diff --git a/Tests/Core.Tests/MemoryLeak/NoMemoryLeakAfterHaltTest.cs b/Tests/Core.Tests/MemoryLeak/NoMemoryLeakAfterHaltTest.cs new file mode 100644 index 000000000..667835013 --- /dev/null +++ b/Tests/Core.Tests/MemoryLeak/NoMemoryLeakAfterHaltTest.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class NoMemoryLeakAfterHaltTest : BaseTest + { + public NoMemoryLeakAfterHaltTest(ITestOutputHelper output) + : base(output) + { + } + + internal class Configure : Event + { + public TaskCompletionSource Tcs; + + public Configure(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + internal class E : Event + { + public MachineId Id; + + public E(MachineId id) + : base() + { + this.Id = id; + } + } + + internal class Unit : Event + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as Configure).Tcs; + + try + { + int counter = 0; + while (counter < 100) + { + var n = this.CreateMachine(typeof(N)); + this.Send(n, new E(this.Id)); + await this.Receive(typeof(E)); + counter++; + } + } + finally + { + tcs.SetResult(true); + } + + tcs.SetResult(true); + } + } + + private class N : Machine + { + private int[] LargeArray; + + [Start] + [OnEntry(nameof(Configure))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void Configure() + { + this.LargeArray = new int[10000000]; + this.LargeArray[this.LargeArray.Length - 1] = 1; + } + + private void Act() + { + var sender = (this.ReceivedEvent as E).Id; + this.Send(sender, new E(this.Id)); + this.Raise(new Halt()); + } + } + + [Fact(Timeout=15000)] + public async Task TestNoMemoryLeakAfterHalt() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M), new Configure(tcs)); + + await WaitAsync(tcs.Task, 15000); + + (r as ProductionRuntime).Stop(); + }); + } + } +} diff --git a/Tests/Core.Tests/MemoryLeak/NoMemoryLeakInEventSendingTest.cs b/Tests/Core.Tests/MemoryLeak/NoMemoryLeakInEventSendingTest.cs new file mode 100644 index 000000000..845f69ab5 --- /dev/null +++ b/Tests/Core.Tests/MemoryLeak/NoMemoryLeakInEventSendingTest.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class NoMemoryLeakInEventSendingTest : BaseTest + { + public NoMemoryLeakInEventSendingTest(ITestOutputHelper output) + : base(output) + { + } + + internal class Configure : Event + { + public TaskCompletionSource Tcs; + + public Configure(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + internal class E : Event + { + public MachineId Id; + public readonly int[] LargeArray; + + public E(MachineId id) + : base() + { + this.Id = id; + this.LargeArray = new int[10000000]; + } + } + + internal class Unit : Event + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as Configure).Tcs; + + try + { + int counter = 0; + var n = this.CreateMachine(typeof(N)); + + while (counter < 1000) + { + this.Send(n, new E(this.Id)); + E e = (E)await this.Receive(typeof(E)); + e.LargeArray[10] = 7; + counter++; + } + } + finally + { + tcs.SetResult(true); + } + + tcs.SetResult(true); + } + } + + private class N : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void Act() + { + var sender = (this.ReceivedEvent as E).Id; + this.Send(sender, new E(this.Id)); + } + } + + [Fact(Timeout=22000)] + public async Task TestNoMemoryLeakInEventSending() + { + await this.RunAsync(async r => + { + var tcs = new TaskCompletionSource(); + r.CreateMachine(typeof(M), new Configure(tcs)); + + await WaitAsync(tcs.Task, 20000); + + (r as ProductionRuntime).Stop(); + }); + } + } +} diff --git a/Tests/Core.Tests/RuntimeInterface/CreateMachineIdFromNameTest.cs b/Tests/Core.Tests/RuntimeInterface/CreateMachineIdFromNameTest.cs new file mode 100644 index 000000000..5a28a6a01 --- /dev/null +++ b/Tests/Core.Tests/RuntimeInterface/CreateMachineIdFromNameTest.cs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class CreateMachineIdFromNameTest : BaseTest + { + public CreateMachineIdFromNameTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class Conf : Event + { + public TaskCompletionSource Tcs; + + public Conf(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + if (this.ReceivedEvent is Conf) + { + (this.ReceivedEvent as Conf).Tcs.SetResult(true); + } + } + } + + [Fact(Timeout=5000)] + public async Task TestCreateWithId1() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + var m1 = r.CreateMachine(typeof(M)); + var m2 = r.CreateMachineIdFromName(typeof(M), "M"); + r.Assert(!m1.Equals(m2)); + r.CreateMachine(m2, typeof(M), new Conf(tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestCreateWithId2() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + var m1 = r.CreateMachineIdFromName(typeof(M), "M1"); + var m2 = r.CreateMachineIdFromName(typeof(M), "M2"); + r.Assert(!m1.Equals(m2)); + r.CreateMachine(m1, typeof(M)); + r.CreateMachine(m2, typeof(M), new Conf(tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }); + } + + private class M2 : Machine + { + [Start] + private class S : MachineState + { + } + } + + private class M3 : Machine + { + [Start] + private class S : MachineState + { + } + } + + [Fact(Timeout=5000)] + public async Task TestCreateWithId4() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + try + { + var m3 = r.CreateMachineIdFromName(typeof(M3), "M3"); + r.CreateMachine(m3, typeof(M2)); + } + catch (Exception) + { + failed = true; + tcs.SetResult(false); + } + + await WaitAsync(tcs.Task); + Assert.True(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestCreateWithId5() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + try + { + var m1 = r.CreateMachineIdFromName(typeof(M2), "M2"); + r.CreateMachine(m1, typeof(M2)); + r.CreateMachine(m1, typeof(M2)); + } + catch (Exception) + { + failed = true; + tcs.SetResult(false); + } + + await WaitAsync(tcs.Task); + Assert.True(failed); + }); + } + + private class E2 : Event + { + public MachineId Mid; + + public E2(MachineId mid) + { + this.Mid = mid; + } + } + + private class M4 : Machine + { + [Start] + [OnEventDoAction(typeof(Conf), nameof(Process))] + private class S : MachineState + { + } + + private void Process() + { + (this.ReceivedEvent as Conf).Tcs.SetResult(true); + } + } + + [Fact(Timeout=5000)] + public void TestCreateWithId9() + { + this.Run(r => + { + var m1 = r.CreateMachineIdFromName(typeof(M4), "M4"); + var m2 = r.CreateMachineIdFromName(typeof(M4), "M4"); + Assert.True(m1.Equals(m2)); + }); + } + + private class M6 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var m = this.Runtime.CreateMachineIdFromName(typeof(M4), "M4"); + this.CreateMachine(m, typeof(M4), "friendly"); + } + } + + [Fact(Timeout=5000)] + public async Task TestCreateWithId10() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + r.CreateMachine(typeof(M6)); + r.CreateMachine(typeof(M6)); + + await WaitAsync(tcs.Task); + Assert.True(failed); + }); + } + + private class M7 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Runtime.CreateMachineAndExecuteAsync(typeof(M6)); + var m = this.Runtime.CreateMachineIdFromName(typeof(M4), "M4"); + this.Runtime.SendEvent(m, this.ReceivedEvent); + } + } + + [Fact(Timeout=5000)] + public async Task TestCreateWithId11() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + r.CreateMachine(typeof(M7), new Conf(tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }); + } + } +} diff --git a/Tests/Core.Tests/RuntimeInterface/SendAndExecuteTest.cs b/Tests/Core.Tests/RuntimeInterface/SendAndExecuteTest.cs new file mode 100644 index 000000000..4f4c6d88d --- /dev/null +++ b/Tests/Core.Tests/RuntimeInterface/SendAndExecuteTest.cs @@ -0,0 +1,446 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class SendAndExecuteTest : BaseTest + { + public SendAndExecuteTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config1 : Event + { + public TaskCompletionSource Tcs; + + public Config1(TaskCompletionSource tcs) + { + this.Tcs = tcs; + } + } + + private class Config2 : Event + { + public bool HandleException; + public TaskCompletionSource Tcs; + + public Config2(bool handleEx, TaskCompletionSource tcs) + { + this.HandleException = handleEx; + this.Tcs = tcs; + } + } + + private class E1 : Event + { + public int Value; + + public E1() + { + this.Value = 0; + } + } + + private class E2 : Event + { + public MachineId Id; + + public E2(MachineId id) + { + this.Id = id; + } + } + + private class E3 : Event + { + } + + private class MHalts : Event + { + } + + private class SEReturns : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as Config1).Tcs; + var e = new E1(); + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(N1)); + await this.Runtime.SendEventAndExecuteAsync(m, e); + this.Assert(e.Value == 1); + tcs.SetResult(true); + } + } + + private class N1 : Machine + { + private bool LEHandled = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(HandleEventE))] + [OnEventDoAction(typeof(E3), nameof(HandleEventLE))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E3()); + } + + private void HandleEventLE() + { + this.LEHandled = true; + } + + private void HandleEventE() + { + this.Assert(this.LEHandled); + var e = this.ReceivedEvent as E1; + e.Value = 1; + } + } + + [Fact(Timeout=5000)] + public async Task TestSyncSendBlocks() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(true); + }; + + r.CreateMachine(typeof(M1), new Config1(tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(E3))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as Config1).Tcs; + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(N2), new E2(this.Id)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E3()); + this.Assert(handled); + tcs.SetResult(true); + } + } + + private class N2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(E3))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var creator = (this.ReceivedEvent as E2).Id; + var handled = await this.Id.Runtime.SendEventAndExecuteAsync(creator, new E3()); + this.Assert(!handled); + } + } + + [Fact(Timeout=5000)] + public async Task TestSendCycleDoesNotDeadlock() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + r.CreateMachine(typeof(M2), new Config1(tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }); + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as Config1).Tcs; + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(N3)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E3()); + this.Monitor(new SEReturns()); + this.Assert(handled); + tcs.TrySetResult(true); + } + } + + private class N3 : Machine + { + [Start] + [OnEventDoAction(typeof(E3), nameof(HandleE))] + private class Init : MachineState + { + } + + private void HandleE() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Monitor(new MHalts()); + } + } + + private class SafetyMonitor : Monitor + { + private bool MHalted = false; + private bool SEReturned = false; + + [Start] + [Hot] + [OnEventDoAction(typeof(MHalts), nameof(OnMHalts))] + [OnEventDoAction(typeof(SEReturns), nameof(OnSEReturns))] + private class Init : MonitorState + { + } + + [Cold] + private class Done : MonitorState + { + } + + private void OnMHalts() + { + this.Assert(this.SEReturned == false); + this.MHalted = true; + } + + private void OnSEReturns() + { + this.Assert(this.MHalted); + this.SEReturned = true; + this.Goto(); + } + } + + [Fact(Timeout=5000)] + public async Task TestMachineHaltsOnSendExec() + { + var config = GetConfiguration(); + config.EnableMonitorsInProduction = true; + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + r.RegisterMonitor(typeof(SafetyMonitor)); + r.CreateMachine(typeof(M3), new Config1(tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }, config); + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as Config2).Tcs; + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(N4), this.ReceivedEvent); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E3()); + this.Assert(handled); + tcs.TrySetResult(true); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.Assert(false); + return OnExceptionOutcome.ThrowException; + } + } + + private class N4 : Machine + { + private bool HandleException = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E3), nameof(HandleE))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.HandleException = (this.ReceivedEvent as Config2).HandleException; + } + + private void HandleE() + { + throw new Exception(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return this.HandleException ? OnExceptionOutcome.HandledException : OnExceptionOutcome.ThrowException; + } + } + + [Fact(Timeout=5000)] + public async Task TestHandledExceptionOnSendExec() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + r.OnFailure += (ex) => + { + failed = true; + tcs.SetResult(false); + }; + + r.CreateMachine(typeof(M4), new Config2(true, tcs)); + + await WaitAsync(tcs.Task); + Assert.False(failed); + }); + } + + [Fact(Timeout=5000)] + public async Task TestUnHandledExceptionOnSendExec() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + var message = string.Empty; + + r.OnFailure += (ex) => + { + if (!failed) + { + message = (ex is MachineActionExceptionFilterException) ? ex.InnerException.Message : ex.Message; + failed = true; + tcs.TrySetResult(false); + } + }; + + r.CreateMachine(typeof(M4), new Config2(false, tcs)); + + await WaitAsync(tcs.Task); + Assert.True(failed); + Assert.StartsWith("Exception of type 'System.Exception' was thrown", message); + }); + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as Config1).Tcs; + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(N5)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E3()); + this.Assert(handled); + tcs.TrySetResult(true); + } + } + + private class N5 : Machine + { + [Start] + private class Init : MachineState + { + } + } + + [Fact(Timeout=5000)] + public async Task TestUnhandledEventOnSendExec() + { + await this.RunAsync(async r => + { + var failed = false; + var tcs = new TaskCompletionSource(); + var message = string.Empty; + + r.OnFailure += (ex) => + { + if (!failed) + { + message = (ex is MachineActionExceptionFilterException) ? ex.InnerException.Message : ex.Message; + failed = true; + tcs.TrySetResult(false); + } + }; + + r.CreateMachine(typeof(M5), new Config1(tcs)); + + await WaitAsync(tcs.Task); + Assert.True(failed); + Assert.Equal( + "Machine 'Microsoft.Coyote.Core.Tests.SendAndExecuteTest+N5(1)' received event " + + "'Microsoft.Coyote.Core.Tests.SendAndExecuteTest+E3' that cannot be handled.", message); + }); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/CompletedTaskTest.cs b/Tests/Core.Tests/Threading/Tasks/CompletedTaskTest.cs new file mode 100644 index 000000000..c77f68dc2 --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/CompletedTaskTest.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class CompletedTaskTest : BaseTest + { + public CompletedTaskTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout = 5000)] + public void TestCompletedTask() + { + ControlledTask task = ControlledTask.CompletedTask; + Assert.True(task.IsCompleted); + } + + [Fact(Timeout = 5000)] + public void TestCanceledTask() + { + CancellationToken token = new CancellationToken(true); + ControlledTask task = ControlledTask.FromCanceled(token); + Assert.True(task.IsCanceled); + } + + [Fact(Timeout = 5000)] + public void TestCanceledTaskWithResult() + { + CancellationToken token = new CancellationToken(true); + ControlledTask task = ControlledTask.FromCanceled(token); + Assert.True(task.IsCanceled); + } + + [Fact(Timeout = 5000)] + public void TestFailedTask() + { + ControlledTask task = ControlledTask.FromException(new InvalidOperationException()); + Assert.True(task.IsFaulted); + Assert.Equal(typeof(AggregateException), task.Exception.GetType()); + Assert.Equal(typeof(InvalidOperationException), task.Exception.InnerException.GetType()); + } + + [Fact(Timeout = 5000)] + public void TestFailedTaskWithResult() + { + ControlledTask task = ControlledTask.FromException(new InvalidOperationException()); + Assert.True(task.IsFaulted); + Assert.Equal(typeof(AggregateException), task.Exception.GetType()); + Assert.Equal(typeof(InvalidOperationException), task.Exception.InnerException.GetType()); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskAwaitTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskAwaitTest.cs new file mode 100644 index 000000000..d80ed22ba --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskAwaitTest.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskAwaitTest : BaseTest + { + public TaskAwaitTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 5); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 5); + Assert.Equal(5, entry.Value); + } + + private static async ControlledTask NestedWriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + await WriteAsync(entry, value); + } + + private static async ControlledTask NestedWriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + await WriteWithDelayAsync(entry, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 5); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 5); + Assert.Equal(5, entry.Value); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + return entry.Value; + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 5); + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 5); + Assert.Equal(5, value); + } + + private static async ControlledTask NestedGetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + return await GetWriteResultAsync(entry, value); + } + + private static async ControlledTask NestedGetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + return await GetWriteResultWithDelayAsync(entry, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 5); + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 5); + Assert.Equal(5, value); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitFalseTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitFalseTest.cs new file mode 100644 index 000000000..7bf847a09 --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitFalseTest.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskConfigureAwaitFalseTest : BaseTest + { + public TaskConfigureAwaitFalseTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, entry.Value); + } + + private static async ControlledTask NestedWriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + await WriteAsync(entry, value).ConfigureAwait(false); + } + + private static async ControlledTask NestedWriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(false); + await WriteWithDelayAsync(entry, value).ConfigureAwait(false); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, entry.Value); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = value; + return entry.Value; + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, value); + } + + private static async ControlledTask NestedGetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + return await GetWriteResultAsync(entry, value).ConfigureAwait(false); + } + + private static async ControlledTask NestedGetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + return await GetWriteResultWithDelayAsync(entry, value).ConfigureAwait(false); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(false); + Assert.Equal(5, value); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitTrueTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitTrueTest.cs new file mode 100644 index 000000000..8c3eea1f6 --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskConfigureAwaitTrueTest.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskConfigureAwaitTrueTest : BaseTest + { + public TaskConfigureAwaitTrueTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, entry.Value); + } + + private static async ControlledTask NestedWriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + await WriteAsync(entry, value).ConfigureAwait(true); + } + + private static async ControlledTask NestedWriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(true); + await WriteWithDelayAsync(entry, value).ConfigureAwait(true); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, entry.Value); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = value; + return entry.Value; + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, value); + } + + private static async ControlledTask NestedGetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + return await GetWriteResultAsync(entry, value).ConfigureAwait(true); + } + + private static async ControlledTask NestedGetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + return await GetWriteResultWithDelayAsync(entry, value).ConfigureAwait(true); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(true); + Assert.Equal(5, value); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskExceptionTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskExceptionTest.cs new file mode 100644 index 000000000..10084722d --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskExceptionTest.cs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskExceptionTest : BaseTest + { + public TaskExceptionTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public async Task TestNoSynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = WriteAsync(entry, 5); + await task; + + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestNoAsynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = WriteWithDelayAsync(entry, 5); + await task; + + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestNoParallelSynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(() => + { + entry.Value = 5; + }); + + await task; + + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestNoParallelAsynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(async () => + { + entry.Value = 5; + await ControlledTask.Delay(1); + }); + await task; + + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestNoParallelFuncTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + async ControlledTask func() + { + entry.Value = 5; + await ControlledTask.Delay(1); + } + + var task = ControlledTask.Run(func); + await task; + + Assert.Equal(TaskStatus.RanToCompletion, task.Status); + Assert.Equal(5, entry.Value); + } + + private static async ControlledTask WriteWithExceptionAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + throw new InvalidOperationException(); + } + + private static async ControlledTask WriteWithDelayedExceptionAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + throw new InvalidOperationException(); + } + + [Fact(Timeout = 5000)] + public async Task TestSynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = WriteWithExceptionAsync(entry, 5); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Assert.IsType(exception); + Assert.Equal(TaskStatus.Faulted, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAsynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = WriteWithDelayedExceptionAsync(entry, 5); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Assert.IsType(exception); + Assert.Equal(TaskStatus.Faulted, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestParallelSynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(() => + { + entry.Value = 5; + throw new InvalidOperationException(); + }); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Assert.IsType(exception); + Assert.Equal(TaskStatus.Faulted, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestParallelAsynchronousTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(async () => + { + entry.Value = 5; + await ControlledTask.Delay(1); + throw new InvalidOperationException(); + }); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Assert.IsType(exception); + Assert.Equal(TaskStatus.Faulted, task.Status); + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestParallelFuncTaskExceptionStatus() + { + SharedEntry entry = new SharedEntry(); + async ControlledTask func() + { + entry.Value = 5; + await ControlledTask.Delay(1); + throw new InvalidOperationException(); + } + + var task = ControlledTask.Run(func); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Assert.IsType(exception); + Assert.Equal(TaskStatus.Faulted, task.Status); + Assert.Equal(5, entry.Value); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitFalseTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitFalseTest.cs new file mode 100644 index 000000000..f67cbc5ed --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitFalseTest.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskRunConfigureAwaitFalseTest : BaseTest + { + public TaskRunConfigureAwaitFalseTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 5; + }).ConfigureAwait(false); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }).ConfigureAwait(false); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 5; + }).ConfigureAwait(false); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }).ConfigureAwait(false); + + entry.Value = 5; + }).ConfigureAwait(false); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedParallelAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 3; + }).ConfigureAwait(false); + + entry.Value = 5; + }).ConfigureAwait(false); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + }).ConfigureAwait(false); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + }).ConfigureAwait(false); + + Assert.Equal(5, value); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitTrueTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitTrueTest.cs new file mode 100644 index 000000000..745ddfe34 --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskRunConfigureAwaitTrueTest.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskRunConfigureAwaitTrueTest : BaseTest + { + public TaskRunConfigureAwaitTrueTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 5; + }).ConfigureAwait(true); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }).ConfigureAwait(true); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 5; + }).ConfigureAwait(true); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }).ConfigureAwait(true); + + entry.Value = 5; + }).ConfigureAwait(true); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedParallelAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 3; + }).ConfigureAwait(true); + + entry.Value = 5; + }).ConfigureAwait(true); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + }).ConfigureAwait(true); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + }).ConfigureAwait(true); + + Assert.Equal(5, value); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskRunTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskRunTest.cs new file mode 100644 index 000000000..22360ad8b --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskRunTest.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskRunTest : BaseTest + { + public TaskRunTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 5; + }); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 5; + }); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelSynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }); + + entry.Value = 5; + }); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestAwaitNestedParallelAsynchronousTask() + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 3; + }); + + entry.Value = 5; + }); + + Assert.Equal(5, entry.Value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 5; + return entry.Value; + }); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunParallelAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 5; + return entry.Value; + }); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelSynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }); + }); + + Assert.Equal(5, value); + } + + [Fact(Timeout = 5000)] + public async Task TestRunNestedParallelAsynchronousTaskResult() + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 5; + return entry.Value; + }); + }); + + Assert.Equal(5, value); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskWhenAllTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskWhenAllTest.cs new file mode 100644 index 000000000..bc37fe280 --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskWhenAllTest.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskWhenAllTest : BaseTest + { + public TaskWhenAllTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAllWithTwoSynchronousTasks() + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteAsync(entry, 5); + ControlledTask task2 = WriteAsync(entry, 3); + await ControlledTask.WhenAll(task1, task2); + Assert.True(task1.IsCompleted); + Assert.True(task2.IsCompleted); + Assert.True(entry.Value == 5 || entry.Value == 3, $"Found unexpected value."); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAllWithTwoAsynchronousTasks() + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteWithDelayAsync(entry, 3); + ControlledTask task2 = WriteWithDelayAsync(entry, 5); + await ControlledTask.WhenAll(task1, task2); + Assert.True(task1.IsCompleted); + Assert.True(task2.IsCompleted); + Assert.True(entry.Value == 5 || entry.Value == 3, $"Found unexpected value."); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAllWithTwoParallelTasks() + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 5); + }); + + await ControlledTask.WhenAll(task1, task2); + + Assert.True(task1.IsCompleted); + Assert.True(task2.IsCompleted); + Assert.True(entry.Value == 5 || entry.Value == 3, $"Found unexpected value."); + } + + private static async ControlledTask GetWriteResultAsync(int value) + { + await ControlledTask.CompletedTask; + return value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(int value) + { + await ControlledTask.Delay(1); + return value; + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAllWithTwoSynchronousTaskResults() + { + ControlledTask task1 = GetWriteResultAsync(5); + ControlledTask task2 = GetWriteResultAsync(3); + int[] results = await ControlledTask.WhenAll(task1, task2); + Assert.True(task1.IsCompleted); + Assert.True(task2.IsCompleted); + Assert.Equal(2, results.Length); + Assert.Equal(5, results[0]); + Assert.Equal(3, results[1]); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAllWithTwoAsynchronousTaskResults() + { + ControlledTask task1 = GetWriteResultWithDelayAsync(5); + ControlledTask task2 = GetWriteResultWithDelayAsync(3); + int[] results = await ControlledTask.WhenAll(task1, task2); + Assert.True(task1.IsCompleted); + Assert.True(task2.IsCompleted); + Assert.Equal(2, results.Length); + Assert.Equal(5, results[0]); + Assert.Equal(3, results[1]); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAllWithTwoParallelSynchronousTaskResults() + { + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(3); + }); + + int[] results = await ControlledTask.WhenAll(task1, task2); + + Assert.True(task1.IsCompleted); + Assert.True(task2.IsCompleted); + Assert.Equal(2, results.Length); + Assert.Equal(5, results[0]); + Assert.Equal(3, results[1]); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAllWithTwoParallelAsynchronousTaskResults() + { + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(3); + }); + + int[] results = await ControlledTask.WhenAll(task1, task2); + + Assert.True(task1.IsCompleted); + Assert.True(task2.IsCompleted); + Assert.Equal(2, results.Length); + Assert.Equal(5, results[0]); + Assert.Equal(3, results[1]); + } + } +} diff --git a/Tests/Core.Tests/Threading/Tasks/TaskWhenAnyTest.cs b/Tests/Core.Tests/Threading/Tasks/TaskWhenAnyTest.cs new file mode 100644 index 000000000..cd5f0d0eb --- /dev/null +++ b/Tests/Core.Tests/Threading/Tasks/TaskWhenAnyTest.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Core.Tests +{ + public class TaskWhenAnyTest : BaseTest + { + public TaskWhenAnyTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public volatile int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAnyWithTwoSynchronousTasks() + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteAsync(entry, 5); + ControlledTask task2 = WriteAsync(entry, 3); + await ControlledTask.WhenAny(task1, task2); + Assert.True(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Assert.True(entry.Value == 5 || entry.Value == 3, $"Found unexpected value."); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAnyWithTwoAsynchronousTasks() + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteWithDelayAsync(entry, 3); + ControlledTask task2 = WriteWithDelayAsync(entry, 5); + await ControlledTask.WhenAny(task1, task2); + Assert.True(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Assert.True(entry.Value == 5 || entry.Value == 3, $"Found unexpected value."); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAnyWithTwoParallelTasks() + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 5); + }); + + await ControlledTask.WhenAny(task1, task2); + + Assert.True(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Assert.True(entry.Value == 5 || entry.Value == 3, $"Found unexpected value."); + } + + private static async ControlledTask GetWriteResultAsync(int value) + { + await ControlledTask.CompletedTask; + return value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(int value) + { + await ControlledTask.Delay(1); + return value; + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAnyWithTwoSynchronousTaskResults() + { + ControlledTask task1 = GetWriteResultAsync(5); + ControlledTask task2 = GetWriteResultAsync(3); + Task result = await ControlledTask.WhenAny(task1, task2); + Assert.True(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Assert.True(result.Result == 5 || result.Result == 3, $"Found unexpected value."); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAnyWithTwoAsynchronousTaskResults() + { + ControlledTask task1 = GetWriteResultWithDelayAsync(5); + ControlledTask task2 = GetWriteResultWithDelayAsync(3); + Task result = await ControlledTask.WhenAny(task1, task2); + Assert.True(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Assert.True(result.Result == 5 || result.Result == 3, $"Found unexpected value."); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAnyWithTwoParallelSynchronousTaskResults() + { + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(3); + }); + + Task result = await ControlledTask.WhenAny(task1, task2); + + Assert.True(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Assert.True(result.Result == 5 || result.Result == 3, $"Found unexpected value."); + } + + [Fact(Timeout = 5000)] + public async Task TestWhenAnyWithTwoParallelAsynchronousTaskResults() + { + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(3); + }); + + Task result = await ControlledTask.WhenAny(task1, task2); + + Assert.True(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Assert.True(result.Result == 5 || result.Result == 3, $"Found unexpected value."); + } + } +} diff --git a/Tests/Core.Tests/xunit.runner.json b/Tests/Core.Tests/xunit.runner.json new file mode 100644 index 000000000..c17b6b2d2 --- /dev/null +++ b/Tests/Core.Tests/xunit.runner.json @@ -0,0 +1,6 @@ +{ + "parallelizeAssembly": false, + "parallelizeTestCollections": false, + "diagnosticMessages": true, + "longRunningTestSeconds": 5 +} \ No newline at end of file diff --git a/Tests/SharedObjects.Tests/BaseTest.cs b/Tests/SharedObjects.Tests/BaseTest.cs new file mode 100644 index 000000000..1305ae1f0 --- /dev/null +++ b/Tests/SharedObjects.Tests/BaseTest.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +using Microsoft.Coyote.TestingServices; + +using Xunit; +using Xunit.Abstractions; + +using Common = Microsoft.Coyote.Tests.Common; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public abstract class BaseTest : Common.BaseTest + { + public BaseTest(ITestOutputHelper output) + : base(output) + { + } + + protected void AssertSucceeded(Action test) + { + var configuration = GetConfiguration(); + this.AssertSucceeded(configuration, test); + } + + protected void AssertSucceeded(Configuration configuration, Action test) + { + var logger = new Common.TestOutputLogger(this.TestOutput); + + try + { + var engine = BugFindingEngine.Create(configuration, test); + engine.SetLogger(logger); + engine.Run(); + + var numErrors = engine.TestReport.NumOfFoundBugs; + Assert.True(numErrors == 0, GetBugReport(engine)); + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + } + + protected void AssertFailed(Action test, int numExpectedErrors) + { + var configuration = GetConfiguration(); + this.AssertFailed(configuration, test, numExpectedErrors); + } + + protected void AssertFailed(Action test, string expectedOutput) + { + var configuration = GetConfiguration(); + this.AssertFailed(configuration, test, 1, new HashSet { expectedOutput }); + } + + protected void AssertFailed(Action test, int numExpectedErrors, ISet expectedOutputs) + { + var configuration = GetConfiguration(); + this.AssertFailed(configuration, test, numExpectedErrors, expectedOutputs); + } + + protected void AssertFailed(Configuration configuration, Action test, int numExpectedErrors) + { + this.AssertFailed(configuration, test, numExpectedErrors, new HashSet()); + } + + protected void AssertFailed(Configuration configuration, Action test, string expectedOutput) + { + this.AssertFailed(configuration, test, 1, new HashSet { expectedOutput }); + } + + protected void AssertFailed(Configuration configuration, Action test, int numExpectedErrors, ISet expectedOutputs) + { + var logger = new Common.TestOutputLogger(this.TestOutput); + + try + { + var bfEngine = BugFindingEngine.Create(configuration, test); + bfEngine.SetLogger(logger); + bfEngine.Run(); + + CheckErrors(bfEngine, numExpectedErrors, expectedOutputs); + + if (!configuration.EnableCycleDetection) + { + var rEngine = ReplayEngine.Create(configuration, test, bfEngine.ReproducableTrace); + rEngine.SetLogger(logger); + rEngine.Run(); + + Assert.True(rEngine.InternalError.Length == 0, rEngine.InternalError); + CheckErrors(rEngine, numExpectedErrors, expectedOutputs); + } + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + } + + private static void CheckErrors(ITestingEngine engine, int numExpectedErrors, ISet expectedOutputs) + { + var numErrors = engine.TestReport.NumOfFoundBugs; + Assert.Equal(numExpectedErrors, numErrors); + + if (expectedOutputs.Count > 0) + { + var bugReports = new HashSet(); + foreach (var bugReport in engine.TestReport.BugReports) + { + var actual = RemoveNonDeterministicValuesFromReport(bugReport); + bugReports.Add(actual); + } + + foreach (var expected in expectedOutputs) + { + Assert.Contains(expected, bugReports); + } + } + } + + protected void AssertFailedWithException(Action test, Type exceptionType) + { + var configuration = GetConfiguration(); + this.AssertFailedWithException(configuration, test, exceptionType); + } + + protected void AssertFailedWithException(Configuration configuration, Action test, Type exceptionType) + { + Assert.True(exceptionType.IsSubclassOf(typeof(Exception)), "Please configure the test correctly. " + + $"Type '{exceptionType}' is not an exception type."); + + var logger = new Common.TestOutputLogger(this.TestOutput); + + try + { + var bfEngine = BugFindingEngine.Create(configuration, test); + bfEngine.SetLogger(logger); + bfEngine.Run(); + + CheckErrors(bfEngine, exceptionType); + + if (!configuration.EnableCycleDetection) + { + var rEngine = ReplayEngine.Create(configuration, test, bfEngine.ReproducableTrace); + rEngine.SetLogger(logger); + rEngine.Run(); + + Assert.True(rEngine.InternalError.Length == 0, rEngine.InternalError); + CheckErrors(rEngine, exceptionType); + } + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + } + + private static void CheckErrors(ITestingEngine engine, Type exceptionType) + { + var numErrors = engine.TestReport.NumOfFoundBugs; + Assert.Equal(1, numErrors); + + var exception = RemoveNonDeterministicValuesFromReport(engine.TestReport.BugReports.First()). + Split(new[] { '\r', '\n' }).FirstOrDefault(); + Assert.Contains("'" + exceptionType.ToString() + "'", exception); + } + + protected static Configuration GetConfiguration() + { + return Configuration.Create(); + } + + private static string GetBugReport(ITestingEngine engine) + { + string report = string.Empty; + foreach (var bug in engine.TestReport.BugReports) + { + report += bug + "\n"; + } + + return report; + } + + private static string RemoveNonDeterministicValuesFromReport(string report) + { + var result = Regex.Replace(report, @"\'[0-9]+\'", "''"); + result = Regex.Replace(result, @"\([0-9]+\)", "()"); + return result; + } + } +} diff --git a/Tests/SharedObjects.Tests/ProductionSharedObjectsTest.cs b/Tests/SharedObjects.Tests/ProductionSharedObjectsTest.cs new file mode 100644 index 000000000..7c4cc81bb --- /dev/null +++ b/Tests/SharedObjects.Tests/ProductionSharedObjectsTest.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public class ProductionSharedObjectsTest : BaseTest + { + public ProductionSharedObjectsTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public ISharedDictionary Dictionary; + public ISharedCounter Counter; + public TaskCompletionSource Tcs; + + public E(ISharedDictionary dictionary, ISharedCounter counter, TaskCompletionSource tcs) + { + this.Dictionary = dictionary; + this.Counter = counter; + this.Tcs = tcs; + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var dictionary = (this.ReceivedEvent as E).Dictionary; + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + for (int i = 0; i < 100; i++) + { + dictionary.TryAdd(i, i.ToString()); + } + + for (int i = 0; i < 100; i++) + { + var b = dictionary.TryRemove(i, out string v); + this.Assert(b == false || v == i.ToString()); + + if (b) + { + counter.Increment(); + } + } + + var c = dictionary.Count; + this.Assert(c == 0); + tcs.TrySetResult(true); + } + } + + private class N : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var dictionary = (this.ReceivedEvent as E).Dictionary; + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + for (int i = 0; i < 100; i++) + { + var b = dictionary.TryRemove(i, out string v); + this.Assert(b == false || v == i.ToString()); + + if (b) + { + counter.Increment(); + } + } + + tcs.TrySetResult(true); + } + } + + [Fact(Timeout=5000)] + public void TestProductionSharedObjects() + { + var runtime = MachineRuntimeFactory.Create(); + var dictionary = SharedDictionary.Create(runtime); + var counter = SharedCounter.Create(runtime); + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.TrySetResult(true); + tcs2.TrySetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M), new E(dictionary, counter, tcs1)); + var m2 = runtime.CreateMachine(typeof(N), new E(dictionary, counter, tcs2)); + + Task.WaitAll(tcs1.Task, tcs2.Task); + Assert.False(failed); + Assert.True(counter.GetValue() == 100); + } + } +} diff --git a/Tests/SharedObjects.Tests/SharedCounter/MockSharedCounterTest.cs b/Tests/SharedObjects.Tests/SharedCounter/MockSharedCounterTest.cs new file mode 100644 index 000000000..764fc8c42 --- /dev/null +++ b/Tests/SharedObjects.Tests/SharedCounter/MockSharedCounterTest.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public class MockSharedCounterTest : BaseTest + { + public MockSharedCounterTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config : Event + { + public int Flag; + + public Config(int flag) + { + this.Flag = flag; + } + } + + private class E : Event + { + public ISharedCounter Counter; + + public E(ISharedCounter counter) + { + this.Counter = counter; + } + } + + private class Done : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = SharedCounter.Create(this.Id.Runtime, 0); + this.CreateMachine(typeof(N1), new E(counter)); + + counter.Increment(); + var v = counter.GetValue(); + this.Assert(v == 1); + } + } + + private class N1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + counter.Decrement(); + } + } + + [Fact(Timeout=5000)] + public void TestMockSharedCounter1() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + r.CreateMachine(typeof(M1)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var flag = (this.ReceivedEvent as Config).Flag; + + var counter = SharedCounter.Create(this.Id.Runtime, 0); + var n = this.CreateMachine(typeof(N2), new E(counter)); + + int v1 = counter.CompareExchange(10, 0); // if 0 then 10 + int v2 = counter.GetValue(); + + if (flag == 0) + { + this.Assert((v1 == 5 && v2 == 5) || + (v1 == 0 && v2 == 10) || + (v1 == 0 && v2 == 15)); + } + else if (flag == 1) + { + this.Assert((v1 == 0 && v2 == 10) || + (v1 == 0 && v2 == 15)); + } + else if (flag == 2) + { + this.Assert((v1 == 5 && v2 == 5) || + (v1 == 0 && v2 == 15)); + } + else if (flag == 3) + { + this.Assert((v1 == 5 && v2 == 5) || + (v1 == 0 && v2 == 10)); + } + } + } + + private class N2 : Machine + { + private ISharedCounter Counter; + + [Start] + [OnEventDoAction(typeof(Done), nameof(Check))] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Counter = (this.ReceivedEvent as E).Counter; + this.Counter.Add(5); + } + + private void Check() + { + var v = this.Counter.GetValue(); + this.Assert(v == 0); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = SharedCounter.Create(this.Id.Runtime, 0); + var n = this.CreateMachine(typeof(N3), new E(counter)); + + counter.Add(4); + counter.Increment(); + counter.Add(5); + + this.Send(n, new Done()); + } + } + + private class N3 : Machine + { + private ISharedCounter Counter; + + [Start] + [OnEventDoAction(typeof(Done), nameof(Check))] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Counter = (this.ReceivedEvent as E).Counter; + this.Counter.Add(-4); + this.Counter.Decrement(); + + var v = this.Counter.Exchange(100); + this.Counter.Add(-5); + this.Counter.Add(v - 100); + } + + private void Check() + { + var v = this.Counter.GetValue(); + this.Assert(v == 0); + } + } + + [Fact(Timeout=5000)] + public void TestMockSharedCounter2() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M2), new Config(0)); + }); + + this.AssertSucceeded(config, test); + } + + [Fact(Timeout=5000)] + public void TestMockSharedCounter3() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M2), new Config(1)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + + [Fact(Timeout=5000)] + public void TestMockSharedCounter4() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M2), new Config(2)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + + [Fact(Timeout=5000)] + public void TestMockSharedCounter5() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M2), new Config(3)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + + [Fact(Timeout=5000)] + public void TestMockSharedCounter6() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + r.CreateMachine(typeof(M3)); + }); + + this.AssertSucceeded(config, test); + } + } +} diff --git a/Tests/SharedObjects.Tests/SharedCounter/ProductionSharedCounterTest.cs b/Tests/SharedObjects.Tests/SharedCounter/ProductionSharedCounterTest.cs new file mode 100644 index 000000000..e0e256918 --- /dev/null +++ b/Tests/SharedObjects.Tests/SharedCounter/ProductionSharedCounterTest.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public class ProductionSharedCounterTest : BaseTest + { + public ProductionSharedCounterTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public ISharedCounter Counter; + public TaskCompletionSource Tcs; + + public E(ISharedCounter counter, TaskCompletionSource tcs) + { + this.Counter = counter; + this.Tcs = tcs; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + for (int i = 0; i < 100000; i++) + { + counter.Increment(); + + var v1 = counter.GetValue(); + this.Assert(v1 == 1 || v1 == 2); + + counter.Decrement(); + + var v2 = counter.GetValue(); + this.Assert(v2 == 0 || v2 == 1); + + counter.Add(1); + + var v3 = counter.GetValue(); + this.Assert(v3 == 1 || v3 == 2); + + counter.Add(-1); + + var v4 = counter.GetValue(); + this.Assert(v4 == 0 || v4 == 1); + } + + tcs.SetResult(true); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + for (int i = 0; i < 1000000; i++) + { + int v; + + do + { + v = counter.GetValue(); + } + while (v != counter.CompareExchange(v + 5, v)); + + counter.Add(15); + counter.Add(-10); + } + + tcs.SetResult(true); + } + } + + [Fact(Timeout=5000)] + public void TestProductionSharedCounter1() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedCounter.Create(runtime, 0); + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + tcs2.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M1), new E(counter, tcs1)); + var m2 = runtime.CreateMachine(typeof(M1), new E(counter, tcs2)); + + Task.WaitAll(tcs1.Task, tcs2.Task); + Assert.False(failed); + } + + [Fact(Timeout=5000)] + public void TestProductionSharedCounter2() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedCounter.Create(runtime, 0); + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + tcs2.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M2), new E(counter, tcs1)); + var m2 = runtime.CreateMachine(typeof(M2), new E(counter, tcs2)); + + Task.WaitAll(tcs1.Task, tcs2.Task); + Assert.False(failed); + Assert.True(counter.GetValue() == 1000000 * 20); + } + } +} diff --git a/Tests/SharedObjects.Tests/SharedDictionary/MockSharedDictionaryTest.cs b/Tests/SharedObjects.Tests/SharedDictionary/MockSharedDictionaryTest.cs new file mode 100644 index 000000000..b91fa0ba3 --- /dev/null +++ b/Tests/SharedObjects.Tests/SharedDictionary/MockSharedDictionaryTest.cs @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public class MockSharedDictionaryTest : BaseTest + { + public MockSharedDictionaryTest(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + public ISharedDictionary Counter; + + public E1(ISharedDictionary counter) + { + this.Counter = counter; + } + } + + private class E2 : Event + { + public ISharedDictionary Counter; + public bool Flag; + + public E2(ISharedDictionary counter, bool flag) + { + this.Counter = counter; + this.Flag = flag; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = SharedDictionary.Create(this.Id.Runtime); + this.CreateMachine(typeof(N1), new E1(counter)); + + counter.TryAdd(1, "M"); + + var v = counter[1]; + + this.Assert(v == "M"); + } + } + + private class N1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E1).Counter; + counter.TryUpdate(1, "N", "M"); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = SharedDictionary.Create(this.Id.Runtime); + counter.TryAdd(1, "M"); + + // Key not present; will throw an exception. + var v = counter[2]; + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = SharedDictionary.Create(this.Id.Runtime); + this.CreateMachine(typeof(N3), new E1(counter)); + + counter.TryAdd(1, "M"); + + var v = counter[1]; + var c = counter.Count; + + this.Assert(c == 1); + } + } + + private class N3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E1).Counter; + counter.TryUpdate(1, "N", "M"); + } + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = SharedDictionary.Create(this.Id.Runtime); + this.CreateMachine(typeof(N4), new E1(counter)); + + counter.TryAdd(1, "M"); + + var b = counter.TryRemove(1, out string v); + + this.Assert(b == false || v == "M"); + this.Assert(counter.Count == 0); + } + } + + private class N4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E1).Counter; + var b = counter.TryRemove(1, out string v); + + this.Assert(b == false || v == "M"); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E2).Counter; + var flag = (this.ReceivedEvent as E2).Flag; + + counter.TryAdd(1, "M"); + + if (flag) + { + this.CreateMachine(typeof(N5), new E2(counter, false)); + } + + var b = counter.TryGetValue(2, out string v); + + if (!flag) + { + this.Assert(!b); + } + + if (b) + { + this.Assert(v == "N"); + } + } + } + + private class N5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E2).Counter; + bool b = counter.TryGetValue(1, out string v); + + this.Assert(b); + this.Assert(v == "M"); + + counter.TryAdd(2, "N"); + } + } + + private class M6 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E1).Counter; + + this.CreateMachine(typeof(N6), new E1(counter)); + counter.TryAdd(1, "M"); + + var b = counter.TryGetValue(2, out string v); + this.Assert(!b); + } + } + + private class N6 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E1).Counter; + counter.TryAdd(2, "N"); + } + } + + [Fact(Timeout=5000)] + public void TestMockSharedDictionary1() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + r.CreateMachine(typeof(M1)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + + [Fact(Timeout=5000)] + public void TestMockSharedDictionary2() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + r.CreateMachine(typeof(M2)); + }); + + this.AssertFailed(config, test, 1); + } + + [Fact(Timeout=5000)] + public void TestMockSharedDictionary3() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + r.CreateMachine(typeof(M3)); + }); + + this.AssertSucceeded(config, test); + } + + [Fact(Timeout=5000)] + public void TestMockSharedDictionary4() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + r.CreateMachine(typeof(M4)); + }); + + this.AssertSucceeded(config, test); + } + + [Fact(Timeout=5000)] + public void TestMockSharedDictionary5() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + var counter = SharedDictionary.Create(r); + r.CreateMachine(typeof(M5), new E2(counter, true)); + }); + + this.AssertSucceeded(config, test); + } + + [Fact(Timeout=5000)] + public void TestMockSharedDictionary6() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + var counter = SharedDictionary.Create(r); + r.CreateMachine(typeof(M5), new E2(counter, false)); + }); + + this.AssertSucceeded(config, test); + } + + [Fact(Timeout=5000)] + public void TestMockSharedDictionary7() + { + var config = Configuration.Create().WithNumberOfIterations(50); + var test = new Action((r) => + { + var counter = SharedDictionary.Create(r); + r.CreateMachine(typeof(M6), new E1(counter)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + } +} diff --git a/Tests/SharedObjects.Tests/SharedDictionary/ProductionSharedDictionaryTest.cs b/Tests/SharedObjects.Tests/SharedDictionary/ProductionSharedDictionaryTest.cs new file mode 100644 index 000000000..8ae712f25 --- /dev/null +++ b/Tests/SharedObjects.Tests/SharedDictionary/ProductionSharedDictionaryTest.cs @@ -0,0 +1,347 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public class ProductionSharedDictionaryTest : BaseTest + { + public ProductionSharedDictionaryTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public ISharedDictionary Counter; + public TaskCompletionSource Tcs; + + public E(ISharedDictionary counter, TaskCompletionSource tcs) + { + this.Counter = counter; + this.Tcs = tcs; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + var n = this.CreateMachine(typeof(N1), this.ReceivedEvent); + + string v; + while (counter.TryRemove(1, out v) == false) + { + } + + this.Assert(v == "N"); + + tcs.SetResult(true); + } + } + + private class N1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + + var b = counter.TryAdd(1, "N"); + this.Assert(b == true); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.CreateMachine(typeof(N2), this.ReceivedEvent); + } + } + + private class N2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + counter.TryAdd(1, "N"); + + // Key doesn't exist. + var v = counter[2]; + tcs.SetResult(true); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + for (int i = 0; i < 100; i++) + { + counter.TryAdd(1, "M"); + counter[1] = "M"; + } + + var c = counter.Count; + this.Assert(c == 1); + tcs.SetResult(true); + } + } + + private class N3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + for (int i = 0; i < 100; i++) + { + counter.TryAdd(1, "N"); + counter[1] = "N"; + } + + tcs.SetResult(true); + } + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.CreateMachine(typeof(N4), this.ReceivedEvent); + } + } + + private class N4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + counter.TryAdd(1, "N"); + + var b = counter.TryGetValue(2, out string v); + this.Assert(!b); + + b = counter.TryGetValue(1, out v); + + this.Assert(b); + this.Assert(v == "N"); + + tcs.SetResult(true); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var n = this.CreateMachine(typeof(N5), this.ReceivedEvent); + + for (int i = 0; i <= 100000; i++) + { + counter[i] = i.ToString(); + } + } + } + + private class N5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + string v; + + while (!counter.TryGetValue(100000, out v)) + { + } + + for (int i = 100000; i >= 0; i--) + { + var b = counter.TryGetValue(i, out v); + this.Assert(b && v == i.ToString()); + } + + tcs.SetResult(true); + } + } + + [Fact(Timeout=5000)] + public void TestProductionSharedDictionary1() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedDictionary.Create(runtime); + var tcs1 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M1), new E(counter, tcs1)); + + Task.WaitAll(tcs1.Task); + Assert.False(failed); + } + + [Fact(Timeout=5000)] + public void TestProductionSharedDictionary2() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedDictionary.Create(runtime); + var tcs1 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M2), new E(counter, tcs1)); + + Task.WaitAll(tcs1.Task); + Assert.True(failed); + } + + [Fact(Timeout=5000)] + public void TestProductionSharedDictionary3() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedDictionary.Create(runtime); + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + tcs2.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M3), new E(counter, tcs1)); + var m2 = runtime.CreateMachine(typeof(N3), new E(counter, tcs2)); + + Task.WaitAll(tcs1.Task, tcs2.Task); + Assert.False(failed); + } + + [Fact(Timeout=5000)] + public void TestProductionSharedDictionary4() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedDictionary.Create(runtime); + var tcs1 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M4), new E(counter, tcs1)); + + Task.WaitAll(tcs1.Task); + Assert.False(failed); + } + + [Fact(Timeout=5000)] + public void TestProductionSharedDictionary5() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedDictionary.Create(runtime); + var tcs1 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M5), new E(counter, tcs1)); + + Task.WaitAll(tcs1.Task); + Assert.False(failed); + } + } +} diff --git a/Tests/SharedObjects.Tests/SharedObjects.Tests.csproj b/Tests/SharedObjects.Tests/SharedObjects.Tests.csproj new file mode 100644 index 000000000..af0a2fa6a --- /dev/null +++ b/Tests/SharedObjects.Tests/SharedObjects.Tests.csproj @@ -0,0 +1,28 @@ + + + + + Tests for the Coyote shared objects library. + Microsoft.Coyote.SharedObjects.Tests + Microsoft.Coyote.SharedObjects.Tests + ..\bin\ + + + netcoreapp2.1;net46;net47 + + + netcoreapp2.1 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/SharedObjects.Tests/SharedRegister/MockSharedRegisterTest.cs b/Tests/SharedObjects.Tests/SharedRegister/MockSharedRegisterTest.cs new file mode 100644 index 000000000..733452d78 --- /dev/null +++ b/Tests/SharedObjects.Tests/SharedRegister/MockSharedRegisterTest.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public class MockSharedRegisterTest : BaseTest + { + public MockSharedRegisterTest(ITestOutputHelper output) + : base(output) + { + } + + private struct S + { + public int Value1; + public int Value2; + + public S(int value1, int value2) + { + this.Value1 = value1; + this.Value2 = value2; + } + } + + private class E : Event + where T : struct + { + public ISharedRegister Counter; + + public E(ISharedRegister counter) + { + this.Counter = counter; + } + } + + private class Setup : Event + { + public bool Flag; + + public Setup(bool flag) + { + this.Flag = flag; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var flag = (this.ReceivedEvent as Setup).Flag; + + var counter = SharedRegister.Create(this.Id.Runtime, 0); + counter.SetValue(5); + + this.CreateMachine(typeof(N1), new E(counter)); + + counter.Update(x => + { + if (x == 5) + { + return 6; + } + return x; + }); + + var v = counter.GetValue(); + + if (flag) + { + // Succeeds. + this.Assert(v == 2 || v == 6); + } + else + { + // Fails. + this.Assert(v == 6); + } + } + } + + private class N1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + counter.SetValue(2); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var flag = (this.ReceivedEvent as Setup).Flag; + + var counter = SharedRegister.Create(this.Id.Runtime); + counter.SetValue(new S(1, 1)); + + this.CreateMachine(typeof(N2), new E(counter)); + + counter.Update(x => + { + return new S(x.Value1 + 1, x.Value2 + 1); + }); + + var v = counter.GetValue(); + + // Succeeds. + this.Assert(v.Value1 == v.Value2); + + if (flag) + { + // Succeeds. + this.Assert(v.Value1 == 2 || v.Value1 == 5 || v.Value1 == 6); + } + else + { + // Fails. + this.Assert(v.Value1 == 2 || v.Value1 == 6); + } + } + } + + private class N2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + counter.SetValue(new S(5, 5)); + } + } + + [Fact(Timeout=5000)] + public void TestMockSharedRegister1() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M1), new Setup(true)); + }); + + this.AssertSucceeded(config, test); + } + + [Fact(Timeout=5000)] + public void TestMockSharedRegister2() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M1), new Setup(false)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + + [Fact(Timeout=5000)] + public void TestMockSharedRegister3() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M2), new Setup(true)); + }); + + this.AssertSucceeded(config, test); + } + + [Fact(Timeout=5000)] + public void TestMockSharedRegister4() + { + var config = Configuration.Create().WithNumberOfIterations(100); + var test = new Action((r) => + { + r.CreateMachine(typeof(M2), new Setup(false)); + }); + + this.AssertFailed(config, test, "Detected an assertion failure."); + } + } +} diff --git a/Tests/SharedObjects.Tests/SharedRegister/ProductionSharedRegisterTest.cs b/Tests/SharedObjects.Tests/SharedRegister/ProductionSharedRegisterTest.cs new file mode 100644 index 000000000..b3f1f5b9f --- /dev/null +++ b/Tests/SharedObjects.Tests/SharedRegister/ProductionSharedRegisterTest.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.SharedObjects.Tests +{ + public class ProductionSharedRegisterTest : BaseTest + { + public ProductionSharedRegisterTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public ISharedRegister Counter; + public TaskCompletionSource Tcs; + + public E(ISharedRegister counter, TaskCompletionSource tcs) + { + this.Counter = counter; + this.Tcs = tcs; + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var counter = (this.ReceivedEvent as E).Counter; + var tcs = (this.ReceivedEvent as E).Tcs; + + for (int i = 0; i < 1000; i++) + { + counter.Update(x => x + 5); + + var v1 = counter.GetValue(); + this.Assert(v1 == 10 || v1 == 15); + + counter.Update(x => x - 5); + + var v2 = counter.GetValue(); + this.Assert(v2 == 5 || v2 == 10); + } + + tcs.SetResult(true); + } + } + + [Fact(Timeout=5000)] + public void TestProductionSharedRegister() + { + var runtime = MachineRuntimeFactory.Create(); + var counter = SharedRegister.Create(runtime, 0); + counter.SetValue(5); + + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + var failed = false; + + runtime.OnFailure += (ex) => + { + failed = true; + tcs1.SetResult(true); + tcs2.SetResult(true); + }; + + var m1 = runtime.CreateMachine(typeof(M), new E(counter, tcs1)); + var m2 = runtime.CreateMachine(typeof(M), new E(counter, tcs2)); + + Task.WaitAll(tcs1.Task, tcs2.Task); + Assert.False(failed); + } + } +} diff --git a/Tests/TestingServices.Tests/BaseTest.cs b/Tests/TestingServices.Tests/BaseTest.cs new file mode 100644 index 000000000..eb63d2c9a --- /dev/null +++ b/Tests/TestingServices.Tests/BaseTest.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +using Common = Microsoft.Coyote.Tests.Common; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public abstract class BaseTest : Common.BaseTest + { + public BaseTest(ITestOutputHelper output) + : base(output) + { + } + + protected ITestingEngine Test(Action test, Configuration configuration = null) => + this.Test(test as Delegate, configuration); + + protected ITestingEngine Test(Action test, Configuration configuration = null) => + this.Test(test as Delegate, configuration); + + protected ITestingEngine Test(Func test, Configuration configuration = null) => + this.Test(test as Delegate, configuration); + + protected ITestingEngine Test(Func test, Configuration configuration = null) => + this.Test(test as Delegate, configuration); + + private ITestingEngine Test(Delegate test, Configuration configuration) + { + configuration = configuration ?? GetConfiguration(); + + ILogger logger; + if (configuration.IsVerbose) + { + logger = new Common.TestOutputLogger(this.TestOutput, true); + } + else + { + logger = new NulLogger(); + } + + BugFindingEngine engine = null; + + try + { + engine = BugFindingEngine.Create(configuration, test); + engine.SetLogger(logger); + engine.Run(); + + var numErrors = engine.TestReport.NumOfFoundBugs; + Assert.True(numErrors == 0, GetBugReport(engine)); + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + + return engine; + } + + protected void TestWithError(Action test, Configuration configuration = null, string expectedError = null, + bool replay = false) + { + this.TestWithError(test as Delegate, configuration, new string[] { expectedError }, replay); + } + + protected void TestWithError(Action test, Configuration configuration = null, + string expectedError = null, bool replay = false) + { + this.TestWithError(test as Delegate, configuration, new string[] { expectedError }, replay); + } + + protected void TestWithError(Func test, Configuration configuration = null, string expectedError = null, + bool replay = false) + { + this.TestWithError(test as Delegate, configuration, new string[] { expectedError }, replay); + } + + protected void TestWithError(Func test, Configuration configuration = null, + string expectedError = null, bool replay = false) + { + this.TestWithError(test as Delegate, configuration, new string[] { expectedError }, replay); + } + + protected void TestWithError(Action test, Configuration configuration = null, string[] expectedErrors = null, + bool replay = false) + { + this.TestWithError(test as Delegate, configuration, expectedErrors, replay); + } + + protected void TestWithError(Action test, Configuration configuration = null, + string[] expectedErrors = null, bool replay = false) + { + this.TestWithError(test as Delegate, configuration, expectedErrors, replay); + } + + protected void TestWithError(Func test, Configuration configuration = null, string[] expectedErrors = null, + bool replay = false) + { + this.TestWithError(test as Delegate, configuration, expectedErrors, replay); + } + + protected void TestWithError(Func test, Configuration configuration = null, + string[] expectedErrors = null, bool replay = false) + { + this.TestWithError(test as Delegate, configuration, expectedErrors, replay); + } + + private void TestWithError(Delegate test, Configuration configuration, string[] expectedErrors, bool replay) + { + configuration = configuration ?? GetConfiguration(); + + ILogger logger; + if (configuration.IsVerbose) + { + logger = new Common.TestOutputLogger(this.TestOutput, true); + } + else + { + logger = new NulLogger(); + } + + try + { + var bfEngine = BugFindingEngine.Create(configuration, test); + bfEngine.SetLogger(logger); + bfEngine.Run(); + + CheckErrors(bfEngine, expectedErrors); + + if (replay && !configuration.EnableCycleDetection) + { + var rEngine = ReplayEngine.Create(configuration, test, bfEngine.ReproducableTrace); + rEngine.SetLogger(logger); + rEngine.Run(); + + Assert.True(rEngine.InternalError.Length == 0, rEngine.InternalError); + CheckErrors(rEngine, expectedErrors); + } + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + } + + protected void TestWithException(Action test, Configuration configuration = null, bool replay = false) + where TException : Exception + { + this.TestWithException(test as Delegate, configuration, replay); + } + + protected void TestWithException(Action test, Configuration configuration = null, + bool replay = false) + where TException : Exception + { + this.TestWithException(test as Delegate, configuration, replay); + } + + protected void TestWithException(Func test, Configuration configuration = null, bool replay = false) + where TException : Exception + { + this.TestWithException(test as Delegate, configuration, replay); + } + + protected void TestWithException(Func test, Configuration configuration = null, + bool replay = false) + where TException : Exception + { + this.TestWithException(test as Delegate, configuration, replay); + } + + private void TestWithException(Delegate test, Configuration configuration, bool replay) + where TException : Exception + { + configuration = configuration ?? GetConfiguration(); + + Type exceptionType = typeof(TException); + Assert.True(exceptionType.IsSubclassOf(typeof(Exception)), "Please configure the test correctly. " + + $"Type '{exceptionType}' is not an exception type."); + + ILogger logger; + if (configuration.IsVerbose) + { + logger = new Common.TestOutputLogger(this.TestOutput, true); + } + else + { + logger = new NulLogger(); + } + + try + { + var bfEngine = BugFindingEngine.Create(configuration, test); + bfEngine.SetLogger(logger); + bfEngine.Run(); + + CheckErrors(bfEngine, exceptionType); + + if (replay && !configuration.EnableCycleDetection) + { + var rEngine = ReplayEngine.Create(configuration, test, bfEngine.ReproducableTrace); + rEngine.SetLogger(logger); + rEngine.Run(); + + Assert.True(rEngine.InternalError.Length == 0, rEngine.InternalError); + CheckErrors(rEngine, exceptionType); + } + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + finally + { + logger.Dispose(); + } + } + + private static void CheckErrors(ITestingEngine engine, IEnumerable expectedErrors) + { + Assert.True(engine.TestReport.NumOfFoundBugs > 0); + foreach (var bugReport in engine.TestReport.BugReports) + { + var actual = RemoveNonDeterministicValuesFromReport(bugReport); + Assert.Contains(actual, expectedErrors); + } + } + + private static void CheckErrors(ITestingEngine engine, Type exceptionType) + { + Assert.Equal(1, engine.TestReport.NumOfFoundBugs); + Assert.Contains("'" + exceptionType.FullName + "'", + engine.TestReport.BugReports.First().Split(new[] { '\r', '\n' }).FirstOrDefault()); + } + + protected static Configuration GetConfiguration() + { + return Configuration.Create(); + } + + protected static string GetBugReport(ITestingEngine engine) + { + string report = string.Empty; + foreach (var bug in engine.TestReport.BugReports) + { + report += bug + "\n"; + } + + return report; + } + + protected static string RemoveNonDeterministicValuesFromReport(string report) + { + string result; + + // Match a GUID or other ids (since they can be nondeterministic). + result = Regex.Replace(report, @"\'[0-9|a-z|A-Z|-]{36}\'|\'[0-9]+\'", "''"); + result = Regex.Replace(result, @"\([^)]*\)", "()"); + result = Regex.Replace(result, @"\[[^)]*\]", "[]"); + + // Match a namespace. + result = RemoveNamespaceReferencesFromReport(result); + return result; + } + + protected static string RemoveNamespaceReferencesFromReport(string report) + { + return Regex.Replace(report, @"Microsoft\.[^+]*\+", string.Empty); + } + + protected static string RemoveExcessiveEmptySpaceFromReport(string report) + { + return Regex.Replace(report, @"\s+", " "); + } + } +} diff --git a/Tests/TestingServices.Tests/Coverage/ActivityCoverageTest.cs b/Tests/TestingServices.Tests/Coverage/ActivityCoverageTest.cs new file mode 100644 index 000000000..b6c4a8682 --- /dev/null +++ b/Tests/TestingServices.Tests/Coverage/ActivityCoverageTest.cs @@ -0,0 +1,361 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.TestingServices.Coverage; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class ActivityCoverageTest : BaseTest + { + public ActivityCoverageTest(ITestOutputHelper output) + : base(output) + { + } + + private class Setup : Event + { + public readonly MachineId Id; + + public Setup(MachineId id) + { + this.Id = id; + } + } + + private class E : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Goto(); + } + + private class Done : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestMachineStateTransitionActivityCoverage() + { + var configuration = Configuration.Create(); + configuration.ReportActivityCoverage = true; + + ITestingEngine testingEngine = this.Test(r => + { + r.CreateMachine(typeof(M1)); + }, + configuration); + + string result; + var activityCoverageReporter = new ActivityCoverageReporter(testingEngine.TestReport.CoverageInfo); + using (var writer = new StringWriter()) + { + activityCoverageReporter.WriteCoverageText(writer); + result = RemoveNamespaceReferencesFromReport(writer.ToString()); + result = RemoveExcessiveEmptySpaceFromReport(result); + } + + var expected = @"Total event coverage: 100.0% +Machine: M1 +*************** +Machine event coverage: 100.0% + State: Init + State event coverage: 100.0% + Next states: Done + State: Done + State event coverage: 100.0% + Previous states: Init +"; + + expected = RemoveExcessiveEmptySpaceFromReport(expected); + Assert.Equal(expected, result); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E), typeof(Done))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new E()); + } + + private class Done : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public void TestMachineRaiseEventActivityCoverage() + { + var configuration = Configuration.Create(); + configuration.ReportActivityCoverage = true; + + ITestingEngine testingEngine = this.Test(r => + { + r.CreateMachine(typeof(M2)); + }, + configuration); + + string result; + var activityCoverageReporter = new ActivityCoverageReporter(testingEngine.TestReport.CoverageInfo); + using (var writer = new StringWriter()) + { + activityCoverageReporter.WriteCoverageText(writer); + result = RemoveNamespaceReferencesFromReport(writer.ToString()); + result = RemoveExcessiveEmptySpaceFromReport(result); + } + + var expected = @"Total event coverage: 100.0% +Machine: M2 +*************** +Machine event coverage: 100.0% + State: Init + State event coverage: 100.0% + Next states: Done + State: Done + State event coverage: 100.0% + Previous states: Init +"; + + expected = RemoveExcessiveEmptySpaceFromReport(expected); + Assert.Equal(expected, result); + } + + private class M3A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E), typeof(Done))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.CreateMachine(typeof(M3B), new Setup(this.Id)); + } + + private class Done : MachineState + { + } + } + + private class M3B : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var id = (this.ReceivedEvent as Setup).Id; + this.Send(id, new E()); + } + } + + [Fact(Timeout = 5000)] + public void TestMachineSendEventActivityCoverage() + { + var configuration = Configuration.Create(); + configuration.ReportActivityCoverage = true; + + ITestingEngine testingEngine = this.Test(r => + { + r.CreateMachine(typeof(M3A)); + }, + configuration); + + string result; + var activityCoverageReporter = new ActivityCoverageReporter(testingEngine.TestReport.CoverageInfo); + using (var writer = new StringWriter()) + { + activityCoverageReporter.WriteCoverageText(writer); + result = RemoveNamespaceReferencesFromReport(writer.ToString()); + result = RemoveExcessiveEmptySpaceFromReport(result); + } + + var expected = @"Total event coverage: 100.0% +Machine: M3A +*************** +Machine event coverage: 100.0% + State: Init + State event coverage: 100.0% + Events received: E + Next states: Done + State: Done + State event coverage: 100.0% + Previous states: Init + +Machine: M3B +*************** +Machine event coverage: 100.0% + State: Init + State event coverage: 100.0% + Events sent: E +"; + + expected = RemoveExcessiveEmptySpaceFromReport(expected); + Assert.Equal(expected, result); + } + + internal class M4 : Machine + { + [Start] + [OnEventGotoState(typeof(E), typeof(Done))] + internal class Init : MachineState + { + } + + internal class Done : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public void TestCoverageOnMultipleTests() + { + var configuration = Configuration.Create(); + configuration.ReportActivityCoverage = true; + + ITestingEngine testingEngine1 = this.Test(r => + { + var m = r.CreateMachine(typeof(M4)); + r.SendEvent(m, new E()); + }, + configuration); + + // Assert that the coverage is as expected. + var coverage1 = testingEngine1.TestReport.CoverageInfo; + Assert.Contains(typeof(M4).FullName, coverage1.MachinesToStates.Keys); + Assert.Contains(typeof(M4.Init).Name, coverage1.MachinesToStates[typeof(M4).FullName]); + Assert.Contains(typeof(M4.Done).Name, coverage1.MachinesToStates[typeof(M4).FullName]); + Assert.Contains(coverage1.RegisteredEvents, tup => tup.Item3 == typeof(E).FullName); + + ITestingEngine testingEngine2 = this.Test(r => + { + var m = r.CreateMachine(typeof(M4)); + r.SendEvent(m, new E()); + }, + configuration); + + // Assert that the coverage is the same as before. + var coverage2 = testingEngine2.TestReport.CoverageInfo; + Assert.Contains(typeof(M4).FullName, coverage2.MachinesToStates.Keys); + Assert.Contains(typeof(M4.Init).Name, coverage2.MachinesToStates[typeof(M4).FullName]); + Assert.Contains(typeof(M4.Done).Name, coverage2.MachinesToStates[typeof(M4).FullName]); + Assert.Contains(coverage2.RegisteredEvents, tup => tup.Item3 == typeof(E).FullName); + + string coverageReport1, coverageReport2; + + var activityCoverageReporter = new ActivityCoverageReporter(coverage1); + using (var writer = new StringWriter()) + { + activityCoverageReporter.WriteCoverageText(writer); + coverageReport1 = writer.ToString(); + } + + activityCoverageReporter = new ActivityCoverageReporter(coverage2); + using (var writer = new StringWriter()) + { + activityCoverageReporter.WriteCoverageText(writer); + coverageReport2 = writer.ToString(); + } + + Assert.Equal(coverageReport1, coverageReport2); + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + internal class M5 : Machine + { + [Start] + [OnEventGotoState(typeof(E1), typeof(Done))] + [OnEventGotoState(typeof(E2), typeof(Done))] + internal class Init : MachineState + { + } + + internal class Done : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public void TestUncoveredEvents() + { + var configuration = Configuration.Create(); + configuration.ReportActivityCoverage = true; + + ITestingEngine testingEngine = this.Test(r => + { + var m = r.CreateMachine(typeof(M5)); + r.SendEvent(m, new E1()); + }, + configuration); + + string result; + var activityCoverageReporter = new ActivityCoverageReporter(testingEngine.TestReport.CoverageInfo); + using (var writer = new StringWriter()) + { + activityCoverageReporter.WriteCoverageText(writer); + result = RemoveNamespaceReferencesFromReport(writer.ToString()); + result = RemoveExcessiveEmptySpaceFromReport(result); + } + + var expected = @"Total event coverage: 50.0% +Machine: M5 +*************** +Machine event coverage: 50.0% + + State: Init + State event coverage: 50.0% + Events received: E1 + Events not covered: E2 + Next states: Done + + State: Done + State event coverage: 100.0% + Previous states: Init + +Machine: Env +*************** +Machine event coverage: 100.0% + + State: Env + State event coverage: 100.0% + Events sent: E1 +"; + + expected = RemoveExcessiveEmptySpaceFromReport(expected); + Assert.Equal(expected, result); + } + } +} diff --git a/Tests/TestingServices.Tests/EntryPoint/EntryPointEventSendingTest.cs b/Tests/TestingServices.Tests/EntryPoint/EntryPointEventSendingTest.cs new file mode 100644 index 000000000..68e5b95f8 --- /dev/null +++ b/Tests/TestingServices.Tests/EntryPoint/EntryPointEventSendingTest.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class EntryPointEventSendingTest : BaseTest + { + public EntryPointEventSendingTest(ITestOutputHelper output) + : base(output) + { + } + + private class Transfer : Event + { + public int Value; + + public Transfer(int value) + { + this.Value = value; + } + } + + private class M : Machine + { + [Start] + [OnEventDoAction(typeof(Transfer), nameof(HandleTransfer))] + private class Init : MachineState + { + } + + private void HandleTransfer() + { + int value = (this.ReceivedEvent as Transfer).Value; + this.Assert(value > 0, "Value is 0."); + } + } + + [Fact(Timeout=5000)] + public void TestEntryPointEventSending() + { + this.TestWithError(r => + { + MachineId m = r.CreateMachine(typeof(M)); + r.SendEvent(m, new Transfer(0)); + }, + expectedError: "Value is 0.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/EntryPoint/EntryPointMachineCreationTest.cs b/Tests/TestingServices.Tests/EntryPoint/EntryPointMachineCreationTest.cs new file mode 100644 index 000000000..629984ac9 --- /dev/null +++ b/Tests/TestingServices.Tests/EntryPoint/EntryPointMachineCreationTest.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class EntryPointMachineCreationTest : BaseTest + { + public EntryPointMachineCreationTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + private class Init : MachineState + { + } + } + + private class N : Machine + { + [Start] + private class Init : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestEntryPointMachineCreation() + { + this.Test(r => + { + MachineId m = r.CreateMachine(typeof(M)); + MachineId n = r.CreateMachine(typeof(N)); + r.Assert(m != null && m != null, "Machine ids are null."); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/EntryPoint/EntryPointMachineExecutionTest.cs b/Tests/TestingServices.Tests/EntryPoint/EntryPointMachineExecutionTest.cs new file mode 100644 index 000000000..6ddfd5547 --- /dev/null +++ b/Tests/TestingServices.Tests/EntryPoint/EntryPointMachineExecutionTest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class EntryPointMachineExecutionTest : BaseTest + { + public EntryPointMachineExecutionTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + private class Init : MachineState + { + } + } + + private class N : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Assert(false, "Reached test assertion."); + } + } + + [Fact(Timeout=5000)] + public void TestEntryPointMachineExecution() + { + this.TestWithError(r => + { + MachineId m = r.CreateMachine(typeof(M)); + MachineId n = r.CreateMachine(typeof(N)); + }, + expectedError: "Reached test assertion.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/EntryPoint/EntryPointRandomChoiceTest.cs b/Tests/TestingServices.Tests/EntryPoint/EntryPointRandomChoiceTest.cs new file mode 100644 index 000000000..15e76891c --- /dev/null +++ b/Tests/TestingServices.Tests/EntryPoint/EntryPointRandomChoiceTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class EntryPointRandomChoiceTest : BaseTest + { + public EntryPointRandomChoiceTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + private class Init : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestEntryPointRandomChoice() + { + this.Test(r => + { + if (r.Random()) + { + r.CreateMachine(typeof(M)); + } + }); + } + } +} diff --git a/Tests/TestingServices.Tests/EntryPoint/EntryPointThrowExceptionTest.cs b/Tests/TestingServices.Tests/EntryPoint/EntryPointThrowExceptionTest.cs new file mode 100644 index 000000000..1bf9ed2be --- /dev/null +++ b/Tests/TestingServices.Tests/EntryPoint/EntryPointThrowExceptionTest.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class EntryPointThrowExceptionTest : BaseTest + { + public EntryPointThrowExceptionTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + private class Init : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestEntryPointThrowException() + { + this.TestWithException(r => + { + MachineId m = r.CreateMachine(typeof(M)); + throw new InvalidOperationException(); + }, + replay: true); + } + + [Fact(Timeout=5000)] + public void TestEntryPointNoMachinesThrowException() + { + this.TestWithException(r => + { + throw new InvalidOperationException(); + }, + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/LogMessages/Common/CustomLogWriter.cs b/Tests/TestingServices.Tests/LogMessages/Common/CustomLogWriter.cs new file mode 100644 index 000000000..7fac4dcac --- /dev/null +++ b/Tests/TestingServices.Tests/LogMessages/Common/CustomLogWriter.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.TestingServices.Tests.LogMessages +{ + internal class CustomLogWriter : RuntimeLogWriter + { + public override void OnEnqueue(MachineId machineId, string eventName) + { + } + + public override void OnSend(MachineId targetMachineId, MachineId senderId, string senderStateName, string eventName, + Guid opGroupId, bool isTargetHalted) + { + } + + protected override string FormatOnCreateMachineLogMessage(MachineId machineId, MachineId creator) => $"."; + + protected override string FormatOnMachineStateLogMessage(MachineId machineId, string stateName, bool isEntry) => $"."; + } +} diff --git a/Tests/TestingServices.Tests/LogMessages/Common/CustomLogger.cs b/Tests/TestingServices.Tests/LogMessages/Common/CustomLogger.cs new file mode 100644 index 000000000..e6209fd49 --- /dev/null +++ b/Tests/TestingServices.Tests/LogMessages/Common/CustomLogger.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text; +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.TestingServices.Tests.LogMessages +{ + internal class CustomLogger : ILogger + { + private StringBuilder StringBuilder; + + public bool IsVerbose { get; set; } = false; + + public CustomLogger(bool isVerbose) + { + this.StringBuilder = new StringBuilder(); + this.IsVerbose = isVerbose; + } + + public void Write(string value) + { + this.StringBuilder.Append(value); + } + + public void Write(string format, object arg0) + { + this.StringBuilder.AppendFormat(format, arg0.ToString()); + } + + public void Write(string format, object arg0, object arg1) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString()); + } + + public void Write(string format, object arg0, object arg1, object arg2) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + + public void Write(string format, params object[] args) + { + this.StringBuilder.AppendFormat(format, args); + } + + public void WriteLine(string value) + { + this.StringBuilder.AppendLine(value); + } + + public void WriteLine(string format, object arg0) + { + this.StringBuilder.AppendFormat(format, arg0.ToString()); + this.StringBuilder.AppendLine(); + } + + public void WriteLine(string format, object arg0, object arg1) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString()); + this.StringBuilder.AppendLine(); + } + + public void WriteLine(string format, object arg0, object arg1, object arg2) + { + this.StringBuilder.AppendFormat(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + this.StringBuilder.AppendLine(); + } + + public void WriteLine(string format, params object[] args) + { + this.StringBuilder.AppendFormat(format, args); + this.StringBuilder.AppendLine(); + } + + public override string ToString() + { + return this.StringBuilder.ToString(); + } + + public void Dispose() + { + this.StringBuilder.Clear(); + this.StringBuilder = null; + } + } +} diff --git a/Tests/TestingServices.Tests/LogMessages/Common/Machines.cs b/Tests/TestingServices.Tests/LogMessages/Common/Machines.cs new file mode 100644 index 000000000..faea96b58 --- /dev/null +++ b/Tests/TestingServices.Tests/LogMessages/Common/Machines.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; + +namespace Microsoft.Coyote.TestingServices.Tests.LogMessages +{ + internal class E : Event + { + public MachineId Id; + + public E(MachineId id) + { + this.Id = id; + } + } + + internal class Unit : Event + { + } + + internal class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var n = this.CreateMachine(typeof(N)); + this.Send(n, new E(this.Id)); + } + + private void Act() + { + this.Assert(false, "Bug found!"); + } + } + + internal class N : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void Act() + { + MachineId m = (this.ReceivedEvent as E).Id; + this.Send(m, new E(this.Id)); + } + } +} diff --git a/Tests/TestingServices.Tests/LogMessages/CustomLogWriterTest.cs b/Tests/TestingServices.Tests/LogMessages/CustomLogWriterTest.cs new file mode 100644 index 000000000..d44cff0af --- /dev/null +++ b/Tests/TestingServices.Tests/LogMessages/CustomLogWriterTest.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests.LogMessages +{ + public class CustomLogWriterTest : BaseTest + { + public CustomLogWriterTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout=5000)] + public void TestCustomLogWriter() + { + Action test = r => + { + r.SetLogWriter(new CustomLogWriter()); + r.CreateMachine(typeof(M)); + }; + + BugFindingEngine engine = BugFindingEngine.Create(GetConfiguration().WithStrategy(SchedulingStrategy.DFS), test); + + try + { + engine.Run(); + + var numErrors = engine.TestReport.NumOfFoundBugs; + Assert.True(numErrors == 1, GetBugReport(engine)); + Assert.True(engine.ReadableTrace != null, "Readable trace is null."); + Assert.True(engine.ReadableTrace.Length > 0, "Readable trace is empty."); + + string expected = @" Running test. +. +. + Machine 'Microsoft.Coyote.TestingServices.Tests.LogMessages.M()' in state 'Init' invoked action 'InitOnEntry'. +. +. + Machine 'Microsoft.Coyote.TestingServices.Tests.LogMessages.N()' in state 'Init' dequeued event 'Microsoft.Coyote.TestingServices.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.TestingServices.Tests.LogMessages.N()' in state 'Init' invoked action 'Act'. + Machine 'Microsoft.Coyote.TestingServices.Tests.LogMessages.M()' in state 'Init' dequeued event 'Microsoft.Coyote.TestingServices.Tests.LogMessages.E'. + Machine 'Microsoft.Coyote.TestingServices.Tests.LogMessages.M()' in state 'Init' invoked action 'Act'. + Bug found! + Found bug using 'DFS' strategy. + Testing statistics: + Found bug. + Scheduling statistics: + Explored schedule: fair and unfair. + Found .% buggy schedules."; + string actual = Regex.Replace(engine.ReadableTrace.ToString(), "[0-9]", string.Empty); + + HashSet expectedSet = new HashSet(Regex.Split(expected, "\r\n|\r|\n")); + HashSet actualSet = new HashSet(Regex.Split(actual, "\r\n|\r|\n")); + + Assert.Equal(expected, actual); + } + catch (Exception ex) + { + Assert.False(true, ex.Message + "\n" + ex.StackTrace); + } + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/EventHandling/ActionsFailTest.cs b/Tests/TestingServices.Tests/Machines/EventHandling/ActionsFailTest.cs new file mode 100644 index 000000000..79a0d825d --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/EventHandling/ActionsFailTest.cs @@ -0,0 +1,364 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class ActionsFailTest : BaseTest + { + public ActionsFailTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config : Event + { + public MachineId Id; + + public Config(MachineId id) + { + this.Id = id; + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class E4 : Event + { + } + + private class E5 : Event + { + public int Value; + + public E5(int value) + { + this.Value = value; + } + } + + private class Unit : Event + { + } + + private class M1A : Machine + { + private MachineId GhostMachine; + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(ExitInit))] + [OnEventGotoState(typeof(E2), typeof(S1))] // exit actions are performed before transition to S1 + [OnEventDoAction(typeof(E4), nameof(Action1))] // E4, E3 have no effect on reachability of assert(false) + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.GhostMachine = this.CreateMachine(typeof(M1B)); + this.Send(this.GhostMachine, new Config(this.Id)); + this.Send(this.GhostMachine, new E1(), options: new SendOptions(assert: 1)); + } + + private void ExitInit() + { + this.Test = true; + } + + [OnEntry(nameof(EntryS1))] + [OnEventGotoState(typeof(Unit), typeof(S2))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Assert(this.Test == true); // holds + this.Raise(new Unit()); + } + + [OnEntry(nameof(EntryS2))] + private class S2 : MachineState + { + } + + private void EntryS2() + { + // this assert is reachable: M1A -E1-> M1B -E2-> M1A; + // then Real_S1 (assert holds), Real_S2 (assert fails) + this.Assert(false); + } + + private void Action1() + { + this.Send(this.GhostMachine, new E3(), options: new SendOptions(assert: 1)); + } + } + + private class M1B : Machine + { + private MachineId RealMachine; + + [Start] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventGotoState(typeof(E1), typeof(S1))] + private class Init : MachineState + { + } + + private void Configure() + { + this.RealMachine = (this.ReceivedEvent as Config).Id; + } + + [OnEntry(nameof(EntryS1))] + [OnEventGotoState(typeof(E3), typeof(S2))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Send(this.RealMachine, new E4(), options: new SendOptions(assert: 1)); + this.Send(this.RealMachine, new E2(), options: new SendOptions(assert: 1)); + } + + private class S2 : MachineState + { + } + } + + /// + /// Tests basic semantics of actions and goto transitions. + /// + [Fact(Timeout=5000)] + public void TestActionsFail1() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1A)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + + private class M2A : Machine + { + private MachineId GhostMachine; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E4), typeof(S2))] + [OnEventPushState(typeof(Unit), typeof(S1))] + [OnEventDoAction(typeof(E2), nameof(Action1))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.GhostMachine = this.CreateMachine(typeof(M2B)); + this.Send(this.GhostMachine, new Config(this.Id)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(EntryS1))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Send(this.GhostMachine, new E1(), options: new SendOptions(assert: 1)); + + // We wait in this state until E2 comes from M2B, + // then handle E2 using the inherited handler Action1 + // installed by Init. + // Then wait until E4 comes from M2B, and since + // there's no handler for E4 in this pushed state, + // this state is popped, and E4 goto handler from Init + // is invoked. + } + + [OnEntry(nameof(EntryS2))] + private class S2 : MachineState + { + } + + private void EntryS2() + { + // this assert is reachable + this.Assert(false); + } + + private void Action1() + { + this.Send(this.GhostMachine, new E3(), options: new SendOptions(assert: 1)); + } + } + + private class M2B : Machine + { + private MachineId RealMachine; + + [Start] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventGotoState(typeof(E1), typeof(S1))] + private class Init : MachineState + { + } + + private void Configure() + { + this.RealMachine = (this.ReceivedEvent as Config).Id; + } + + [OnEntry(nameof(EntryS1))] + [OnEventGotoState(typeof(E3), typeof(S2))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Send(this.RealMachine, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(EntryS2))] + private class S2 : MachineState + { + } + + private void EntryS2() + { + this.Send(this.RealMachine, new E4(), options: new SendOptions(assert: 1)); + } + } + + [Fact(Timeout = 5000)] + public void TestActionsFail2() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2A)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + + private class M3A : Machine + { + private MachineId GhostMachine; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E4), typeof(S2))] + [OnEventPushState(typeof(Unit), typeof(S1))] + [OnEventDoAction(typeof(E5), nameof(Action1))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.GhostMachine = this.CreateMachine(typeof(M3B)); + this.Send(this.GhostMachine, new Config(this.Id)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(EntryS1))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Send(this.GhostMachine, new E1(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(EntryS2))] + private class S2 : MachineState + { + } + + private void EntryS2() + { + // this assert is reachable + this.Assert(false); + } + + private void Action1() + { + this.Send(this.GhostMachine, new E3(), options: new SendOptions(assert: 1)); + } + } + + private class M3B : Machine + { + private MachineId RealMachine; + + [Start] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventGotoState(typeof(E1), typeof(S1))] + private class Init : MachineState + { + } + + private void Configure() + { + this.RealMachine = (this.ReceivedEvent as Config).Id; + } + + [OnEntry(nameof(EntryS1))] + [OnEventGotoState(typeof(E3), typeof(S2))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Send(this.RealMachine, new E5(100), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(EntryS2))] + private class S2 : MachineState + { + } + + private void EntryS2() + { + this.Send(this.RealMachine, new E4(), options: new SendOptions(assert: 1)); + } + } + + [Fact(Timeout = 5000)] + public void TestActionsFail3() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3A)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/EventHandling/IgnoreEvent/IgnoreRaisedTest.cs b/Tests/TestingServices.Tests/Machines/EventHandling/IgnoreEvent/IgnoreRaisedTest.cs new file mode 100644 index 000000000..30a3d0606 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/EventHandling/IgnoreEvent/IgnoreRaisedTest.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class IgnoreRaisedTest : BaseTest + { + public IgnoreRaisedTest(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + public MachineId Mid; + + public E2(MachineId mid) + { + this.Mid = mid; + } + } + + private class Unit : Event + { + } + + private class A : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Foo))] + [IgnoreEvents(typeof(Unit))] + [OnEventDoAction(typeof(E2), nameof(Bar))] + private class Init : MachineState + { + } + + private void Foo() + { + this.Raise(new Unit()); + } + + private void Bar() + { + var e = this.ReceivedEvent as E2; + this.Send(e.Mid, new E2(this.Id)); + } + } + + private class Harness : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var m = this.CreateMachine(typeof(A)); + this.Send(m, new E1()); + this.Send(m, new E2(this.Id)); + var e = await this.Receive(typeof(E2)) as E2; + } + } + + /// + /// Coyote semantics test: testing for ignore of a raised event. + /// + [Fact(Timeout=5000)] + public void TestIgnoreRaisedEventHandled() + { + this.Test(r => + { + r.CreateMachine(typeof(Harness)); + }, + configuration: GetConfiguration().WithNumberOfIterations(5)); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/EventHandling/MaxEventInstancesTest.cs b/Tests/TestingServices.Tests/Machines/EventHandling/MaxEventInstancesTest.cs new file mode 100644 index 000000000..6024672c8 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/EventHandling/MaxEventInstancesTest.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MaxEventInstancesTest : BaseTest + { + public MaxEventInstancesTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config : Event + { + public MachineId Id; + + public Config(MachineId id) + { + this.Id = id; + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + public int Value; + + public E2(int value) + { + this.Value = value; + } + } + + private class E3 : Event + { + } + + private class E4 : Event + { + } + + private class Unit : Event + { + } + + private class M : Machine + { + private MachineId N; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventPushState(typeof(Unit), typeof(S1))] + [OnEventGotoState(typeof(E4), typeof(S2))] + [OnEventDoAction(typeof(E2), nameof(Action1))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.N = this.CreateMachine(typeof(N)); + this.Send(this.N, new Config(this.Id)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(EntryS1))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Send(this.N, new E1(), options: new SendOptions(assert: 1)); + this.Send(this.N, new E1(), options: new SendOptions(assert: 1)); // Error. + } + + [OnEntry(nameof(EntryS2))] + [OnEventGotoState(typeof(Unit), typeof(S3))] + private class S2 : MachineState + { + } + + private void EntryS2() + { + this.Raise(new Unit()); + } + + [OnEventGotoState(typeof(E4), typeof(S3))] + private class S3 : MachineState + { + } + + private void Action1() + { + this.Assert((this.ReceivedEvent as E2).Value == 100); + this.Send(this.N, new E3()); + this.Send(this.N, new E3()); + } + } + + private class N : Machine + { + private MachineId M; + + [Start] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventGotoState(typeof(Unit), typeof(GhostInit))] + private class Init : MachineState + { + } + + private void Configure() + { + this.M = (this.ReceivedEvent as Config).Id; + this.Raise(new Unit()); + } + + [OnEventGotoState(typeof(E1), typeof(S1))] + private class GhostInit : MachineState + { + } + + [OnEntry(nameof(EntryS1))] + [OnEventGotoState(typeof(E3), typeof(S2))] + [IgnoreEvents(typeof(E1))] + private class S1 : MachineState + { + } + + private void EntryS1() + { + this.Send(this.M, new E2(100), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(EntryS2))] + [OnEventGotoState(typeof(E3), typeof(GhostInit))] + private class S2 : MachineState + { + } + + private void EntryS2() + { + this.Send(this.M, new E4()); + this.Send(this.M, new E4()); + this.Send(this.M, new E4()); + } + } + + [Fact(Timeout=5000)] + public void TestMaxEventInstancesAssertionFailure() + { + var configuration = GetConfiguration(); + configuration.SchedulingStrategy = SchedulingStrategy.DFS; + configuration.MaxSchedulingSteps = 6; + + this.TestWithError(r => + { + r.CreateMachine(typeof(M)); + }, + configuration: configuration, + expectedError: "There are more than 1 instances of 'E1' in the input queue of machine 'N()'.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventFailTest.cs b/Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventFailTest.cs new file mode 100644 index 000000000..fd5e47bcf --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventFailTest.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class ReceiveEventFailTest : BaseTest + { + public ReceiveEventFailTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config : Event + { + public MachineId Id; + + public Config(MachineId id) + { + this.Id = id; + } + } + + private class Unit : Event + { + } + + private class Ping : Event + { + } + + private class Pong : Event + { + } + + private class Server : Machine + { + private MachineId Client; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Client = this.CreateMachine(typeof(Client)); + this.Send(this.Client, new Config(this.Id)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [IgnoreEvents(typeof(Pong))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.Client, new Ping()); + } + } + + private class Client : Machine + { + private MachineId Server; + private int Counter; + + [Start] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventGotoState(typeof(Unit), typeof(Active))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Server = (this.ReceivedEvent as Config).Id; + this.Counter = 0; + this.Raise(new Unit()); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private async Task ActiveOnEntry() + { + while (this.Counter < 5) + { + await this.Receive(typeof(Ping)); + this.SendPong(); + } + + this.Raise(new Halt()); + } + + private void SendPong() + { + this.Counter++; + this.Send(this.Server, new Pong()); + } + } + + [Fact(Timeout=5000)] + public void TestOneMachineReceiveEventFailure() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(Server)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Deadlock detected. 'Client()' is waiting to receive an event, but no other controlled tasks are enabled.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestTwoMachinesReceiveEventFailure() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(Server)); + r.CreateMachine(typeof(Server)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Deadlock detected. 'Client()' and 'Client()' are waiting to " + + "receive an event, but no other controlled tasks are enabled.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestThreeMachinesReceiveEventFailure() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(Server)); + r.CreateMachine(typeof(Server)); + r.CreateMachine(typeof(Server)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Deadlock detected. 'Client()', 'Client()' and 'Client()' are " + + "waiting to receive an event, but no other controlled tasks are enabled.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventTest.cs b/Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventTest.cs new file mode 100644 index 000000000..4564b77ae --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/EventHandling/ReceiveEvent/ReceiveEventTest.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class ReceiveEventTest : BaseTest + { + public ReceiveEventTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config : Event + { + public MachineId Id; + + public Config(MachineId id) + { + this.Id = id; + } + } + + private class Unit : Event + { + } + + private class Ping : Event + { + } + + private class Pong : Event + { + } + + private class Server : Machine + { + private MachineId Client; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Client = this.CreateMachine(typeof(Client)); + this.Send(this.Client, new Config(this.Id)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(Pong), nameof(SendPing))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.SendPing(); + } + + private void SendPing() + { + this.Send(this.Client, new Ping()); + } + } + + private class Client : Machine + { + private MachineId Server; + private int Counter; + + [Start] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventGotoState(typeof(Unit), typeof(Active))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Server = (this.ReceivedEvent as Config).Id; + this.Counter = 0; + this.Raise(new Unit()); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private async Task ActiveOnEntry() + { + while (this.Counter < 5) + { + await this.Receive(typeof(Ping)); + this.SendPong(); + } + + this.Raise(new Halt()); + } + + private void SendPong() + { + this.Counter++; + this.Send(this.Server, new Pong()); + } + } + + /// + /// Coyote semantics test: two machines, monitor instantiation parameter. + /// + [Fact(Timeout=5000)] + public void TestReceiveEvent() + { + this.Test(r => + { + r.CreateMachine(typeof(Server)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS)); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/EventHandling/Wildcard/WildCardEventTest.cs b/Tests/TestingServices.Tests/Machines/EventHandling/Wildcard/WildCardEventTest.cs new file mode 100644 index 000000000..d17e553ba --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/EventHandling/Wildcard/WildCardEventTest.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class WildCardEventTest : BaseTest + { + public WildCardEventTest(ITestOutputHelper output) + : base(output) + { + } + + private class A : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Foo))] + [OnEventGotoState(typeof(E2), typeof(S1))] + [DeferEvents(typeof(WildCardEvent))] + private class S0 : MachineState + { + } + + [OnEventDoAction(typeof(E3), nameof(Bar))] + private class S1 : MachineState + { + } + + private void Foo() + { + } + + private void Bar() + { + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class B : Machine + { + [Start] + [OnEntry(nameof(Conf))] + private class Init : MachineState + { + } + + private void Conf() + { + var a = this.CreateMachine(typeof(A)); + this.Send(a, new E3()); + this.Send(a, new E1()); + this.Send(a, new E2()); + } + } + + [Fact(Timeout=5000)] + public void TestWildCardEvent() + { + this.Test(r => + { + r.CreateMachine(typeof(B)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/AmbiguousEventHandlerTest.cs b/Tests/TestingServices.Tests/Machines/Features/AmbiguousEventHandlerTest.cs new file mode 100644 index 000000000..b07463a8a --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/AmbiguousEventHandlerTest.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class AmbiguousEventHandlerTest : BaseTest + { + public AmbiguousEventHandlerTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(HandleE))] + public class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void HandleE() + { + } + +#pragma warning disable CA1801 // Parameter not used + private void HandleE(int k) + { + } +#pragma warning restore CA1801 // Parameter not used + } + + private class Safety : Monitor + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(HandleE))] + public class Init : MonitorState + { + } + + private void InitOnEntry() + { + this.Raise(new E()); + } + + private void HandleE() + { + } + +#pragma warning disable CA1801 // Parameter not used + private void HandleE(int k) + { + } +#pragma warning restore CA1801 // Parameter not used + } + + [Fact(Timeout=5000)] + public void TestAmbiguousMachineEventHandler() + { + this.Test(r => + { + r.CreateMachine(typeof(M)); + }); + } + + [Fact(Timeout=5000)] + public void TestAmbiguousMonitorEventHandler() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Safety)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/CurrentStateTest.cs b/Tests/TestingServices.Tests/Machines/Features/CurrentStateTest.cs new file mode 100644 index 000000000..73f3fecbf --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/CurrentStateTest.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CurrentStateTest : BaseTest + { + public CurrentStateTest(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class Server : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Assert(this.CurrentState == typeof(Init)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Assert(this.CurrentState == typeof(Active)); + } + } + + /// + /// Coyote semantics test: current state must be of the expected type. + /// + [Fact(Timeout=5000)] + public void TestCurrentState() + { + this.Test(r => + { + r.CreateMachine(typeof(Server)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS)); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/DuplicateEventHandlersTest.cs b/Tests/TestingServices.Tests/Machines/Features/DuplicateEventHandlersTest.cs new file mode 100644 index 000000000..5c4124a88 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/DuplicateEventHandlersTest.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class DuplicateEventHandlersTest : BaseTest + { + public DuplicateEventHandlersTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Check1))] + [OnEventDoAction(typeof(E), nameof(Check2))] + private class Init : MachineState + { + } + + private void Check1() + { + } + + private void Check2() + { + } + } + + private class M2 : Machine + { + [Start] + [OnEventGotoState(typeof(E), typeof(S1))] + [OnEventGotoState(typeof(E), typeof(S2))] + private class Init : MachineState + { + } + + private class S1 : MachineState + { + } + + private class S2 : MachineState + { + } + } + + private class M3 : Machine + { + [Start] + [OnEventPushState(typeof(E), typeof(S1))] + [OnEventPushState(typeof(E), typeof(S2))] + private class Init : MachineState + { + } + + private class S1 : MachineState + { + } + + private class S2 : MachineState + { + } + } + + private class M4 : Machine + { + [Start] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(Check1))] + [OnEventDoAction(typeof(E), nameof(Check2))] + private class BaseState : MachineState + { + } + + private void Check1() + { + } + + private void Check2() + { + } + } + + private class M5 : Machine + { + [Start] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(S1))] + [OnEventGotoState(typeof(E), typeof(S2))] + private class BaseState : MachineState + { + } + + private class S1 : MachineState + { + } + + private class S2 : MachineState + { + } + } + + private class M6 : Machine + { + [Start] + private class Init : BaseState + { + } + + [OnEventPushState(typeof(E), typeof(S1))] + [OnEventPushState(typeof(E), typeof(S2))] + private class BaseState : MachineState + { + } + + private class S1 : MachineState + { + } + + private class S2 : MachineState + { + } + } + + private class M7 : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Check))] + [OnEventGotoState(typeof(E), typeof(S1))] + [OnEventPushState(typeof(E), typeof(S2))] + private class Init : MachineState + { + } + + private class S1 : MachineState + { + } + + private class S2 : MachineState + { + } + + private void Check() + { + } + } + + [Fact(Timeout=5000)] + public void TestMachineDuplicateEventHandlerDo() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + expectedError: "Machine 'M1()' declared multiple handlers for event 'E' in state 'M1+Init'."); + } + + [Fact(Timeout=5000)] + public void TestMachineDuplicateEventHandlerGoto() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2)); + }, + expectedError: "Machine 'M2()' declared multiple handlers for event 'E' in state 'M2+Init'."); + } + + [Fact(Timeout=5000)] + public void TestMachineDuplicateEventHandlerPush() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3)); + }, + expectedError: "Machine 'M3()' declared multiple handlers for event 'E' in state 'M3+Init'."); + } + + [Fact(Timeout=5000)] + public void TestMachineDuplicateEventHandlerInheritanceDo() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M4)); + }, + expectedError: "Machine 'M4()' inherited multiple handlers for event 'E' from state 'M4+BaseState' in state 'M4+Init'."); + } + + [Fact(Timeout=5000)] + public void TestMachineDuplicateEventHandlerInheritanceGoto() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M5)); + }, + expectedError: "Machine 'M5()' inherited multiple handlers for event 'E' from state 'M5+BaseState' in state 'M5+Init'."); + } + + [Fact(Timeout=5000)] + public void TestMachineDuplicateEventHandlerInheritancePush() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M6)); + }, + expectedError: "Machine 'M6()' inherited multiple handlers for event 'E' from state 'M6+BaseState' in state 'M6+Init'."); + } + + [Fact(Timeout=5000)] + public void TestMachineDuplicateEventHandlerMixed() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M7)); + }, + expectedError: "Machine 'M7()' declared multiple handlers for event 'E' in state 'M7+Init'."); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/EventInheritanceTest.cs b/Tests/TestingServices.Tests/Machines/Features/EventInheritanceTest.cs new file mode 100644 index 000000000..5ac7e8dd7 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/EventInheritanceTest.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public sealed class MultiPayloadMultiLevelTester + { + internal class E10 : Event + { + public short A10; + public ushort B10; + + public E10(short a10, ushort b10) + : base() + { + Assert.True(a10 == 1); + Assert.True(b10 == 2); + this.A10 = a10; + this.B10 = b10; + } + } + + internal class E1 : E10 + { + public byte A1; + public bool B1; + + public E1(short a10, ushort b10, byte a1, bool b1) + : base(a10, b10) + { + Assert.True(a1 == 30); + Assert.True(b1 == true); + this.A1 = a1; + this.B1 = b1; + } + } + + internal class E2 : E1 + { + public int A2; + public uint B2; + + public E2(short a10, ushort b10, byte a1, bool b1, int a2, uint b2) + : base(a10, b10, a1, b1) + { + Assert.True(a2 == 100); + Assert.True(b2 == 101); + this.A2 = a2; + this.B2 = b2; + } + } + + public static void Test() + { + Assert.True(new E2(1, 2, 30, true, 100, 101) is E1); + } + } + + public sealed class MultiPayloadMultiLevelGenericTester + { + internal class E10 : Event + { + public short A10; + public ushort B10; + + public E10(short a10, ushort b10) + : base() + { + Assert.True(a10 == 1); + Assert.True(b10 == 2); + this.A10 = a10; + this.B10 = b10; + } + } + + internal class E1 : E10 + { + public byte A1; + public bool B1; + + public E1(short a10, ushort b10, byte a1, bool b1) + : base(a10, b10) + { + Assert.True(a1 == 30); + Assert.True(b1 == true); + this.A1 = a1; + this.B1 = b1; + } + } + + internal class E2 : E1 + { + public int A2; + public uint B2; + + public E2(short a10, ushort b10, byte a1, bool b1, int a2, uint b2) + : base(a10, b10, a1, b1) + { + Assert.True(a2 == 100); + Assert.True(b2 == 101); + this.A2 = a2; + this.B2 = b2; + } + } + + public static void Test() + { + var e2 = new E2(1, 2, 30, true, 100, 101); + Assert.True(e2 is E1); + Assert.True(e2 is E10); + } + } + + public class EventInheritanceTest : BaseTest + { + public EventInheritanceTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout=5000)] + public void TestMultiPayloadMultiLevel() + { + MultiPayloadMultiLevelTester.Test(); + } + + [Fact(Timeout=5000)] + public void TestMultiPayloadMultiLevelGeneric() + { + MultiPayloadMultiLevelGenericTester.Test(); + } + + private class A : Machine + { + internal class Configure : Event + { + public TaskCompletionSource TCS; + + public Configure(TaskCompletionSource tcs) + { + this.TCS = tcs; + } + } + + public static int E1count; + public static int E2count; + public static int E3count; + + private TaskCompletionSource TCS; + + public class E3 : E2 + { + } + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(E1_handler))] + [OnEventDoAction(typeof(E2), nameof(E2_handler))] + [OnEventDoAction(typeof(E3), nameof(E3_handler))] + private class S0 : MachineState + { + } + + private void InitOnEntry() + { + this.TCS = (this.ReceivedEvent as Configure).TCS; + } + + private void E1_handler() + { + ++E1count; + Xunit.Assert.True(this.ReceivedEvent is E1); + this.CheckComplete(); + } + + private void E2_handler() + { + ++E2count; + Xunit.Assert.True(this.ReceivedEvent is E1); + Xunit.Assert.True(this.ReceivedEvent is E2); + this.CheckComplete(); + } + + private void E3_handler() + { + ++E3count; + Xunit.Assert.True(this.ReceivedEvent is E1); + Xunit.Assert.True(this.ReceivedEvent is E2); + Xunit.Assert.True(this.ReceivedEvent is E3); + this.CheckComplete(); + } + + private void CheckComplete() + { + if (E1count == 1 && E2count == 1 && E3count == 1) + { + this.TCS.SetResult(true); + } + } + } + + private class E1 : Event + { + } + + private class E2 : E1 + { + } + + [Fact(Timeout=5000)] + public void TestEventInheritanceRun() + { + var tcs = new TaskCompletionSource(); + var configuration = Configuration.Create(); + var runtime = new ProductionRuntime(configuration); + var a = runtime.CreateMachine(typeof(A), null, new A.Configure(tcs)); + runtime.SendEvent(a, new A.E3()); + runtime.SendEvent(a, new E1()); + runtime.SendEvent(a, new E2()); + Assert.True(tcs.Task.Wait(3000), "Test timed out"); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/GroupStateTest.cs b/Tests/TestingServices.Tests/Machines/Features/GroupStateTest.cs new file mode 100644 index 000000000..71b11d341 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/GroupStateTest.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GroupStateTest : BaseTest + { + public GroupStateTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M : Machine + { + private class States1 : StateGroup + { + [Start] + [OnEntry(nameof(States1S1OnEntry))] + [OnEventGotoState(typeof(E), typeof(S2))] + public class S1 : MachineState + { + } + + [OnEntry(nameof(States1S2OnEntry))] + [OnEventGotoState(typeof(E), typeof(States2.S1))] + public class S2 : MachineState + { + } + } + + private class States2 : StateGroup + { + [OnEntry(nameof(States2S1OnEntry))] + [OnEventGotoState(typeof(E), typeof(S2))] + public class S1 : MachineState + { + } + + [OnEntry(nameof(States2S2OnEntry))] + public class S2 : MachineState + { + } + } + + private void States1S1OnEntry() + { + this.Raise(new E()); + } + + private void States1S2OnEntry() + { + this.Raise(new E()); + } + + private void States2S1OnEntry() + { + this.Raise(new E()); + } + + private void States2S2OnEntry() + { + this.Monitor(new E()); + } + } + + private class Safety : Monitor + { + private class States1 : StateGroup + { + [Start] + [OnEventGotoState(typeof(E), typeof(S2))] + public class S1 : MonitorState + { + } + + [OnEntry(nameof(States1S2OnEntry))] + [OnEventGotoState(typeof(E), typeof(States2.S1))] + public class S2 : MonitorState + { + } + } + + private class States2 : StateGroup + { + [OnEntry(nameof(States2S1OnEntry))] + [OnEventGotoState(typeof(E), typeof(S2))] + public class S1 : MonitorState + { + } + + [OnEntry(nameof(States2S2OnEntry))] + public class S2 : MonitorState + { + } + } + + private void States1S2OnEntry() + { + this.Raise(new E()); + } + + private void States2S1OnEntry() + { + this.Raise(new E()); + } + + private void States2S2OnEntry() + { + this.Assert(false); + } + } + + [Fact(Timeout=5000)] + public void TestGroupState() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(Safety)); + r.CreateMachine(typeof(M)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/MachineStateInheritanceTest.cs b/Tests/TestingServices.Tests/Machines/Features/MachineStateInheritanceTest.cs new file mode 100644 index 000000000..4b3a9f6e6 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/MachineStateInheritanceTest.cs @@ -0,0 +1,696 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MachineStateInheritanceTest : BaseTest + { + public MachineStateInheritanceTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(Check))] + private abstract class BaseState : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void Check() + { + this.Assert(false, "Error reached."); + } + } + + private class M2 : Machine + { + [Start] + private class Init : BaseState + { + } + + [Start] + private class BaseState : MachineState + { + } + } + + private class M3 : Machine + { + [Start] + private class Init : BaseState + { + } + + [OnEntry(nameof(BaseOnEntry))] + private class BaseState : MachineState + { + } + + private void BaseOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEntry(nameof(BaseOnEntry))] + private class BaseState : MachineState + { + } + + private void InitOnEntry() + { + } + + private void BaseOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(Check))] + private class BaseState : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void Check() + { + this.Assert(false, "Error reached."); + } + } + + private class M6 : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Check))] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseCheck))] + private class BaseState : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void Check() + { + } + + private void BaseCheck() + { + this.Assert(false, "Error reached."); + } + } + + private class M7 : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Check))] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseCheck))] + private class BaseState : BaseBaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseBaseCheck))] + private class BaseBaseState : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void Check() + { + } + + private void BaseCheck() + { + this.Assert(false, "Error reached."); + } + + private void BaseBaseCheck() + { + this.Assert(false, "Error reached."); + } + } + + private class M8 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseCheck))] + private class BaseState : BaseBaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseBaseCheck))] + private class BaseBaseState : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void BaseCheck() + { + } + + private void BaseBaseCheck() + { + this.Assert(false, "Error reached."); + } + } + + private class M9 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Done))] + private class BaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + } + + private class M10 : Machine + { + [Start] + [OnEventGotoState(typeof(E), typeof(Done))] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M11 : Machine + { + [Start] + [OnEventGotoState(typeof(E), typeof(Done))] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseState : BaseBaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseBaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M12 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Done))] + private class BaseState : BaseBaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseBaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M13 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventPushState(typeof(E), typeof(Done))] + private class BaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + } + + private class M14 : Machine + { + [Start] + [OnEventPushState(typeof(E), typeof(Done))] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventPushState(typeof(E), typeof(Error))] + private class BaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M15 : Machine + { + [Start] + [OnEventPushState(typeof(E), typeof(Done))] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventPushState(typeof(E), typeof(Error))] + private class BaseState : BaseBaseState + { + } + + [OnEventPushState(typeof(E), typeof(Error))] + private class BaseBaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M16 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEventPushState(typeof(E), typeof(Done))] + private class BaseState : BaseBaseState + { + } + + [OnEventPushState(typeof(E), typeof(Error))] + private class BaseBaseState : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + [Fact(Timeout=5000)] + public void TestMachineStateInheritingAbstractState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + expectedError: "Error reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateInheritingStateDuplicateStart() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2)); + }, + expectedError: "Machine 'M2()' can not declare more than one start states."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateInheritingStateOnEntry() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3)); + }, + expectedError: "Error reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingStateOnEntry() + { + this.Test(r => + { + r.CreateMachine(typeof(M4)); + }); + } + + [Fact(Timeout=5000)] + public void TestMachineStateInheritingStateOnEventDoAction() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M5)); + }, + expectedError: "Error reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingStateOnEventDoAction() + { + this.Test(r => + { + r.CreateMachine(typeof(M6)); + }); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingTwoStatesOnEventDoAction() + { + this.Test(r => + { + r.CreateMachine(typeof(M7)); + }); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingDeepStateOnEventDoAction() + { + this.Test(r => + { + r.CreateMachine(typeof(M8)); + }); + } + + [Fact(Timeout=5000)] + public void TestMachineStateInheritingStateOnEventGotoState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M9)); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingStateOnEventGotoState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M10)); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingTwoStatesOnEventGotoState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M11)); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingDeepStateOnEventGotoState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M12)); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateInheritingStateOnEventPushState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M13)); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingStateOnEventPushState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M14)); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingTwoStatesOnEventPushState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M15)); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMachineStateOverridingDeepStateOnEventPushState() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M16)); + }, + expectedError: "Done reached."); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/MethodCallTest.cs b/Tests/TestingServices.Tests/Machines/Features/MethodCallTest.cs new file mode 100644 index 000000000..040f2b929 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/MethodCallTest.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MethodCallTest : BaseTest + { + public MethodCallTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M : Machine + { + private int X; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.X = 2; + Foo(1, 3, this.X); + } + +#pragma warning disable CA1801 // Parameter not used + private static int Foo(int x, int y, int z) => 0; +#pragma warning restore CA1801 // Parameter not used + } + + [Fact(Timeout=5000)] + public void TestMethodCall() + { + this.Test(r => + { + r.CreateMachine(typeof(M)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/NameofTest.cs b/Tests/TestingServices.Tests/Machines/Features/NameofTest.cs new file mode 100644 index 000000000..70bcd3f8a --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/NameofTest.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class NameofTest : BaseTest + { + public NameofTest(ITestOutputHelper output) + : base(output) + { + } + + private static int WithNameofValue; + private static int WithoutNameofValue; + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class M_With_nameof : Machine + { + [Start] + [OnEntry(nameof(Coyote_Init_on_entry_action))] + [OnExit(nameof(Coyote_Init_on_exit_action))] + [OnEventGotoState(typeof(E1), typeof(Next), nameof(Coyote_Init_E1_action))] + private class Init : MachineState + { + } + + [OnEntry(nameof(Coyote_Next_on_entry_action))] + [OnEventDoAction(typeof(E2), nameof(Coyote_Next_E2_action))] + private class Next : MachineState + { + } + + protected void Coyote_Init_on_entry_action() + { + WithNameofValue += 1; + this.Raise(new E1()); + } + + protected void Coyote_Init_on_exit_action() + { + WithNameofValue += 10; + } + + protected void Coyote_Next_on_entry_action() + { + WithNameofValue += 1000; + this.Raise(new E2()); + } + + protected void Coyote_Init_E1_action() + { + WithNameofValue += 100; + } + + protected void Coyote_Next_E2_action() + { + WithNameofValue += 10000; + } + } + + private class M_Without_nameof : Machine + { + [Start] + [OnEntry("Coyote_Init_on_entry_action")] + [OnExit("Coyote_Init_on_exit_action")] + [OnEventGotoState(typeof(E1), typeof(Next), "Coyote_Init_E1_action")] + private class Init : MachineState + { + } + + [OnEntry("Coyote_Next_on_entry_action")] + [OnEventDoAction(typeof(E2), "Coyote_Next_E2_action")] + private class Next : MachineState + { + } + + protected void Coyote_Init_on_entry_action() + { + WithoutNameofValue += 1; + this.Raise(new E1()); + } + + protected void Coyote_Init_on_exit_action() + { + WithoutNameofValue += 10; + } + + protected void Coyote_Next_on_entry_action() + { + WithoutNameofValue += 1000; + this.Raise(new E2()); + } + + protected void Coyote_Init_E1_action() + { + WithoutNameofValue += 100; + } + + protected void Coyote_Next_E2_action() + { + WithoutNameofValue += 10000; + } + } + + [Fact(Timeout=5000)] + public void TestAllNameofWithNameof() + { + this.Test(r => + { + r.CreateMachine(typeof(M_With_nameof)); + }); + + Assert.Equal(11111, WithNameofValue); + } + + [Fact(Timeout=5000)] + public void TestAllNameofWithoutNameof() + { + this.Test(r => + { + r.CreateMachine(typeof(M_Without_nameof)); + }); + + Assert.Equal(11111, WithoutNameofValue); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/PopTest.cs b/Tests/TestingServices.Tests/Machines/Features/PopTest.cs new file mode 100644 index 000000000..5d3bae1b0 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/PopTest.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class PopTest : BaseTest + { + public PopTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(Init))] + public class S1 : MachineState + { + } + + private void Init() + { + this.Pop(); + } + } + + private class N : Machine + { + [Start] + [OnEntry(nameof(Init))] + [OnExit(nameof(Exit))] + public class S1 : MachineState + { + } + + private void Init() + { + this.Goto(); + } + + private void Exit() + { + this.Pop(); + } + + public class S2 : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestUnbalancedPop() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M), "M"); + }, + expectedError: "Machine 'M()' popped with no matching push.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestPopDuringOnExit() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(N), "N"); + }, + expectedError: "Machine 'N()' has called raise, goto, push or pop inside an OnExit method.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Features/ReceiveTest.cs b/Tests/TestingServices.Tests/Machines/Features/ReceiveTest.cs new file mode 100644 index 000000000..edc162c23 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Features/ReceiveTest.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class ReceiveTest : BaseTest + { + public ReceiveTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + this.Send(this.Id, new E()); + await this.Receive(typeof(E)); + this.Assert(false); + } + } + + [Fact(Timeout=5000)] + public void TestAsyncReceiveEvent() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/GenericMachineTest.cs b/Tests/TestingServices.Tests/Machines/GenericMachineTest.cs new file mode 100644 index 000000000..2d178bffe --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/GenericMachineTest.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GenericMachineTest : BaseTest + { + public GenericMachineTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + private T Item; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Item = default; + this.Goto(); + } + + [OnEntry(nameof(ActiveInit))] + private class Active : MachineState + { + } + + private void ActiveInit() + { + this.Assert(this.Item is int); + } + } + + private class N : M + { + } + + [Fact(Timeout=5000)] + public void TestGenericMachine1() + { + this.Test(r => + { + r.CreateMachine(typeof(M)); + }); + } + + [Fact(Timeout=5000)] + public void TestGenericMachine2() + { + this.Test(r => + { + r.CreateMachine(typeof(N)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/BubbleSortAlgorithmTest.cs b/Tests/TestingServices.Tests/Machines/Integration/BubbleSortAlgorithmTest.cs new file mode 100644 index 000000000..843ad8488 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/BubbleSortAlgorithmTest.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class BubbleSortAlgorithmTest : BaseTest + { + public BubbleSortAlgorithmTest(ITestOutputHelper output) + : base(output) + { + } + + private class BubbleSortMachine : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var rev = new List(); + var sorted = new List(); + + for (int i = 0; i < 10; i++) + { + rev.Insert(0, i); + sorted.Add(i); + } + + this.Assert(rev.Count == 10); + + // Assert that simply reversing the list produces a sorted list. + sorted = Reverse(rev); + this.Assert(sorted.Count == 10); + this.Assert(IsSorted(sorted)); + this.Assert(!IsSorted(rev)); + + // Assert that the algorithm returns the sorted list. + sorted = Sort(rev); + this.Assert(sorted.Count == 10); + this.Assert(IsSorted(sorted)); + this.Assert(!IsSorted(rev)); + } + + private static List Reverse(List l) + { + var result = l.ToList(); + + int i = 0; + int s = result.Count; + while (i < s) + { + int temp = result[i]; + result.RemoveAt(i); + result.Insert(0, temp); + i = i + 1; + } + + return result; + } + + private static List Sort(List l) + { + var result = l.ToList(); + + var swapped = true; + while (swapped) + { + int i = 0; + swapped = false; + while (i < result.Count - 1) + { + if (result[i] > result[i + 1]) + { + int temp = result[i]; + result[i] = result[i + 1]; + result[i + 1] = temp; + swapped = true; + } + + i = i + 1; + } + } + + return result; + } + + private static bool IsSorted(List l) + { + int i = 0; + while (i < l.Count - 1) + { + if (l[i] > l[i + 1]) + { + return false; + } + + i = i + 1; + } + + return true; + } + } + + [Fact(Timeout=10000)] + public void TestBubbleSortAlgorithm() + { + this.Test(r => + { + r.CreateMachine(typeof(BubbleSortMachine)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/ChainReplicationTest.cs b/Tests/TestingServices.Tests/Machines/Integration/ChainReplicationTest.cs new file mode 100644 index 000000000..38ba29f84 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/ChainReplicationTest.cs @@ -0,0 +1,1560 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + /// + /// A single-process implementation of the chain replication protocol. + /// + /// The chain replication protocol is described in the following paper: + /// http://www.cs.cornell.edu/home/rvr/papers/OSDI04.pdf + /// + /// This test contains a bug that leads to a safety assertion failure. + /// + public class ChainReplicationTest : BaseTest + { + public ChainReplicationTest(ITestOutputHelper output) + : base(output) + { + } + + private class SentLog + { + public int NextSeqId; + public MachineId Client; + public int Key; + public int Value; + + public SentLog(int nextSeqId, MachineId client, int key, int val) + { + this.NextSeqId = nextSeqId; + this.Client = client; + this.Key = key; + this.Value = val; + } + } + + private class Environment : Machine + { + private List Servers; + private List Clients; + + private int NumOfServers; + + private MachineId ChainReplicationMaster; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Servers = new List(); + this.Clients = new List(); + + this.NumOfServers = 3; + + for (int i = 0; i < this.NumOfServers; i++) + { + MachineId server = null; + + if (i == 0) + { + server = this.CreateMachine( + typeof(ChainReplicationServer), + new ChainReplicationServer.Config(i, true, false)); + } + else if (i == this.NumOfServers - 1) + { + server = this.CreateMachine( + typeof(ChainReplicationServer), + new ChainReplicationServer.Config(i, false, true)); + } + else + { + server = this.CreateMachine( + typeof(ChainReplicationServer), + new ChainReplicationServer.Config(i, false, false)); + } + + this.Servers.Add(server); + } + + this.Monitor( + new InvariantMonitor.Config(this.Servers)); + this.Monitor( + new ServerResponseSeqMonitor.Config(this.Servers)); + + for (int i = 0; i < this.NumOfServers; i++) + { + MachineId pred = null; + MachineId succ = null; + + if (i > 0) + { + pred = this.Servers[i - 1]; + } + else + { + pred = this.Servers[0]; + } + + if (i < this.NumOfServers - 1) + { + succ = this.Servers[i + 1]; + } + else + { + succ = this.Servers[this.NumOfServers - 1]; + } + + this.Send(this.Servers[i], new ChainReplicationServer.PredSucc(pred, succ)); + } + + this.Clients.Add(this.CreateMachine( + typeof(Client), + new Client.Config(0, this.Servers[0], this.Servers[this.NumOfServers - 1], 1))); + + this.Clients.Add(this.CreateMachine( + typeof(Client), + new Client.Config(1, this.Servers[0], this.Servers[this.NumOfServers - 1], 100))); + + this.ChainReplicationMaster = this.CreateMachine( + typeof(ChainReplicationMaster), + new ChainReplicationMaster.Config(this.Servers, this.Clients)); + + this.Raise(new Halt()); + } + } + + private class FailureDetector : Machine + { + internal class Config : Event + { + public MachineId Master; + public List Servers; + + public Config(MachineId master, List servers) + : base() + { + this.Master = master; + this.Servers = servers; + } + } + + internal class FailureDetected : Event + { + public MachineId Server; + + public FailureDetected(MachineId server) + : base() + { + this.Server = server; + } + } + + internal class FailureCorrected : Event + { + public List Servers; + + public FailureCorrected(List servers) + : base() + { + this.Servers = servers; + } + } + + internal class Ping : Event + { + public MachineId Target; + + public Ping(MachineId target) + : base() + { + this.Target = target; + } + } + + internal class Pong : Event + { + } + + private class InjectFailure : Event + { + } + + private class Local : Event + { + } + + private MachineId Master; + private List Servers; + + private int CheckNodeIdx; + private int Failures; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Local), typeof(StartMonitoring))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Master = (this.ReceivedEvent as Config).Master; + this.Servers = (this.ReceivedEvent as Config).Servers; + + this.CheckNodeIdx = 0; + this.Failures = 100; + + this.Raise(new Local()); + } + + [OnEntry(nameof(StartMonitoringOnEntry))] + [OnEventGotoState(typeof(Pong), typeof(StartMonitoring), nameof(HandlePong))] + [OnEventGotoState(typeof(InjectFailure), typeof(HandleFailure))] + private class StartMonitoring : MachineState + { + } + + private void StartMonitoringOnEntry() + { + if (this.Failures < 1) + { + this.Raise(new Halt()); + } + else + { + this.Send(this.Servers[this.CheckNodeIdx], new Ping(this.Id)); + + if (this.Servers.Count > 1) + { + if (this.Random()) + { + this.Send(this.Id, new InjectFailure()); + } + else + { + this.Send(this.Id, new Pong()); + } + } + else + { + this.Send(this.Id, new Pong()); + } + + this.Failures--; + } + } + + private void HandlePong() + { + this.CheckNodeIdx++; + if (this.CheckNodeIdx == this.Servers.Count) + { + this.CheckNodeIdx = 0; + } + } + + [OnEntry(nameof(HandleFailureOnEntry))] + [OnEventGotoState(typeof(FailureCorrected), typeof(StartMonitoring), nameof(ProcessFailureCorrected))] + [IgnoreEvents(typeof(Pong), typeof(InjectFailure))] + private class HandleFailure : MachineState + { + } + + private void HandleFailureOnEntry() + { + this.Send(this.Master, new FailureDetected(this.Servers[this.CheckNodeIdx])); + } + + private void ProcessFailureCorrected() + { + this.CheckNodeIdx = 0; + this.Servers = (this.ReceivedEvent as FailureCorrected).Servers; + } + } + + private class ChainReplicationMaster : Machine + { + internal class Config : Event + { + public List Servers; + public List Clients; + + public Config(List servers, List clients) + : base() + { + this.Servers = servers; + this.Clients = clients; + } + } + + internal class BecomeHead : Event + { + public MachineId Target; + + public BecomeHead(MachineId target) + : base() + { + this.Target = target; + } + } + + internal class BecomeTail : Event + { + public MachineId Target; + + public BecomeTail(MachineId target) + : base() + { + this.Target = target; + } + } + + internal class Success : Event + { + } + + internal class HeadChanged : Event + { + } + + internal class TailChanged : Event + { + } + + private class HeadFailed : Event + { + } + + private class TailFailed : Event + { + } + + private class ServerFailed : Event + { + } + + private class FixSuccessor : Event + { + } + + private class FixPredecessor : Event + { + } + + private class Local : Event + { + } + + private class Done : Event + { + } + + private List Servers; + private List Clients; + + private MachineId FailureDetector; + + private MachineId Head; + private MachineId Tail; + + private int FaultyNodeIndex; + private int LastUpdateReceivedSucc; + private int LastAckSent; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Local), typeof(WaitForFailure))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Servers = (this.ReceivedEvent as Config).Servers; + this.Clients = (this.ReceivedEvent as Config).Clients; + + this.FailureDetector = this.CreateMachine( + typeof(FailureDetector), + new FailureDetector.Config(this.Id, this.Servers)); + + this.Head = this.Servers[0]; + this.Tail = this.Servers[this.Servers.Count - 1]; + + this.Raise(new Local()); + } + + [OnEventGotoState(typeof(HeadFailed), typeof(CorrectHeadFailure))] + [OnEventGotoState(typeof(TailFailed), typeof(CorrectTailFailure))] + [OnEventGotoState(typeof(ServerFailed), typeof(CorrectServerFailure))] + [OnEventDoAction(typeof(FailureDetector.FailureDetected), nameof(CheckWhichNodeFailed))] + private class WaitForFailure : MachineState + { + } + + private void CheckWhichNodeFailed() + { + this.Assert(this.Servers.Count > 1, "All nodes have failed."); + + var failedServer = (this.ReceivedEvent as FailureDetector.FailureDetected).Server; + + if (this.Head.Equals(failedServer)) + { + this.Raise(new HeadFailed()); + } + else if (this.Tail.Equals(failedServer)) + { + this.Raise(new TailFailed()); + } + else + { + for (int i = 0; i < this.Servers.Count - 1; i++) + { + if (this.Servers[i].Equals(failedServer)) + { + this.FaultyNodeIndex = i; + } + } + + this.Raise(new ServerFailed()); + } + } + + [OnEntry(nameof(CorrectHeadFailureOnEntry))] + [OnEventGotoState(typeof(Done), typeof(WaitForFailure), nameof(UpdateFailureDetector))] + [OnEventDoAction(typeof(HeadChanged), nameof(UpdateClients))] + private class CorrectHeadFailure : MachineState + { + } + + private void CorrectHeadFailureOnEntry() + { + this.Servers.RemoveAt(0); + + this.Monitor( + new InvariantMonitor.UpdateServers(this.Servers)); + this.Monitor( + new ServerResponseSeqMonitor.UpdateServers(this.Servers)); + + this.Head = this.Servers[0]; + + this.Send(this.Head, new BecomeHead(this.Id)); + } + + private void UpdateClients() + { + for (int i = 0; i < this.Clients.Count; i++) + { + this.Send(this.Clients[i], new Client.UpdateHeadTail(this.Head, this.Tail)); + } + + this.Raise(new Done()); + } + + private void UpdateFailureDetector() + { + this.Send(this.FailureDetector, new FailureDetector.FailureCorrected(this.Servers)); + } + + [OnEntry(nameof(CorrectTailFailureOnEntry))] + [OnEventGotoState(typeof(Done), typeof(WaitForFailure), nameof(UpdateFailureDetector))] + [OnEventDoAction(typeof(TailChanged), nameof(UpdateClients))] + private class CorrectTailFailure : MachineState + { + } + + private void CorrectTailFailureOnEntry() + { + this.Servers.RemoveAt(this.Servers.Count - 1); + + this.Monitor( + new InvariantMonitor.UpdateServers(this.Servers)); + this.Monitor( + new ServerResponseSeqMonitor.UpdateServers(this.Servers)); + + this.Tail = this.Servers[this.Servers.Count - 1]; + + this.Send(this.Tail, new BecomeTail(this.Id)); + } + + [OnEntry(nameof(CorrectServerFailureOnEntry))] + [OnEventGotoState(typeof(Done), typeof(WaitForFailure), nameof(UpdateFailureDetector))] + [OnEventDoAction(typeof(FixSuccessor), nameof(UpdateClients))] + [OnEventDoAction(typeof(FixPredecessor), nameof(ProcessFixPredecessor))] + [OnEventDoAction(typeof(ChainReplicationServer.NewSuccInfo), nameof(SetLastUpdate))] + [OnEventDoAction(typeof(Success), nameof(ProcessSuccess))] + private class CorrectServerFailure : MachineState + { + } + + private void CorrectServerFailureOnEntry() + { + this.Servers.RemoveAt(this.FaultyNodeIndex); + + this.Monitor( + new InvariantMonitor.UpdateServers(this.Servers)); + this.Monitor( + new ServerResponseSeqMonitor.UpdateServers(this.Servers)); + + this.Raise(new FixSuccessor()); + } + + private void ProcessFixSuccessor() + { + this.Send(this.Servers[this.FaultyNodeIndex], new ChainReplicationServer.NewPredecessor( + this.Id, this.Servers[this.FaultyNodeIndex - 1])); + } + + private void ProcessFixPredecessor() + { + this.Send(this.Servers[this.FaultyNodeIndex - 1], new ChainReplicationServer.NewSuccessor( + this.Id, this.Servers[this.FaultyNodeIndex], this.LastAckSent, this.LastUpdateReceivedSucc)); + } + + private void SetLastUpdate() + { + this.LastUpdateReceivedSucc = (this.ReceivedEvent as + ChainReplicationServer.NewSuccInfo).LastUpdateReceivedSucc; + this.LastAckSent = (this.ReceivedEvent as + ChainReplicationServer.NewSuccInfo).LastAckSent; + this.Raise(new FixPredecessor()); + } + + private void ProcessSuccess() + { + this.Raise(new Done()); + } + } + + private class ChainReplicationServer : Machine + { + internal class Config : Event + { + public int Id; + public bool IsHead; + public bool IsTail; + + public Config(int id, bool isHead, bool isTail) + : base() + { + this.Id = id; + this.IsHead = isHead; + this.IsTail = isTail; + } + } + + internal class PredSucc : Event + { + public MachineId Predecessor; + public MachineId Successor; + + public PredSucc(MachineId pred, MachineId succ) + : base() + { + this.Predecessor = pred; + this.Successor = succ; + } + } + + internal class ForwardUpdate : Event + { + public MachineId Predecessor; + public int NextSeqId; + public MachineId Client; + public int Key; + public int Value; + + public ForwardUpdate(MachineId pred, int nextSeqId, MachineId client, int key, int val) + : base() + { + this.Predecessor = pred; + this.NextSeqId = nextSeqId; + this.Client = client; + this.Key = key; + this.Value = val; + } + } + + internal class BackwardAck : Event + { + public int NextSeqId; + + public BackwardAck(int nextSeqId) + : base() + { + this.NextSeqId = nextSeqId; + } + } + + internal class NewPredecessor : Event + { + public MachineId Master; + public MachineId Predecessor; + + public NewPredecessor(MachineId master, MachineId pred) + : base() + { + this.Master = master; + this.Predecessor = pred; + } + } + + internal class NewSuccessor : Event + { + public MachineId Master; + public MachineId Successor; + public int LastUpdateReceivedSucc; + public int LastAckSent; + + public NewSuccessor(MachineId master, MachineId succ, + int lastUpdateReceivedSucc, int lastAckSent) + : base() + { + this.Master = master; + this.Successor = succ; + this.LastUpdateReceivedSucc = lastUpdateReceivedSucc; + this.LastAckSent = lastAckSent; + } + } + + internal class NewSuccInfo : Event + { + public int LastUpdateReceivedSucc; + public int LastAckSent; + + public NewSuccInfo(int lastUpdateReceivedSucc, int lastAckSent) + : base() + { + this.LastUpdateReceivedSucc = lastUpdateReceivedSucc; + this.LastAckSent = lastAckSent; + } + } + + internal class ResponseToQuery : Event + { + public int Value; + + public ResponseToQuery(int val) + : base() + { + this.Value = val; + } + } + + internal class ResponseToUpdate : Event + { + } + + private class Local : Event + { + } + + private int ServerId; + private bool IsHead; + private bool IsTail; + + private MachineId Predecessor; + private MachineId Successor; + + private Dictionary KeyValueStore; + private List History; + private List SentHistory; + + private int NextSeqId; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Local), typeof(WaitForRequest))] + [OnEventDoAction(typeof(PredSucc), nameof(SetupPredSucc))] + [DeferEvents(typeof(Client.Update), typeof(Client.Query), + typeof(BackwardAck), typeof(ForwardUpdate))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.ServerId = (this.ReceivedEvent as Config).Id; + this.IsHead = (this.ReceivedEvent as Config).IsHead; + this.IsTail = (this.ReceivedEvent as Config).IsTail; + + this.KeyValueStore = new Dictionary(); + this.History = new List(); + this.SentHistory = new List(); + + this.NextSeqId = 0; + } + + private void SetupPredSucc() + { + this.Predecessor = (this.ReceivedEvent as PredSucc).Predecessor; + this.Successor = (this.ReceivedEvent as PredSucc).Successor; + this.Raise(new Local()); + } + + [OnEventGotoState(typeof(Client.Update), typeof(ProcessUpdate), nameof(ProcessUpdateAction))] + [OnEventGotoState(typeof(ForwardUpdate), typeof(ProcessFwdUpdate))] + [OnEventGotoState(typeof(BackwardAck), typeof(ProcessBckAck))] + [OnEventDoAction(typeof(Client.Query), nameof(ProcessQueryAction))] + [OnEventDoAction(typeof(NewPredecessor), nameof(UpdatePredecessor))] + [OnEventDoAction(typeof(NewSuccessor), nameof(UpdateSuccessor))] + [OnEventDoAction(typeof(ChainReplicationMaster.BecomeHead), nameof(ProcessBecomeHead))] + [OnEventDoAction(typeof(ChainReplicationMaster.BecomeTail), nameof(ProcessBecomeTail))] + [OnEventDoAction(typeof(FailureDetector.Ping), nameof(SendPong))] + private class WaitForRequest : MachineState + { + } + + private void ProcessUpdateAction() + { + this.NextSeqId++; + this.Assert(this.IsHead, "Server {0} is not head", this.ServerId); + } + + private void ProcessQueryAction() + { + var client = (this.ReceivedEvent as Client.Query).Client; + var key = (this.ReceivedEvent as Client.Query).Key; + + this.Assert(this.IsTail, "Server {0} is not tail", this.Id); + + if (this.KeyValueStore.ContainsKey(key)) + { + this.Monitor(new ServerResponseSeqMonitor.ResponseToQuery( + this.Id, key, this.KeyValueStore[key])); + + this.Send(client, new ResponseToQuery(this.KeyValueStore[key])); + } + else + { + this.Send(client, new ResponseToQuery(-1)); + } + } + + private void ProcessBecomeHead() + { + this.IsHead = true; + this.Predecessor = this.Id; + + var target = (this.ReceivedEvent as ChainReplicationMaster.BecomeHead).Target; + this.Send(target, new ChainReplicationMaster.HeadChanged()); + } + + private void ProcessBecomeTail() + { + this.IsTail = true; + this.Successor = this.Id; + + for (int i = 0; i < this.SentHistory.Count; i++) + { + this.Monitor(new ServerResponseSeqMonitor.ResponseToUpdate( + this.Id, this.SentHistory[i].Key, this.SentHistory[i].Value)); + + this.Send(this.SentHistory[i].Client, new ResponseToUpdate()); + this.Send(this.Predecessor, new BackwardAck(this.SentHistory[i].NextSeqId)); + } + + var target = (this.ReceivedEvent as ChainReplicationMaster.BecomeTail).Target; + this.Send(target, new ChainReplicationMaster.TailChanged()); + } + + private void SendPong() + { + var target = (this.ReceivedEvent as FailureDetector.Ping).Target; + this.Send(target, new FailureDetector.Pong()); + } + + private void UpdatePredecessor() + { + var master = (this.ReceivedEvent as NewPredecessor).Master; + this.Predecessor = (this.ReceivedEvent as NewPredecessor).Predecessor; + + if (this.History.Count > 0) + { + if (this.SentHistory.Count > 0) + { + this.Send(master, new NewSuccInfo( + this.History[this.History.Count - 1], + this.SentHistory[0].NextSeqId)); + } + else + { + this.Send(master, new NewSuccInfo( + this.History[this.History.Count - 1], + this.History[this.History.Count - 1])); + } + } + } + + private void UpdateSuccessor() + { + var master = (this.ReceivedEvent as NewSuccessor).Master; + this.Successor = (this.ReceivedEvent as NewSuccessor).Successor; + var lastUpdateReceivedSucc = (this.ReceivedEvent as NewSuccessor).LastUpdateReceivedSucc; + var lastAckSent = (this.ReceivedEvent as NewSuccessor).LastAckSent; + + if (this.SentHistory.Count > 0) + { + for (int i = 0; i < this.SentHistory.Count; i++) + { + if (this.SentHistory[i].NextSeqId > lastUpdateReceivedSucc) + { + this.Send(this.Successor, new ForwardUpdate(this.Id, this.SentHistory[i].NextSeqId, + this.SentHistory[i].Client, this.SentHistory[i].Key, this.SentHistory[i].Value)); + } + } + + int tempIndex = -1; + for (int i = this.SentHistory.Count - 1; i >= 0; i--) + { + if (this.SentHistory[i].NextSeqId == lastAckSent) + { + tempIndex = i; + } + } + + for (int i = 0; i < tempIndex; i++) + { + this.Send(this.Predecessor, new BackwardAck(this.SentHistory[0].NextSeqId)); + this.SentHistory.RemoveAt(0); + } + } + + this.Send(master, new ChainReplicationMaster.Success()); + } + + [OnEntry(nameof(ProcessUpdateOnEntry))] + [OnEventGotoState(typeof(Local), typeof(WaitForRequest))] + private class ProcessUpdate : MachineState + { + } + + private void ProcessUpdateOnEntry() + { + var client = (this.ReceivedEvent as Client.Update).Client; + var key = (this.ReceivedEvent as Client.Update).Key; + var value = (this.ReceivedEvent as Client.Update).Value; + + if (this.KeyValueStore.ContainsKey(key)) + { + this.KeyValueStore[key] = value; + } + else + { + this.KeyValueStore.Add(key, value); + } + + this.History.Add(this.NextSeqId); + + this.Monitor( + new InvariantMonitor.HistoryUpdate(this.Id, new List(this.History))); + + this.SentHistory.Add(new SentLog(this.NextSeqId, client, key, value)); + this.Monitor( + new InvariantMonitor.SentUpdate(this.Id, new List(this.SentHistory))); + + this.Send(this.Successor, new ForwardUpdate(this.Id, this.NextSeqId, client, key, value)); + + this.Raise(new Local()); + } + + [OnEntry(nameof(ProcessFwdUpdateOnEntry))] + [OnEventGotoState(typeof(Local), typeof(WaitForRequest))] + private class ProcessFwdUpdate : MachineState + { + } + + private void ProcessFwdUpdateOnEntry() + { + var pred = (this.ReceivedEvent as ForwardUpdate).Predecessor; + var nextSeqId = (this.ReceivedEvent as ForwardUpdate).NextSeqId; + var client = (this.ReceivedEvent as ForwardUpdate).Client; + var key = (this.ReceivedEvent as ForwardUpdate).Key; + var value = (this.ReceivedEvent as ForwardUpdate).Value; + + if (pred.Equals(this.Predecessor)) + { + this.NextSeqId = nextSeqId; + + if (this.KeyValueStore.ContainsKey(key)) + { + this.KeyValueStore[key] = value; + } + else + { + this.KeyValueStore.Add(key, value); + } + + if (!this.IsTail) + { + this.History.Add(nextSeqId); + + this.Monitor( + new InvariantMonitor.HistoryUpdate(this.Id, new List(this.History))); + + this.SentHistory.Add(new SentLog(this.NextSeqId, client, key, value)); + this.Monitor( + new InvariantMonitor.SentUpdate(this.Id, new List(this.SentHistory))); + + this.Send(this.Successor, new ForwardUpdate(this.Id, this.NextSeqId, client, key, value)); + } + else + { + if (!this.IsHead) + { + this.History.Add(nextSeqId); + } + + this.Monitor(new ServerResponseSeqMonitor.ResponseToUpdate( + this.Id, key, value)); + + this.Send(client, new ResponseToUpdate()); + this.Send(this.Predecessor, new BackwardAck(nextSeqId)); + } + } + + this.Raise(new Local()); + } + + [OnEntry(nameof(ProcessBckAckOnEntry))] + [OnEventGotoState(typeof(Local), typeof(WaitForRequest))] + private class ProcessBckAck : MachineState + { + } + + private void ProcessBckAckOnEntry() + { + var nextSeqId = (this.ReceivedEvent as BackwardAck).NextSeqId; + + this.RemoveItemFromSent(nextSeqId); + + if (!this.IsHead) + { + this.Send(this.Predecessor, new BackwardAck(nextSeqId)); + } + + this.Raise(new Local()); + } + + private void RemoveItemFromSent(int seqId) + { + int removeIdx = -1; + + for (int i = this.SentHistory.Count - 1; i >= 0; i--) + { + if (seqId == this.SentHistory[i].NextSeqId) + { + removeIdx = i; + } + } + + if (removeIdx != -1) + { + this.SentHistory.RemoveAt(removeIdx); + } + } + } + + private class Client : Machine + { + internal class Config : Event + { + public int Id; + public MachineId HeadNode; + public MachineId TailNode; + public int Value; + + public Config(int id, MachineId head, MachineId tail, int val) + : base() + { + this.Id = id; + this.HeadNode = head; + this.TailNode = tail; + this.Value = val; + } + } + + internal class UpdateHeadTail : Event + { + public MachineId Head; + public MachineId Tail; + + public UpdateHeadTail(MachineId head, MachineId tail) + : base() + { + this.Head = head; + this.Tail = tail; + } + } + + internal class Update : Event + { + public MachineId Client; + public int Key; + public int Value; + + public Update(MachineId client, int key, int value) + : base() + { + this.Client = client; + this.Key = key; + this.Value = value; + } + } + + internal class Query : Event + { + public MachineId Client; + public int Key; + + public Query(MachineId client, int key) + : base() + { + this.Client = client; + this.Key = key; + } + } + + private class Local : Event + { + } + + private class Done : Event + { + } + + private int ClientId; + + private MachineId HeadNode; + private MachineId TailNode; + + private int StartIn; + private int Next; + + private Dictionary KeyValueStore; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Local), typeof(PumpUpdateRequests))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.ClientId = (this.ReceivedEvent as Config).Id; + + this.HeadNode = (this.ReceivedEvent as Config).HeadNode; + this.TailNode = (this.ReceivedEvent as Config).TailNode; + + this.StartIn = (this.ReceivedEvent as Config).Value; + this.Next = 1; + + this.KeyValueStore = new Dictionary(); + this.KeyValueStore.Add(1 * this.StartIn, 100); + this.KeyValueStore.Add(2 * this.StartIn, 200); + this.KeyValueStore.Add(3 * this.StartIn, 300); + this.KeyValueStore.Add(4 * this.StartIn, 400); + + this.Raise(new Local()); + } + + [OnEntry(nameof(PumpUpdateRequestsOnEntry))] + [OnEventGotoState(typeof(Local), typeof(PumpUpdateRequests), nameof(PumpRequestsLocalAction))] + [OnEventGotoState(typeof(Done), typeof(PumpQueryRequests), nameof(PumpRequestsDoneAction))] + [IgnoreEvents(typeof(ChainReplicationServer.ResponseToUpdate), typeof(ChainReplicationServer.ResponseToQuery))] + private class PumpUpdateRequests : MachineState + { + } + + private void PumpUpdateRequestsOnEntry() + { + this.Send(this.HeadNode, new Update(this.Id, this.Next * this.StartIn, + this.KeyValueStore[this.Next * this.StartIn])); + + if (this.Next >= 3) + { + this.Raise(new Done()); + } + else + { + this.Raise(new Local()); + } + } + + [OnEntry(nameof(PumpQueryRequestsOnEntry))] + [OnEventGotoState(typeof(Local), typeof(PumpQueryRequests), nameof(PumpRequestsLocalAction))] + [IgnoreEvents(typeof(ChainReplicationServer.ResponseToUpdate), typeof(ChainReplicationServer.ResponseToQuery))] + private class PumpQueryRequests : MachineState + { + } + + private void PumpQueryRequestsOnEntry() + { + this.Send(this.TailNode, new Query(this.Id, this.Next * this.StartIn)); + + if (this.Next >= 3) + { + this.Raise(new Halt()); + } + else + { + this.Raise(new Local()); + } + } + + private void PumpRequestsLocalAction() + { + this.Next++; + } + + private void PumpRequestsDoneAction() + { + this.Next = 1; + } + } + + private class InvariantMonitor : Monitor + { + internal class Config : Event + { + public List Servers; + + public Config(List servers) + : base() + { + this.Servers = servers; + } + } + + internal class UpdateServers : Event + { + public List Servers; + + public UpdateServers(List servers) + : base() + { + this.Servers = servers; + } + } + + internal class HistoryUpdate : Event + { + public MachineId Server; + public List History; + + public HistoryUpdate(MachineId server, List history) + : base() + { + this.Server = server; + this.History = history; + } + } + + internal class SentUpdate : Event + { + public MachineId Server; + public List SentHistory; + + public SentUpdate(MachineId server, List sentHistory) + : base() + { + this.Server = server; + this.SentHistory = sentHistory; + } + } + + private class Local : Event + { + } + + private List Servers; + + private Dictionary> History; + private Dictionary> SentHistory; + private List TempSeq; + + private MachineId Next; + private MachineId Prev; + + [Start] + [OnEventGotoState(typeof(Local), typeof(WaitForUpdateMessage))] + [OnEventDoAction(typeof(Config), nameof(Configure))] + private class Init : MonitorState + { + } + + private void Configure() + { + this.Servers = (this.ReceivedEvent as Config).Servers; + this.History = new Dictionary>(); + this.SentHistory = new Dictionary>(); + this.TempSeq = new List(); + + this.Raise(new Local()); + } + + [OnEventDoAction(typeof(HistoryUpdate), nameof(CheckUpdatePropagationInvariant))] + [OnEventDoAction(typeof(SentUpdate), nameof(CheckInprocessRequestsInvariant))] + [OnEventDoAction(typeof(UpdateServers), nameof(ProcessUpdateServers))] + private class WaitForUpdateMessage : MonitorState + { + } + + private void CheckUpdatePropagationInvariant() + { + var server = (this.ReceivedEvent as HistoryUpdate).Server; + var history = (this.ReceivedEvent as HistoryUpdate).History; + + this.IsSorted(history); + + if (this.History.ContainsKey(server)) + { + this.History[server] = history; + } + else + { + this.History.Add(server, history); + } + + // HIST(i+1) <= HIST(i) + this.GetNext(server); + if (this.Next != null && this.History.ContainsKey(this.Next)) + { + this.CheckLessOrEqualThan(this.History[this.Next], this.History[server]); + } + + // HIST(i) <= HIST(i-1) + this.GetPrev(server); + if (this.Prev != null && this.History.ContainsKey(this.Prev)) + { + this.CheckLessOrEqualThan(this.History[server], this.History[this.Prev]); + } + } + + private void CheckInprocessRequestsInvariant() + { + this.ClearTempSeq(); + + var server = (this.ReceivedEvent as SentUpdate).Server; + var sentHistory = (this.ReceivedEvent as SentUpdate).SentHistory; + + this.ExtractSeqId(sentHistory); + + if (this.SentHistory.ContainsKey(server)) + { + this.SentHistory[server] = this.TempSeq; + } + else + { + this.SentHistory.Add(server, this.TempSeq); + } + + this.ClearTempSeq(); + + // HIST(i) == HIST(i+1) + SENT(i) + this.GetNext(server); + if (this.Next != null && this.History.ContainsKey(this.Next)) + { + this.MergeSeq(this.History[this.Next], this.SentHistory[server]); + this.CheckEqual(this.History[server], this.TempSeq); + } + + this.ClearTempSeq(); + + // HIST(i-1) == HIST(i) + SENT(i-1) + this.GetPrev(server); + if (this.Prev != null && this.History.ContainsKey(this.Prev)) + { + this.MergeSeq(this.History[server], this.SentHistory[this.Prev]); + this.CheckEqual(this.History[this.Prev], this.TempSeq); + } + + this.ClearTempSeq(); + } + + private void GetNext(MachineId curr) + { + this.Next = null; + + for (int i = 1; i < this.Servers.Count; i++) + { + if (this.Servers[i - 1].Equals(curr)) + { + this.Next = this.Servers[i]; + } + } + } + + private void GetPrev(MachineId curr) + { + this.Prev = null; + + for (int i = 1; i < this.Servers.Count; i++) + { + if (this.Servers[i].Equals(curr)) + { + this.Prev = this.Servers[i - 1]; + } + } + } + + private void ExtractSeqId(List seq) + { + this.ClearTempSeq(); + + for (int i = seq.Count - 1; i >= 0; i--) + { + if (this.TempSeq.Count > 0) + { + this.TempSeq.Insert(0, seq[i].NextSeqId); + } + else + { + this.TempSeq.Add(seq[i].NextSeqId); + } + } + + this.IsSorted(this.TempSeq); + } + + private void MergeSeq(List seq1, List seq2) + { + this.ClearTempSeq(); + this.IsSorted(seq1); + + if (seq1.Count == 0) + { + this.TempSeq = seq2; + } + else if (seq2.Count == 0) + { + this.TempSeq = seq1; + } + else + { + for (int i = 0; i < seq1.Count; i++) + { + if (seq1[i] < seq2[0]) + { + this.TempSeq.Add(seq1[i]); + } + } + + for (int i = 0; i < seq2.Count; i++) + { + this.TempSeq.Add(seq2[i]); + } + } + + this.IsSorted(this.TempSeq); + } + + private void IsSorted(List seq) + { + for (int i = 0; i < seq.Count - 1; i++) + { + this.Assert(seq[i] < seq[i + 1], "Sequence is not sorted."); + } + } + + private void CheckLessOrEqualThan(List seq1, List seq2) + { + this.IsSorted(seq1); + this.IsSorted(seq2); + + for (int i = 0; i < seq1.Count; i++) + { + if ((i == seq1.Count) || (i == seq2.Count)) + { + break; + } + + this.Assert(seq1[i] <= seq2[i], "{0} not less or equal than {1}.", seq1[i], seq2[i]); + } + } + + private void CheckEqual(List seq1, List seq2) + { + this.IsSorted(seq1); + this.IsSorted(seq2); + + for (int i = 0; i < seq1.Count; i++) + { + if ((i == seq1.Count) || (i == seq2.Count)) + { + break; + } + + this.Assert(seq1[i] == seq2[i], "{0} not equal with {1}.", seq1[i], seq2[i]); + } + } + + private void ClearTempSeq() + { + this.Assert(this.TempSeq.Count <= 6, "Temp sequence has more than 6 elements."); + this.TempSeq.Clear(); + this.Assert(this.TempSeq.Count == 0, "Temp sequence is not cleared."); + } + + private void ProcessUpdateServers() + { + this.Servers = (this.ReceivedEvent as UpdateServers).Servers; + } + } + + private class ServerResponseSeqMonitor : Monitor + { + internal class Config : Event + { + public List Servers; + + public Config(List servers) + : base() + { + this.Servers = servers; + } + } + + internal class UpdateServers : Event + { + public List Servers; + + public UpdateServers(List servers) + : base() + { + this.Servers = servers; + } + } + + internal class ResponseToUpdate : Event + { + public MachineId Tail; + public int Key; + public int Value; + + public ResponseToUpdate(MachineId tail, int key, int val) + : base() + { + this.Tail = tail; + this.Key = key; + this.Value = val; + } + } + + internal class ResponseToQuery : Event + { + public MachineId Tail; + public int Key; + public int Value; + + public ResponseToQuery(MachineId tail, int key, int val) + : base() + { + this.Tail = tail; + this.Key = key; + this.Value = val; + } + } + + private class Local : Event + { + } + + private List Servers; + private Dictionary LastUpdateResponse; + + [Start] + [OnEventGotoState(typeof(Local), typeof(Wait))] + [OnEventDoAction(typeof(Config), nameof(Configure))] + private class Init : MonitorState + { + } + + private void Configure() + { + this.Servers = (this.ReceivedEvent as Config).Servers; + this.LastUpdateResponse = new Dictionary(); + this.Raise(new Local()); + } + + [OnEventDoAction(typeof(ResponseToUpdate), nameof(ResponseToUpdateAction))] + [OnEventDoAction(typeof(ResponseToQuery), nameof(ResponseToQueryAction))] + [OnEventDoAction(typeof(UpdateServers), nameof(ProcessUpdateServers))] + private class Wait : MonitorState + { + } + + private void ResponseToUpdateAction() + { + var tail = (this.ReceivedEvent as ResponseToUpdate).Tail; + var key = (this.ReceivedEvent as ResponseToUpdate).Key; + var value = (this.ReceivedEvent as ResponseToUpdate).Value; + + if (this.Servers.Contains(tail)) + { + if (this.LastUpdateResponse.ContainsKey(key)) + { + this.LastUpdateResponse[key] = value; + } + else + { + this.LastUpdateResponse.Add(key, value); + } + } + } + + private void ResponseToQueryAction() + { + var tail = (this.ReceivedEvent as ResponseToQuery).Tail; + var key = (this.ReceivedEvent as ResponseToQuery).Key; + var value = (this.ReceivedEvent as ResponseToQuery).Value; + + if (this.Servers.Contains(tail)) + { + this.Assert(value == this.LastUpdateResponse[key], "Value {0} is not " + + "equal to {1}", value, this.LastUpdateResponse[key]); + } + } + + private void ProcessUpdateServers() + { + this.Servers = (this.ReceivedEvent as UpdateServers).Servers; + } + } + + [Theory(Timeout = 10000)] + [InlineData(90)] + public void TestSequenceNotSortedInChainReplicationProtocol(int seed) + { + var configuration = GetConfiguration(); + configuration.SchedulingStrategy = Utilities.SchedulingStrategy.FairPCT; + configuration.PrioritySwitchBound = 1; + configuration.MaxSchedulingSteps = 100; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 2; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(InvariantMonitor)); + r.RegisterMonitor(typeof(ServerResponseSeqMonitor)); + r.CreateMachine(typeof(Environment)); + }, + configuration: configuration, + expectedError: "Sequence is not sorted.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/ChordTest.cs b/Tests/TestingServices.Tests/Machines/Integration/ChordTest.cs new file mode 100644 index 000000000..6a8497c1a --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/ChordTest.cs @@ -0,0 +1,912 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + /// + /// A single-process implementation of the chord peer-to-peer look up service. + /// + /// The Chord protocol is described in the following paper: + /// https://pdos.csail.mit.edu/papers/chord:sigcomm01/chord_sigcomm.pdf + /// + /// This test contains a bug that leads to a liveness assertion failure. + /// + public class ChordTest : BaseTest + { + public ChordTest(ITestOutputHelper output) + : base(output) + { + } + + private class Finger + { + public int Start; + public int End; + public MachineId Node; + + public Finger(int start, int end, MachineId node) + { + this.Start = start; + this.End = end; + this.Node = node; + } + } + + private class ClusterManager : Machine + { + internal class CreateNewNode : Event + { + } + + internal class TerminateNode : Event + { + } + + private class Local : Event + { + } + + private int NumOfNodes; + private int NumOfIds; + + private List ChordNodes; + + private List Keys; + private List NodeIds; + + private MachineId Client; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Local), typeof(Waiting))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.NumOfNodes = 3; + this.NumOfIds = (int)Math.Pow(2, this.NumOfNodes); + + this.ChordNodes = new List(); + this.NodeIds = new List { 0, 1, 3 }; + this.Keys = new List { 1, 2, 6 }; + + for (int idx = 0; idx < this.NodeIds.Count; idx++) + { + this.ChordNodes.Add(this.CreateMachine(typeof(ChordNode))); + } + + var nodeKeys = this.AssignKeysToNodes(); + for (int idx = 0; idx < this.ChordNodes.Count; idx++) + { + var keys = nodeKeys[this.NodeIds[idx]]; + this.Send(this.ChordNodes[idx], new ChordNode.Config(this.NodeIds[idx], new HashSet(keys), + new List(this.ChordNodes), new List(this.NodeIds), this.Id)); + } + + this.Client = this.CreateMachine(typeof(Client), new Client.Config(this.Id, new List(this.Keys))); + + this.Raise(new Local()); + } + + [OnEventDoAction(typeof(ChordNode.FindSuccessor), nameof(ForwardFindSuccessor))] + [OnEventDoAction(typeof(CreateNewNode), nameof(ProcessCreateNewNode))] + [OnEventDoAction(typeof(TerminateNode), nameof(ProcessTerminateNode))] + [OnEventDoAction(typeof(ChordNode.JoinAck), nameof(QueryStabilize))] + private class Waiting : MachineState + { + } + + private void ForwardFindSuccessor() + { + this.Send(this.ChordNodes[0], this.ReceivedEvent); + } + + private void ProcessCreateNewNode() + { + int newId = -1; + while ((newId < 0 || this.NodeIds.Contains(newId)) && + this.NodeIds.Count < this.NumOfIds) + { + for (int i = 0; i < this.NumOfIds; i++) + { + if (this.Random()) + { + newId = i; + } + } + } + + this.Assert(newId >= 0, "Cannot create a new node, no ids available."); + + var newNode = this.CreateMachine(typeof(ChordNode)); + + this.NumOfNodes++; + this.NodeIds.Add(newId); + this.ChordNodes.Add(newNode); + + this.Send(newNode, new ChordNode.Join(newId, new List(this.ChordNodes), + new List(this.NodeIds), this.NumOfIds, this.Id)); + } + + private void ProcessTerminateNode() + { + int endId = -1; + while ((endId < 0 || !this.NodeIds.Contains(endId)) && + this.NodeIds.Count > 0) + { + for (int i = 0; i < this.ChordNodes.Count; i++) + { + if (this.Random()) + { + endId = i; + } + } + } + + this.Assert(endId >= 0, "Cannot find a node to terminate."); + + var endNode = this.ChordNodes[endId]; + + this.NumOfNodes--; + this.NodeIds.Remove(endId); + this.ChordNodes.Remove(endNode); + + this.Send(endNode, new ChordNode.Terminate()); + } + + private void QueryStabilize() + { + foreach (var node in this.ChordNodes) + { + this.Send(node, new ChordNode.Stabilize()); + } + } + + private Dictionary> AssignKeysToNodes() + { + var nodeKeys = new Dictionary>(); + for (int i = this.Keys.Count - 1; i >= 0; i--) + { + bool assigned = false; + for (int j = 0; j < this.NodeIds.Count; j++) + { + if (this.Keys[i] <= this.NodeIds[j]) + { + if (nodeKeys.ContainsKey(this.NodeIds[j])) + { + nodeKeys[this.NodeIds[j]].Add(this.Keys[i]); + } + else + { + nodeKeys.Add(this.NodeIds[j], new List()); + nodeKeys[this.NodeIds[j]].Add(this.Keys[i]); + } + + assigned = true; + break; + } + } + + if (!assigned) + { + if (nodeKeys.ContainsKey(this.NodeIds[0])) + { + nodeKeys[this.NodeIds[0]].Add(this.Keys[i]); + } + else + { + nodeKeys.Add(this.NodeIds[0], new List()); + nodeKeys[this.NodeIds[0]].Add(this.Keys[i]); + } + } + } + + return nodeKeys; + } + } + + private class ChordNode : Machine + { + internal class Config : Event + { + public int Id; + public HashSet Keys; + public List Nodes; + public List NodeIds; + public MachineId Manager; + + public Config(int id, HashSet keys, List nodes, + List nodeIds, MachineId manager) + : base() + { + this.Id = id; + this.Keys = keys; + this.Nodes = nodes; + this.NodeIds = nodeIds; + this.Manager = manager; + } + } + + internal class Join : Event + { + public int Id; + public List Nodes; + public List NodeIds; + public int NumOfIds; + public MachineId Manager; + + public Join(int id, List nodes, List nodeIds, + int numOfIds, MachineId manager) + : base() + { + this.Id = id; + this.Nodes = nodes; + this.NodeIds = nodeIds; + this.NumOfIds = numOfIds; + this.Manager = manager; + } + } + + internal class FindSuccessor : Event + { + public MachineId Sender; + public int Key; + + public FindSuccessor(MachineId sender, int key) + : base() + { + this.Sender = sender; + this.Key = key; + } + } + + internal class FindSuccessorResp : Event + { + public MachineId Node; + public int Key; + + public FindSuccessorResp(MachineId node, int key) + : base() + { + this.Node = node; + this.Key = key; + } + } + + internal class FindPredecessor : Event + { + public MachineId Sender; + + public FindPredecessor(MachineId sender) + : base() + { + this.Sender = sender; + } + } + + internal class FindPredecessorResp : Event + { + public MachineId Node; + + public FindPredecessorResp(MachineId node) + : base() + { + this.Node = node; + } + } + + internal class QueryId : Event + { + public MachineId Sender; + + public QueryId(MachineId sender) + : base() + { + this.Sender = sender; + } + } + + internal class QueryIdResp : Event + { + public int Id; + + public QueryIdResp(int id) + : base() + { + this.Id = id; + } + } + + internal class AskForKeys : Event + { + public MachineId Node; + public int Id; + + public AskForKeys(MachineId node, int id) + : base() + { + this.Node = node; + this.Id = id; + } + } + + internal class AskForKeysResp : Event + { + public List Keys; + + public AskForKeysResp(List keys) + : base() + { + this.Keys = keys; + } + } + + private class NotifySuccessor : Event + { + public MachineId Node; + + public NotifySuccessor(MachineId node) + : base() + { + this.Node = node; + } + } + + internal class JoinAck : Event + { + } + + internal class Stabilize : Event + { + } + + internal class Terminate : Event + { + } + + private class Local : Event + { + } + + private int NodeId; + private HashSet Keys; + private int NumOfIds; + + private Dictionary FingerTable; + private MachineId Predecessor; + + private MachineId Manager; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Local), typeof(Waiting))] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventDoAction(typeof(Join), nameof(JoinCluster))] + [DeferEvents(typeof(AskForKeys), typeof(NotifySuccessor), typeof(Stabilize))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.FingerTable = new Dictionary(); + } + + private void Configure() + { + this.NodeId = (this.ReceivedEvent as Config).Id; + this.Keys = (this.ReceivedEvent as Config).Keys; + this.Manager = (this.ReceivedEvent as Config).Manager; + + var nodes = (this.ReceivedEvent as Config).Nodes; + var nodeIds = (this.ReceivedEvent as Config).NodeIds; + + this.NumOfIds = (int)Math.Pow(2, nodes.Count); + + for (var idx = 1; idx <= nodes.Count; idx++) + { + var start = (this.NodeId + (int)Math.Pow(2, idx - 1)) % this.NumOfIds; + var end = (this.NodeId + (int)Math.Pow(2, idx)) % this.NumOfIds; + + var nodeId = GetSuccessorNodeId(start, nodeIds); + this.FingerTable.Add(start, new Finger(start, end, nodes[nodeId])); + } + + for (var idx = 0; idx < nodeIds.Count; idx++) + { + if (nodeIds[idx] == this.NodeId) + { + this.Predecessor = nodes[WrapSubtract(idx, 1, nodeIds.Count)]; + break; + } + } + + this.Raise(new Local()); + } + + private void JoinCluster() + { + this.NodeId = (this.ReceivedEvent as Join).Id; + this.Manager = (this.ReceivedEvent as Join).Manager; + this.NumOfIds = (this.ReceivedEvent as Join).NumOfIds; + + var nodes = (this.ReceivedEvent as Join).Nodes; + var nodeIds = (this.ReceivedEvent as Join).NodeIds; + + for (var idx = 1; idx <= nodes.Count; idx++) + { + var start = (this.NodeId + (int)Math.Pow(2, idx - 1)) % this.NumOfIds; + var end = (this.NodeId + (int)Math.Pow(2, idx)) % this.NumOfIds; + + var nodeId = GetSuccessorNodeId(start, nodeIds); + this.FingerTable.Add(start, new Finger(start, end, nodes[nodeId])); + } + + var successor = this.FingerTable[(this.NodeId + 1) % this.NumOfIds].Node; + + this.Send(this.Manager, new JoinAck()); + this.Send(successor, new NotifySuccessor(this.Id)); + } + + [OnEventDoAction(typeof(FindSuccessor), nameof(ProcessFindSuccessor))] + [OnEventDoAction(typeof(FindSuccessorResp), nameof(ProcessFindSuccessorResp))] + [OnEventDoAction(typeof(FindPredecessor), nameof(ProcessFindPredecessor))] + [OnEventDoAction(typeof(FindPredecessorResp), nameof(ProcessFindPredecessorResp))] + [OnEventDoAction(typeof(QueryId), nameof(ProcessQueryId))] + [OnEventDoAction(typeof(AskForKeys), nameof(SendKeys))] + [OnEventDoAction(typeof(AskForKeysResp), nameof(UpdateKeys))] + [OnEventDoAction(typeof(NotifySuccessor), nameof(UpdatePredecessor))] + [OnEventDoAction(typeof(Stabilize), nameof(ProcessStabilize))] + [OnEventDoAction(typeof(Terminate), nameof(ProcessTerminate))] + private class Waiting : MachineState + { + } + + private void ProcessFindSuccessor() + { + var sender = (this.ReceivedEvent as FindSuccessor).Sender; + var key = (this.ReceivedEvent as FindSuccessor).Key; + + if (this.Keys.Contains(key)) + { + this.Send(sender, new FindSuccessorResp(this.Id, key)); + } + else if (this.FingerTable.ContainsKey(key)) + { + this.Send(sender, new FindSuccessorResp(this.FingerTable[key].Node, key)); + } + else if (this.NodeId.Equals(key)) + { + this.Send(sender, new FindSuccessorResp( + this.FingerTable[(this.NodeId + 1) % this.NumOfIds].Node, key)); + } + else + { + int idToAsk = -1; + foreach (var finger in this.FingerTable) + { + if (((finger.Value.Start > finger.Value.End) && + (finger.Value.Start <= key || key < finger.Value.End)) || + ((finger.Value.Start < finger.Value.End) && + finger.Value.Start <= key && key < finger.Value.End)) + { + idToAsk = finger.Key; + } + } + + if (idToAsk < 0) + { + idToAsk = (this.NodeId + 1) % this.NumOfIds; + } + + if (this.FingerTable[idToAsk].Node.Equals(this.Id)) + { + foreach (var finger in this.FingerTable) + { + if (finger.Value.End == idToAsk || + finger.Value.End == idToAsk - 1) + { + idToAsk = finger.Key; + break; + } + } + + this.Assert(!this.FingerTable[idToAsk].Node.Equals(this.Id), "Cannot locate successor of {0}.", key); + } + + this.Send(this.FingerTable[idToAsk].Node, new FindSuccessor(sender, key)); + } + } + + private void ProcessFindPredecessor() + { + var sender = (this.ReceivedEvent as FindPredecessor).Sender; + if (this.Predecessor != null) + { + this.Send(sender, new FindPredecessorResp(this.Predecessor)); + } + } + + private void ProcessQueryId() + { + var sender = (this.ReceivedEvent as QueryId).Sender; + this.Send(sender, new QueryIdResp(this.NodeId)); + } + + private void SendKeys() + { + var sender = (this.ReceivedEvent as AskForKeys).Node; + var senderId = (this.ReceivedEvent as AskForKeys).Id; + + this.Assert(this.Predecessor.Equals(sender), "Predecessor is corrupted."); + + List keysToSend = new List(); + foreach (var key in this.Keys) + { + if (key <= senderId) + { + keysToSend.Add(key); + } + } + + if (keysToSend.Count > 0) + { + foreach (var key in keysToSend) + { + this.Keys.Remove(key); + } + + this.Send(sender, new AskForKeysResp(keysToSend)); + } + } + + private void ProcessStabilize() + { + var successor = this.FingerTable[(this.NodeId + 1) % this.NumOfIds].Node; + this.Send(successor, new FindPredecessor(this.Id)); + + foreach (var finger in this.FingerTable) + { + if (!finger.Value.Node.Equals(successor)) + { + this.Send(successor, new FindSuccessor(this.Id, finger.Key)); + } + } + } + + private void ProcessFindSuccessorResp() + { + var successor = (this.ReceivedEvent as FindSuccessorResp).Node; + var key = (this.ReceivedEvent as FindSuccessorResp).Key; + + this.Assert(this.FingerTable.ContainsKey(key), "Finger table of {0} does not contain {1}.", this.NodeId, key); + this.FingerTable[key] = new Finger(this.FingerTable[key].Start, this.FingerTable[key].End, successor); + } + + private void ProcessFindPredecessorResp() + { + var successor = (this.ReceivedEvent as FindPredecessorResp).Node; + if (!successor.Equals(this.Id)) + { + this.FingerTable[(this.NodeId + 1) % this.NumOfIds] = new Finger( + this.FingerTable[(this.NodeId + 1) % this.NumOfIds].Start, + this.FingerTable[(this.NodeId + 1) % this.NumOfIds].End, + successor); + + this.Send(successor, new NotifySuccessor(this.Id)); + this.Send(successor, new AskForKeys(this.Id, this.NodeId)); + } + } + + private void UpdatePredecessor() + { + var predecessor = (this.ReceivedEvent as NotifySuccessor).Node; + if (!predecessor.Equals(this.Id)) + { + this.Predecessor = predecessor; + } + } + + private void UpdateKeys() + { + var keys = (this.ReceivedEvent as AskForKeysResp).Keys; + foreach (var key in keys) + { + this.Keys.Add(key); + } + } + + private void ProcessTerminate() + { + this.Raise(new Halt()); + } + + private static int GetSuccessorNodeId(int start, List nodeIds) + { + var candidate = -1; + foreach (var id in nodeIds.Where(v => v >= start)) + { + if (candidate < 0 || id < candidate) + { + candidate = id; + } + } + + if (candidate < 0) + { + foreach (var id in nodeIds.Where(v => v < start)) + { + if (candidate < 0 || id < candidate) + { + candidate = id; + } + } + } + + for (int idx = 0; idx < nodeIds.Count; idx++) + { + if (nodeIds[idx] == candidate) + { + candidate = idx; + break; + } + } + + return candidate; + } + + private int WrapAdd(int left, int right, int ceiling) + { + int result = left + right; + if (result > ceiling) + { + result = ceiling - result; + } + + return result; + } + + private static int WrapSubtract(int left, int right, int ceiling) + { + int result = left - right; + if (result < 0) + { + result = ceiling + result; + } + + return result; + } + + private void EmitFingerTableAndKeys() + { + this.Logger.WriteLine(" ... Printing finger table of node {0}:", this.NodeId); + foreach (var finger in this.FingerTable) + { + this.Logger.WriteLine(" >> " + finger.Key + " | [" + finger.Value.Start + + ", " + finger.Value.End + ") | " + finger.Value.Node); + } + + this.Logger.WriteLine(" ... Printing keys of node {0}:", this.NodeId); + foreach (var key in this.Keys) + { + this.Logger.WriteLine(" >> Key-" + key); + } + } + } + + private class Client : Machine + { + internal class Config : Event + { + public MachineId ClusterManager; + public List Keys; + + public Config(MachineId clusterManager, List keys) + : base() + { + this.ClusterManager = clusterManager; + this.Keys = keys; + } + } + + private class Local : Event + { + } + + private MachineId ClusterManager; + + private List Keys; + private int QueryCounter; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Local), typeof(Querying))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.ClusterManager = (this.ReceivedEvent as Config).ClusterManager; + this.Keys = (this.ReceivedEvent as Config).Keys; + + // LIVENESS BUG: can never detect the key, and keeps looping without + // exiting the process. Enable to introduce the bug. + this.Keys.Add(17); + + this.QueryCounter = 0; + + this.Raise(new Local()); + } + + [OnEntry(nameof(QueryingOnEntry))] + [OnEventGotoState(typeof(Local), typeof(Waiting))] + private class Querying : MachineState + { + } + + private void QueryingOnEntry() + { + if (this.QueryCounter < 5) + { + if (this.Random()) + { + var key = this.GetNextQueryKey(); + this.Logger.WriteLine($" Client is searching for successor of key '{key}'."); + this.Send(this.ClusterManager, new ChordNode.FindSuccessor(this.Id, key)); + this.Monitor(new LivenessMonitor.NotifyClientRequest(key)); + } + else if (this.Random()) + { + this.Send(this.ClusterManager, new ClusterManager.CreateNewNode()); + } + else + { + this.Send(this.ClusterManager, new ClusterManager.TerminateNode()); + } + + this.QueryCounter++; + } + + this.Raise(new Local()); + } + + private int GetNextQueryKey() + { + int keyIndex = -1; + while (keyIndex < 0) + { + for (int i = 0; i < this.Keys.Count; i++) + { + if (this.Random()) + { + keyIndex = i; + break; + } + } + } + + return this.Keys[keyIndex]; + } + + [OnEventGotoState(typeof(Local), typeof(Querying))] + [OnEventDoAction(typeof(ChordNode.FindSuccessorResp), nameof(ProcessFindSuccessorResp))] + [OnEventDoAction(typeof(ChordNode.QueryIdResp), nameof(ProcessQueryIdResp))] + private class Waiting : MachineState + { + } + + private void ProcessFindSuccessorResp() + { + var successor = (this.ReceivedEvent as ChordNode.FindSuccessorResp).Node; + var key = (this.ReceivedEvent as ChordNode.FindSuccessorResp).Key; + this.Monitor(new LivenessMonitor.NotifyClientResponse(key)); + this.Send(successor, new ChordNode.QueryId(this.Id)); + } + + private void ProcessQueryIdResp() + { + this.Raise(new Local()); + } + } + + private class LivenessMonitor : Monitor + { + public class NotifyClientRequest : Event + { + public int Key; + + public NotifyClientRequest(int key) + : base() + { + this.Key = key; + } + } + + public class NotifyClientResponse : Event + { + public int Key; + + public NotifyClientResponse(int key) + : base() + { + this.Key = key; + } + } + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MonitorState + { + } + + private void InitOnEntry() + { + this.Goto(); + } + + [Cold] + [OnEventGotoState(typeof(NotifyClientRequest), typeof(Requested))] + private class Responded : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(NotifyClientResponse), typeof(Responded))] + private class Requested : MonitorState + { + } + } + + [Theory(Timeout = 10000)] + [InlineData(0)] + public void TestLivenessBugInChordProtocol(int seed) + { + var configuration = GetConfiguration(); + configuration.MaxUnfairSchedulingSteps = 200; + configuration.MaxFairSchedulingSteps = 2000; + configuration.LivenessTemperatureThreshold = 1000; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(ClusterManager)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected potential liveness bug in hot state 'Requested'.", + replay: true); + } + + [Theory(Timeout = 10000)] + [InlineData(2)] + public void TestLivenessBugInChordProtocolWithCycleReplay(int seed) + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.MaxSchedulingSteps = 100; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(ClusterManager)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/DiningPhilosophersTest.cs b/Tests/TestingServices.Tests/Machines/Integration/DiningPhilosophersTest.cs new file mode 100644 index 000000000..64cfad6c0 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/DiningPhilosophersTest.cs @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + /// + /// A single-process implementation of the dining philosophers problem. + /// + public class DiningPhilosophersTest : BaseTest + { + public DiningPhilosophersTest(ITestOutputHelper output) + : base(output) + { + } + + private class Environment : Machine + { + private Dictionary LockMachines; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.LockMachines = new Dictionary(); + + int n = 3; + for (int i = 0; i < n; i++) + { + var lck = this.CreateMachine(typeof(Lock)); + this.LockMachines.Add(i, lck); + } + + for (int i = 0; i < n; i++) + { + this.CreateMachine(typeof(Philosopher), new Philosopher.Config(this.LockMachines[i], this.LockMachines[(i + 1) % n])); + } + } + } + + private class Lock : Machine + { + public class TryLock : Event + { + public MachineId Target; + + public TryLock(MachineId target) + { + this.Target = target; + } + } + + public class Release : Event + { + } + + public class LockResp : Event + { + public bool LockResult; + + public LockResp(bool res) + { + this.LockResult = res; + } + } + + private bool LockVar; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + [OnEventDoAction(typeof(TryLock), nameof(OnTryLock))] + [OnEventDoAction(typeof(Release), nameof(OnRelease))] + private class Waiting : MachineState + { + } + + private void InitOnEntry() + { + this.LockVar = false; + this.Goto(); + } + + private void OnTryLock() + { + var target = (this.ReceivedEvent as TryLock).Target; + if (this.LockVar) + { + this.Send(target, new LockResp(false)); + } + else + { + this.LockVar = true; + this.Send(target, new LockResp(true)); + } + } + + private void OnRelease() + { + this.LockVar = false; + } + } + + private class Philosopher : Machine + { + public class Config : Event + { + public MachineId Left; + public MachineId Right; + + public Config(MachineId left, MachineId right) + { + this.Left = left; + this.Right = right; + } + } + + private class TryAgain : Event + { + } + + private MachineId left; + private MachineId right; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + [OnEntry(nameof(TryAccess))] + [OnEventDoAction(typeof(TryAgain), nameof(TryAccess))] + private class Trying : MachineState + { + } + + [OnEntry(nameof(OnDone))] + private class Done : MachineState + { + } + + private void InitOnEntry() + { + var e = this.ReceivedEvent as Config; + this.left = e.Left; + this.right = e.Right; + this.Goto(); + } + + private async Task TryAccess() + { + this.Send(this.left, new Lock.TryLock(this.Id)); + var ev = await this.Receive(typeof(Lock.LockResp)); + if ((ev as Lock.LockResp).LockResult) + { + this.Send(this.right, new Lock.TryLock(this.Id)); + var evr = await this.Receive(typeof(Lock.LockResp)); + if ((evr as Lock.LockResp).LockResult) + { + this.Goto(); + return; + } + else + { + this.Send(this.left, new Lock.Release()); + } + } + + this.Send(this.Id, new TryAgain()); + } + + private void OnDone() + { + this.Send(this.left, new Lock.Release()); + this.Send(this.right, new Lock.Release()); + this.Monitor(new LivenessMonitor.NotifyDone()); + this.Raise(new Halt()); + } + } + + private class LivenessMonitor : Monitor + { + public class NotifyDone : Event + { + } + + [Start] + [Hot] + [OnEventGotoState(typeof(NotifyDone), typeof(Done))] + private class Init : MonitorState + { + } + + [Cold] + [OnEventGotoState(typeof(NotifyDone), typeof(Done))] + private class Done : MonitorState + { + } + } + + [Theory(Timeout = 10000)] + [InlineData(469)] + public void TestDiningPhilosophersLivenessBugWithCycleReplay(int seed) + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.MaxUnfairSchedulingSteps = 100; + configuration.MaxFairSchedulingSteps = 1000; + configuration.LivenessTemperatureThreshold = 500; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(Environment)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/OneMachineIntegrationTests.cs b/Tests/TestingServices.Tests/Machines/Integration/OneMachineIntegrationTests.cs new file mode 100644 index 000000000..56434a99b --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/OneMachineIntegrationTests.cs @@ -0,0 +1,964 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class OneMachineIntegrationTests : BaseTest + { + public OneMachineIntegrationTests(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class M1 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(HandleE1))] + [OnEventDoAction(typeof(E2), nameof(HandleE2))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + this.Raise(new E1()); + } + + private void HandleE1() + { + this.Test = true; + } + + private void HandleE2() + { + this.Assert(this.Test == false); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E2), nameof(HandleE2))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + private void HandleE2() + { + this.Assert(false); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + private class Active : MachineState + { + } + } + + private class M4 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(E2), nameof(HandleE2))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + } + + private void HandleE2() + { + this.Assert(this.Test == false); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Init))] + [OnEventDoAction(typeof(E2), nameof(HandleE2))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + private void HandleE2() + { + this.Assert(false); + } + } + + private class M6 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Init))] + [OnEventPushState(typeof(E2), typeof(Init))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + } + + private class M7 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Init))] + [OnEventPushState(typeof(E2), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Assert(false); + } + } + + private class M8 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Init))] + [OnEventPushState(typeof(E2), typeof(Active))] + [OnEventDoAction(typeof(E3), nameof(HandleE3))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + this.Send(this.Id, new E3(), options: new SendOptions(assert: 1)); + } + + private void HandleE3() + { + this.Assert(this.Test == false); + } + } + + private class M9 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventPushState(typeof(E1), typeof(Active))] + [OnEventDoAction(typeof(E3), nameof(HandleE3))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + this.Send(this.Id, new E3(), options: new SendOptions(assert: 1)); + this.Pop(); + } + + private void HandleE3() + { + this.Assert(this.Test == false); + } + } + + private class M10 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventPushState(typeof(E1), typeof(Active))] + [OnEventDoAction(typeof(E3), nameof(HandleE3))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + this.Send(this.Id, new E3(), options: new SendOptions(assert: 1)); + } + + private void HandleE3() + { + this.Assert(this.Test == false); + } + } + + private class M11 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E1), typeof(Active))] + [OnEventGotoState(typeof(E3), typeof(Checking))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnExit(nameof(ActiveOnExit))] + [OnEventGotoState(typeof(E3), typeof(Init))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + this.Send(this.Id, new E3(), options: new SendOptions(assert: 1)); + } + + private void ActiveOnExit() + { + this.Send(this.Id, new E3(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(CheckingOnEntry))] + private class Checking : MachineState + { + } + + private void CheckingOnEntry() + { + this.Assert(this.Test == false); + } + } + + private class M12 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Init))] + [OnEventPushState(typeof(E2), typeof(Active))] + [OnEventDoAction(typeof(E3), nameof(HandleE3))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnExit(nameof(ActiveOnExit))] + [OnEventGotoState(typeof(E3), typeof(Init))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + this.Send(this.Id, new E3(), options: new SendOptions(assert: 1)); + } + + private void ActiveOnExit() + { + this.Assert(this.Test == false); + } + + private void HandleE3() + { + } + } + + private class M13 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventPushState(typeof(E1), typeof(Active))] + [OnEventDoAction(typeof(E3), nameof(HandleE3))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnExit(nameof(ActiveOnExit))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + this.Pop(); + } + + private void ActiveOnExit() + { + this.Send(this.Id, new E3(), options: new SendOptions(assert: 1)); + } + + private void HandleE3() + { + this.Assert(this.Test == false); + } + } + + private class M14 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Init))] + [OnEventPushState(typeof(E2), typeof(Active))] + [OnEventDoAction(typeof(E3), nameof(HandleE3))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + private void InitOnExit() + { + this.Send(this.Id, new E2(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Raise(new E1()); + } + + private void HandleE3() + { + } + } + + private class M15 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventPushState(typeof(E), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new E()); + } + + private void InitOnExit() + { + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnExit(nameof(ActiveOnExit))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Pop(); + } + + private void ActiveOnExit() + { + this.Assert(false); + } + } + + private class M16 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventPushState(typeof(Halt), typeof(Active))] + [OnEventDoAction(typeof(E1), nameof(HandleE1))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + this.Raise(new Halt()); + } + + private void InitOnExit() + { + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Test = true; + } + + private void HandleE1() + { + this.Assert(this.Test == false); + } + } + + private class M17 : Machine + { + private bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(Default), typeof(Active))] + [OnEventDoAction(typeof(E1), nameof(HandleE1))] + [OnEventDoAction(typeof(E2), nameof(HandleE2))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new E2()); + } + + private void InitOnExit() + { + } + + private void HandleE1() + { + this.Test = true; + } + + private void HandleE2() + { + this.Send(this.Id, new E1(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Assert(this.Test == false); + } + } + + private class M18 : Machine + { + private readonly bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(Default), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + } + + private void InitOnExit() + { + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Assert(this.Test == true); + } + } + + private class M19 : Machine + { + private int Value; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventPushState(typeof(E), typeof(Active))] + [OnEventDoAction(typeof(Default), nameof(DefaultAction))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Value = 0; + this.Raise(new E()); + } + + private void InitOnExit() + { + } + + private void DefaultAction() + { + this.Assert(false); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnExit(nameof(ActiveOnExit))] + [IgnoreEvents(typeof(E))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + if (this.Value == 0) + { + this.Raise(new E()); + } + else + { + this.Value++; + } + } + + private void ActiveOnExit() + { + } + } + + private class M20 : Machine + { + [Start] + [OnEventGotoState(typeof(Default), typeof(Active))] + private class Init : MachineState + { + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Assert(this.ReceivedEvent.GetType() == typeof(Default)); + } + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration1() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration2() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration3() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3)); + }, + expectedError: "Machine 'M3()' received event 'E2' that cannot be handled.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration4() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M4)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration5() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M5)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration6() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M6)); + }, + expectedError: "There are more than 1 instances of 'E1' in the input queue of machine 'M6()'.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration7() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M7)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration8() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M8)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration9() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M9)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration10() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M10)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration11() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M11)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration12() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M12)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration13() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M13)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration14() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M14)); + }, + expectedError: "There are more than 1 instances of 'E1' in the input queue of machine 'M14()'.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration15() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M15)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration16() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M16)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration17() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M17)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration18() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M18)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration19() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M19)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOneMachineIntegration20() + { + this.Test(r => + { + r.CreateMachine(typeof(M20)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/ProcessSchedulerTest.cs b/Tests/TestingServices.Tests/Machines/Integration/ProcessSchedulerTest.cs new file mode 100644 index 000000000..c5d669530 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/ProcessSchedulerTest.cs @@ -0,0 +1,644 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + /// + /// A single-process implementation of a process scheduling algorithm. + /// + public class ProcessSchedulerTest : BaseTest + { + public ProcessSchedulerTest(ITestOutputHelper output) + : base(output) + { + } + + public enum MType + { + WakeUp, + Run + } + + private class Environment : Machine + { + [Start] + [OnEntry(nameof(OnInitEntry))] + private class Init : MachineState + { + } + + private void OnInitEntry() + { + var lkMachine = this.CreateMachine(typeof(LkMachine)); + var rLockMachine = this.CreateMachine(typeof(RLockMachine)); + var rWantMachine = this.CreateMachine(typeof(RWantMachine)); + var nodeMachine = this.CreateMachine(typeof(Node)); + this.CreateMachine(typeof(Client), new Client.Configure(lkMachine, rLockMachine, rWantMachine, nodeMachine)); + this.CreateMachine(typeof(Server), new Server.Configure(lkMachine, rLockMachine, rWantMachine, nodeMachine)); + } + } + + private class Server : Machine + { + public class Configure : Event + { + public MachineId LKMachineId; + public MachineId RLockMachineId; + public MachineId RWantMachineId; + public MachineId NodeMachineId; + + public Configure(MachineId lkMachineId, MachineId rLockMachineId, + MachineId rWantMachineId, MachineId nodeMachineId) + { + this.LKMachineId = lkMachineId; + this.RLockMachineId = rLockMachineId; + this.RWantMachineId = rWantMachineId; + this.NodeMachineId = nodeMachineId; + } + } + + public class Wakeup : Event + { + } + + private MachineId LKMachineId; + private MachineId RLockMachineId; + private MachineId RWantMachineId; + public MachineId NodeMachineId; + + [Start] + [OnEntry(nameof(OnInitialize))] + [OnEventDoAction(typeof(Wakeup), nameof(OnWakeup))] + private class Init : MachineState + { + } + + private void OnInitialize() + { + var e = this.ReceivedEvent as Configure; + this.LKMachineId = e.LKMachineId; + this.RLockMachineId = e.RLockMachineId; + this.RWantMachineId = e.RWantMachineId; + this.NodeMachineId = e.NodeMachineId; + this.Raise(new Wakeup()); + } + + private async Task OnWakeup() + { + this.Send(this.RLockMachineId, new RLockMachine.SetReq(this.Id, false)); + await this.Receive(typeof(RLockMachine.SetResp)); + this.Send(this.LKMachineId, new LkMachine.Waiting(this.Id, false)); + await this.Receive(typeof(LkMachine.WaitResp)); + this.Send(this.RWantMachineId, new RWantMachine.ValueReq(this.Id)); + var receivedEvent = await this.Receive(typeof(RWantMachine.ValueResp)); + + if ((receivedEvent as RWantMachine.ValueResp).Value == true) + { + this.Send(this.RWantMachineId, new RWantMachine.SetReq(this.Id, false)); + await this.Receive(typeof(RWantMachine.SetResp)); + + this.Send(this.NodeMachineId, new Node.ValueReq(this.Id)); + var receivedEvent1 = await this.Receive(typeof(Node.ValueResp)); + if ((receivedEvent1 as Node.ValueResp).Value == MType.WakeUp) + { + this.Send(this.NodeMachineId, new Node.SetReq(this.Id, MType.Run)); + await this.Receive(typeof(Node.SetResp)); + } + } + + this.Send(this.Id, new Wakeup()); + } + } + + private class Client : Machine + { + public class Configure : Event + { + public MachineId LKMachineId; + public MachineId RLockMachineId; + public MachineId RWantMachineId; + public MachineId NodeMachineId; + + public Configure(MachineId lkMachineId, MachineId rLockMachineId, + MachineId rWantMachineId, MachineId nodeMachineId) + { + this.LKMachineId = lkMachineId; + this.RLockMachineId = rLockMachineId; + this.RWantMachineId = rWantMachineId; + this.NodeMachineId = nodeMachineId; + } + } + + public class Sleep : Event + { + } + + public class Progress : Event + { + } + + private MachineId LKMachineId; + private MachineId RLockMachineId; + private MachineId RWantMachineId; + public MachineId NodeMachineId; + + [Start] + [OnEntry(nameof(OnInitialize))] + [OnEventDoAction(typeof(Sleep), nameof(OnSleep))] + [OnEventDoAction(typeof(Progress), nameof(OnProgress))] + private class Init : MachineState + { + } + + private void OnInitialize() + { + var e = this.ReceivedEvent as Configure; + this.LKMachineId = e.LKMachineId; + this.RLockMachineId = e.RLockMachineId; + this.RWantMachineId = e.RWantMachineId; + this.NodeMachineId = e.NodeMachineId; + this.Raise(new Progress()); + } + + private async Task OnSleep() + { + this.Send(this.LKMachineId, new LkMachine.AtomicTestSet(this.Id)); + await this.Receive(typeof(LkMachine.AtomicTestSet_Resp)); + while (true) + { + this.Send(this.RLockMachineId, new RLockMachine.ValueReq(this.Id)); + var receivedEvent = await this.Receive(typeof(RLockMachine.ValueResp)); + if ((receivedEvent as RLockMachine.ValueResp).Value == true) + { + this.Send(this.RWantMachineId, new RWantMachine.SetReq(this.Id, true)); + await this.Receive(typeof(RWantMachine.SetResp)); + this.Send(this.NodeMachineId, new Node.SetReq(this.Id, MType.WakeUp)); + await this.Receive(typeof(Node.SetResp)); + this.Send(this.LKMachineId, new LkMachine.SetReq(this.Id, false)); + await this.Receive(typeof(LkMachine.SetResp)); + + this.Monitor(new LivenessMonitor.NotifyClientSleep()); + + this.Send(this.NodeMachineId, new Node.Waiting(this.Id, MType.Run)); + await this.Receive(typeof(Node.WaitResp)); + + this.Monitor(new LivenessMonitor.NotifyClientProgress()); + } + else + { + break; + } + } + + this.Send(this.Id, new Progress()); + } + + private async Task OnProgress() + { + this.Send(this.RLockMachineId, new RLockMachine.ValueReq(this.Id)); + var receivedEvent = await this.Receive(typeof(RLockMachine.ValueResp)); + this.Assert((receivedEvent as RLockMachine.ValueResp).Value == false); + this.Send(this.RLockMachineId, new RLockMachine.SetReq(this.Id, true)); + await this.Receive(typeof(RLockMachine.SetResp)); + this.Send(this.LKMachineId, new LkMachine.SetReq(this.Id, false)); + await this.Receive(typeof(LkMachine.SetResp)); + this.Send(this.Id, new Sleep()); + } + } + + private class Node : Machine + { + public class ValueReq : Event + { + public MachineId Target; + + public ValueReq(MachineId target) + { + this.Target = target; + } + } + + public class ValueResp : Event + { + public MType Value; + + public ValueResp(MType value) + { + this.Value = value; + } + } + + public class SetReq : Event + { + public MachineId Target; + public MType Value; + + public SetReq(MachineId target, MType value) + { + this.Target = target; + this.Value = value; + } + } + + public class SetResp : Event + { + } + + public class Waiting : Event + { + public MachineId Target; + public MType WaitingOn; + + public Waiting(MachineId target, MType waitingOn) + { + this.Target = target; + this.WaitingOn = waitingOn; + } + } + + public class WaitResp : Event + { + } + + private MType State; + private Dictionary blockedMachines; + + [Start] + [OnEntry(nameof(OnInitialize))] + [OnEventDoAction(typeof(SetReq), nameof(OnSetReq))] + [OnEventDoAction(typeof(ValueReq), nameof(OnValueReq))] + [OnEventDoAction(typeof(Waiting), nameof(OnWaiting))] + private class Init : MachineState + { + } + + private void OnInitialize() + { + this.State = MType.Run; + this.blockedMachines = new Dictionary(); + } + + private void OnSetReq() + { + var e = this.ReceivedEvent as SetReq; + this.State = e.Value; + this.Unblock(); + this.Send(e.Target, new SetResp()); + } + + private void OnValueReq() + { + var e = this.ReceivedEvent as ValueReq; + this.Send(e.Target, new ValueResp(this.State)); + } + + private void OnWaiting() + { + var e = this.ReceivedEvent as Waiting; + if (this.State == e.WaitingOn) + { + this.Send(e.Target, new WaitResp()); + } + else + { + this.blockedMachines.Add(e.Target, e.WaitingOn); + } + } + + private void Unblock() + { + List remove = new List(); + foreach (var target in this.blockedMachines.Keys) + { + if (this.blockedMachines[target] == this.State) + { + this.Send(target, new WaitResp()); + remove.Add(target); + } + } + + foreach (var key in remove) + { + this.blockedMachines.Remove(key); + } + } + } + + private class LkMachine : Machine + { + public class AtomicTestSet : Event + { + public MachineId Target; + + public AtomicTestSet(MachineId target) + { + this.Target = target; + } + } + + public class AtomicTestSet_Resp : Event + { + } + + public class SetReq : Event + { + public MachineId Target; + public bool Value; + + public SetReq(MachineId target, bool value) + { + this.Target = target; + this.Value = value; + } + } + + public class SetResp : Event + { + } + + public class Waiting : Event + { + public MachineId Target; + public bool WaitingOn; + + public Waiting(MachineId target, bool waitingOn) + { + this.Target = target; + this.WaitingOn = waitingOn; + } + } + + public class WaitResp : Event + { + } + + private bool LK; + private Dictionary BlockedMachines; + + [Start] + [OnEntry(nameof(OnInitialize))] + [OnEventDoAction(typeof(AtomicTestSet), nameof(OnAtomicTestSet))] + [OnEventDoAction(typeof(SetReq), nameof(OnSetReq))] + [OnEventDoAction(typeof(Waiting), nameof(OnWaiting))] + private class Init : MachineState + { + } + + private void OnInitialize() + { + this.LK = false; + this.BlockedMachines = new Dictionary(); + } + + private void OnAtomicTestSet() + { + var e = this.ReceivedEvent as AtomicTestSet; + if (this.LK == false) + { + this.LK = true; + this.Unblock(); + } + + this.Send(e.Target, new AtomicTestSet_Resp()); + } + + private void OnSetReq() + { + var e = this.ReceivedEvent as SetReq; + this.LK = e.Value; + this.Unblock(); + this.Send(e.Target, new SetResp()); + } + + private void OnWaiting() + { + var e = this.ReceivedEvent as Waiting; + if (this.LK == e.WaitingOn) + { + this.Send(e.Target, new WaitResp()); + } + else + { + this.BlockedMachines.Add(e.Target, e.WaitingOn); + } + } + + private void Unblock() + { + List remove = new List(); + foreach (var target in this.BlockedMachines.Keys) + { + if (this.BlockedMachines[target] == this.LK) + { + this.Send(target, new WaitResp()); + remove.Add(target); + } + } + + foreach (var key in remove) + { + this.BlockedMachines.Remove(key); + } + } + } + + private class RLockMachine : Machine + { + public class ValueReq : Event + { + public MachineId Target; + + public ValueReq(MachineId target) + { + this.Target = target; + } + } + + public class ValueResp : Event + { + public bool Value; + + public ValueResp(bool value) + { + this.Value = value; + } + } + + public class SetReq : Event + { + public MachineId Target; + public bool Value; + + public SetReq(MachineId target, bool value) + { + this.Target = target; + this.Value = value; + } + } + + public class SetResp : Event + { + } + + private bool RLock; + + [Start] + [OnEntry(nameof(OnInitialize))] + [OnEventDoAction(typeof(SetReq), nameof(OnSetReq))] + [OnEventDoAction(typeof(ValueReq), nameof(OnValueReq))] + private class Init : MachineState + { + } + + private void OnInitialize() + { + this.RLock = false; + } + + private void OnSetReq() + { + var e = this.ReceivedEvent as SetReq; + this.RLock = e.Value; + this.Send(e.Target, new SetResp()); + } + + private void OnValueReq() + { + var e = this.ReceivedEvent as ValueReq; + this.Send(e.Target, new ValueResp(this.RLock)); + } + } + + private class RWantMachine : Machine + { + public class ValueReq : Event + { + public MachineId Target; + + public ValueReq(MachineId target) + { + this.Target = target; + } + } + + public class ValueResp : Event + { + public bool Value; + + public ValueResp(bool value) + { + this.Value = value; + } + } + + public class SetReq : Event + { + public MachineId Target; + public bool Value; + + public SetReq(MachineId target, bool value) + { + this.Target = target; + this.Value = value; + } + } + + public class SetResp : Event + { + } + + private bool RWant; + + [Start] + [OnEntry(nameof(OnInitialize))] + [OnEventDoAction(typeof(SetReq), nameof(OnSetReq))] + [OnEventDoAction(typeof(ValueReq), nameof(OnValueReq))] + private class Init : MachineState + { + } + + private void OnInitialize() + { + this.RWant = false; + } + + private void OnSetReq() + { + var e = this.ReceivedEvent as SetReq; + this.RWant = e.Value; + this.Send(e.Target, new SetResp()); + } + + private void OnValueReq() + { + var e = this.ReceivedEvent as ValueReq; + this.Send(e.Target, new ValueResp(this.RWant)); + } + } + + private class LivenessMonitor : Monitor + { + public class NotifyClientSleep : Event + { + } + + public class NotifyClientProgress : Event + { + } + + [Start] + [OnEntry(nameof(InitOnEntry))] + + private class Init : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(NotifyClientProgress), typeof(Progressing))] + private class Suspended : MonitorState + { + } + + [Cold] + [OnEventGotoState(typeof(NotifyClientSleep), typeof(Suspended))] + private class Progressing : MonitorState + { + } + + private void InitOnEntry() + { + this.Goto(); + } + } + + [Theory(Timeout = 10000)] + [InlineData(3163)] + public void TestProcessSchedulerLivenessBugWithCycleReplay(int seed) + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.PrioritySwitchBound = 1; + configuration.MaxUnfairSchedulingSteps = 100; + configuration.MaxFairSchedulingSteps = 1000; + configuration.LivenessTemperatureThreshold = 500; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(Environment)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/RaftTest.cs b/Tests/TestingServices.Tests/Machines/Integration/RaftTest.cs new file mode 100644 index 000000000..511d334d6 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/RaftTest.cs @@ -0,0 +1,1245 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + /// + /// This is a simple implementation of the Raft consensus protocol + /// described in the following paper: + /// + /// https://raft.github.io/raft.pdf + /// + /// This test contains a bug that leads to duplicate leader election + /// in the same term. + /// + public class RaftTest : BaseTest + { + public RaftTest(ITestOutputHelper output) + : base(output) + { + } + + private class Log + { + public readonly int Term; + public readonly int Command; + + public Log(int term, int command) + { + this.Term = term; + this.Command = command; + } + } + + private class ClusterManager : Machine + { + internal class NotifyLeaderUpdate : Event + { + public MachineId Leader; + public int Term; + + public NotifyLeaderUpdate(MachineId leader, int term) + : base() + { + this.Leader = leader; + this.Term = term; + } + } + + internal class RedirectRequest : Event + { + public Event Request; + + public RedirectRequest(Event request) + : base() + { + this.Request = request; + } + } + + internal class ShutDown : Event + { + } + + private class LocalEvent : Event + { + } + + private MachineId[] Servers; + private int NumberOfServers; + + private MachineId Leader; + private int LeaderTerm; + + private MachineId Client; + + [Start] + [OnEntry(nameof(EntryOnInit))] + [OnEventGotoState(typeof(LocalEvent), typeof(Configuring))] + private class Init : MachineState + { + } + + private void EntryOnInit() + { + this.NumberOfServers = 5; + this.LeaderTerm = 0; + + this.Servers = new MachineId[this.NumberOfServers]; + + for (int idx = 0; idx < this.NumberOfServers; idx++) + { + this.Servers[idx] = this.CreateMachine(typeof(Server)); + } + + this.Client = this.CreateMachine(typeof(Client)); + + this.Raise(new LocalEvent()); + } + + [OnEntry(nameof(ConfiguringOnInit))] + [OnEventGotoState(typeof(LocalEvent), typeof(Availability.Unavailable))] + private class Configuring : MachineState + { + } + + private void ConfiguringOnInit() + { + for (int idx = 0; idx < this.NumberOfServers; idx++) + { + this.Send(this.Servers[idx], new Server.ConfigureEvent(idx, this.Servers, this.Id)); + } + + this.Send(this.Client, new Client.ConfigureEvent(this.Id)); + + this.Raise(new LocalEvent()); + } + + private class Availability : StateGroup + { + [OnEventDoAction(typeof(NotifyLeaderUpdate), nameof(BecomeAvailable))] + [OnEventDoAction(typeof(ShutDown), nameof(ShuttingDown))] + [OnEventGotoState(typeof(LocalEvent), typeof(Available))] + [DeferEvents(typeof(Client.Request))] + public class Unavailable : MachineState + { + } + + [OnEventDoAction(typeof(Client.Request), nameof(SendClientRequestToLeader))] + [OnEventDoAction(typeof(RedirectRequest), nameof(RedirectClientRequest))] + [OnEventDoAction(typeof(NotifyLeaderUpdate), nameof(RefreshLeader))] + [OnEventDoAction(typeof(ShutDown), nameof(ShuttingDown))] + [OnEventGotoState(typeof(LocalEvent), typeof(Unavailable))] + public class Available : MachineState + { + } + } + + private void BecomeAvailable() + { + this.UpdateLeader(this.ReceivedEvent as NotifyLeaderUpdate); + this.Raise(new LocalEvent()); + } + + private void SendClientRequestToLeader() + { + this.Send(this.Leader, this.ReceivedEvent); + } + + private void RedirectClientRequest() + { + this.Send(this.Id, (this.ReceivedEvent as RedirectRequest).Request); + } + + private void RefreshLeader() + { + this.UpdateLeader(this.ReceivedEvent as NotifyLeaderUpdate); + } + + private void ShuttingDown() + { + for (int idx = 0; idx < this.NumberOfServers; idx++) + { + this.Send(this.Servers[idx], new Server.ShutDown()); + } + + this.Raise(new Halt()); + } + + private void UpdateLeader(NotifyLeaderUpdate request) + { + if (this.LeaderTerm < request.Term) + { + this.Leader = request.Leader; + this.LeaderTerm = request.Term; + } + } + } + + /// + /// A server in Raft can be one of the following three roles: + /// follower, candidate or leader. + /// + private class Server : Machine + { + /// + /// Used to configure the server. + /// + public class ConfigureEvent : Event + { + public int Id; + public MachineId[] Servers; + public MachineId ClusterManager; + + public ConfigureEvent(int id, MachineId[] servers, MachineId manager) + : base() + { + this.Id = id; + this.Servers = servers; + this.ClusterManager = manager; + } + } + + /// + /// Initiated by candidates during elections. + /// + public class VoteRequest : Event + { + public int Term; // candidate’s term + public MachineId CandidateId; // candidate requesting vote + public int LastLogIndex; // index of candidate’s last log entry + public int LastLogTerm; // term of candidate’s last log entry + + public VoteRequest(int term, MachineId candidateId, int lastLogIndex, int lastLogTerm) + : base() + { + this.Term = term; + this.CandidateId = candidateId; + this.LastLogIndex = lastLogIndex; + this.LastLogTerm = lastLogTerm; + } + } + + /// + /// Response to a vote request. + /// + public class VoteResponse : Event + { + public int Term; // currentTerm, for candidate to update itself + public bool VoteGranted; // true means candidate received vote + + public VoteResponse(int term, bool voteGranted) + : base() + { + this.Term = term; + this.VoteGranted = voteGranted; + } + } + + /// + /// Initiated by leaders to replicate log entries and + /// to provide a form of heartbeat. + /// + public class AppendEntriesRequest : Event + { + public int Term; // leader's term + public MachineId LeaderId; // so follower can redirect clients + public int PrevLogIndex; // index of log entry immediately preceding new ones + public int PrevLogTerm; // term of PrevLogIndex entry + public List Entries; // log entries to store (empty for heartbeat; may send more than one for efficiency) + public int LeaderCommit; // leader’s CommitIndex + + public MachineId ReceiverEndpoint; // client + + public AppendEntriesRequest(int term, MachineId leaderId, int prevLogIndex, + int prevLogTerm, List entries, int leaderCommit, MachineId client) + : base() + { + this.Term = term; + this.LeaderId = leaderId; + this.PrevLogIndex = prevLogIndex; + this.PrevLogTerm = prevLogTerm; + this.Entries = entries; + this.LeaderCommit = leaderCommit; + this.ReceiverEndpoint = client; + } + } + + /// + /// Response to an append entries request. + /// + public class AppendEntriesResponse : Event + { + public int Term; // current Term, for leader to update itself + public bool Success; // true if follower contained entry matching PrevLogIndex and PrevLogTerm + + public MachineId Server; + public MachineId ReceiverEndpoint; // client + + public AppendEntriesResponse(int term, bool success, MachineId server, MachineId client) + : base() + { + this.Term = term; + this.Success = success; + this.Server = server; + this.ReceiverEndpoint = client; + } + } + + // Events for transitioning a server between roles. + private class BecomeFollower : Event + { + } + + private class BecomeCandidate : Event + { + } + + private class BecomeLeader : Event + { + } + + internal class ShutDown : Event + { + } + + /// + /// The id of this server. + /// + private int ServerId; + + /// + /// The cluster manager machine. + /// + private MachineId ClusterManager; + + /// + /// The servers. + /// + private MachineId[] Servers; + + /// + /// Leader id. + /// + private MachineId LeaderId; + + /// + /// The election timer of this server. + /// + private MachineId ElectionTimer; + + /// + /// The periodic timer of this server. + /// + private MachineId PeriodicTimer; + + /// + /// Latest term server has seen (initialized to 0 on + /// first boot, increases monotonically). + /// + private int CurrentTerm; + + /// + /// Candidate id that received vote in current term (or null if none). + /// + private MachineId VotedFor; + + /// + /// Log entries. + /// + private List Logs; + + /// + /// Index of highest log entry known to be committed (initialized + /// to 0, increases monotonically). + /// + private int CommitIndex; + + /// + /// Index of highest log entry applied to state machine (initialized + /// to 0, increases monotonically). + /// + private int LastApplied; + + /// + /// For each server, index of the next log entry to send to that + /// server (initialized to leader last log index + 1). + /// + private Dictionary NextIndex; + + /// + /// For each server, index of highest log entry known to be replicated + /// on server (initialized to 0, increases monotonically). + /// + private Dictionary MatchIndex; + + /// + /// Number of received votes. + /// + private int VotesReceived; + + /// + /// The latest client request. + /// + private Client.Request LastClientRequest; + + [Start] + [OnEntry(nameof(EntryOnInit))] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(BecomeFollower), typeof(Follower))] + [DeferEvents(typeof(VoteRequest), typeof(AppendEntriesRequest))] + private class Init : MachineState + { + } + + private void EntryOnInit() + { + this.CurrentTerm = 0; + + this.LeaderId = null; + this.VotedFor = null; + + this.Logs = new List(); + + this.CommitIndex = 0; + this.LastApplied = 0; + + this.NextIndex = new Dictionary(); + this.MatchIndex = new Dictionary(); + } + + private void Configure() + { + this.ServerId = (this.ReceivedEvent as ConfigureEvent).Id; + this.Servers = (this.ReceivedEvent as ConfigureEvent).Servers; + this.ClusterManager = (this.ReceivedEvent as ConfigureEvent).ClusterManager; + + this.ElectionTimer = this.CreateMachine(typeof(ElectionTimer)); + this.Send(this.ElectionTimer, new ElectionTimer.ConfigureEvent(this.Id)); + + this.PeriodicTimer = this.CreateMachine(typeof(PeriodicTimer)); + this.Send(this.PeriodicTimer, new PeriodicTimer.ConfigureEvent(this.Id)); + + this.Raise(new BecomeFollower()); + } + + [OnEntry(nameof(FollowerOnInit))] + [OnEventDoAction(typeof(Client.Request), nameof(RedirectClientRequest))] + [OnEventDoAction(typeof(VoteRequest), nameof(VoteAsFollower))] + [OnEventDoAction(typeof(VoteResponse), nameof(RespondVoteAsFollower))] + [OnEventDoAction(typeof(AppendEntriesRequest), nameof(AppendEntriesAsFollower))] + [OnEventDoAction(typeof(AppendEntriesResponse), nameof(RespondAppendEntriesAsFollower))] + [OnEventDoAction(typeof(ElectionTimer.Timeout), nameof(StartLeaderElection))] + [OnEventDoAction(typeof(ShutDown), nameof(ShuttingDown))] + [OnEventGotoState(typeof(BecomeFollower), typeof(Follower))] + [OnEventGotoState(typeof(BecomeCandidate), typeof(Candidate))] + [IgnoreEvents(typeof(PeriodicTimer.Timeout))] + private class Follower : MachineState + { + } + + private void FollowerOnInit() + { + this.LeaderId = null; + this.VotesReceived = 0; + + this.Send(this.ElectionTimer, new ElectionTimer.StartTimerEvent()); + } + + private void RedirectClientRequest() + { + if (this.LeaderId != null) + { + this.Send(this.LeaderId, this.ReceivedEvent); + } + else + { + this.Send(this.ClusterManager, new ClusterManager.RedirectRequest(this.ReceivedEvent)); + } + } + + private void StartLeaderElection() + { + this.Raise(new BecomeCandidate()); + } + + private void VoteAsFollower() + { + var request = this.ReceivedEvent as VoteRequest; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + } + + this.Vote(this.ReceivedEvent as VoteRequest); + } + + private void RespondVoteAsFollower() + { + var request = this.ReceivedEvent as VoteResponse; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + } + } + + private void AppendEntriesAsFollower() + { + var request = this.ReceivedEvent as AppendEntriesRequest; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + } + + this.AppendEntries(this.ReceivedEvent as AppendEntriesRequest); + } + + private void RespondAppendEntriesAsFollower() + { + var request = this.ReceivedEvent as AppendEntriesResponse; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + } + } + + [OnEntry(nameof(CandidateOnInit))] + [OnEventDoAction(typeof(Client.Request), nameof(RedirectClientRequest))] + [OnEventDoAction(typeof(VoteRequest), nameof(VoteAsCandidate))] + [OnEventDoAction(typeof(VoteResponse), nameof(RespondVoteAsCandidate))] + [OnEventDoAction(typeof(AppendEntriesRequest), nameof(AppendEntriesAsCandidate))] + [OnEventDoAction(typeof(AppendEntriesResponse), nameof(RespondAppendEntriesAsCandidate))] + [OnEventDoAction(typeof(ElectionTimer.Timeout), nameof(StartLeaderElection))] + [OnEventDoAction(typeof(PeriodicTimer.Timeout), nameof(BroadcastVoteRequests))] + [OnEventDoAction(typeof(ShutDown), nameof(ShuttingDown))] + [OnEventGotoState(typeof(BecomeLeader), typeof(Leader))] + [OnEventGotoState(typeof(BecomeFollower), typeof(Follower))] + [OnEventGotoState(typeof(BecomeCandidate), typeof(Candidate))] + private class Candidate : MachineState + { + } + + private void CandidateOnInit() + { + this.CurrentTerm++; + this.VotedFor = this.Id; + this.VotesReceived = 1; + + this.Send(this.ElectionTimer, new ElectionTimer.StartTimerEvent()); + + this.BroadcastVoteRequests(); + } + + private void BroadcastVoteRequests() + { + // BUG: duplicate votes from same follower + this.Send(this.PeriodicTimer, new PeriodicTimer.StartTimerEvent()); + + for (int idx = 0; idx < this.Servers.Length; idx++) + { + if (idx == this.ServerId) + { + continue; + } + + var lastLogIndex = this.Logs.Count; + var lastLogTerm = this.GetLogTermForIndex(lastLogIndex); + + this.Send(this.Servers[idx], new VoteRequest(this.CurrentTerm, this.Id, + lastLogIndex, lastLogTerm)); + } + } + + private void VoteAsCandidate() + { + var request = this.ReceivedEvent as VoteRequest; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + this.Vote(this.ReceivedEvent as VoteRequest); + this.Raise(new BecomeFollower()); + } + else + { + this.Vote(this.ReceivedEvent as VoteRequest); + } + } + + private void RespondVoteAsCandidate() + { + var request = this.ReceivedEvent as VoteResponse; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + this.Raise(new BecomeFollower()); + return; + } + else if (request.Term != this.CurrentTerm) + { + return; + } + + if (request.VoteGranted) + { + this.VotesReceived++; + if (this.VotesReceived >= (this.Servers.Length / 2) + 1) + { + this.VotesReceived = 0; + this.Raise(new BecomeLeader()); + } + } + } + + private void AppendEntriesAsCandidate() + { + var request = this.ReceivedEvent as AppendEntriesRequest; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + this.AppendEntries(this.ReceivedEvent as AppendEntriesRequest); + this.Raise(new BecomeFollower()); + } + else + { + this.AppendEntries(this.ReceivedEvent as AppendEntriesRequest); + } + } + + private void RespondAppendEntriesAsCandidate() + { + var request = this.ReceivedEvent as AppendEntriesResponse; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + this.Raise(new BecomeFollower()); + } + } + + [OnEntry(nameof(LeaderOnInit))] + [OnEventDoAction(typeof(Client.Request), nameof(ProcessClientRequest))] + [OnEventDoAction(typeof(VoteRequest), nameof(VoteAsLeader))] + [OnEventDoAction(typeof(VoteResponse), nameof(RespondVoteAsLeader))] + [OnEventDoAction(typeof(AppendEntriesRequest), nameof(AppendEntriesAsLeader))] + [OnEventDoAction(typeof(AppendEntriesResponse), nameof(RespondAppendEntriesAsLeader))] + [OnEventDoAction(typeof(ShutDown), nameof(ShuttingDown))] + [OnEventGotoState(typeof(BecomeFollower), typeof(Follower))] + [IgnoreEvents(typeof(ElectionTimer.Timeout), typeof(PeriodicTimer.Timeout))] + private class Leader : MachineState + { + } + + private void LeaderOnInit() + { + this.Monitor(new SafetyMonitor.NotifyLeaderElected(this.CurrentTerm)); + this.Send(this.ClusterManager, new ClusterManager.NotifyLeaderUpdate(this.Id, this.CurrentTerm)); + + var logIndex = this.Logs.Count; + var logTerm = this.GetLogTermForIndex(logIndex); + + this.NextIndex.Clear(); + this.MatchIndex.Clear(); + for (int idx = 0; idx < this.Servers.Length; idx++) + { + if (idx == this.ServerId) + { + continue; + } + + this.NextIndex.Add(this.Servers[idx], logIndex + 1); + this.MatchIndex.Add(this.Servers[idx], 0); + } + + for (int idx = 0; idx < this.Servers.Length; idx++) + { + if (idx == this.ServerId) + { + continue; + } + + this.Send(this.Servers[idx], new AppendEntriesRequest(this.CurrentTerm, this.Id, + logIndex, logTerm, new List(), this.CommitIndex, null)); + } + } + + private void ProcessClientRequest() + { + this.LastClientRequest = this.ReceivedEvent as Client.Request; + + var log = new Log(this.CurrentTerm, this.LastClientRequest.Command); + this.Logs.Add(log); + + this.BroadcastLastClientRequest(); + } + + private void BroadcastLastClientRequest() + { + var lastLogIndex = this.Logs.Count; + + this.VotesReceived = 1; + for (int idx = 0; idx < this.Servers.Length; idx++) + { + if (idx == this.ServerId) + { + continue; + } + + var server = this.Servers[idx]; + if (lastLogIndex < this.NextIndex[server]) + { + continue; + } + + var logs = this.Logs.GetRange(this.NextIndex[server] - 1, this.Logs.Count - (this.NextIndex[server] - 1)); + + var prevLogIndex = this.NextIndex[server] - 1; + var prevLogTerm = this.GetLogTermForIndex(prevLogIndex); + + this.Send(server, new AppendEntriesRequest(this.CurrentTerm, this.Id, prevLogIndex, + prevLogTerm, logs, this.CommitIndex, this.LastClientRequest.Client)); + } + } + + private void VoteAsLeader() + { + var request = this.ReceivedEvent as VoteRequest; + + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + + this.RedirectLastClientRequestToClusterManager(); + this.Vote(this.ReceivedEvent as VoteRequest); + + this.Raise(new BecomeFollower()); + } + else + { + this.Vote(this.ReceivedEvent as VoteRequest); + } + } + + private void RespondVoteAsLeader() + { + var request = this.ReceivedEvent as VoteResponse; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + + this.RedirectLastClientRequestToClusterManager(); + this.Raise(new BecomeFollower()); + } + } + + private void AppendEntriesAsLeader() + { + var request = this.ReceivedEvent as AppendEntriesRequest; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + + this.RedirectLastClientRequestToClusterManager(); + this.AppendEntries(this.ReceivedEvent as AppendEntriesRequest); + + this.Raise(new BecomeFollower()); + } + } + + private void RespondAppendEntriesAsLeader() + { + var request = this.ReceivedEvent as AppendEntriesResponse; + if (request.Term > this.CurrentTerm) + { + this.CurrentTerm = request.Term; + this.VotedFor = null; + + this.RedirectLastClientRequestToClusterManager(); + this.Raise(new BecomeFollower()); + return; + } + else if (request.Term != this.CurrentTerm) + { + return; + } + + if (request.Success) + { + this.NextIndex[request.Server] = this.Logs.Count + 1; + this.MatchIndex[request.Server] = this.Logs.Count; + + this.VotesReceived++; + if (request.ReceiverEndpoint != null && + this.VotesReceived >= (this.Servers.Length / 2) + 1) + { + var commitIndex = this.MatchIndex[request.Server]; + if (commitIndex > this.CommitIndex && + this.Logs[commitIndex - 1].Term == this.CurrentTerm) + { + this.CommitIndex = commitIndex; + } + + this.VotesReceived = 0; + this.LastClientRequest = null; + + this.Send(request.ReceiverEndpoint, new Client.Response()); + } + } + else + { + if (this.NextIndex[request.Server] > 1) + { + this.NextIndex[request.Server] = this.NextIndex[request.Server] - 1; + } + + var logs = this.Logs.GetRange(this.NextIndex[request.Server] - 1, this.Logs.Count - (this.NextIndex[request.Server] - 1)); + + var prevLogIndex = this.NextIndex[request.Server] - 1; + var prevLogTerm = this.GetLogTermForIndex(prevLogIndex); + + this.Send(request.Server, new AppendEntriesRequest(this.CurrentTerm, this.Id, prevLogIndex, + prevLogTerm, logs, this.CommitIndex, request.ReceiverEndpoint)); + } + } + + /// + /// Processes the given vote request. + /// + /// VoteRequest + private void Vote(VoteRequest request) + { + var lastLogIndex = this.Logs.Count; + var lastLogTerm = this.GetLogTermForIndex(lastLogIndex); + + if (request.Term < this.CurrentTerm || + (this.VotedFor != null && this.VotedFor != request.CandidateId) || + lastLogIndex > request.LastLogIndex || + lastLogTerm > request.LastLogTerm) + { + this.Send(request.CandidateId, new VoteResponse(this.CurrentTerm, false)); + } + else + { + this.VotedFor = request.CandidateId; + this.LeaderId = null; + + this.Send(request.CandidateId, new VoteResponse(this.CurrentTerm, true)); + } + } + + /// + /// Processes the given append entries request. + /// + /// AppendEntriesRequest + private void AppendEntries(AppendEntriesRequest request) + { + if (request.Term < this.CurrentTerm) + { + this.Send(request.LeaderId, new AppendEntriesResponse(this.CurrentTerm, false, + this.Id, request.ReceiverEndpoint)); + } + else + { + if (request.PrevLogIndex > 0 && + (this.Logs.Count < request.PrevLogIndex || + this.Logs[request.PrevLogIndex - 1].Term != request.PrevLogTerm)) + { + this.Send(request.LeaderId, new AppendEntriesResponse(this.CurrentTerm, false, this.Id, request.ReceiverEndpoint)); + } + else + { + if (request.Entries.Count > 0) + { + var currentIndex = request.PrevLogIndex + 1; + foreach (var entry in request.Entries) + { + if (this.Logs.Count < currentIndex) + { + this.Logs.Add(entry); + } + else if (this.Logs[currentIndex - 1].Term != entry.Term) + { + this.Logs.RemoveRange(currentIndex - 1, this.Logs.Count - (currentIndex - 1)); + this.Logs.Add(entry); + } + + currentIndex++; + } + } + + if (request.LeaderCommit > this.CommitIndex && + this.Logs.Count < request.LeaderCommit) + { + this.CommitIndex = this.Logs.Count; + } + else if (request.LeaderCommit > this.CommitIndex) + { + this.CommitIndex = request.LeaderCommit; + } + + if (this.CommitIndex > this.LastApplied) + { + this.LastApplied++; + } + + this.LeaderId = request.LeaderId; + this.Send(request.LeaderId, new AppendEntriesResponse(this.CurrentTerm, true, this.Id, request.ReceiverEndpoint)); + } + } + } + + private void RedirectLastClientRequestToClusterManager() + { + if (this.LastClientRequest != null) + { + this.Send(this.ClusterManager, this.LastClientRequest); + } + } + + /// + /// Returns the log term for the given log index. + /// + /// Index + /// Term + private int GetLogTermForIndex(int logIndex) + { + var logTerm = 0; + if (logIndex > 0) + { + logTerm = this.Logs[logIndex - 1].Term; + } + + return logTerm; + } + + private void ShuttingDown() + { + this.Send(this.ElectionTimer, new Halt()); + this.Send(this.PeriodicTimer, new Halt()); + + this.Raise(new Halt()); + } + } + + private class Client : Machine + { + /// + /// Used to configure the client. + /// + public class ConfigureEvent : Event + { + public MachineId Cluster; + + public ConfigureEvent(MachineId cluster) + : base() + { + this.Cluster = cluster; + } + } + + /// + /// Used for a client request. + /// + internal class Request : Event + { + public MachineId Client; + public int Command; + + public Request(MachineId client, int command) + : base() + { + this.Client = client; + this.Command = command; + } + } + + internal class Response : Event + { + } + + private class LocalEvent : Event + { + } + + private MachineId Cluster; + + private int LatestCommand; + private int Counter; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(LocalEvent), typeof(PumpRequest))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.LatestCommand = -1; + this.Counter = 0; + } + + private void Configure() + { + this.Cluster = (this.ReceivedEvent as ConfigureEvent).Cluster; + this.Raise(new LocalEvent()); + } + + [OnEntry(nameof(PumpRequestOnEntry))] + [OnEventDoAction(typeof(Response), nameof(ProcessResponse))] + [OnEventGotoState(typeof(LocalEvent), typeof(PumpRequest))] + private class PumpRequest : MachineState + { + } + + private void PumpRequestOnEntry() + { + this.LatestCommand = this.RandomInteger(100); + this.Counter++; + this.Send(this.Cluster, new Request(this.Id, this.LatestCommand)); + } + + private void ProcessResponse() + { + if (this.Counter == 3) + { + this.Send(this.Cluster, new ClusterManager.ShutDown()); + this.Raise(new Halt()); + } + else + { + this.Raise(new LocalEvent()); + } + } + } + + private class ElectionTimer : Machine + { + internal class ConfigureEvent : Event + { + public MachineId Target; + + public ConfigureEvent(MachineId id) + : base() + { + this.Target = id; + } + } + + internal class StartTimerEvent : Event + { + } + + internal class CancelTimer : Event + { + } + + internal class Timeout : Event + { + } + + private class TickEvent : Event + { + } + + private MachineId Target; + + [Start] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Target = (this.ReceivedEvent as ConfigureEvent).Target; + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(TickEvent), nameof(Tick))] + [OnEventGotoState(typeof(CancelTimer), typeof(Inactive))] + [IgnoreEvents(typeof(StartTimerEvent))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.Id, new TickEvent()); + } + + private void Tick() + { + if (this.Random()) + { + this.Send(this.Target, new Timeout()); + } + + this.Raise(new CancelTimer()); + } + + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + [IgnoreEvents(typeof(CancelTimer), typeof(TickEvent))] + private class Inactive : MachineState + { + } + } + + private class PeriodicTimer : Machine + { + internal class ConfigureEvent : Event + { + public MachineId Target; + + public ConfigureEvent(MachineId id) + : base() + { + this.Target = id; + } + } + + internal class StartTimerEvent : Event + { + } + + internal class CancelTimer : Event + { + } + + internal class Timeout : Event + { + } + + private class TickEvent : Event + { + } + + private MachineId Target; + + [Start] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Target = (this.ReceivedEvent as ConfigureEvent).Target; + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(TickEvent), nameof(Tick))] + [OnEventGotoState(typeof(CancelTimer), typeof(Inactive))] + [IgnoreEvents(typeof(StartTimerEvent))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.Id, new TickEvent()); + } + + private void Tick() + { + if (this.Random()) + { + this.Send(this.Target, new Timeout()); + } + + this.Raise(new CancelTimer()); + } + + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + [IgnoreEvents(typeof(CancelTimer), typeof(TickEvent))] + private class Inactive : MachineState + { + } + } + + private class SafetyMonitor : Monitor + { + internal class NotifyLeaderElected : Event + { + public int Term; + + public NotifyLeaderElected(int term) + : base() + { + this.Term = term; + } + } + + private class LocalEvent : Event + { + } + + private HashSet TermsWithLeader; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(LocalEvent), typeof(Monitoring))] + private class Init : MonitorState + { + } + + private void InitOnEntry() + { + this.TermsWithLeader = new HashSet(); + this.Raise(new LocalEvent()); + } + + [OnEventDoAction(typeof(NotifyLeaderElected), nameof(ProcessLeaderElected))] + private class Monitoring : MonitorState + { + } + + private void ProcessLeaderElected() + { + var term = (this.ReceivedEvent as NotifyLeaderElected).Term; + + this.Assert(!this.TermsWithLeader.Contains(term), $"Detected more than one leader."); + this.TermsWithLeader.Add(term); + } + } + + [Theory(Timeout = 10000)] + [InlineData(79)] + public void TestMultipleLeadersInRaftProtocol(int seed) + { + var configuration = GetConfiguration(); + configuration.MaxUnfairSchedulingSteps = 100; + configuration.MaxFairSchedulingSteps = 1000; + configuration.LivenessTemperatureThreshold = 500; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(SafetyMonitor)); + r.CreateMachine(typeof(ClusterManager)); + }, + configuration: configuration, + expectedError: "Detected more than one leader.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/ReplicatingStorageTest.cs b/Tests/TestingServices.Tests/Machines/Integration/ReplicatingStorageTest.cs new file mode 100644 index 000000000..c72692030 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/ReplicatingStorageTest.cs @@ -0,0 +1,903 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + /// + /// This is a (much) simplified version of the replicating storage system described + /// in the following paper: + /// + /// https://www.usenix.org/system/files/conference/fast16/fast16-papers-deligiannis.pdf + /// + /// This test contains the liveness bug discussed in the above paper. + /// + public class ReplicatingStorageTest : BaseTest + { + public ReplicatingStorageTest(ITestOutputHelper output) + : base(output) + { + } + + private class Environment : Machine + { + public class NotifyNode : Event + { + public MachineId Node; + + public NotifyNode(MachineId node) + : base() + { + this.Node = node; + } + } + + public class FaultInject : Event + { + } + + private class CreateFailure : Event + { + } + + private class LocalEvent : Event + { + } + + private MachineId NodeManager; + private int NumberOfReplicas; + + private List AliveNodes; + private int NumberOfFaults; + + private MachineId Client; + + private MachineId FailureTimer; + + [Start] + [OnEntry(nameof(EntryOnInit))] + [OnEventGotoState(typeof(LocalEvent), typeof(Configuring))] + private class Init : MachineState + { + } + + private void EntryOnInit() + { + this.NumberOfReplicas = 3; + this.NumberOfFaults = 1; + this.AliveNodes = new List(); + + this.Monitor(new LivenessMonitor.ConfigureEvent(this.NumberOfReplicas)); + + this.NodeManager = this.CreateMachine(typeof(NodeManager)); + this.Client = this.CreateMachine(typeof(Client)); + + this.Raise(new LocalEvent()); + } + + [OnEntry(nameof(ConfiguringOnInit))] + [OnEventGotoState(typeof(LocalEvent), typeof(Active))] + [DeferEvents(typeof(FailureTimer.Timeout))] + private class Configuring : MachineState + { + } + + private void ConfiguringOnInit() + { + this.Send(this.NodeManager, new NodeManager.ConfigureEvent(this.Id, this.NumberOfReplicas)); + this.Send(this.Client, new Client.ConfigureEvent(this.NodeManager)); + this.Raise(new LocalEvent()); + } + + [OnEventDoAction(typeof(NotifyNode), nameof(UpdateAliveNodes))] + [OnEventDoAction(typeof(FailureTimer.Timeout), nameof(InjectFault))] + private class Active : MachineState + { + } + + private void UpdateAliveNodes() + { + var node = (this.ReceivedEvent as NotifyNode).Node; + this.AliveNodes.Add(node); + + if (this.AliveNodes.Count == this.NumberOfReplicas && + this.FailureTimer is null) + { + this.FailureTimer = this.CreateMachine(typeof(FailureTimer)); + this.Send(this.FailureTimer, new FailureTimer.ConfigureEvent(this.Id)); + } + } + + private void InjectFault() + { + if (this.NumberOfFaults == 0 || + this.AliveNodes.Count == 0) + { + return; + } + + int nodeId = this.RandomInteger(this.AliveNodes.Count); + var node = this.AliveNodes[nodeId]; + + this.Send(node, new FaultInject()); + this.Send(this.NodeManager, new NodeManager.NotifyFailure(node)); + this.AliveNodes.Remove(node); + + this.NumberOfFaults--; + if (this.NumberOfFaults == 0) + { + this.Send(this.FailureTimer, new Halt()); + } + } + } + + private class NodeManager : Machine + { + public class ConfigureEvent : Event + { + public MachineId Environment; + public int NumberOfReplicas; + + public ConfigureEvent(MachineId env, int numOfReplicas) + : base() + { + this.Environment = env; + this.NumberOfReplicas = numOfReplicas; + } + } + + public class NotifyFailure : Event + { + public MachineId Node; + + public NotifyFailure(MachineId node) + : base() + { + this.Node = node; + } + } + + internal class ShutDown : Event + { + } + + private class LocalEvent : Event + { + } + + private MachineId Environment; + private List StorageNodes; + private int NumberOfReplicas; + private Dictionary StorageNodeMap; + private Dictionary DataMap; + private MachineId RepairTimer; + private MachineId Client; + + [Start] + [OnEntry(nameof(EntryOnInit))] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(LocalEvent), typeof(Active))] + [DeferEvents(typeof(Client.Request), typeof(RepairTimer.Timeout))] + private class Init : MachineState + { + } + + private void EntryOnInit() + { + this.StorageNodes = new List(); + this.StorageNodeMap = new Dictionary(); + this.DataMap = new Dictionary(); + + this.RepairTimer = this.CreateMachine(typeof(RepairTimer)); + this.Send(this.RepairTimer, new RepairTimer.ConfigureEvent(this.Id)); + } + + private void Configure() + { + this.Environment = (this.ReceivedEvent as ConfigureEvent).Environment; + this.NumberOfReplicas = (this.ReceivedEvent as ConfigureEvent).NumberOfReplicas; + + for (int idx = 0; idx < this.NumberOfReplicas; idx++) + { + this.CreateNewNode(); + } + + this.Raise(new LocalEvent()); + } + + private void CreateNewNode() + { + var idx = this.StorageNodes.Count; + var node = this.CreateMachine(typeof(StorageNode)); + this.StorageNodes.Add(node); + this.StorageNodeMap.Add(idx, true); + this.Send(node, new StorageNode.ConfigureEvent(this.Environment, this.Id, idx)); + } + + [OnEventDoAction(typeof(Client.Request), nameof(ProcessClientRequest))] + [OnEventDoAction(typeof(RepairTimer.Timeout), nameof(RepairNodes))] + [OnEventDoAction(typeof(StorageNode.SyncReport), nameof(ProcessSyncReport))] + [OnEventDoAction(typeof(NotifyFailure), nameof(ProcessFailure))] + private class Active : MachineState + { + } + + private void ProcessClientRequest() + { + this.Client = (this.ReceivedEvent as Client.Request).Client; + var command = (this.ReceivedEvent as Client.Request).Command; + + var aliveNodeIds = this.StorageNodeMap.Where(n => n.Value).Select(n => n.Key); + foreach (var nodeId in aliveNodeIds) + { + this.Send(this.StorageNodes[nodeId], new StorageNode.StoreRequest(command)); + } + } + + private void RepairNodes() + { + if (this.DataMap.Count == 0) + { + return; + } + + var latestData = this.DataMap.Values.Max(); + var numOfReplicas = this.DataMap.Count(kvp => kvp.Value == latestData); + if (numOfReplicas >= this.NumberOfReplicas) + { + return; + } + + foreach (var node in this.DataMap) + { + if (node.Value != latestData) + { + this.Send(this.StorageNodes[node.Key], new StorageNode.SyncRequest(latestData)); + numOfReplicas++; + } + + if (numOfReplicas == this.NumberOfReplicas) + { + break; + } + } + } + + private void ProcessSyncReport() + { + var nodeId = (this.ReceivedEvent as StorageNode.SyncReport).NodeId; + var data = (this.ReceivedEvent as StorageNode.SyncReport).Data; + + // LIVENESS BUG: can fail to ever repair again as it thinks there + // are enough replicas. Enable to introduce a bug fix. + // if (!this.StorageNodeMap.ContainsKey(nodeId)) + // { + // return; + // } + + if (!this.DataMap.ContainsKey(nodeId)) + { + this.DataMap.Add(nodeId, 0); + } + + this.DataMap[nodeId] = data; + } + + private void ProcessFailure() + { + var node = (this.ReceivedEvent as NotifyFailure).Node; + var nodeId = this.StorageNodes.IndexOf(node); + this.StorageNodeMap.Remove(nodeId); + this.DataMap.Remove(nodeId); + this.CreateNewNode(); + } + } + + private class StorageNode : Machine + { + public class ConfigureEvent : Event + { + public MachineId Environment; + public MachineId NodeManager; + public int Id; + + public ConfigureEvent(MachineId env, MachineId manager, int id) + : base() + { + this.Environment = env; + this.NodeManager = manager; + this.Id = id; + } + } + + public class StoreRequest : Event + { + public int Command; + + public StoreRequest(int cmd) + : base() + { + this.Command = cmd; + } + } + + public class SyncReport : Event + { + public int NodeId; + public int Data; + + public SyncReport(int id, int data) + : base() + { + this.NodeId = id; + this.Data = data; + } + } + + public class SyncRequest : Event + { + public int Data; + + public SyncRequest(int data) + : base() + { + this.Data = data; + } + } + + internal class ShutDown : Event + { + } + + private class LocalEvent : Event + { + } + + private MachineId Environment; + private MachineId NodeManager; + private int NodeId; + private int Data; + private MachineId SyncTimer; + + [Start] + [OnEntry(nameof(EntryOnInit))] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(LocalEvent), typeof(Active))] + [DeferEvents(typeof(SyncTimer.Timeout))] + private class Init : MachineState + { + } + + private void EntryOnInit() + { + this.Data = 0; + this.SyncTimer = this.CreateMachine(typeof(SyncTimer)); + this.Send(this.SyncTimer, new SyncTimer.ConfigureEvent(this.Id)); + } + + private void Configure() + { + this.Environment = (this.ReceivedEvent as ConfigureEvent).Environment; + this.NodeManager = (this.ReceivedEvent as ConfigureEvent).NodeManager; + this.NodeId = (this.ReceivedEvent as ConfigureEvent).Id; + + this.Monitor(new LivenessMonitor.NotifyNodeCreated(this.NodeId)); + this.Send(this.Environment, new Environment.NotifyNode(this.Id)); + + this.Raise(new LocalEvent()); + } + + [OnEventDoAction(typeof(StoreRequest), nameof(Store))] + [OnEventDoAction(typeof(SyncRequest), nameof(Sync))] + [OnEventDoAction(typeof(SyncTimer.Timeout), nameof(GenerateSyncReport))] + [OnEventDoAction(typeof(Environment.FaultInject), nameof(Terminate))] + private class Active : MachineState + { + } + + private void Store() + { + var cmd = (this.ReceivedEvent as StoreRequest).Command; + this.Data += cmd; + this.Monitor(new LivenessMonitor.NotifyNodeUpdate(this.NodeId, this.Data)); + } + + private void Sync() + { + var data = (this.ReceivedEvent as SyncRequest).Data; + this.Data = data; + this.Monitor(new LivenessMonitor.NotifyNodeUpdate(this.NodeId, this.Data)); + } + + private void GenerateSyncReport() + { + this.Send(this.NodeManager, new SyncReport(this.NodeId, this.Data)); + } + + private void Terminate() + { + this.Monitor(new LivenessMonitor.NotifyNodeFail(this.NodeId)); + this.Send(this.SyncTimer, new Halt()); + this.Raise(new Halt()); + } + } + + private class FailureTimer : Machine + { + internal class ConfigureEvent : Event + { + public MachineId Target; + + public ConfigureEvent(MachineId id) + : base() + { + this.Target = id; + } + } + + internal class StartTimerEvent : Event + { + } + + internal class CancelTimer : Event + { + } + + internal class Timeout : Event + { + } + + private class TickEvent : Event + { + } + + private MachineId Target; + + [Start] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Target = (this.ReceivedEvent as ConfigureEvent).Target; + this.Raise(new StartTimerEvent()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(TickEvent), nameof(Tick))] + [OnEventGotoState(typeof(CancelTimer), typeof(Inactive))] + [IgnoreEvents(typeof(StartTimerEvent))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.Id, new TickEvent()); + } + + private void Tick() + { + if (this.Random()) + { + this.Send(this.Target, new Timeout()); + } + + this.Send(this.Id, new TickEvent()); + } + + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + [IgnoreEvents(typeof(CancelTimer), typeof(TickEvent))] + private class Inactive : MachineState + { + } + } + + private class RepairTimer : Machine + { + internal class ConfigureEvent : Event + { + public MachineId Target; + + public ConfigureEvent(MachineId id) + : base() + { + this.Target = id; + } + } + + internal class StartTimerEvent : Event + { + } + + internal class CancelTimer : Event + { + } + + internal class Timeout : Event + { + } + + private class TickEvent : Event + { + } + + private MachineId Target; + + [Start] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Target = (this.ReceivedEvent as ConfigureEvent).Target; + this.Raise(new StartTimerEvent()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(TickEvent), nameof(Tick))] + [OnEventGotoState(typeof(CancelTimer), typeof(Inactive))] + [IgnoreEvents(typeof(StartTimerEvent))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.Id, new TickEvent()); + } + + private void Tick() + { + if (this.Random()) + { + this.Send(this.Target, new Timeout()); + } + + this.Send(this.Id, new TickEvent()); + } + + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + [IgnoreEvents(typeof(CancelTimer), typeof(TickEvent))] + private class Inactive : MachineState + { + } + } + + private class SyncTimer : Machine + { + internal class ConfigureEvent : Event + { + public MachineId Target; + + public ConfigureEvent(MachineId id) + : base() + { + this.Target = id; + } + } + + internal class StartTimerEvent : Event + { + } + + internal class CancelTimer : Event + { + } + + internal class Timeout : Event + { + } + + private class TickEvent : Event + { + } + + private MachineId Target; + + [Start] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Target = (this.ReceivedEvent as ConfigureEvent).Target; + this.Raise(new StartTimerEvent()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(TickEvent), nameof(Tick))] + [OnEventGotoState(typeof(CancelTimer), typeof(Inactive))] + [IgnoreEvents(typeof(StartTimerEvent))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.Id, new TickEvent()); + } + + private void Tick() + { + if (this.Random()) + { + this.Send(this.Target, new Timeout()); + } + + this.Send(this.Id, new TickEvent()); + } + + [OnEventGotoState(typeof(StartTimerEvent), typeof(Active))] + [IgnoreEvents(typeof(CancelTimer), typeof(TickEvent))] + private class Inactive : MachineState + { + } + } + + private class Client : Machine + { + public class ConfigureEvent : Event + { + public MachineId NodeManager; + + public ConfigureEvent(MachineId manager) + : base() + { + this.NodeManager = manager; + } + } + + internal class Request : Event + { + public MachineId Client; + public int Command; + + public Request(MachineId client, int cmd) + : base() + { + this.Client = client; + this.Command = cmd; + } + } + + private class LocalEvent : Event + { + } + + private MachineId NodeManager; + + private int Counter; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(LocalEvent), typeof(PumpRequest))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Counter = 0; + } + + private void Configure() + { + this.NodeManager = (this.ReceivedEvent as ConfigureEvent).NodeManager; + this.Raise(new LocalEvent()); + } + + [OnEntry(nameof(PumpRequestOnEntry))] + [OnEventGotoState(typeof(LocalEvent), typeof(PumpRequest))] + private class PumpRequest : MachineState + { + } + + private void PumpRequestOnEntry() + { + int command = this.RandomInteger(100) + 1; + this.Counter++; + + this.Send(this.NodeManager, new Request(this.Id, command)); + + if (this.Counter == 1) + { + this.Raise(new Halt()); + } + else + { + this.Raise(new LocalEvent()); + } + } + } + + private class LivenessMonitor : Monitor + { + public class ConfigureEvent : Event + { + public int NumberOfReplicas; + + public ConfigureEvent(int numOfReplicas) + : base() + { + this.NumberOfReplicas = numOfReplicas; + } + } + + public class NotifyNodeCreated : Event + { + public int NodeId; + + public NotifyNodeCreated(int id) + : base() + { + this.NodeId = id; + } + } + + public class NotifyNodeFail : Event + { + public int NodeId; + + public NotifyNodeFail(int id) + : base() + { + this.NodeId = id; + } + } + + public class NotifyNodeUpdate : Event + { + public int NodeId; + public int Data; + + public NotifyNodeUpdate(int id, int data) + : base() + { + this.NodeId = id; + this.Data = data; + } + } + + private class LocalEvent : Event + { + } + + private Dictionary DataMap; + private int NumberOfReplicas; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(ConfigureEvent), nameof(Configure))] + [OnEventGotoState(typeof(LocalEvent), typeof(Repaired))] + private class Init : MonitorState + { + } + + private void InitOnEntry() + { + this.DataMap = new Dictionary(); + } + + private void Configure() + { + this.NumberOfReplicas = (this.ReceivedEvent as ConfigureEvent).NumberOfReplicas; + this.Raise(new LocalEvent()); + } + + [Cold] + [OnEventDoAction(typeof(NotifyNodeCreated), nameof(ProcessNodeCreated))] + [OnEventDoAction(typeof(NotifyNodeFail), nameof(FailAndCheckRepair))] + [OnEventDoAction(typeof(NotifyNodeUpdate), nameof(ProcessNodeUpdate))] + [OnEventGotoState(typeof(LocalEvent), typeof(Repairing))] + private class Repaired : MonitorState + { + } + + private void ProcessNodeCreated() + { + var nodeId = (this.ReceivedEvent as NotifyNodeCreated).NodeId; + this.DataMap.Add(nodeId, 0); + } + + private void FailAndCheckRepair() + { + this.ProcessNodeFail(); + this.Raise(new LocalEvent()); + } + + private void ProcessNodeUpdate() + { + var nodeId = (this.ReceivedEvent as NotifyNodeUpdate).NodeId; + var data = (this.ReceivedEvent as NotifyNodeUpdate).Data; + this.DataMap[nodeId] = data; + } + + [Hot] + [OnEventDoAction(typeof(NotifyNodeCreated), nameof(ProcessNodeCreated))] + [OnEventDoAction(typeof(NotifyNodeFail), nameof(ProcessNodeFail))] + [OnEventDoAction(typeof(NotifyNodeUpdate), nameof(CheckIfRepaired))] + [OnEventGotoState(typeof(LocalEvent), typeof(Repaired))] + private class Repairing : MonitorState + { + } + + private void ProcessNodeFail() + { + var nodeId = (this.ReceivedEvent as NotifyNodeFail).NodeId; + this.DataMap.Remove(nodeId); + } + + private void CheckIfRepaired() + { + this.ProcessNodeUpdate(); + var consensus = this.DataMap.Select(kvp => kvp.Value).GroupBy(v => v). + OrderByDescending(v => v.Count()).FirstOrDefault(); + + var numOfReplicas = consensus.Count(); + if (numOfReplicas >= this.NumberOfReplicas) + { + this.Raise(new LocalEvent()); + } + } + } + + [Fact(Timeout=10000)] + public void TestReplicatingStorageLivenessBug() + { + var configuration = GetConfiguration(); + configuration.MaxUnfairSchedulingSteps = 200; + configuration.MaxFairSchedulingSteps = 2000; + configuration.LivenessTemperatureThreshold = 1000; + configuration.RandomSchedulingSeed = 315; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(Environment)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected potential liveness bug in hot state 'Repairing'.", + replay: true); + } + + [Theory(Timeout = 10000)] + [InlineData(855)] + public void TestReplicatingStorageLivenessBugWithCycleReplay(int seed) + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.MaxUnfairSchedulingSteps = 100; + configuration.MaxFairSchedulingSteps = 1000; + configuration.LivenessTemperatureThreshold = 500; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(Environment)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/SendInterleavingsTest.cs b/Tests/TestingServices.Tests/Machines/Integration/SendInterleavingsTest.cs new file mode 100644 index 000000000..ecd52e930 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/SendInterleavingsTest.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class SendInterleavingsTest : BaseTest + { + public SendInterleavingsTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config : Event + { + public MachineId Id; + + public Config(MachineId id) + { + this.Id = id; + } + } + + private class Event1 : Event + { + } + + private class Event2 : Event + { + } + + private class Receiver : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + [OnEventDoAction(typeof(Event1), nameof(OnEvent1))] + [OnEventDoAction(typeof(Event2), nameof(OnEvent2))] + private class Init : MachineState + { + } + + private int count1 = 0; + + private void Initialize() + { + var s1 = this.CreateMachine(typeof(Sender1)); + this.Send(s1, new Config(this.Id)); + var s2 = this.CreateMachine(typeof(Sender2)); + this.Send(s2, new Config(this.Id)); + } + + private void OnEvent1() + { + this.count1++; + } + + private void OnEvent2() + { + this.Assert(this.count1 != 1); + } + } + + private class Sender1 : Machine + { + [Start] + [OnEventDoAction(typeof(Config), nameof(Run))] + private class State : MachineState + { + } + + private void Run() + { + this.Send((this.ReceivedEvent as Config).Id, new Event1()); + this.Send((this.ReceivedEvent as Config).Id, new Event1()); + } + } + + private class Sender2 : Machine + { + [Start] + [OnEventDoAction(typeof(Config), nameof(Run))] + private class State : MachineState + { + } + + private void Run() + { + this.Send((this.ReceivedEvent as Config).Id, new Event2()); + } + } + + [Fact(Timeout=5000)] + public void TestSendInterleavingsAssertionFailure() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(Receiver)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS).WithNumberOfIterations(600), + expectedError: "Detected an assertion failure.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Integration/TwoMachineIntegrationTests.cs b/Tests/TestingServices.Tests/Machines/Integration/TwoMachineIntegrationTests.cs new file mode 100644 index 000000000..5fa27d719 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Integration/TwoMachineIntegrationTests.cs @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TwoMachineIntegrationTests : BaseTest + { + public TwoMachineIntegrationTests(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + public bool Value; + + public E3(bool value) + { + this.Value = value; + } + } + + private class E4 : Event + { + public MachineId Id; + + public E4(MachineId id) + { + this.Id = id; + } + } + + private class SuccessE : Event + { + } + + private class IgnoredE : Event + { + } + + private class M1 : Machine + { + private bool Test = false; + private MachineId TargetId; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(Default), typeof(S1))] + [OnEventDoAction(typeof(E1), nameof(Action1))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.TargetId = this.CreateMachine(typeof(M2)); + this.Raise(new E1()); + } + + private void InitOnExit() + { + this.Send(this.TargetId, new E3(this.Test), options: new SendOptions(assert: 1)); + } + + private class S1 : MachineState + { + } + + private void Action1() + { + this.Test = true; + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E3), nameof(EntryAction))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + } + + private void EntryAction() + { + if (this.ReceivedEvent.GetType() == typeof(E3)) + { + this.Action2(); + } + } + + private void Action2() + { + this.Assert((this.ReceivedEvent as E3).Value == false); + } + } + + private class M3 : Machine + { + private MachineId TargetId; + private int Count; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.TargetId = this.CreateMachine(typeof(M4)); + this.Raise(new SuccessE()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(WaitEvent))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Count += 1; + if (this.Count == 1) + { + this.Send(this.TargetId, new E4(this.Id), options: new SendOptions(assert: 1)); + } + + if (this.Count == 2) + { + this.Send(this.TargetId, new IgnoredE()); + } + + this.Raise(new SuccessE()); + } + + [OnEventGotoState(typeof(E1), typeof(Active))] + private class WaitEvent : MachineState + { + } + + private class Done : MachineState + { + } + } + + private class M4 : Machine + { + [Start] + [OnEventGotoState(typeof(E4), typeof(Active))] + [OnEventDoAction(typeof(IgnoredE), nameof(Action1))] + private class Waiting : MachineState + { + } + + private void Action1() + { + this.Assert(false); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(Waiting))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send((this.ReceivedEvent as E4).Id, new E1(), options: new SendOptions(assert: 1)); + this.Raise(new SuccessE()); + } + } + + private class M5 : Machine + { + private MachineId TargetId; + private int Count; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.TargetId = this.CreateMachine(typeof(M6)); + this.Raise(new SuccessE()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(WaitEvent))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Count += 1; + if (this.Count == 1) + { + this.Send(this.TargetId, new E4(this.Id), options: new SendOptions(assert: 1)); + } + + if (this.Count == 2) + { + this.Send(this.TargetId, new Halt()); + this.Send(this.TargetId, new IgnoredE()); + } + + this.Raise(new SuccessE()); + } + + [OnEventGotoState(typeof(E1), typeof(Active))] + private class WaitEvent : MachineState + { + } + + private class Done : MachineState + { + } + } + + private class M6 : Machine + { + [Start] + [OnEventGotoState(typeof(E4), typeof(Active))] + [OnEventGotoState(typeof(Halt), typeof(Inactive))] + private class Waiting : MachineState + { + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(Waiting))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send((this.ReceivedEvent as E4).Id, new E1(), options: new SendOptions(assert: 1)); + this.Raise(new SuccessE()); + } + + [OnEventDoAction(typeof(IgnoredE), nameof(Action1))] + [IgnoreEvents(typeof(E4))] + private class Inactive : MachineState + { + } + + private void Action1() + { + this.Assert(false); + } + } + + private class M7 : Machine + { + private MachineId TargetId; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.TargetId = this.CreateMachine(typeof(M8)); + this.Raise(new SuccessE()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(Waiting))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.TargetId, new E4(this.Id), options: new SendOptions(assert: 1)); + this.Raise(new SuccessE()); + } + + [OnEventGotoState(typeof(E1), typeof(Active))] + private class Waiting : MachineState + { + } + + private class Done : MachineState + { + } + } + + private class M8 : Machine + { + private int Count2 = 0; + + [Start] + [OnEntry(nameof(EntryWaitPing))] + [OnEventGotoState(typeof(E4), typeof(Active))] + private class Waiting : MachineState + { + } + + private void EntryWaitPing() + { + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventGotoState(typeof(SuccessE), typeof(Waiting))] + [OnEventDoAction(typeof(Halt), nameof(Action1))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Count2 += 1; + + if (this.Count2 == 1) + { + this.Send((this.ReceivedEvent as E4).Id, new E1(), options: new SendOptions(assert: 1)); + } + + if (this.Count2 == 2) + { + this.Send((this.ReceivedEvent as E4).Id, new E1(), options: new SendOptions(assert: 1)); + this.Raise(new Halt()); + return; + } + + this.Raise(new SuccessE()); + } + + private void Action1() + { + this.Assert(false); + } + } + + private class M9 : Machine + { + private MachineId TargetId; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(InitOnExit))] + [OnEventGotoState(typeof(E1), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new E1()); + } + + private void InitOnExit() + { + this.TargetId = this.CreateMachine(typeof(M10)); + this.Send(this.TargetId, new E1(), options: new SendOptions(assert: 1)); + } + + [OnEntry(nameof(ActiveOnEntry))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + this.Send(this.TargetId, new E2(), options: new SendOptions(assert: 1)); + } + } + + private class M10 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(HandleE1))] + [OnEventDoAction(typeof(E2), nameof(HandleE2))] + private class Init : MachineState + { + } + + private void HandleE1() + { + } + + private void HandleE2() + { + this.Assert(false); + } + } + + [Fact(Timeout=5000)] + public void TestTwoMachineIntegration1() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestTwoMachineIntegration2() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestTwoMachineIntegration3() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M5)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestTwoMachineIntegration4() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M7)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestTwoMachineIntegration5() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M9)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/SingleStateMachineTest.cs b/Tests/TestingServices.Tests/Machines/SingleStateMachineTest.cs new file mode 100644 index 000000000..68788606f --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/SingleStateMachineTest.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class SingleStateMachineTest : BaseTest + { + public SingleStateMachineTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public int Counter; + public MachineId Id; + + public E(MachineId id) + { + this.Counter = 0; + this.Id = id; + } + + public E(int c, MachineId id) + { + this.Counter = c; + this.Id = id; + } + } + + private class M : SingleStateMachine + { + private int count; + private MachineId sender; + + protected override Task InitOnEntry(Event e) + { + this.count = 1; + this.sender = (e as E).Id; + return Task.CompletedTask; + } + + protected override Task ProcessEvent(Event e) + { + this.count++; + return Task.CompletedTask; + } + + protected override void OnHalt() + { + this.count++; + this.Runtime.SendEvent(this.sender, new E(this.count, this.Id)); + } + } + + private class Harness : SingleStateMachine + { + protected override async Task InitOnEntry(Event e) + { + var m = this.CreateMachine(typeof(M), new E(this.Id)); + this.Send(m, new E(this.Id)); + this.Send(m, new Halt()); + var r = await this.Receive(typeof(E)); + this.Assert((r as E).Counter == 3); + } + + protected override Task ProcessEvent(Event e) + { + throw new NotImplementedException(); + } + } + + [Fact(Timeout=5000)] + public void TestSingleStateMachine() + { + this.Test(r => + { + r.CreateMachine(typeof(Harness)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100)); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineDelayTest.cs b/Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineDelayTest.cs new file mode 100644 index 000000000..7305e7236 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineDelayTest.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class UncontrolledMachineDelayTest : BaseTest + { + public UncontrolledMachineDelayTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(E))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + this.Send(this.Id, new E()); + await Task.Delay(10); + this.Send(this.Id, new E()); + } + } + + [Fact(Timeout = 5000)] + public void TestUncontrolledMachineDelay() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + expectedError: "Machine '' is trying to wait for an uncontrolled task or awaiter to complete. Please make sure to " + + "avoid using concurrency APIs such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers. If you " + + "are using external libraries that are executing concurrently, you will need to mock them during testing.", + replay: true); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(E))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + this.Send(this.Id, new E()); + await Task.Delay(10).ConfigureAwait(false); + this.Send(this.Id, new E()); + } + } + + [Fact(Timeout = 5000)] + public void TestUncontrolledMachineDelayWithOtherSynchronizationContext() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2)); + }, + expectedError: "Machine '' is trying to wait for an uncontrolled task or awaiter to complete. Please make sure to " + + "avoid using concurrency APIs such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers. If you " + + "are using external libraries that are executing concurrently, you will need to mock them during testing.", + replay: true); + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + Task[] tasks = new Task[2]; + for (int i = 0; i < 2; i++) + { + tasks[i] = this.DelayedRandomAsync(); + } + + await Task.WhenAll(tasks); + } + + private async Task DelayedRandomAsync() + { + await Task.Delay(10).ConfigureAwait(false); + this.Random(); + } + } + + [Fact(Timeout = 5000)] + public void TestUncontrolledMachineDelayLoopWithOtherSynchronizationContext() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3)); + }, + expectedError: "Machine '' is trying to wait for an uncontrolled task or awaiter to complete. Please make sure to " + + "avoid using concurrency APIs such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers. If you " + + "are using external libraries that are executing concurrently, you will need to mock them during testing.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineTaskTest.cs b/Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineTaskTest.cs new file mode 100644 index 000000000..bb469d507 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Threading/UncontrolledMachineTaskTest.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class UncontrolledMachineTaskTest : BaseTest + { + public UncontrolledMachineTaskTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await Task.Run(() => + { + this.Send(this.Id, new E()); + }); + } + } + + [Fact(Timeout = 5000)] + public void TestUncontrolledTaskSendingEvent() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + expectedErrors: new string[] + { + "Machine '' is trying to wait for an uncontrolled task or awaiter to complete. Please make sure to avoid using " + + "concurrency APIs such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers. If you are " + + "using external libraries that are executing concurrently, you will need to mock them during testing.", + "Uncontrolled task with id '' invoked a runtime method. Please make sure to avoid using concurrency APIs such " + + "as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers or controlled tasks. If you are " + + "using external libraries that are executing concurrently, you will need to mock them during testing.", + }, + replay: true); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await Task.Run(() => + { + this.Random(); + }); + } + } + + [Fact(Timeout=5000)] + public void TestUncontrolledTaskInvokingRandom() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2)); + }, + expectedErrors: new string[] + { + "Machine '' is trying to wait for an uncontrolled task or awaiter to complete. Please make sure to avoid using " + + "concurrency APIs such as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers. If you are " + + "using external libraries that are executing concurrently, you will need to mock them during testing.", + "Uncontrolled task with id '' invoked a runtime method. Please make sure to avoid using concurrency APIs such " + + "as 'Task.Run', 'Task.Delay' or 'Task.Yield' inside machine handlers or controlled tasks. If you are " + + "using external libraries that are executing concurrently, you will need to mock them during testing.", + }, + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Transitions/GotoStateExitFailTest.cs b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateExitFailTest.cs new file mode 100644 index 000000000..e95e7fb62 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateExitFailTest.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GotoStateExitFailTest : BaseTest + { + public GotoStateExitFailTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(ExitInit))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Goto(); + } + + private void ExitInit() + { + // This assertion is reachable. + this.Assert(false, "Bug found."); + } + + private class Done : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestGotoStateExitFail() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M)); + }, + expectedError: "Bug found.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Transitions/GotoStateFailTest.cs b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateFailTest.cs new file mode 100644 index 000000000..16c432086 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateFailTest.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GotoStateFailTest : BaseTest + { + public GotoStateFailTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + // This line no longer builds after converting from Goto(typeof(T)) to Goto() + // due to the "where T: MachineState" constraint on Goto(). + // this.Goto(); + + // Added a different failure mode here; try to Goto a state from another machine. + this.Goto(); + } + + private class Done : MachineState + { + } + } + + private class N : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + } + + internal class Done : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestGotoStateFail() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M)); + }, + expectedError: "Machine 'M()' is trying to transition to non-existing state 'Done'.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Transitions/GotoStateMultipleInActionFailTest.cs b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateMultipleInActionFailTest.cs new file mode 100644 index 000000000..70ad082fc --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateMultipleInActionFailTest.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GotoStateTopLevelActionFailTest : BaseTest + { + public GotoStateTopLevelActionFailTest(ITestOutputHelper output) + : base(output) + { + } + + public enum ErrorType + { + CallGoto, + CallPush, + CallRaise, + CallSend, + OnExit + } + + private class Configure : Event + { + public ErrorType ErrorTypeVal; + + public Configure(ErrorType errorTypeVal) + { + this.ErrorTypeVal = errorTypeVal; + } + } + + private class E : Event + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(ExitMethod))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var errorType = (this.ReceivedEvent as Configure).ErrorTypeVal; + this.Foo(); + switch (errorType) + { + case ErrorType.CallGoto: + this.Goto(); + break; + case ErrorType.CallPush: + this.Push(); + break; + case ErrorType.CallRaise: + this.Raise(new E()); + break; + case ErrorType.CallSend: + this.Send(this.Id, new E()); + break; + case ErrorType.OnExit: + break; + default: + break; + } + } + + private void ExitMethod() + { + this.Pop(); + } + + private void Foo() + { + this.Goto(); + } + + private class Done : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestGotoStateTopLevelActionFail1() + { + var expectedError = "Machine 'M()' has called multiple raise, goto, push or pop in the same action."; + this.TestWithError(r => + { + r.CreateMachine(typeof(M), new Configure(ErrorType.CallGoto)); + }, + expectedError: expectedError, + replay: true); + } + + [Fact(Timeout=5000)] + public void TestGotoStateTopLevelActionFail2() + { + var expectedError = "Machine 'M()' has called multiple raise, goto, push or pop in the same action."; + this.TestWithError(r => + { + r.CreateMachine(typeof(M), new Configure(ErrorType.CallRaise)); + }, + expectedError: expectedError, + replay: true); + } + + [Fact(Timeout=5000)] + public void TestGotoStateTopLevelActionFail3() + { + var expectedError = "Machine 'M()' cannot send an event after calling raise, goto, push or pop in the same action."; + this.TestWithError(r => + { + r.CreateMachine(typeof(M), new Configure(ErrorType.CallSend)); + }, + expectedError: expectedError, + replay: true); + } + + [Fact(Timeout=5000)] + public void TestGotoStateTopLevelActionFail4() + { + var expectedError = "Machine 'M()' has called raise, goto, push or pop inside an OnExit method."; + this.TestWithError(r => + { + r.CreateMachine(typeof(M), new Configure(ErrorType.OnExit)); + }, + expectedError: expectedError, + replay: true); + } + + [Fact(Timeout=5000)] + public void TestGotoStateTopLevelActionFail5() + { + var expectedError = "Machine 'M()' has called multiple raise, goto, push or pop in the same action."; + this.TestWithError(r => + { + r.CreateMachine(typeof(M), new Configure(ErrorType.CallPush)); + }, + expectedError: expectedError, + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Transitions/GotoStateTest.cs b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateTest.cs new file mode 100644 index 000000000..931c8d2fd --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Transitions/GotoStateTest.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GotoStateTest : BaseTest + { + public GotoStateTest(ITestOutputHelper output) + : base(output) + { + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Goto(); + } + + private class Done : MachineState + { + } + } + + internal static int MonitorValue; + + private class Safety : Monitor + { + [Start] + [OnEntry(nameof(Init))] + private class S1 : MonitorState + { + } + + [OnEntry(nameof(IncrementValue))] + private class S2 : MonitorState + { + } + + private void Init() + { + this.Goto(); + } + + private void IncrementValue() + { + MonitorValue = 101; + } + } + + [Fact(Timeout=5000)] + public void TestGotoMachineState() + { + this.Test(r => + { + r.CreateMachine(typeof(M)); + }); + } + + [Fact(Timeout=5000)] + public void TestGotoMonitorState() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Safety)); + }); + + Assert.Equal(101, MonitorValue); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Transitions/PushApiTest.cs b/Tests/TestingServices.Tests/Machines/Transitions/PushApiTest.cs new file mode 100644 index 000000000..47dd2fc70 --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Transitions/PushApiTest.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class PushApiTest : BaseTest + { + public PushApiTest(ITestOutputHelper output) + : base(output) + { + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Push(); + } + + [OnEntry(nameof(EntryDone))] + private class Done : MachineState + { + } + + private void EntryDone() + { + // This assert is reachable. + this.Assert(false, "Bug found."); + } + } + + private class E : Event + { + } + + private class M2 : Machine + { + private int cnt = 0; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(E))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Assert(this.cnt == 0); // called once + this.cnt++; + + this.Push(); + } + + [OnEntry(nameof(EntryDone))] + private class Done : MachineState + { + } + + private void EntryDone() + { + this.Pop(); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnExit(nameof(ExitInit))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Push(); + } + + private void ExitInit() + { + // This assert is not reachable. + this.Assert(false, "Bug found."); + } + + private class Done : MachineState + { + } + } + + private class M4a : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + // Added a different failure mode here; try to Goto a state from another machine. + this.Push(); + } + + private class Done : MachineState + { + } + } + + private class M4b : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + } + + internal class Done : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestPushSimple() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + expectedError: "Bug found.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestPushPopSimple() + { + this.Test(r => + { + var m = r.CreateMachine(typeof(M2)); + r.SendEvent(m, new E()); + }); + } + + [Fact(Timeout=5000)] + public void TestPushStateExit() + { + this.Test(r => + { + r.CreateMachine(typeof(M3)); + }); + } + + [Fact(Timeout=5000)] + public void TestPushBadStateFail() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M4a)); + }, + expectedError: "Machine 'M4a()' is trying to transition to non-existing state 'Done'.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Machines/Transitions/PushStateTest.cs b/Tests/TestingServices.Tests/Machines/Transitions/PushStateTest.cs new file mode 100644 index 000000000..25dc1b65c --- /dev/null +++ b/Tests/TestingServices.Tests/Machines/Transitions/PushStateTest.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class PushStateTest : BaseTest + { + public PushStateTest(ITestOutputHelper output) + : base(output) + { + } + + private class A : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Foo))] + [OnEventPushState(typeof(E2), typeof(S1))] + private class S0 : MachineState + { + } + + [OnEventDoAction(typeof(E3), nameof(Bar))] + private class S1 : MachineState + { + } + + private void Foo() + { + } + + private void Bar() + { + this.Pop(); + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class E4 : Event + { + } + + private class B : Machine + { + [Start] + [OnEntry(nameof(Conf))] + private class Init : MachineState + { + } + + private void Conf() + { + var a = this.CreateMachine(typeof(A)); + this.Send(a, new E2()); // push(S1) + this.Send(a, new E1()); // execute foo without popping + this.Send(a, new E3()); // can handle it because A is still in S1 + } + } + + [Fact(Timeout=5000)] + public void TestPushStateEvent() + { + this.Test(r => + { + r.CreateMachine(typeof(B)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/CompletenessTest.cs b/Tests/TestingServices.Tests/Runtime/CompletenessTest.cs new file mode 100644 index 000000000..986c83fe3 --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/CompletenessTest.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CompletenessTest : BaseTest + { + public CompletenessTest(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class P : Monitor + { + [Cold] + [Start] + [OnEventDoAction(typeof(E1), nameof(Fail))] + [OnEventGotoState(typeof(E2), typeof(S2))] + private class S1 : MonitorState + { + } + + [Cold] + [IgnoreEvents(typeof(E1), typeof(E2))] + private class S2 : MonitorState + { + } + + private void Fail() + { + this.Assert(false); + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class S : MachineState + { + } + + private void InitOnEntry() + { + this.Monitor

(new E1()); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class S : MachineState + { + } + + private void InitOnEntry() + { + this.Monitor

(new E2()); + } + } + + [Fact(Timeout=5000)] + public void TestCompleteness1() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(P)); + r.CreateMachine(typeof(M2)); + r.CreateMachine(typeof(M1)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100), + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestCompleteness2() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(P)); + r.CreateMachine(typeof(M1)); + r.CreateMachine(typeof(M2)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100), + expectedError: "Detected an assertion failure.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/CreateMachineIdFromNameTest.cs b/Tests/TestingServices.Tests/Runtime/CreateMachineIdFromNameTest.cs new file mode 100644 index 000000000..762a5eb1d --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/CreateMachineIdFromNameTest.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CreateMachineIdFromNameTest : BaseTest + { + public CreateMachineIdFromNameTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class LivenessMonitor : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(E), typeof(S2))] + private class S1 : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(E), typeof(S3))] + private class S2 : MonitorState + { + } + + [Cold] + private class S3 : MonitorState + { + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Monitor(typeof(LivenessMonitor), new E()); + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName1() + { + this.Test(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + var m1 = r.CreateMachine(typeof(M)); + var m2 = r.CreateMachineIdFromName(typeof(M), "M"); + r.Assert(!m1.Equals(m2)); + r.CreateMachine(m2, typeof(M)); + }); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName2() + { + this.Test(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + var m1 = r.CreateMachineIdFromName(typeof(M), "M1"); + var m2 = r.CreateMachineIdFromName(typeof(M), "M2"); + r.Assert(!m1.Equals(m2)); + r.CreateMachine(m1, typeof(M)); + r.CreateMachine(m2, typeof(M)); + }); + } + + private class M2 : Machine + { + [Start] + private class S : MachineState + { + } + } + + private class M3 : Machine + { + [Start] + private class S : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName4() + { + this.TestWithError(r => + { + var m3 = r.CreateMachineIdFromName(typeof(M3), "M3"); + r.CreateMachine(m3, typeof(M2)); + }, + expectedError: "Cannot bind machine id '' of type 'M3' to a machine of type 'M2'.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName5() + { + this.TestWithError(r => + { + var m1 = r.CreateMachineIdFromName(typeof(M2), "M2"); + r.CreateMachine(m1, typeof(M2)); + r.CreateMachine(m1, typeof(M2)); + }, + expectedError: "Machine id '' is used by an existing machine.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName6() + { + this.TestWithError(r => + { + var m = r.CreateMachineIdFromName(typeof(M2), "M2"); + r.SendEvent(m, new E()); + }, + expectedError: "Cannot send event 'E' to machine id '' that was never previously bound to a machine of type 'M2'", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName7() + { + this.TestWithError(r => + { + var m = r.CreateMachineIdFromName(typeof(M2), "M2"); + r.CreateMachine(m, typeof(M2)); + + // Make sure that the machine halts. + for (int i = 0; i < 100; i++) + { + r.SendEvent(m, new Halt()); + } + + // Trying to bring up a halted machine. + r.CreateMachine(m, typeof(M2)); + }, + configuration: GetConfiguration(), + expectedErrors: new string[] + { + // Note: because RunMachineEventHandler is async, the halted machine + // may or may not be removed by the time we call CreateMachine. + "Machine id '' is used by an existing machine.", + "Machine id '' of a previously halted machine cannot be reused to create a new machine of type 'M2'" + }); + } + + private class E2 : Event + { + public MachineId Mid; + + public E2(MachineId mid) + { + this.Mid = mid; + } + } + + private class M4 : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Process))] + private class S : MachineState + { + } + + private void Process() + { + this.Monitor(new Done()); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class S : MachineState + { + } + + private void InitOnEntry() + { + var mid = (this.ReceivedEvent as E2).Mid; + this.Send(mid, new E()); + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName8() + { + var configuration = Configuration.Create(); + configuration.SchedulingIterations = 100; + + this.TestWithError(r => + { + var m = r.CreateMachineIdFromName(typeof(M4), "M4"); + r.CreateMachine(typeof(M5), new E2(m)); + r.CreateMachine(m, typeof(M4)); + }, + configuration, + "Cannot send event 'E' to machine id '' that was never previously bound to a machine of type 'M4'", + false); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName9() + { + this.Test(r => + { + var m1 = r.CreateMachineIdFromName(typeof(M4), "M4"); + var m2 = r.CreateMachineIdFromName(typeof(M4), "M4"); + r.Assert(m1.Equals(m2)); + }); + } + + private class M6 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var m = this.Runtime.CreateMachineIdFromName(typeof(M4), "M4"); + this.CreateMachine(m, typeof(M4), "friendly"); + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName10() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M6)); + r.CreateMachine(typeof(M6)); + }, + expectedError: "Machine id '' is used by an existing machine.", + replay: true); + } + + private class Done : Event + { + } + + private class WaitUntilDone : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(Done), typeof(S2))] + private class S1 : MonitorState + { + } + + [Cold] + private class S2 : MonitorState + { + } + } + + private class M7 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Runtime.CreateMachineAndExecuteAsync(typeof(M6)); + var m = this.Runtime.CreateMachineIdFromName(typeof(M4), "M4"); + this.Runtime.SendEvent(m, new E()); + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineIdFromName11() + { + this.Test(r => + { + r.RegisterMonitor(typeof(WaitUntilDone)); + r.CreateMachine(typeof(M7)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/CreateMachineWithIdTest.cs b/Tests/TestingServices.Tests/Runtime/CreateMachineWithIdTest.cs new file mode 100644 index 000000000..11bc634f8 --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/CreateMachineWithIdTest.cs @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CreateMachineWithIdTest : BaseTest + { + public CreateMachineWithIdTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class LivenessMonitor : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(E), typeof(S2))] + private class S1 : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(E), typeof(S3))] + private class S2 : MonitorState + { + } + + [Cold] + private class S3 : MonitorState + { + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Monitor(typeof(LivenessMonitor), new E()); + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineWithId1() + { + this.Test(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + var m = r.CreateMachine(typeof(M)); + var mprime = r.CreateMachineId(typeof(M)); + r.Assert(m != mprime); + r.CreateMachine(mprime, typeof(M)); + }); + } + + private class Data + { + public int X; + + public Data() + { + this.X = 0; + } + } + + private class E1 : Event + { + public Data Data; + + public E1(Data data) + { + this.Data = data; + } + } + + private class TerminateReq : Event + { + public MachineId Sender; + + public TerminateReq(MachineId sender) + { + this.Sender = sender; + } + } + + private class TerminateResp : Event + { + } + + private class M1 : Machine + { + private Data data; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Process))] + [OnEventDoAction(typeof(TerminateReq), nameof(Terminate))] + private class S : MachineState + { + } + + private void InitOnEntry() + { + this.data = (this.ReceivedEvent as E1).Data; + this.Process(); + } + + private void Process() + { + if (this.data.X != 10) + { + this.data.X++; + this.Send(this.Id, new E()); + } + else + { + this.Monitor(typeof(LivenessMonitor), new E()); + this.Monitor(typeof(LivenessMonitor), new E()); + } + } + + private void Terminate() + { + this.Send((this.ReceivedEvent as TerminateReq).Sender, new TerminateResp()); + this.Raise(new Halt()); + } + } + + private class Harness : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class S : MachineState + { + } + + private void InitOnEntry() + { + var data = new Data(); + var m1 = this.CreateMachine(typeof(M1), new E1(data)); + var m2 = this.Id.Runtime.CreateMachineId(typeof(M1)); + this.Send(m1, new TerminateReq(this.Id)); + this.Receive(typeof(TerminateResp)); + this.Id.Runtime.CreateMachine(m2, typeof(M1), new E1(data)); + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineWithId2() + { + this.Test(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + var m = r.CreateMachine(typeof(Harness)); + }); + } + + private class M2 : Machine + { + [Start] + private class S : MachineState + { + } + } + + private class M3 : Machine + { + [Start] + private class S : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineWithId3() + { + this.TestWithError(r => + { + MachineId mid = r.CreateMachineId(typeof(M3)); + r.CreateMachine(mid, typeof(M2)); + }, + expectedError: "Cannot bind machine id '' of type 'M3' to a machine of type 'M2'.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineWithId4() + { + this.TestWithError(r => + { + MachineId mid = r.CreateMachine(typeof(M2)); + r.CreateMachine(mid, typeof(M2)); + }, + expectedError: "Machine id '' is used by an existing machine.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineWithId5() + { + this.TestWithError(r => + { + MachineId mid = r.CreateMachineId(typeof(M2)); + r.SendEvent(mid, new E()); + }, + expectedError: "Cannot send event 'E' to machine id '' that was never previously bound to a machine of type 'M2'", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestCreateMachineWithId6() + { + this.TestWithError(r => + { + bool isEventDropped = false; + r.OnEventDropped += (Event e, MachineId target) => + { + isEventDropped = true; + }; + + MachineId mid = r.CreateMachine(typeof(M2)); + while (!isEventDropped) + { + // Make sure the machine halts before trying to reuse its id. + r.SendEvent(mid, new Halt()); + } + + // Trying to bring up a halted machine. + r.CreateMachine(mid, typeof(M2)); + }, + configuration: GetConfiguration(), + expectedErrors: new string[] + { + // Note: because RunMachineEventHandler is async, the halted machine + // may or may not be removed by the time we call CreateMachine. + "Machine id '' is used by an existing machine.", + "Machine id '' of a previously halted machine cannot be reused to create a new machine of type 'M2'" + }); + } + + private class E2 : Event + { + public MachineId Mid; + + public E2(MachineId mid) + { + this.Mid = mid; + } + } + + private class M4 : Machine + { + [Start] + [IgnoreEvents(typeof(E))] + [OnEntry(nameof(InitOnEntry))] + private class S : MachineState + { + } + + private void InitOnEntry() + { + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class S : MachineState + { + } + + private void InitOnEntry() + { + MachineId mid = (this.ReceivedEvent as E2).Mid; + this.Send(mid, new E()); + } + } + + [Fact(Timeout=5000)] + public void TestCreateMachineWithId7() + { + this.TestWithError(r => + { + MachineId mid = r.CreateMachineId(typeof(M4)); + r.CreateMachine(typeof(M5), new E2(mid)); + r.CreateMachine(mid, typeof(M4)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100), + expectedError: "Cannot send event 'E' to machine id '' that was never previously bound to a machine of type 'M4'", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/FairRandomTest.cs b/Tests/TestingServices.Tests/Runtime/FairRandomTest.cs new file mode 100644 index 000000000..e92d3472e --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/FairRandomTest.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class FairRandomTest : BaseTest + { + public FairRandomTest(ITestOutputHelper output) + : base(output) + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class Engine + { + public static bool FairRandom(IMachineRuntime runtime) + { + return runtime.FairRandom(); + } + } + + private class UntilDone : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(E2), typeof(End))] + private class Waiting : MonitorState + { + } + + [Cold] + private class End : MonitorState + { + } + } + + private class M : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(HandleEvent1))] + [OnEventDoAction(typeof(E2), nameof(HandleEvent2))] + private class Init : MachineState + { + } + + private void HandleEvent1() + { + if (Engine.FairRandom(this.Id.Runtime)) + { + this.Send(this.Id, new E2()); + } + else + { + this.Send(this.Id, new E1()); + } + } + + private void HandleEvent2() + { + this.Monitor(new E2()); + this.Raise(new Halt()); + } + } + + [Fact(Timeout=5000)] + public void TestFairRandom() + { + this.Test(r => + { + r.RegisterMonitor(typeof(UntilDone)); + var m = r.CreateMachine(typeof(M)); + r.SendEvent(m, new E1()); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/GetOperationGroupIdTest.cs b/Tests/TestingServices.Tests/Runtime/GetOperationGroupIdTest.cs new file mode 100644 index 000000000..775b01aef --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/GetOperationGroupIdTest.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GetOperationGroupIdTest : BaseTest + { + public GetOperationGroupIdTest(ITestOutputHelper output) + : base(output) + { + } + + private static Guid OperationGroup = Guid.NewGuid(); + + private class E : Event + { + public MachineId Id; + + public E(MachineId id) + { + this.Id = id; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var id = this.Runtime.GetCurrentOperationGroupId(this.Id); + this.Assert(id == Guid.Empty, $"OperationGroupId is not '{Guid.Empty}', but {id}."); + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Runtime.SendEvent(this.Id, new E(this.Id), OperationGroup); + } + + private void CheckEvent() + { + var id = this.Runtime.GetCurrentOperationGroupId(this.Id); + this.Assert(id == OperationGroup, $"OperationGroupId is not '{OperationGroup}', but {id}."); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var target = this.CreateMachine(typeof(M4)); + this.Runtime.GetCurrentOperationGroupId(target); + } + } + + private class M4 : Machine + { + [Start] + private class Init : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestGetOperationGroupIdNotSet() + { + this.Test(r => + { + r.CreateMachine(typeof(M1)); + }); + } + + [Fact(Timeout=5000)] + public void TestGetOperationGroupIdSet() + { + this.Test(r => + { + r.CreateMachine(typeof(M2)); + }); + } + + [Fact(Timeout=5000)] + public void TestGetOperationGroupIdOfNotCurrentMachine() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3)); + }, + expectedError: "Trying to access the operation group id of 'M4()', which is not the currently executing machine.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/MustHandleEventTest.cs b/Tests/TestingServices.Tests/Runtime/MustHandleEventTest.cs new file mode 100644 index 000000000..cb5584063 --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/MustHandleEventTest.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MustHandleEventTest : BaseTest + { + public MustHandleEventTest(ITestOutputHelper output) + : base(output) + { + } + + private class MustHandleEvent : Event + { + public MachineId Id; + + public MustHandleEvent() + { + } + + public MustHandleEvent(MachineId id) + { + this.Id = id; + } + } + + private class MoveEvent : Event + { + } + + private class M1 : Machine + { + [Start] + [IgnoreEvents(typeof(MustHandleEvent))] + private class Init : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public void TestMustHandleEventNotTriggered() + { + this.Test(r => + { + var m = r.CreateMachine(typeof(M1)); + r.SendEvent(m, new MustHandleEvent(), options: new SendOptions(mustHandle: true)); + r.SendEvent(m, new Halt()); + }, + configuration: Configuration.Create().WithNumberOfIterations(100)); + } + + private class M2 : Machine + { + [Start] + [DeferEvents(typeof(MustHandleEvent))] + private class Init : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public void TestMustHandleDeferredEvent() + { + this.TestWithError(r => + { + var m = r.CreateMachine(typeof(M2)); + r.SendEvent(m, new MustHandleEvent(), options: new SendOptions(mustHandle: true)); + r.SendEvent(m, new Halt()); + }, + configuration: Configuration.Create().WithNumberOfIterations(1), + expectedError: "Machine 'M2()' halted before dequeueing must-handle event 'MustHandleEvent'.", + replay: true); + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [DeferEvents(typeof(MustHandleEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + } + + [Fact(Timeout = 5000)] + public void TestMustHandleEventAfterRaisingHalt() + { + this.TestWithError(r => + { + var m = r.CreateMachine(typeof(M3)); + r.SendEvent(m, new MustHandleEvent(), options: new SendOptions(mustHandle: true)); + }, + configuration: Configuration.Create().WithNumberOfIterations(500), + expectedErrors: new string[] + { + "A must-handle event 'MustHandleEvent' was sent to the halted machine 'M3()'.", + "Machine 'M3()' halted before dequeueing must-handle event 'MustHandleEvent'." + }, + replay: true); + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [DeferEvents(typeof(MustHandleEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new Halt()); + } + } + + [Fact(Timeout = 5000)] + public void TestMustHandleEventAfterSendingHalt() + { + this.TestWithError(r => + { + var m = r.CreateMachine(typeof(M4)); + r.SendEvent(m, new MustHandleEvent(), options: new SendOptions(mustHandle: true)); + }, + configuration: Configuration.Create().WithNumberOfIterations(500), + expectedErrors: new string[] + { + "A must-handle event 'MustHandleEvent' was sent to the halted machine 'M4()'.", + "Machine 'M4()' halted before dequeueing must-handle event 'MustHandleEvent'." + }, + replay: true); + } + + private class M5 : Machine + { + [Start] + [DeferEvents(typeof(MustHandleEvent), typeof(Halt))] + [OnEventGotoState(typeof(MoveEvent), typeof(Next))] + private class Init : MachineState + { + } + + private class Next : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public void TestMustHandleDeferredEventAfterStateTransition() + { + this.TestWithError(r => + { + var m = r.CreateMachine(typeof(M5)); + r.SendEvent(m, new Halt()); + r.SendEvent(m, new MustHandleEvent(), options: new SendOptions(mustHandle: true)); + r.SendEvent(m, new MoveEvent()); + }, + configuration: Configuration.Create().WithNumberOfIterations(1), + expectedError: "Machine 'M5()' halted before dequeueing must-handle event 'MustHandleEvent'.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/OnEventDequeueOrHandledTest.cs b/Tests/TestingServices.Tests/Runtime/OnEventDequeueOrHandledTest.cs new file mode 100644 index 000000000..6e661001d --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/OnEventDequeueOrHandledTest.cs @@ -0,0 +1,586 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class OnEventDequeueOrHandledTest : BaseTest + { + public OnEventDequeueOrHandledTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + private class Begin : Event + { + public Event Ev; + + public Begin(Event ev) + { + this.Ev = ev; + } + } + + private class End : Event + { + public Event Ev; + + public End(Event ev) + { + this.Ev = ev; + } + } + + private class Done : Event + { + } + + // Ensures that machine M1 sees the following calls: + // OnEventDequeueAsync(E1), OnEventHandledAsync(E1), OnEventDequeueAsync(E2), OnEventHandledAsync(E2) + private class Spec1 : Monitor + { + private int counter = 0; + + [Start] + [Hot] + [OnEventDoAction(typeof(Begin), nameof(Process))] + [OnEventDoAction(typeof(End), nameof(Process))] + private class S1 : MonitorState + { + } + + [Cold] + private class S2 : MonitorState + { + } + + private void Process() + { + if (this.counter == 0 && this.ReceivedEvent is Begin && (this.ReceivedEvent as Begin).Ev is E1) + { + this.counter++; + } + else if (this.counter == 1 && this.ReceivedEvent is End && (this.ReceivedEvent as End).Ev is E1) + { + this.counter++; + } + else if (this.counter == 2 && this.ReceivedEvent is Begin && (this.ReceivedEvent as Begin).Ev is E2) + { + this.counter++; + } + else if (this.counter == 3 && this.ReceivedEvent is End && (this.ReceivedEvent as End).Ev is E2) + { + this.counter++; + } + else + { + this.Assert(false); + } + + if (this.counter == 4) + { + this.Goto(); + } + } + } + + private class M1 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + [OnEventDoAction(typeof(E2), nameof(Process))] + [OnEventDoAction(typeof(E3), nameof(ProcessE3))] + private class Init : MachineState + { + } + + private void Process() + { + this.Raise(new E3()); + } + + private void ProcessE3() + { + } + + protected override Task OnEventDequeueAsync(Event ev) + { + this.Monitor(new Begin(ev)); + return Task.CompletedTask; + } + + protected override Task OnEventHandledAsync(Event ev) + { + this.Monitor(new End(ev)); + return Task.CompletedTask; + } + } + + [Fact(Timeout=5000)] + public void TestOnProcessingCalled() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec1)); + var m = r.CreateMachine(typeof(M1), new E()); + r.SendEvent(m, new E1()); + r.SendEvent(m, new E2()); + }); + } + + // Ensures that machine M2 sees the following calls: + // OnEventDequeueAsync(E1) + private class Spec2 : Monitor + { + private int counter = 0; + + [Start] + [Hot] + [OnEventDoAction(typeof(Begin), nameof(Process))] + private class S1 : MonitorState + { + } + + [Cold] + private class S2 : MonitorState + { + } + + private void Process() + { + if (this.counter == 0 && this.ReceivedEvent is Begin && (this.ReceivedEvent as Begin).Ev is E1) + { + this.counter++; + } + else + { + this.Assert(false); + } + + if (this.counter == 1) + { + this.Goto(); + } + } + } + + private class M2 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class Init : MachineState + { + } + + private void Process() + { + this.Raise(new Halt()); + } + + protected override Task OnEventDequeueAsync(Event ev) + { + this.Monitor(new Begin(ev)); + return Task.CompletedTask; + } + + protected override Task OnEventHandledAsync(Event ev) + { + this.Monitor(new End(ev)); + return Task.CompletedTask; + } + } + + [Fact(Timeout=5000)] + public void TestOnProcessingNotCalledOnHalt() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec2)); + var m = r.CreateMachine(typeof(M2)); + r.SendEvent(m, new E1()); + }); + } + + private class Spec3 : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(Done), typeof(S2))] + private class S1 : MonitorState + { + } + + [Cold] + private class S2 : MonitorState + { + } + } + + private class M3 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private class S2 : MachineState + { + } + + [OnEntry(nameof(Finish))] + private class S3 : MachineState + { + } + + private void Process() + { + this.Goto(); + } + + private void Finish() + { + this.Monitor(new Done()); + } + + protected override Task OnEventHandledAsync(Event ev) + { + this.Assert(ev is E1); + this.Assert(this.CurrentState.Name == typeof(S2).Name); + this.Goto(); + return Task.CompletedTask; + } + } + + [Fact(Timeout=5000)] + public void TestOnProcessingCanGoto() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec3)); + var m = r.CreateMachine(typeof(M3)); + r.SendEvent(m, new E1()); + }); + } + + private class Spec4 : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(Done), typeof(S2))] + private class S1 : MonitorState + { + } + + [Cold] + private class S2 : MonitorState + { + } + } + + private class M4 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private void Process() + { + } + + protected override Task OnEventHandledAsync(Event ev) + { + this.Raise(new Halt()); + return Task.CompletedTask; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + [Fact(Timeout=5000)] + public void TestOnProcessingCanHalt() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec4)); + var m = r.CreateMachine(typeof(M4)); + r.SendEvent(m, new E1()); + r.SendEvent(m, new E2()); // Dropped silently. + }); + } + + private class M5 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private void Process() + { + } + + protected override Task OnEventDequeueAsync(Event e) + { + throw new InvalidOperationException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HaltMachine; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + [Fact(Timeout = 5000)] + public void TestExceptionOnEventDequeueWithHaltMachineOutcome() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec4)); + var m = r.CreateMachine(typeof(M5)); + r.SendEvent(m, new E1()); + r.SendEvent(m, new E2()); // Dropped silently. + }); + } + + private class M6 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private void Process() + { + } + + protected override Task OnEventDequeueAsync(Event e) + { + throw new InvalidOperationException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HandledException; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + [Fact(Timeout = 5000)] + public void TestExceptionOnEventDequeueWithHandledExceptionOutcome() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(Spec4)); + var m = r.CreateMachine(typeof(M6)); + r.SendEvent(m, new E1()); + }, + configuration: GetConfiguration().WithNumberOfIterations(100), + expectedError: "Monitor 'Spec4' detected liveness bug in hot state 'S1' at the end of program execution.", + replay: true); + } + + private class M7 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private void Process() + { + } + + protected override Task OnEventDequeueAsync(Event e) + { + throw new InvalidOperationException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.ThrowException; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + [Fact(Timeout = 5000)] + public void TestExceptionOnEventDequeueWithThrowExceptionOutcome() + { + this.TestWithException(r => + { + r.RegisterMonitor(typeof(Spec4)); + var m = r.CreateMachine(typeof(M7)); + r.SendEvent(m, new E1()); + }, + configuration: GetConfiguration().WithNumberOfIterations(100), + replay: true); + } + + private class M8 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private void Process() + { + } + + protected override Task OnEventHandledAsync(Event e) + { + throw new InvalidOperationException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HaltMachine; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + [Fact(Timeout = 5000)] + public void TestExceptionOnEventHandledWithHaltMachineOutcome() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec4)); + var m = r.CreateMachine(typeof(M8)); + r.SendEvent(m, new E1()); + r.SendEvent(m, new E2()); // Dropped silently. + }); + } + + private class M9 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private void Process() + { + } + + protected override Task OnEventHandledAsync(Event e) + { + throw new InvalidOperationException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HandledException; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + [Fact(Timeout = 5000)] + public void TestExceptionOnEventHandledWithHandledExceptionOutcome() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(Spec4)); + var m = r.CreateMachine(typeof(M9)); + r.SendEvent(m, new E1()); + }, + configuration: GetConfiguration().WithNumberOfIterations(100), + expectedError: "Monitor 'Spec4' detected liveness bug in hot state 'S1' at the end of program execution.", + replay: true); + } + + private class M10 : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Process))] + private class S1 : MachineState + { + } + + private void Process() + { + } + + protected override Task OnEventHandledAsync(Event e) + { + throw new InvalidOperationException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.ThrowException; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + [Fact(Timeout = 5000)] + public void TestExceptionOnEventHandledWithThrowExceptionOutcome() + { + this.TestWithException(r => + { + r.RegisterMonitor(typeof(Spec4)); + var m = r.CreateMachine(typeof(M10)); + r.SendEvent(m, new E1()); + }, + configuration: GetConfiguration().WithNumberOfIterations(100), + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/OnEventDroppedTest.cs b/Tests/TestingServices.Tests/Runtime/OnEventDroppedTest.cs new file mode 100644 index 000000000..ba8cee830 --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/OnEventDroppedTest.cs @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class OnEventDroppedTest : BaseTest + { + public OnEventDroppedTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public MachineId Id; + + public E() + { + } + + public E(MachineId id) + { + this.Id = id; + } + } + + private class M1 : Machine + { + [Start] + private class Init : MachineState + { + } + + protected override void OnHalt() + { + this.Send(this.Id, new E()); + } + } + + [Fact(Timeout=5000)] + public void TestOnDroppedCalled1() + { + this.TestWithError(r => + { + r.OnEventDropped += (e, target) => + { + r.Assert(false); + }; + + var m = r.CreateMachine(typeof(M1)); + r.SendEvent(m, new Halt()); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new Halt()); + this.Send(this.Id, new E()); + } + } + + [Fact(Timeout=5000)] + public void TestOnDroppedCalled2() + { + this.TestWithError(r => + { + r.OnEventDropped += (e, target) => + { + r.Assert(false); + }; + + var m = r.CreateMachine(typeof(M2)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestOnDroppedParams() + { + this.Test(r => + { + var m = r.CreateMachine(typeof(M1)); + + r.OnEventDropped += (e, target) => + { + r.Assert(e is E); + r.Assert(target == m); + }; + + r.SendEvent(m, new Halt()); + }); + } + + private class EventProcessed : Event + { + } + + private class EventDropped : Event + { + } + + private class Monitor3 : Monitor + { + [Hot] + [Start] + [OnEventGotoState(typeof(EventProcessed), typeof(S2))] + [OnEventGotoState(typeof(EventDropped), typeof(S2))] + private class S1 : MonitorState + { + } + + [Cold] + private class S2 : MonitorState + { + } + } + + private class M3a : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send((this.ReceivedEvent as E).Id, new Halt()); + } + } + + private class M3b : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send((this.ReceivedEvent as E).Id, new E()); + } + } + + private class M3c : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Processed))] + private class Init : MachineState + { + } + + private void Processed() + { + this.Monitor(new EventProcessed()); + } + } + + [Fact(Timeout=5000)] + public void TestProcessedOrDropped() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Monitor3)); + r.OnEventDropped += (e, target) => + { + r.InvokeMonitor(typeof(Monitor3), new EventDropped()); + }; + + var m = r.CreateMachine(typeof(M3c)); + r.CreateMachine(typeof(M3a), new E(m)); + r.CreateMachine(typeof(M3b), new E(m)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200)); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/OnEventUnhandledTest.cs b/Tests/TestingServices.Tests/Runtime/OnEventUnhandledTest.cs new file mode 100644 index 000000000..e2d5434ca --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/OnEventUnhandledTest.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class OnEventUnhandledTest : BaseTest + { + public OnEventUnhandledTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M1 : Machine + { + [Start] + private class S : MachineState + { + } + + protected override Task OnEventUnhandledAsync(Event e, string currentState) + { + this.Assert(currentState == "S"); + this.Assert(e is E); + this.Assert(false, "The 'OnEventUnhandled' callback was called."); + return Task.CompletedTask; + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HaltMachine; + } + } + + [Fact(Timeout = 5000)] + public void TestOnEventUnhandledCalled() + { + this.TestWithError(r => + { + var m = r.CreateMachine(typeof(M1)); + r.SendEvent(m, new E()); + }, + expectedError: "The 'OnEventUnhandled' callback was called."); + } + + private class M2 : Machine + { + private int x = 0; + + [Start] + private class S : MachineState + { + } + + protected override Task OnEventUnhandledAsync(Event e, string currentState) + { + this.Assert(this.x == 0, "The 'OnEventUnhandled' callback was not called first."); + this.x++; + return Task.CompletedTask; + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.Assert(this.x == 1, "The 'OnException' callback was not called second."); + return OnExceptionOutcome.HaltMachine; + } + } + + [Fact(Timeout = 5000)] + public void TestOnExceptionCalledAfterOnEventUnhandled() + { + this.Test(r => + { + var m = r.CreateMachine(typeof(M2)); + r.SendEvent(m, new E()); + }); + } + + private class M3 : Machine + { + private int x = 0; + + [Start] + private class S : MachineState + { + } + + protected override Task OnEventUnhandledAsync(Event e, string currentState) + { + this.Assert(this.x == 0, "The 'OnEventUnhandled' callback was not called first."); + this.x++; + return Task.CompletedTask; + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.Assert(this.x == 1, "The 'OnException' callback was not called second."); + return OnExceptionOutcome.ThrowException; + } + } + + [Fact(Timeout = 5000)] + public void TestEventUnhandledExceptionPropagation() + { + this.TestWithError(r => + { + var m = r.CreateMachine(typeof(M3)); + r.SendEvent(m, new E()); + }, + expectedError: "Machine 'M3()' received event 'E' that cannot be handled."); + } + + private class M4 : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(HandleE))] + private class S : MachineState + { + } + + private void HandleE() + { + throw new Exception(); + } + + protected override Task OnEventUnhandledAsync(Event e, string currentState) + { + this.Assert(false, "The 'OnEventUnhandled' callback was called."); + return Task.CompletedTask; + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HaltMachine; + } + } + + [Fact(Timeout = 5000)] + public void TestOnEventUnhandledNotCalled() + { + this.Test(r => + { + var m = r.CreateMachine(typeof(M4)); + r.SendEvent(m, new E()); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/OnExceptionTest.cs b/Tests/TestingServices.Tests/Runtime/OnExceptionTest.cs new file mode 100644 index 000000000..ada4bc04c --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/OnExceptionTest.cs @@ -0,0 +1,443 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class OnExceptionTest : BaseTest + { + public OnExceptionTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public MachineId Id; + + public E() + { + } + + public E(MachineId id) + { + this.Id = id; + } + } + + private class Ex1 : Exception + { + } + + private class Ex2 : Exception + { + } + + private class M1a : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + throw new Ex1(); + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + if (ex is Ex1) + { + return OnExceptionOutcome.HandledException; + } + + return OnExceptionOutcome.ThrowException; + } + } + + private class M1b : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new E(this.Id)); + } + + private void Act() + { + throw new Ex1(); + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + if (ex is Ex1) + { + return OnExceptionOutcome.HandledException; + } + + return OnExceptionOutcome.ThrowException; + } + } + + private class M1c : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new E(this.Id)); + } + + private async Task Act() + { + await Task.Delay(0); + throw new Ex1(); + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + if (ex is Ex1) + { + return OnExceptionOutcome.HandledException; + } + + return OnExceptionOutcome.ThrowException; + } + } + + private class M1d : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E), typeof(Done))] + [OnExit(nameof(InitOnExit))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new E(this.Id)); + } + + private void InitOnExit() + { + throw new Ex1(); + } + + private class Done : MachineState + { + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + if (ex is Ex1) + { + return OnExceptionOutcome.HandledException; + } + + return OnExceptionOutcome.ThrowException; + } + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + throw new Ex2(); + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + if (ex is Ex1) + { + return OnExceptionOutcome.HandledException; + } + + return OnExceptionOutcome.ThrowException; + } + } + + private class M3a : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + throw new Ex1(); + } + + private void Act() + { + this.Assert(false); + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + if (ex is Ex1) + { + this.Raise(new E(this.Id)); + return OnExceptionOutcome.HandledException; + } + + return OnExceptionOutcome.HandledException; + } + } + + private class M3b : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(Act))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + throw new Ex1(); + } + + private void Act() + { + this.Assert(false); + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + if (ex is Ex1) + { + this.Send(this.Id, new E(this.Id)); + return OnExceptionOutcome.HandledException; + } + + return OnExceptionOutcome.ThrowException; + } + } + + private class Done : Event + { + } + + private class GetsDone : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(Done), typeof(Ok))] + private class Init : MonitorState + { + } + + [Cold] + private class Ok : MonitorState + { + } + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + throw new NotImplementedException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return OnExceptionOutcome.HaltMachine; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + private class M5 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + if (ex is UnhandledEventException) + { + return OnExceptionOutcome.HaltMachine; + } + + return OnExceptionOutcome.ThrowException; + } + + protected override void OnHalt() + { + this.Monitor(new Done()); + } + } + + private class M6 : Machine + { + [Start] + private class Init : MachineState + { + } + + protected override OnExceptionOutcome OnException(string method, Exception ex) + { + try + { + this.Assert(ex is UnhandledEventException); + this.Send(this.Id, new E(this.Id)); + this.Raise(new E()); + } + catch (Exception) + { + this.Assert(false); + } + + return OnExceptionOutcome.HandledException; + } + } + + [Fact(Timeout=5000)] + public void TestExceptionSuppressed1() + { + this.Test(r => + { + r.CreateMachine(typeof(M1a)); + }); + } + + [Fact(Timeout=5000)] + public void TestExceptionSuppressed2() + { + this.Test(r => + { + r.CreateMachine(typeof(M1b)); + }); + } + + [Fact(Timeout=5000)] + public void TestExceptionSuppressed3() + { + this.Test(r => + { + r.CreateMachine(typeof(M1c)); + }); + } + + [Fact(Timeout=5000)] + public void TestExceptionSuppressed4() + { + this.Test(r => + { + r.CreateMachine(typeof(M1d)); + }); + } + + [Fact(Timeout=5000)] + public void TestExceptionNotSuppressed() + { + this.TestWithException(r => + { + r.CreateMachine(typeof(M2)); + }, + replay: true); + } + + [Fact(Timeout=5000)] + public void TestRaiseOnException() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3a)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestSendOnException() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M3b)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestMachineHalt1() + { + this.Test(r => + { + r.RegisterMonitor(typeof(GetsDone)); + r.CreateMachine(typeof(M4)); + }); + } + + [Fact(Timeout=5000)] + public void TestMachineHalt2() + { + this.Test(r => + { + r.RegisterMonitor(typeof(GetsDone)); + var m = r.CreateMachine(typeof(M5)); + r.SendEvent(m, new E()); + }); + } + + [Fact(Timeout=5000)] + public void TestSendOnUnhandledEventException() + { + this.Test(r => + { + var m = r.CreateMachine(typeof(M6)); + r.SendEvent(m, new E()); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/OnHaltTest.cs b/Tests/TestingServices.Tests/Runtime/OnHaltTest.cs new file mode 100644 index 000000000..5875f3bf1 --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/OnHaltTest.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class OnHaltTest : BaseTest + { + public OnHaltTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public MachineId Id; + + public E() + { + } + + public E(MachineId id) + { + this.Id = id; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Assert(false); + } + } + + private class M2a : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Receive(typeof(Event)).Wait(); + } + } + + private class M2b : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Raise(new E()); + } + } + + private class M2c : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Goto(); + } + } + + private class Dummy : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Halt()); + } + } + + private class M3 : Machine + { + private MachineId sender; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.sender = (this.ReceivedEvent as E).Id; + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + // no-ops but no failure + this.Send(this.sender, new E()); + this.Random(); + this.Assert(true); + this.CreateMachine(typeof(Dummy)); + } + } + + private class M4 : Machine + { + [Start] + [IgnoreEvents(typeof(E))] + private class Init : MachineState + { + } + } + + [Fact(Timeout=5000)] + public void TestHaltCalled() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1)); + }, + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestReceiveOnHalt() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2a)); + }, + expectedError: "Machine 'M2a()' invoked Receive while halted.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestRaiseOnHalt() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2b)); + }, + expectedError: "Machine 'M2b()' invoked Raise while halted.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestGotoOnHalt() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2c)); + }, + expectedError: "Machine 'M2c()' invoked Goto while halted.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestAPIsOnHalt() + { + this.Test(r => + { + var m = r.CreateMachine(typeof(M4)); + r.CreateMachine(typeof(M3), new E(m)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/OperationGroupingTest.cs b/Tests/TestingServices.Tests/Runtime/OperationGroupingTest.cs new file mode 100644 index 000000000..71747ed55 --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/OperationGroupingTest.cs @@ -0,0 +1,615 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class OperationGroupingTest : BaseTest + { + public OperationGroupingTest(ITestOutputHelper output) + : base(output) + { + } + + private static Guid OperationGroup1 = Guid.NewGuid(); + private static Guid OperationGroup2 = Guid.NewGuid(); + + private class E : Event + { + public MachineId Id; + + public E() + { + } + + public E(MachineId id) + { + this.Id = id; + } + } + + private class M1 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var id = this.OperationGroupId; + this.Assert(id == Guid.Empty, $"Operation group id is not '{Guid.Empty}', but {id}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingSingleMachineNoSend() + { + this.Test(r => + { + r.CreateMachine(typeof(M1)); + }); + } + + private class M2 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E()); + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == Guid.Empty, $"Operation group id is not '{Guid.Empty}', but {id}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingSingleMachineSend() + { + this.Test(r => + { + r.CreateMachine(typeof(M2)); + }); + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Runtime.SendEvent(this.Id, new E(), OperationGroup1); + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup1, $"Operation group id is not '{OperationGroup1}', but {id}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingSingleMachineSendStarter() + { + this.Test(r => + { + r.CreateMachine(typeof(M3)); + }); + } + + private class M4A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.CreateMachine(typeof(M4B)); + } + } + + private class M4B : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var id = this.OperationGroupId; + this.Assert(id == Guid.Empty, $"Operation group id is not '{Guid.Empty}', but {id}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingTwoMachinesCreate() + { + this.Test(r => + { + r.CreateMachine(typeof(M4A)); + }); + } + + private class M5A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var target = this.CreateMachine(typeof(M5B)); + this.Send(target, new E()); + } + } + + private class M5B : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == Guid.Empty, $"Operation group id is not '{Guid.Empty}', but {id}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingTwoMachinesSend() + { + this.Test(r => + { + r.CreateMachine(typeof(M5A)); + }); + } + + private class M6A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var target = this.CreateMachine(typeof(M6B)); + this.Runtime.SendEvent(target, new E(), OperationGroup1); + } + } + + private class M6B : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup1, $"Operation group id is not '{OperationGroup1}', but {id}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingTwoMachinesSendStarter() + { + this.Test(r => + { + r.CreateMachine(typeof(M6A)); + }); + } + + private class M7A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var target = this.CreateMachine(typeof(M7B)); + this.Runtime.SendEvent(target, new E(this.Id), OperationGroup1); + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup1, $"Operation group id is not '{OperationGroup1}', but {id}."); + } + } + + private class M7B : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup1, $"Operation group id is not '{OperationGroup1}', but {id}."); + this.Send((this.ReceivedEvent as E).Id, new E()); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingTwoMachinesSendBack() + { + this.Test(r => + { + r.CreateMachine(typeof(M7A)); + }); + } + + private class M8A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var target = this.CreateMachine(typeof(M8B)); + this.Runtime.SendEvent(target, new E(this.Id), OperationGroup1); + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup2, $"Operation group id is not '{OperationGroup2}', but {id}."); + } + } + + private class M8B : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup1, $"Operation group id is not '{OperationGroup1}', but {id}."); + this.Runtime.SendEvent((this.ReceivedEvent as E).Id, new E(), OperationGroup2); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingTwoMachinesSendBackStarter() + { + this.Test(r => + { + r.CreateMachine(typeof(M8A)); + }); + } + + private class M9A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var target = this.CreateMachine(typeof(M9B)); + this.Runtime.SendEvent(target, new E(this.Id), OperationGroup1); + } + + private void CheckEvent() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup2, $"Operation group id is not '{OperationGroup2}', but {id}."); + } + } + + private class M9B : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(CheckEvent))] + private class Init : MachineState + { + } + + private void CheckEvent() + { + this.CreateMachine(typeof(M9C)); + var id = this.OperationGroupId; + this.Assert(id == OperationGroup1, $"Operation group id is not '{OperationGroup1}', but {id}."); + this.Runtime.SendEvent((this.ReceivedEvent as E).Id, new E(), OperationGroup2); + } + } + + private class M9C : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var id = this.OperationGroupId; + this.Assert(id == OperationGroup1, $"Operation group id is not '{OperationGroup1}', but {id}."); + } + } + + [Fact(Timeout=5000)] + public void TestOperationGroupingThreeMachinesSendStarter() + { + this.Test(r => + { + r.CreateMachine(typeof(M9A)); + }); + } + + private class M10 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E), typeof(Final))] + private class Init : MachineState + { + } + + [OnEntry(nameof(FinalOnEntry))] + [OnEventDoAction(typeof(E), nameof(Check))] + private class Final : MachineState + { + } + + private void InitOnEntry() + { + var e = new E(this.Id); + this.Send(this.Id, e, OperationGroup1); + this.Runtime.SendEvent(this.Id, e, OperationGroup2); + } + + private void FinalOnEntry() + { + this.Assert(this.OperationGroupId == OperationGroup1, + $"[1] Operation group id is not '{OperationGroup1}', but {this.OperationGroupId}."); + } + + private void Check() + { + this.Assert(this.OperationGroupId == OperationGroup2, + $"[2] Operation group id is not '{OperationGroup2}', but {this.OperationGroupId}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingSendSameEventWithOtherOpId() + { + this.Test(r => + { + r.CreateMachine(typeof(M10)); + }); + } + + private class M11 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E), typeof(Final))] + private class Init : MachineState + { + } + + [OnEntry(nameof(FinalOnEntry))] + [OnEventDoAction(typeof(E), nameof(Check))] + private class Final : MachineState + { + } + + private void InitOnEntry() + { + var e = new E(this.Id); + this.Send(this.Id, e, OperationGroup1); + this.OperationGroupId = OperationGroup2; + this.Runtime.SendEvent(this.Id, e); + } + + private void FinalOnEntry() + { + this.Assert(this.OperationGroupId == OperationGroup1, + $"[1] Operation group id is not '{OperationGroup1}', but {this.OperationGroupId}."); + } + + private void Check() + { + this.Assert(this.OperationGroupId == OperationGroup2, + $"[2] Operation group id is not '{OperationGroup2}', but {this.OperationGroupId}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingSendSameEventWithOtherMachineOpId() + { + this.Test(r => + { + r.CreateMachine(typeof(M11)); + }); + } + + private class M12 : Machine + { + private E Event; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E), typeof(Intermediate))] + private class Init : MachineState + { + } + + [OnEntry(nameof(IntermediateOnEntry))] + [IgnoreEvents(typeof(E))] + private class Intermediate : MachineState + { + } + + [OnEntry(nameof(FinalOnEntry))] + private class Final : MachineState + { + } + + private void InitOnEntry() + { + this.Event = new E(this.Id); + this.Raise(this.Event, OperationGroup1); + } + + private void IntermediateOnEntry() + { + this.Assert(this.OperationGroupId == OperationGroup1, + $"[1] Operation group id is not '{OperationGroup1}', but {this.OperationGroupId}."); + this.Raise(this.Event, OperationGroup2); + } + + private void FinalOnEntry() + { + this.Assert(this.OperationGroupId == OperationGroup2, + $"[2] Operation group id is not '{OperationGroup2}', but {this.OperationGroupId}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingRaiseSameEventWithOtherOpId() + { + this.Test(r => + { + r.CreateMachine(typeof(M12)); + }); + } + + private class M13 : Machine + { + private E Event; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(E), typeof(Intermediate))] + private class Init : MachineState + { + } + + [OnEntry(nameof(IntermediateOnEntry))] + [IgnoreEvents(typeof(E))] + private class Intermediate : MachineState + { + } + + [OnEntry(nameof(FinalOnEntry))] + private class Final : MachineState + { + } + + private void InitOnEntry() + { + this.Event = new E(this.Id); + this.Raise(this.Event, OperationGroup1); + } + + private void IntermediateOnEntry() + { + this.Assert(this.OperationGroupId == OperationGroup1, + $"[1] Operation group id is not '{OperationGroup1}', but {this.OperationGroupId}."); + this.OperationGroupId = OperationGroup2; + this.Assert(this.OperationGroupId == OperationGroup2, + $"[2] Operation group id is not '{OperationGroup2}', but {this.OperationGroupId}."); + this.Raise(this.Event); + } + + private void FinalOnEntry() + { + this.Assert(this.OperationGroupId == OperationGroup2, + $"[3] Operation group id is not '{OperationGroup2}', but {this.OperationGroupId}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingRaiseSameEventWithOtherMachineOpId() + { + this.Test(r => + { + r.CreateMachine(typeof(M13)); + }); + } + + private class M14 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + this.Send(this.Id, new E(this.Id), OperationGroup1); + this.Assert(this.OperationGroupId == Guid.Empty, + $"[1] Operation group id is not '{Guid.Empty}', but {this.OperationGroupId}."); + await this.Receive(typeof(E)); + this.Assert(this.OperationGroupId == OperationGroup1, + $"[2] Operation group id is not '{OperationGroup1}', but {this.OperationGroupId}."); + } + } + + [Fact(Timeout = 5000)] + public void TestOperationGroupingReceivedEvent() + { + this.Test(r => + { + r.CreateMachine(typeof(M14)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/ReceivingExternalEventTest.cs b/Tests/TestingServices.Tests/Runtime/ReceivingExternalEventTest.cs new file mode 100644 index 000000000..af22a7a69 --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/ReceivingExternalEventTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class ReceivingExternalEventTest : BaseTest + { + public ReceivingExternalEventTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + public int Value; + + public E(int value) + { + this.Value = value; + } + } + + private class Engine + { + public static void Send(IMachineRuntime runtime, MachineId target) + { + runtime.SendEvent(target, new E(2)); + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E), nameof(HandleEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + Engine.Send(this.Runtime, this.Id); + } + + private void HandleEvent() + { + this.Assert((this.ReceivedEvent as E).Value == 2); + } + } + + [Fact(Timeout=5000)] + public void TestReceivingExternalEvents() + { + this.Test(r => + { + r.CreateMachine(typeof(M)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Runtime/SendAndExecuteTest.cs b/Tests/TestingServices.Tests/Runtime/SendAndExecuteTest.cs new file mode 100644 index 000000000..d18ad71fa --- /dev/null +++ b/Tests/TestingServices.Tests/Runtime/SendAndExecuteTest.cs @@ -0,0 +1,569 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class SendAndExecuteTest : BaseTest + { + public SendAndExecuteTest(ITestOutputHelper output) + : base(output) + { + } + + private class Configure : Event + { + public bool ExecuteSynchronously; + + public Configure(bool executeSynchronously) + { + this.ExecuteSynchronously = executeSynchronously; + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + public MachineId Id; + + public E2(MachineId id) + { + this.Id = id; + } + } + + private class E3 : Event + { + } + + private class M1A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var e = this.ReceivedEvent as Configure; + MachineId b; + + if (e.ExecuteSynchronously) + { + b = await this.Runtime.CreateMachineAndExecuteAsync(typeof(M1B)); + } + else + { + b = this.Runtime.CreateMachine(typeof(M1B)); + } + + this.Send(b, new E1()); + } + } + + private class M1B : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Receive(typeof(E1)); + } + } + + [Fact(Timeout=5000)] + public void TestSendAndExecuteNoDeadlockWithReceive() + { + this.Test(r => + { + r.CreateMachine(typeof(M1A), new Configure(false)); + }); + } + + [Fact(Timeout=5000)] + public void TestSendAndExecuteDeadlockWithReceive() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M1A), new Configure(true)); + }, + configuration: Configuration.Create().WithNumberOfIterations(10), + expectedError: "Deadlock detected. 'M1A()' and 'M1B()' are waiting to receive " + + "an event, but no other controlled tasks are enabled.", + replay: true); + } + + private class M2A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var b = this.CreateMachine(typeof(M2B)); + var handled = await this.Runtime.SendEventAndExecuteAsync(b, new E1()); + this.Assert(!handled); + } + } + + private class M2B : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + await this.Receive(typeof(E1)); + } + } + + private class M2C : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var d = this.CreateMachine(typeof(M2D)); + var handled = await this.Runtime.SendEventAndExecuteAsync(d, new E1()); + this.Assert(handled); + } + } + + private class M2D : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(Handle))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1()); + } + + private void Handle() + { + } + } + + [Fact(Timeout = 5000)] + public void TestSyncSendToReceive() + { + this.Test(r => + { + r.CreateMachine(typeof(M2A)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestSyncSendSometimesDoesNotHandle() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M2C)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200), + expectedError: "Detected an assertion failure.", + replay: true); + } + + private class E4 : Event + { + public int X; + + public E4() + { + this.X = 0; + } + } + + private class M3A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var e = new E4(); + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(M3B)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, e); + this.Assert(handled); + this.Assert(e.X == 1); + } + } + + private class M3B : Machine + { + private bool E1Handled = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(HandleEventE1))] + [OnEventDoAction(typeof(E4), nameof(HandleEventE4))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Send(this.Id, new E1()); + } + + private void HandleEventE1() + { + this.E1Handled = true; + } + + private void HandleEventE4() + { + this.Assert(this.E1Handled); + var e = this.ReceivedEvent as E4; + e.X = 1; + } + } + + [Fact(Timeout = 5000)] + public void TestSendBlocks() + { + this.Test(r => + { + r.CreateMachine(typeof(M3A)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100)); + } + + private class M4A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(E1))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(M4B), new E2(this.Id)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E1()); + this.Assert(handled); + } + } + + private class M4B : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(E1))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var creator = (this.ReceivedEvent as E2).Id; + var handled = await this.Id.Runtime.SendEventAndExecuteAsync(creator, new E1()); + this.Assert(!handled); + } + } + + [Fact(Timeout = 5000)] + public void TestSendCycleDoesNotDeadlock() + { + this.Test(r => + { + r.CreateMachine(typeof(M4A)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100)); + } + + private class M5A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(M5B)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E1()); + this.Monitor(new SE_Returns()); + this.Assert(handled); + } + } + + private class M5B : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(HandleE))] + private class Init : MachineState + { + } + + private void HandleE() + { + this.Raise(new Halt()); + } + + protected override void OnHalt() + { + this.Monitor(new M_Halts()); + } + } + + private class M_Halts : Event + { + } + + private class SE_Returns : Event + { + } + + private class M5SafetyMonitor : Monitor + { + private bool MHalted = false; + private bool SEReturned = false; + + [Start] + [Hot] + [OnEventDoAction(typeof(M_Halts), nameof(OnMHalts))] + [OnEventDoAction(typeof(SE_Returns), nameof(OnSEReturns))] + private class Init : MonitorState + { + } + + [Cold] + private class Done : MonitorState + { + } + + private void OnMHalts() + { + this.Assert(this.SEReturned == false); + this.MHalted = true; + } + + private void OnSEReturns() + { + this.Assert(this.MHalted); + this.SEReturned = true; + this.Goto(); + } + } + + [Fact(Timeout = 5000)] + public void TestMachineHaltsOnSendExec() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M5SafetyMonitor)); + r.CreateMachine(typeof(M5A)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100)); + } + + private class Config : Event + { + public bool HandleException; + + public Config(bool handleEx) + { + this.HandleException = handleEx; + } + } + + private class M6A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(M6B), this.ReceivedEvent); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E1()); + this.Monitor(new SE_Returns()); + this.Assert(handled); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + this.Assert(false); + return OnExceptionOutcome.ThrowException; + } + } + + private class M6B : Machine + { + private bool HandleException = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(E1), nameof(HandleE))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.HandleException = (this.ReceivedEvent as Config).HandleException; + } + + private void HandleE() + { + throw new InvalidOperationException(); + } + + protected override OnExceptionOutcome OnException(string methodName, Exception ex) + { + return this.HandleException ? OnExceptionOutcome.HandledException : OnExceptionOutcome.ThrowException; + } + } + + private class M6SafetyMonitor : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(SE_Returns), typeof(Done))] + private class Init : MonitorState + { + } + + [Cold] + private class Done : MonitorState + { + } + } + + [Fact(Timeout = 5000)] + public void TestHandledExceptionOnSendExec() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M6SafetyMonitor)); + r.CreateMachine(typeof(M6A), new Config(true)); + }, + configuration: Configuration.Create().WithNumberOfIterations(100)); + } + + [Fact(Timeout = 5000)] + public void TestUnhandledExceptionOnSendExec() + { + this.TestWithException(r => + { + r.RegisterMonitor(typeof(M6SafetyMonitor)); + r.CreateMachine(typeof(M6A), new Config(false)); + }, + replay: true); + } + + private class M7A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(M7B)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E1()); + this.Assert(handled); + } + } + + private class M7B : Machine + { + [Start] + private class Init : MachineState + { + } + } + + [Fact(Timeout = 5000)] + public void TestUnhandledEventOnSendExec1() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(M7A)); + }, + expectedError: "Machine 'M7B()' received event 'E1' that cannot be handled.", + replay: true); + } + + private class M8A : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var m = await this.Runtime.CreateMachineAndExecuteAsync(typeof(M8B)); + var handled = await this.Runtime.SendEventAndExecuteAsync(m, new E1()); + this.Assert(handled); + } + } + + private class M8B : Machine + { + [Start] + [OnEventDoAction(typeof(E1), nameof(Handle))] + [IgnoreEvents(typeof(E3))] + private class Init : MachineState + { + } + + private void Handle() + { + this.Raise(new E3()); + } + } + + [Fact(Timeout = 5000)] + public void TestUnhandledEventOnSendExec2() + { + this.Test(r => + { + r.CreateMachine(typeof(M8A)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionBasicTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionBasicTest.cs new file mode 100644 index 000000000..0a0a1a653 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionBasicTest.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CycleDetectionBasicTest : BaseTest + { + public CycleDetectionBasicTest(ITestOutputHelper output) + : base(output) + { + } + + private class Configure : Event + { + public bool ApplyFix; + + public Configure(bool applyFix) + { + this.ApplyFix = applyFix; + } + } + + private class Message : Event + { + } + + private class EventHandler : Machine + { + private bool ApplyFix; + + [Start] + [OnEntry(nameof(OnInitEntry))] + [OnEventDoAction(typeof(Message), nameof(OnMessage))] + private class Init : MachineState + { + } + + private void OnInitEntry() + { + this.ApplyFix = (this.ReceivedEvent as Configure).ApplyFix; + this.Send(this.Id, new Message()); + } + + private void OnMessage() + { + this.Send(this.Id, new Message()); + if (this.ApplyFix) + { + this.Monitor(new WatchDog.NotifyMessage()); + } + } + } + + private class WatchDog : Monitor + { + public class NotifyMessage : Event + { + } + + [Start] + [Hot] + [OnEventGotoState(typeof(NotifyMessage), typeof(ColdState))] + private class HotState : MonitorState + { + } + + [Cold] + [OnEventGotoState(typeof(NotifyMessage), typeof(HotState))] + private class ColdState : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionBasicNoBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingIterations = 10; + configuration.MaxSchedulingSteps = 200; + + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler), new Configure(true)); + }, + configuration: configuration); + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionBasicBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.MaxSchedulingSteps = 200; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler), new Configure(false)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionCounterTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionCounterTest.cs new file mode 100644 index 000000000..825b86531 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionCounterTest.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CycleDetectionCounterTest : BaseTest + { + public CycleDetectionCounterTest(ITestOutputHelper output) + : base(output) + { + } + + private class Message : Event + { + } + + private class EventHandler : Machine + { + private int Counter; + + [Start] + [OnEntry(nameof(OnInitEntry))] + [OnEventDoAction(typeof(Message), nameof(OnMessage))] + private class Init : MachineState + { + } + + private void OnInitEntry() + { + this.Counter = 0; + this.Send(this.Id, new Message()); + } + + private void OnMessage() + { + this.Send(this.Id, new Message()); + this.Counter++; + } + + protected override int HashedState + { + get + { + // The counter contributes to the cached machine state. + // This allows the liveness checker to detect progress. + return this.Counter; + } + } + } + + private class WatchDog : Monitor + { + [Start] + [Hot] + private class HotState : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionCounterNoBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.EnableUserDefinedStateHashing = true; + configuration.SchedulingIterations = 10; + configuration.MaxSchedulingSteps = 200; + + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration); + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionCounterBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.MaxSchedulingSteps = 200; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionDefaultHandlerTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionDefaultHandlerTest.cs new file mode 100644 index 000000000..79ffb3f7c --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionDefaultHandlerTest.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CycleDetectionDefaultHandlerTest : BaseTest + { + public CycleDetectionDefaultHandlerTest(ITestOutputHelper output) + : base(output) + { + } + + private class Configure : Event + { + public bool ApplyFix; + + public Configure(bool applyFix) + { + this.ApplyFix = applyFix; + } + } + + private class Message : Event + { + } + + private class EventHandler : Machine + { + private bool ApplyFix; + + [Start] + [OnEntry(nameof(OnInitEntry))] + [OnEventDoAction(typeof(Default), nameof(OnDefault))] + private class Init : MachineState + { + } + + private void OnInitEntry() + { + this.ApplyFix = (this.ReceivedEvent as Configure).ApplyFix; + } + + private void OnDefault() + { + if (this.ApplyFix) + { + this.Monitor(new WatchDog.NotifyMessage()); + } + } + } + + private class WatchDog : Monitor + { + public class NotifyMessage : Event + { + } + + [Start] + [Hot] + [OnEventGotoState(typeof(NotifyMessage), typeof(ColdState))] + private class HotState : MonitorState + { + } + + [Cold] + [OnEventGotoState(typeof(NotifyMessage), typeof(HotState))] + private class ColdState : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionDefaultHandlerNoBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingIterations = 10; + configuration.MaxSchedulingSteps = 200; + + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler), new Configure(true)); + }, + configuration: configuration); + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionDefaultHandlerBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.MaxSchedulingSteps = 200; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler), new Configure(false)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRandomChoiceTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRandomChoiceTest.cs new file mode 100644 index 000000000..82ae22c0d --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRandomChoiceTest.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CycleDetectionRandomChoiceTest : BaseTest + { + public CycleDetectionRandomChoiceTest(ITestOutputHelper output) + : base(output) + { + } + + private class Configure : Event + { + public bool ApplyFix; + + public Configure(bool applyFix) + { + this.ApplyFix = applyFix; + } + } + + private class Message : Event + { + } + + private class EventHandler : Machine + { + private bool ApplyFix; + + [Start] + [OnEntry(nameof(OnInitEntry))] + [OnEventDoAction(typeof(Message), nameof(OnMessage))] + private class Init : MachineState + { + } + + private void OnInitEntry() + { + this.ApplyFix = (this.ReceivedEvent as Configure).ApplyFix; + this.Send(this.Id, new Message()); + } + + private void OnMessage() + { + this.Send(this.Id, new Message()); + this.Monitor(new WatchDog.NotifyMessage()); + if (this.Choose()) + { + this.Monitor(new WatchDog.NotifyDone()); + this.Raise(new Halt()); + } + } + + private bool Choose() + { + if (this.ApplyFix) + { + return this.FairRandom(); + } + else + { + return this.Random(); + } + } + } + + private class WatchDog : Monitor + { + public class NotifyMessage : Event + { + } + + public class NotifyDone : Event + { + } + + [Start] + [Hot] + [OnEventGotoState(typeof(NotifyMessage), typeof(HotState))] + [OnEventGotoState(typeof(NotifyDone), typeof(ColdState))] + private class HotState : MonitorState + { + } + + [Cold] + private class ColdState : MonitorState + { + } + } + + [Theory(Timeout = 5000)] + [InlineData(906)] + public void TestCycleDetectionRandomChoiceNoBug(int seed) + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 7; + configuration.MaxSchedulingSteps = 200; + + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler), new Configure(true)); + }, + configuration: configuration); + } + + [Theory(Timeout = 5000)] + [InlineData(906)] + public void TestCycleDetectionRandomChoiceBug(int seed) + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.RandomSchedulingSeed = seed; + configuration.SchedulingIterations = 10; + configuration.MaxSchedulingSteps = 200; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler), new Configure(false)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRingOfNodesTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRingOfNodesTest.cs new file mode 100644 index 000000000..2a9080c40 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/CycleDetectionRingOfNodesTest.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class CycleDetectionRingOfNodesTest : BaseTest + { + public CycleDetectionRingOfNodesTest(ITestOutputHelper output) + : base(output) + { + } + + private class Configure : Event + { + public bool ApplyFix; + + public Configure(bool applyFix) + { + this.ApplyFix = applyFix; + } + } + + private class Message : Event + { + } + + private class Environment : Machine + { + [Start] + [OnEntry(nameof(OnInitEntry))] + private class Init : MachineState + { + } + + private void OnInitEntry() + { + var applyFix = (this.ReceivedEvent as Configure).ApplyFix; + var machine1 = this.CreateMachine(typeof(Node), new Configure(applyFix)); + var machine2 = this.CreateMachine(typeof(Node), new Configure(applyFix)); + this.Send(machine1, new Node.SetNeighbour(machine2)); + this.Send(machine2, new Node.SetNeighbour(machine1)); + } + } + + private class Node : Machine + { + public class SetNeighbour : Event + { + public MachineId Next; + + public SetNeighbour(MachineId next) + { + this.Next = next; + } + } + + private MachineId Next; + private bool ApplyFix; + + [Start] + [OnEntry(nameof(OnInitEntry))] + [OnEventDoAction(typeof(SetNeighbour), nameof(OnSetNeighbour))] + [OnEventDoAction(typeof(Message), nameof(OnMessage))] + private class Init : MachineState + { + } + + private void OnInitEntry() + { + this.ApplyFix = (this.ReceivedEvent as Configure).ApplyFix; + } + + private void OnSetNeighbour() + { + var e = this.ReceivedEvent as SetNeighbour; + this.Next = e.Next; + this.Send(this.Id, new Message()); + } + + private void OnMessage() + { + if (this.Next != null) + { + this.Send(this.Next, new Message()); + if (this.ApplyFix) + { + this.Monitor(new WatchDog.NotifyMessage()); + } + } + } + } + + private class WatchDog : Monitor + { + public class NotifyMessage : Event + { + } + + [Start] + [Hot] + [OnEventGotoState(typeof(NotifyMessage), typeof(ColdState))] + private class HotState : MonitorState + { + } + + [Cold] + [OnEventGotoState(typeof(NotifyMessage), typeof(HotState))] + private class ColdState : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionRingOfNodesNoBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingIterations = 10; + configuration.MaxSchedulingSteps = 200; + + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(Environment), new Configure(true)); + }, + configuration: configuration); + } + + [Fact(Timeout=5000)] + public void TestCycleDetectionRingOfNodesBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.MaxSchedulingSteps = 200; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(Environment), new Configure(false)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/FairNondet1Test.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/FairNondet1Test.cs new file mode 100644 index 000000000..6c0ee26b1 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/FairNondet1Test.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class FairNondet1Test : BaseTest + { + public FairNondet1Test(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Loop : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + [OnEventGotoState(typeof(Done), typeof(WaitForUser))] + [OnEventGotoState(typeof(Loop), typeof(HandleEvent))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + if (this.FairRandom()) + { + this.Send(this.Id, new Done()); + } + else + { + this.Send(this.Id, new Loop()); + } + } + } + + private class WatchDog : Monitor + { + [Start] + [Cold] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestFairNondet1() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.LivenessTemperatureThreshold = 0; + configuration.SchedulingStrategy = SchedulingStrategy.DFS; + configuration.MaxSchedulingSteps = 300; + + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness2Test.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness2Test.cs new file mode 100644 index 000000000..e1b3d9341 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness2Test.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class Liveness2Test : BaseTest + { + public Liveness2Test(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + [OnEventGotoState(typeof(Done), typeof(HandleEvent))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + this.Send(this.Id, new Done()); + } + } + + private class WatchDog : Monitor + { + [Start] + [Cold] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestLiveness2() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingStrategy = SchedulingStrategy.DFS; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness3Test.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness3Test.cs new file mode 100644 index 000000000..ee687a46d --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Liveness3Test.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class Liveness3Test : BaseTest + { + public Liveness3Test(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.CreateMachine(typeof(Loop)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + [OnEventGotoState(typeof(Done), typeof(HandleEvent))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + } + } + + private class Loop : Machine + { + [Start] + [OnEntry(nameof(LoopingOnEntry))] + [OnEventGotoState(typeof(Done), typeof(Looping))] + private class Looping : MachineState + { + } + + private void LoopingOnEntry() + { + this.Send(this.Id, new Done()); + } + } + + private class WatchDog : Monitor + { + [Start] + [Cold] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestLiveness3() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingIterations = 100; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Nondet1Test.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Nondet1Test.cs new file mode 100644 index 000000000..a0315aeb3 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/Nondet1Test.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class Nondet1Test : BaseTest + { + public Nondet1Test(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Loop : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + [OnEventGotoState(typeof(Done), typeof(WaitForUser))] + [OnEventGotoState(typeof(Loop), typeof(HandleEvent))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + if (this.Random()) + { + this.Send(this.Id, new Done()); + } + else + { + this.Send(this.Id, new Loop()); + } + } + } + + private class WatchDog : Monitor + { + [Start] + [Cold] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestNondet1() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingStrategy = SchedulingStrategy.DFS; + configuration.RandomSchedulingSeed = 96; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/WarmStateBugTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/WarmStateBugTest.cs new file mode 100644 index 000000000..aeaa9b68a --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/CycleDetection/WarmStateBugTest.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class WarmStateBugTest : BaseTest + { + public WarmStateBugTest(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + [OnEventGotoState(typeof(Done), typeof(WaitForUser))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + this.Send(this.Id, new Done()); + } + } + + private class WatchDog : Monitor + { + [Start] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestWarmStateBug() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingStrategy = SchedulingStrategy.DFS; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected infinite execution that violates a liveness property.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/HotStateTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/HotStateTest.cs new file mode 100644 index 000000000..6bd187d2e --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/HotStateTest.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class HotStateTest : BaseTest + { + public HotStateTest(ITestOutputHelper output) + : base(output) + { + } + + private class Config : Event + { + public MachineId Id; + + public Config(MachineId id) + { + this.Id = id; + } + } + + private class MConfig : Event + { + public List Ids; + + public MConfig(List ids) + { + this.Ids = ids; + } + } + + private class Unit : Event + { + } + + private class DoProcessing : Event + { + } + + private class FinishedProcessing : Event + { + } + + private class NotifyWorkerIsDone : Event + { + } + + private class Master : Machine + { + private List Workers; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(Active))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Workers = new List(); + + for (int idx = 0; idx < 3; idx++) + { + var worker = this.CreateMachine(typeof(Worker)); + this.Send(worker, new Config(this.Id)); + this.Workers.Add(worker); + } + + this.Monitor(new MConfig(this.Workers)); + + this.Raise(new Unit()); + } + + [OnEntry(nameof(ActiveOnEntry))] + [OnEventDoAction(typeof(FinishedProcessing), nameof(ProcessWorkerIsDone))] + private class Active : MachineState + { + } + + private void ActiveOnEntry() + { + foreach (var worker in this.Workers) + { + this.Send(worker, new DoProcessing()); + } + } + + private void ProcessWorkerIsDone() + { + this.Monitor(new NotifyWorkerIsDone()); + } + } + + private class Worker : Machine + { + private MachineId Master; + + [Start] + [OnEventDoAction(typeof(Config), nameof(Configure))] + [OnEventGotoState(typeof(Unit), typeof(Processing))] + private class Init : MachineState + { + } + + private void Configure() + { + this.Master = (this.ReceivedEvent as Config).Id; + this.Raise(new Unit()); + } + + [OnEventGotoState(typeof(DoProcessing), typeof(Done))] + private class Processing : MachineState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MachineState + { + } + + private void DoneOnEntry() + { + if (this.Random()) + { + this.Send(this.Master, new FinishedProcessing()); + } + + this.Raise(new Halt()); + } + } + + private class M : Monitor + { + private List Workers; + + [Start] + [Hot] + [OnEventDoAction(typeof(MConfig), nameof(Configure))] + [OnEventGotoState(typeof(Unit), typeof(Done))] + [OnEventDoAction(typeof(NotifyWorkerIsDone), nameof(ProcessNotification))] + private class Init : MonitorState + { + } + + private void Configure() + { + this.Workers = (this.ReceivedEvent as MConfig).Ids; + } + + private void ProcessNotification() + { + this.Workers.RemoveAt(0); + + if (this.Workers.Count == 0) + { + this.Raise(new Unit()); + } + } + + private class Done : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestHotStateMonitor() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingStrategy = SchedulingStrategy.DFS; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M)); + r.CreateMachine(typeof(Master)); + }, + configuration: configuration, + expectedError: "Monitor 'M' detected liveness bug in hot state 'Init' at the end of program execution.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/Liveness1Test.cs b/Tests/TestingServices.Tests/Specifications/Liveness/Liveness1Test.cs new file mode 100644 index 000000000..d0425c456 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/Liveness1Test.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class Liveness1Test : BaseTest + { + public Liveness1Test(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + [OnEventGotoState(typeof(Done), typeof(WaitForUser))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + this.Send(this.Id, new Done()); + } + } + + private class WatchDog : Monitor + { + [Start] + [Cold] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestLiveness1() + { + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS).WithMaxSteps(300)); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/Liveness2BugFoundTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/Liveness2BugFoundTest.cs new file mode 100644 index 000000000..a7de717de --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/Liveness2BugFoundTest.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class Liveness2BugFoundTest : BaseTest + { + public Liveness2BugFoundTest(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + [OnEventGotoState(typeof(Done), typeof(HandleEvent))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + } + } + + private class WatchDog : Monitor + { + [Start] + [Cold] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestLiveness2BugFound() + { + var configuration = GetConfiguration(); + configuration.EnableCycleDetection = true; + configuration.SchedulingStrategy = SchedulingStrategy.DFS; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration, + expectedError: "Monitor 'WatchDog' detected liveness bug in hot state " + + "'CannotGetUserInput' at the end of program execution.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/Liveness2LoopMachineTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/Liveness2LoopMachineTest.cs new file mode 100644 index 000000000..77d66ddf6 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/Liveness2LoopMachineTest.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class Liveness2LoopMachineTest : BaseTest + { + public Liveness2LoopMachineTest(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.CreateMachine(typeof(Loop)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + } + } + + private class Loop : Machine + { + [Start] + [OnEntry(nameof(LoopingOnEntry))] + [OnEventGotoState(typeof(Done), typeof(Looping))] + private class Looping : MachineState + { + } + + private void LoopingOnEntry() + { + this.Send(this.Id, new Done()); + } + } + + private class LivenessMonitor : Monitor + { + [Start] + [Cold] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [Hot] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestLiveness2LoopMachine() + { + var configuration = GetConfiguration(); + configuration.LivenessTemperatureThreshold = 200; + configuration.SchedulingIterations = 1; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected potential liveness bug in hot state 'CannotGetUserInput'.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/UnfairExecutionTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/UnfairExecutionTest.cs new file mode 100644 index 000000000..9c9a070ee --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/UnfairExecutionTest.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class UnfairExecutionTest : BaseTest + { + public UnfairExecutionTest(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class E : Event + { + public MachineId A; + + public E(MachineId a) + { + this.A = a; + } + } + + private class M : Machine + { + private MachineId N; + + [Start] + [OnEntry(nameof(SOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(S2))] + private class S : MachineState + { + } + + private void SOnEntry() + { + this.N = this.CreateMachine(typeof(N)); + this.Send(this.N, new E(this.Id)); + this.Raise(new Unit()); + } + + [OnEntry(nameof(S2OnEntry))] + [OnEventGotoState(typeof(Unit), typeof(S2))] + [OnEventGotoState(typeof(E), typeof(S3))] + private class S2 : MachineState + { + } + + private void S2OnEntry() + { + this.Send(this.Id, new Unit()); + } + + [OnEntry(nameof(S3OnEntry))] + private class S3 : MachineState + { + } + + private void S3OnEntry() + { + this.Monitor(new E(this.Id)); + this.Raise(new Halt()); + } + } + + private class N : Machine + { + [Start] + [OnEventDoAction(typeof(E), nameof(Foo))] + private class S : MachineState + { + } + + private void Foo() + { + this.Send((this.ReceivedEvent as E).A, new E(this.Id)); + } + } + + private class LivenessMonitor : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(E), typeof(S2))] + private class S : MonitorState + { + } + + [Cold] + private class S2 : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestUnfairExecution() + { + var configuration = GetConfiguration(); + configuration.LivenessTemperatureThreshold = 150; + configuration.SchedulingStrategy = SchedulingStrategy.PCT; + configuration.MaxSchedulingSteps = 300; + + this.Test(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(M)); + }, + configuration: configuration); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Liveness/WarmStateTest.cs b/Tests/TestingServices.Tests/Specifications/Liveness/WarmStateTest.cs new file mode 100644 index 000000000..8b1893927 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Liveness/WarmStateTest.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class WarmStateTest : BaseTest + { + public WarmStateTest(ITestOutputHelper output) + : base(output) + { + } + + private class Unit : Event + { + } + + private class UserEvent : Event + { + } + + private class Done : Event + { + } + + private class Waiting : Event + { + } + + private class Computing : Event + { + } + + private class EventHandler : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventGotoState(typeof(Unit), typeof(WaitForUser))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Raise(new Unit()); + } + + [OnEntry(nameof(WaitForUserOnEntry))] + [OnEventGotoState(typeof(UserEvent), typeof(HandleEvent))] + private class WaitForUser : MachineState + { + } + + private void WaitForUserOnEntry() + { + this.Monitor(new Waiting()); + this.Send(this.Id, new UserEvent()); + } + + [OnEntry(nameof(HandleEventOnEntry))] + private class HandleEvent : MachineState + { + } + + private void HandleEventOnEntry() + { + this.Monitor(new Computing()); + } + } + + private class WatchDog : Monitor + { + [Start] + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CanGetUserInput : MonitorState + { + } + + [OnEventGotoState(typeof(Waiting), typeof(CanGetUserInput))] + [OnEventGotoState(typeof(Computing), typeof(CannotGetUserInput))] + private class CannotGetUserInput : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestWarmState() + { + this.Test(r => + { + r.RegisterMonitor(typeof(WatchDog)); + r.CreateMachine(typeof(EventHandler)); + }, + configuration: Configuration.Create().WithStrategy(SchedulingStrategy.DFS)); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Monitors/GenericMonitorTest.cs b/Tests/TestingServices.Tests/Specifications/Monitors/GenericMonitorTest.cs new file mode 100644 index 000000000..426b3ab56 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Monitors/GenericMonitorTest.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class GenericMonitorTest : BaseTest + { + public GenericMonitorTest(ITestOutputHelper output) + : base(output) + { + } + + private class Program : Machine + { + private T Item; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Item = default; + this.Goto(); + } + + [OnEntry(nameof(ActiveInit))] + private class Active : MachineState + { + } + + private void ActiveInit() + { + this.Assert(this.Item is int); + } + } + + private class E : Event + { + } + + private class M : Monitor + { + [Start] + [OnEntry(nameof(Init))] + private class S1 : MonitorState + { + } + + private class S2 : MonitorState + { + } + + private void Init() + { + this.Goto(); + } + } + + [Fact(Timeout=5000)] + public void TestGenericMonitor() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M)); + r.CreateMachine(typeof(Program)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Monitors/IdempotentRegisterMonitorTest.cs b/Tests/TestingServices.Tests/Specifications/Monitors/IdempotentRegisterMonitorTest.cs new file mode 100644 index 000000000..de40abd8c --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Monitors/IdempotentRegisterMonitorTest.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class IdempotentRegisterMonitorTest : BaseTest + { + public IdempotentRegisterMonitorTest(ITestOutputHelper output) + : base(output) + { + } + + private class Counter + { + public int Value; + + public Counter() + { + this.Value = 0; + } + } + + private class E : Event + { + public Counter Counter; + + public E(Counter counter) + { + this.Counter = counter; + } + } + + private class M : Monitor + { + [Start] + [OnEventDoAction(typeof(E), nameof(Check))] + private class Init : MonitorState + { + } + + private void Check() + { + var counter = (this.ReceivedEvent as E).Counter; + counter.Value++; + } + } + + private class N : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + Counter counter = new Counter(); + this.Monitor(typeof(M), new E(counter)); + this.Assert(counter.Value == 1, "Monitor created more than once."); + } + } + + [Fact(Timeout=5000)] + public void TestIdempotentRegisterMonitorInvocation() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M)); + r.RegisterMonitor(typeof(M)); + MachineId n = r.CreateMachine(typeof(N)); + }); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Monitors/MachineMonitorIntegrationTests.cs b/Tests/TestingServices.Tests/Specifications/Monitors/MachineMonitorIntegrationTests.cs new file mode 100644 index 000000000..e37c9bad9 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Monitors/MachineMonitorIntegrationTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MachineMonitorIntegrationTests : BaseTest + { + public MachineMonitorIntegrationTests(ITestOutputHelper output) + : base(output) + { + } + + private class CheckE : Event + { + public bool Value; + + public CheckE(bool v) + { + this.Value = v; + } + } + + private class M1 : Machine + { + private readonly bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Monitor(new CheckE(this.Test)); + } + } + + private class M2 : Machine + { + private readonly bool Test = false; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Monitor(new CheckE(true)); + this.Monitor(new CheckE(this.Test)); + } + } + + private class Spec1 : Monitor + { + [Start] + [OnEventDoAction(typeof(CheckE), nameof(Check))] + private class Checking : MonitorState + { + } + + private void Check() + { + this.Assert((this.ReceivedEvent as CheckE).Value == true); + } + } + + private class Spec2 : Monitor + { + [Start] + [OnEventDoAction(typeof(CheckE), nameof(Check))] + private class Checking : MonitorState + { + } + + private void Check() + { + // this.Assert((this.ReceivedEvent as CheckE).Value == true); // passes + } + } + + private class Spec3 : Monitor + { + [Start] + [OnEventDoAction(typeof(CheckE), nameof(Check))] + private class Checking : MonitorState + { + } + + private void Check() + { + this.Assert((this.ReceivedEvent as CheckE).Value == false); + } + } + + [Fact(Timeout=5000)] + public void TestMachineMonitorIntegration1() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(Spec1)); + r.CreateMachine(typeof(M1)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS), + expectedError: "Detected an assertion failure.", + replay: true); + } + + [Fact(Timeout=5000)] + public void TestMachineMonitorIntegration2() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec2)); + r.CreateMachine(typeof(M2)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS)); + } + + [Fact(Timeout=5000)] + public void TestMachineMonitorIntegration3() + { + this.Test(r => + { + r.RegisterMonitor(typeof(Spec3)); + r.CreateMachine(typeof(M1)); + }, + configuration: GetConfiguration().WithStrategy(SchedulingStrategy.DFS)); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Monitors/MonitorStateInheritanceTest.cs b/Tests/TestingServices.Tests/Specifications/Monitors/MonitorStateInheritanceTest.cs new file mode 100644 index 000000000..2a96b69f8 --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Monitors/MonitorStateInheritanceTest.cs @@ -0,0 +1,454 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MonitorStateInheritanceTest : BaseTest + { + public MonitorStateInheritanceTest(ITestOutputHelper output) + : base(output) + { + } + + private class E : Event + { + } + + private class M1 : Monitor + { + [Start] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(Check))] + private abstract class BaseState : MonitorState + { + } + + private void Check() + { + this.Assert(false, "Error reached."); + } + } + + private class M2 : Monitor + { + [Start] + private class Init : BaseState + { + } + + [Start] + private class BaseState : MonitorState + { + } + } + + private class M3 : Monitor + { + [Start] + private class Init : BaseState + { + } + + [OnEntry(nameof(BaseOnEntry))] + private class BaseState : MonitorState + { + } + + private void BaseOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M4 : Monitor + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : BaseState + { + } + + [OnEntry(nameof(BaseOnEntry))] + private class BaseState : MonitorState + { + } + + private void InitOnEntry() + { + } + + private void BaseOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M5 : Monitor + { + [Start] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(Check))] + private class BaseState : MonitorState + { + } + + private void Check() + { + this.Assert(false, "Error reached."); + } + } + + private class M6 : Monitor + { + [Start] + [OnEventDoAction(typeof(E), nameof(Check))] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseCheck))] + private class BaseState : MonitorState + { + } + + private void Check() + { + } + + private void BaseCheck() + { + this.Assert(false, "Error reached."); + } + } + + private class M7 : Monitor + { + [Start] + [OnEventDoAction(typeof(E), nameof(Check))] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseCheck))] + private class BaseState : BaseBaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseBaseCheck))] + private class BaseBaseState : MonitorState + { + } + + private void Check() + { + } + + private void BaseCheck() + { + this.Assert(false, "Error reached."); + } + + private void BaseBaseCheck() + { + this.Assert(false, "Error reached."); + } + } + + private class M8 : Monitor + { + [Start] + private class Init : BaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseCheck))] + private class BaseState : BaseBaseState + { + } + + [OnEventDoAction(typeof(E), nameof(BaseBaseCheck))] + private class BaseBaseState : MonitorState + { + } + + private void BaseCheck() + { + } + + private void BaseBaseCheck() + { + this.Assert(false, "Error reached."); + } + } + + private class M9 : Monitor + { + [Start] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Done))] + private class BaseState : MonitorState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MonitorState + { + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + } + + private class M10 : Monitor + { + [Start] + [OnEventGotoState(typeof(E), typeof(Done))] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseState : MonitorState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MonitorState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MonitorState + { + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M11 : Monitor + { + [Start] + [OnEventGotoState(typeof(E), typeof(Done))] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseState : BaseBaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseBaseState : MonitorState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MonitorState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MonitorState + { + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + private class M12 : Monitor + { + [Start] + private class Init : BaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Done))] + private class BaseState : BaseBaseState + { + } + + [OnEventGotoState(typeof(E), typeof(Error))] + private class BaseBaseState : MonitorState + { + } + + [OnEntry(nameof(DoneOnEntry))] + private class Done : MonitorState + { + } + + [OnEntry(nameof(ErrorOnEntry))] + private class Error : MonitorState + { + } + + private void DoneOnEntry() + { + this.Assert(false, "Done reached."); + } + + private void ErrorOnEntry() + { + this.Assert(false, "Error reached."); + } + } + + [Fact(Timeout=5000)] + public void TestMonitorStateInheritingAbstractState() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M1)); + r.InvokeMonitor(new E()); + }, + expectedError: "Error reached."); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateInheritingStateDuplicateStart() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M2)); + }, + expectedError: "Monitor 'M2' can not declare more than one start states."); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateInheritingStateOnEntry() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M3)); + }, + expectedError: "Error reached."); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateOverridingStateOnEntry() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M4)); + }); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateInheritingStateOnEventDoAction() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M5)); + r.InvokeMonitor(new E()); + }, + expectedError: "Error reached."); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateOverridingStateOnEventDoAction() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M6)); + r.InvokeMonitor(new E()); + }); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateOverridingTwoStatesOnEventDoAction() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M7)); + r.InvokeMonitor(new E()); + }); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateOverridingDeepStateOnEventDoAction() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M8)); + r.InvokeMonitor(new E()); + }); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateInheritingStateOnEventGotoState() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M9)); + r.InvokeMonitor(new E()); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateOverridingStateOnEventGotoState() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M10)); + r.InvokeMonitor(new E()); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateOverridingTwoStatesOnEventGotoState() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M11)); + r.InvokeMonitor(new E()); + }, + expectedError: "Done reached."); + } + + [Fact(Timeout=5000)] + public void TestMonitorStateOverridingDeepStateOnEventGotoState() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M12)); + r.InvokeMonitor(new E()); + }, + expectedError: "Done reached."); + } + } +} diff --git a/Tests/TestingServices.Tests/Specifications/Monitors/MonitorWildCardEventTest.cs b/Tests/TestingServices.Tests/Specifications/Monitors/MonitorWildCardEventTest.cs new file mode 100644 index 000000000..d8e102fdc --- /dev/null +++ b/Tests/TestingServices.Tests/Specifications/Monitors/MonitorWildCardEventTest.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MonitorWildCardEventTest : BaseTest + { + public MonitorWildCardEventTest(ITestOutputHelper output) + : base(output) + { + } + + private class M1 : Monitor + { + [Start] + [IgnoreEvents(typeof(WildCardEvent))] + private class S0 : MonitorState + { + } + } + + private class M2 : Monitor + { + [Start] + [OnEventDoAction(typeof(WildCardEvent), nameof(Check))] + private class S0 : MonitorState + { + } + + private void Check() + { + this.Assert(false, "Check reached."); + } + } + + private class M3 : Monitor + { + [Start] + [OnEventGotoState(typeof(WildCardEvent), typeof(S1))] + private class S0 : MonitorState + { + } + + [OnEntry(nameof(Check))] + private class S1 : MonitorState + { + } + + private void Check() + { + this.Assert(false, "Check reached."); + } + } + + private class E1 : Event + { + } + + private class E2 : Event + { + } + + private class E3 : Event + { + } + + [Fact(Timeout=5000)] + public void TestIgnoreWildCardEvent() + { + this.Test(r => + { + r.RegisterMonitor(typeof(M1)); + r.InvokeMonitor(new E1()); + r.InvokeMonitor(new E2()); + r.InvokeMonitor(new E3()); + }); + } + + [Fact(Timeout = 5000)] + public void TestDoActionOnWildCardEvent() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M2)); + r.InvokeMonitor(new E1()); + }, + expectedError: "Check reached."); + } + + [Fact(Timeout = 5000)] + public void TestGotoStateOnWildCardEvent() + { + this.TestWithError(r => + { + r.RegisterMonitor(typeof(M3)); + r.InvokeMonitor(new E1()); + }, + expectedError: "Check reached."); + } + } +} diff --git a/Tests/TestingServices.Tests/TestingServices.Tests.csproj b/Tests/TestingServices.Tests/TestingServices.Tests.csproj new file mode 100644 index 000000000..7f4b11c63 --- /dev/null +++ b/Tests/TestingServices.Tests/TestingServices.Tests.csproj @@ -0,0 +1,28 @@ + + + + + Tests for the Coyote testing services library. + Microsoft.Coyote.TestingServices.Tests + Microsoft.Coyote.TestingServices.Tests + ..\bin\ + + + netcoreapp2.1;net46;net47 + + + netcoreapp2.1 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/TestingServices.Tests/Threading/ControlledLockTest.cs b/Tests/TestingServices.Tests/Threading/ControlledLockTest.cs new file mode 100644 index 000000000..68d20ed6a --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/ControlledLockTest.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class ControlledLockTest : BaseTest + { + public ControlledLockTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout = 5000)] + public void TestLockUnlock() + { + this.Test(async () => + { + ControlledLock mutex = ControlledLock.Create(); + var releaser = await mutex.AcquireAsync(); + releaser.Dispose(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestLockTwice() + { + this.TestWithError(async () => + { + ControlledLock mutex = ControlledLock.Create(); + await mutex.AcquireAsync(); + await mutex.AcquireAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Deadlock detected. 'ControlledTask()' is waiting to access a concurrent resource " + + "that is acquired by another task, but no other controlled tasks are enabled.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestSynchronizeTwoAsynchronousTasks() + { + this.Test(async () => + { + int entry = 0; + ControlledLock mutex = ControlledLock.Create(); + + async ControlledTask WriteAsync(int value) + { + using (await mutex.AcquireAsync()) + { + entry = value; + } + } + + ControlledTask task1 = WriteAsync(3); + ControlledTask task2 = WriteAsync(5); + await ControlledTask.WhenAll(task1, task2); + Specification.Assert(entry == 5, "Value is '{0}' instead of 5.", entry); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestSynchronizeTwoParallelTasks() + { + this.TestWithError(async () => + { + int entry = 0; + ControlledLock mutex = ControlledLock.Create(); + + async ControlledTask WriteAsync(int value) + { + using (await mutex.AcquireAsync()) + { + entry = value; + } + } + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(5); + }); + + await ControlledTask.WhenAll(task1, task2); + Specification.Assert(entry == 5, "Value is '{0}' instead of 5.", entry); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is '' instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestSynchronizeTwoParallelTasksWithYield() + { + this.Test(async () => + { + int entry = 0; + ControlledLock mutex = ControlledLock.Create(); + + async ControlledTask WriteAsync(int value) + { + using (await mutex.AcquireAsync()) + { + entry = value; + await ControlledTask.Yield(); + Specification.Assert(entry == value, "Value is '{0}' instead of '{1}'.", entry, value); + } + } + + ControlledTask task1 = WriteAsync(3); + ControlledTask task2 = WriteAsync(5); + await ControlledTask.WhenAll(task1, task2); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskBooleanNondeterminismTest.cs b/Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskBooleanNondeterminismTest.cs new file mode 100644 index 000000000..248307b1b --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskBooleanNondeterminismTest.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskBooleanNondeterminismTest : BaseTest + { + public TaskBooleanNondeterminismTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + [Fact(Timeout = 5000)] + public void TestBooleanNondeterminismInSynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + async ControlledTask WriteAsync() + { + await ControlledTask.CompletedTask; + if (Specification.ChooseRandomBoolean()) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + } + + await WriteAsync(); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestBooleanNondeterminismInAsynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + async ControlledTask WriteWithDelayAsync() + { + await ControlledTask.Delay(1); + if (Specification.ChooseRandomBoolean()) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + } + + await WriteWithDelayAsync(); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestBooleanNondeterminismInParallelTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + if (Specification.ChooseRandomBoolean()) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestBooleanNondeterminismInParallelSynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + if (Specification.ChooseRandomBoolean()) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestBooleanNondeterminismInParallelAsynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + if (Specification.ChooseRandomBoolean()) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestBooleanNondeterminismInNestedParallelSynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + if (Specification.ChooseRandomBoolean()) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskIntegerNondeterminismTest.cs b/Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskIntegerNondeterminismTest.cs new file mode 100644 index 000000000..6c352e4fe --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/DataNondeterminism/TaskIntegerNondeterminismTest.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskIntegerNondeterminismTest : BaseTest + { + public TaskIntegerNondeterminismTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + [Fact(Timeout = 5000)] + public void TestIntegerNondeterminismInSynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + async ControlledTask WriteAsync() + { + await ControlledTask.CompletedTask; + if (Specification.ChooseRandomInteger(5) == 0) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + } + + await WriteAsync(); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestIntegerNondeterminismInAsynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + async ControlledTask WriteWithDelayAsync() + { + await ControlledTask.Delay(1); + if (Specification.ChooseRandomInteger(5) == 0) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + } + + await WriteWithDelayAsync(); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestIntegerNondeterminismInParallelTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + if (Specification.ChooseRandomInteger(5) == 0) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestIntegerNondeterminismInParallelSynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + if (Specification.ChooseRandomInteger(5) == 0) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestIntegerNondeterminismInParallelAsynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + if (Specification.ChooseRandomInteger(5) == 0) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestIntegerNondeterminismInNestedParallelSynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + if (Specification.ChooseRandomInteger(5) == 0) + { + entry.Value = 3; + } + else + { + entry.Value = 5; + } + }); + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Specifications/TaskLivenessMonitorTest.cs b/Tests/TestingServices.Tests/Threading/Specifications/TaskLivenessMonitorTest.cs new file mode 100644 index 000000000..2d0c3f15a --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Specifications/TaskLivenessMonitorTest.cs @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskLivenessMonitorTest : BaseTest + { + public TaskLivenessMonitorTest(ITestOutputHelper output) + : base(output) + { + } + + private class Notify : Event + { + } + + private class LivenessMonitor : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(Notify), typeof(Done))] + private class Init : MonitorState + { + } + + [Cold] + private class Done : MonitorState + { + } + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInSynchronousTask() + { + this.Test(async () => + { + Specification.RegisterMonitor(); + async ControlledTask WriteAsync() + { + await ControlledTask.CompletedTask; + Specification.Monitor(new Notify()); + } + + await WriteAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInAsynchronousTask() + { + this.Test(async () => + { + Specification.RegisterMonitor(); + async ControlledTask WriteWithDelayAsync() + { + await ControlledTask.Delay(1); + Specification.Monitor(new Notify()); + } + + await WriteWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInParallelTask() + { + this.Test(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(() => + { + Specification.Monitor(new Notify()); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInParallelSynchronousTask() + { + this.Test(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + Specification.Monitor(new Notify()); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInParallelAsynchronousTask() + { + this.Test(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + Specification.Monitor(new Notify()); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInNestedParallelSynchronousTask() + { + this.Test(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + Specification.Monitor(new Notify()); + }); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInSynchronousTaskFailure() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + async ControlledTask WriteAsync() + { + await ControlledTask.CompletedTask; + } + + await WriteAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Monitor 'LivenessMonitor' detected liveness bug in hot state 'Init' at the end of program execution.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + async ControlledTask WriteWithDelayAsync() + { + await ControlledTask.Delay(1); + } + + await WriteWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Monitor 'LivenessMonitor' detected liveness bug in hot state 'Init' at the end of program execution.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInParallelTaskFailure() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(() => + { + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Monitor 'LivenessMonitor' detected liveness bug in hot state 'Init' at the end of program execution.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Monitor 'LivenessMonitor' detected liveness bug in hot state 'Init' at the end of program execution.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInParallelAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Monitor 'LivenessMonitor' detected liveness bug in hot state 'Init' at the end of program execution.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestLivenessMonitorInvocationInNestedParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + }); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Monitor 'LivenessMonitor' detected liveness bug in hot state 'Init' at the end of program execution.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Specifications/TaskSafetyMonitorTest.cs b/Tests/TestingServices.Tests/Threading/Specifications/TaskSafetyMonitorTest.cs new file mode 100644 index 000000000..d71a4fa24 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Specifications/TaskSafetyMonitorTest.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskSafetyMonitorTest : BaseTest + { + public TaskSafetyMonitorTest(ITestOutputHelper output) + : base(output) + { + } + + private class Notify : Event + { + } + + private class SafetyMonitor : Monitor + { + [Start] + [OnEventDoAction(typeof(Notify), nameof(HandleNotify))] + private class Init : MonitorState + { + } + + private void HandleNotify() + { + this.Assert(false, "Bug found!"); + } + } + + [Fact(Timeout = 5000)] + public void TestSafetyMonitorInvocationInSynchronousTask() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + async ControlledTask WriteAsync() + { + await ControlledTask.CompletedTask; + Specification.Monitor(new Notify()); + } + + await WriteAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Bug found!", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestSafetyMonitorInvocationInAsynchronousTask() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + async ControlledTask WriteWithDelayAsync() + { + await ControlledTask.Delay(1); + Specification.Monitor(new Notify()); + } + + await WriteWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Bug found!", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestSafetyMonitorInvocationInParallelTask() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(() => + { + Specification.Monitor(new Notify()); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Bug found!", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestSafetyMonitorInvocationInParallelSynchronousTask() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + Specification.Monitor(new Notify()); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Bug found!", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestSafetyMonitorInvocationInParallelAsynchronousTask() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + Specification.Monitor(new Notify()); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Bug found!", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestSafetyMonitorInvocationInNestedParallelSynchronousTask() + { + this.TestWithError(async () => + { + Specification.RegisterMonitor(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + Specification.Monitor(new Notify()); + }); + }); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Bug found!", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitFalseTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitFalseTest.cs new file mode 100644 index 000000000..fda6d4147 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitFalseTest.cs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskConfigureAwaitFalseTest : BaseTest + { + public TaskConfigureAwaitFalseTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask NestedWriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + await WriteAsync(entry, value).ConfigureAwait(false); + } + + private static async ControlledTask NestedWriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(false); + await WriteWithDelayAsync(entry, value).ConfigureAwait(false); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = value; + return entry.Value; + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask NestedGetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + return await GetWriteResultAsync(entry, value).ConfigureAwait(false); + } + + private static async ControlledTask NestedGetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(false); + return await GetWriteResultWithDelayAsync(entry, value).ConfigureAwait(false); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 3).ConfigureAwait(false); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitTrueTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitTrueTest.cs new file mode 100644 index 000000000..977a9cf78 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskConfigureAwaitTrueTest.cs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskConfigureAwaitTrueTest : BaseTest + { + public TaskConfigureAwaitTrueTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask NestedWriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + await WriteAsync(entry, value).ConfigureAwait(true); + } + + private static async ControlledTask NestedWriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(true); + await WriteWithDelayAsync(entry, value).ConfigureAwait(true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = value; + return entry.Value; + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask NestedGetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + return await GetWriteResultAsync(entry, value).ConfigureAwait(true); + } + + private static async ControlledTask NestedGetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1).ConfigureAwait(true); + return await GetWriteResultWithDelayAsync(entry, value).ConfigureAwait(true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 5).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 3).ConfigureAwait(true); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitFalseTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitFalseTest.cs new file mode 100644 index 000000000..982a4ec05 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitFalseTest.cs @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskRunConfigureAwaitFalseTest : BaseTest + { + public TaskRunConfigureAwaitFalseTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 5; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 3; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 5; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 3; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }).ConfigureAwait(false); + + entry.Value = 5; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }).ConfigureAwait(false); + + entry.Value = 3; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 3; + }).ConfigureAwait(false); + + entry.Value = 5; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 5; + }).ConfigureAwait(false); + + entry.Value = 3; + }).ConfigureAwait(false); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(false); + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(false); + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(false); + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(false); + }).ConfigureAwait(false); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitTrueTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitTrueTest.cs new file mode 100644 index 000000000..e71c72596 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/ConfigureAwait/TaskRunConfigureAwaitTrueTest.cs @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskRunConfigureAwaitTrueTest : BaseTest + { + public TaskRunConfigureAwaitTrueTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 5; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 3; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 5; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 3; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }).ConfigureAwait(true); + + entry.Value = 5; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }).ConfigureAwait(true); + + entry.Value = 3; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 3; + }).ConfigureAwait(true); + + entry.Value = 5; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 5; + }).ConfigureAwait(true); + + entry.Value = 3; + }).ConfigureAwait(true); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(true); + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 5; + return entry.Value; + }).ConfigureAwait(true); + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1).ConfigureAwait(true); + entry.Value = 3; + return entry.Value; + }).ConfigureAwait(true); + }).ConfigureAwait(true); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedControlledTaskAwaitTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedControlledTaskAwaitTest.cs new file mode 100644 index 000000000..862397d46 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedControlledTaskAwaitTest.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MixedControlledTaskAwaitTest : BaseTest + { + public MixedControlledTaskAwaitTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitSynchronousTask() + { + this.Test(async () => + { + async Task CallAsync() + { + await ControlledTask.CompletedTask; + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitAsynchronousTask() + { + this.TestWithError(async () => + { + async Task CallAsync() + { + await ControlledTask.Delay(100); + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitNestedSynchronousTask() + { + this.Test(async () => + { + async Task NestedCallAsync() + { + async Task CallAsync() + { + await ControlledTask.CompletedTask; + } + + await ControlledTask.CompletedTask; + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitNestedAsynchronousTask() + { + this.TestWithError(async () => + { + async Task NestedCallAsync() + { + async Task CallAsync() + { + await ControlledTask.Delay(100); + } + + await ControlledTask.Delay(100); + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitSynchronousTaskWithResult() + { + this.Test(async () => + { + async Task GetWriteResultAsync() + { + await ControlledTask.CompletedTask; + return 5; + } + + await GetWriteResultAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitAsynchronousTaskWithResult() + { + this.TestWithError(async () => + { + async Task GetWriteResultWithDelayAsync() + { + await ControlledTask.Delay(100); + return 5; + } + + await GetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitNestedSynchronousTaskWithResult() + { + this.Test(async () => + { + async Task NestedGetWriteResultAsync() + { + async Task GetWriteResultAsync() + { + await ControlledTask.CompletedTask; + return 5; + } + + await ControlledTask.CompletedTask; + return await GetWriteResultAsync(); + } + + await NestedGetWriteResultAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedControlledAwaitNestedAsynchronousTaskWithResult() + { + this.TestWithError(async () => + { + async Task NestedGetWriteResultWithDelayAsync() + { + async Task GetWriteResultWithDelayAsync() + { + await ControlledTask.Delay(100); + return 5; + } + + await ControlledTask.Delay(100); + return await GetWriteResultWithDelayAsync(); + } + + await NestedGetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedMultipleTaskAwaitTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedMultipleTaskAwaitTest.cs new file mode 100644 index 000000000..c029e97d5 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedMultipleTaskAwaitTest.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MixedMultipleTaskAwaitTest : BaseTest + { + public MixedMultipleTaskAwaitTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitAsynchronousTasks() + { + this.TestWithError(async () => + { + async Task CallAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitAsynchronousTasksInControlledTask() + { + this.TestWithError(async () => + { + async ControlledTask CallAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitNestedAsynchronousTasks() + { + this.TestWithError(async () => + { + async Task NestedCallAsync() + { + async Task CallAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + } + + await Task.Delay(10); + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitNestedAsynchronousTasksInControlledTask() + { + this.TestWithError(async () => + { + async Task NestedCallAsync() + { + async ControlledTask CallAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + } + + await Task.Delay(10); + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitAsynchronousTasksWithResult() + { + this.TestWithError(async () => + { + async Task GetWriteResultWithDelayAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await GetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitAsynchronousTasksInControlledTaskWithResult() + { + this.TestWithError(async () => + { + async ControlledTask GetWriteResultWithDelayAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await GetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitNestedAsynchronousTasksWithResult() + { + this.TestWithError(async () => + { + async Task NestedGetWriteResultWithDelayAsync() + { + async Task GetWriteResultWithDelayAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await Task.Delay(10); + return await GetWriteResultWithDelayAsync(); + } + + await NestedGetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedMultipleAwaitNestedAsynchronousTasksInControlledTaskWithResult() + { + this.TestWithError(async () => + { + async Task NestedGetWriteResultWithDelayAsync() + { + async ControlledTask GetWriteResultWithDelayAsync() + { + await ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await Task.Delay(10); + return await GetWriteResultWithDelayAsync(); + } + + await NestedGetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedSkipTaskAwaitTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedSkipTaskAwaitTest.cs new file mode 100644 index 000000000..dca195360 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedSkipTaskAwaitTest.cs @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MixedSkipTaskAwaitTest : BaseTest + { + public MixedSkipTaskAwaitTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitAsynchronousTasks() + { + this.TestWithError(async () => + { + async Task CallAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitAsynchronousTasksInControlledTask() + { + this.TestWithError(async () => + { + async ControlledTask CallAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitNestedAsynchronousTasks() + { + this.TestWithError(async () => + { + async Task NestedCallAsync() + { + async Task CallAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + } + + await Task.Delay(10); + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitNestedAsynchronousTasksInControlledTask() + { + this.TestWithError(async () => + { + async Task NestedCallAsync() + { + async ControlledTask CallAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + } + + await Task.Delay(10); + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitAsynchronousTasksWithResult() + { + this.TestWithError(async () => + { + async Task GetWriteResultWithDelayAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await GetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitAsynchronousTasksInControlledTaskWithResult() + { + this.TestWithError(async () => + { + async ControlledTask GetWriteResultWithDelayAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await GetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitNestedAsynchronousTasksWithResult() + { + this.TestWithError(async () => + { + async Task NestedGetWriteResultWithDelayAsync() + { + async Task GetWriteResultWithDelayAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await Task.Delay(10); + return await GetWriteResultWithDelayAsync(); + } + + await NestedGetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedSkipAwaitNestedAsynchronousTasksInControlledTaskWithResult() + { + this.TestWithError(async () => + { + async Task NestedGetWriteResultWithDelayAsync() + { + async ControlledTask GetWriteResultWithDelayAsync() + { + _ = ControlledTask.Delay(10); + await Task.Delay(10); + return 5; + } + + await Task.Delay(10); + return await GetWriteResultWithDelayAsync(); + } + + await NestedGetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedUncontrolledTaskAwaitTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedUncontrolledTaskAwaitTest.cs new file mode 100644 index 000000000..f1de5caca --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/MixedTypes/MixedUncontrolledTaskAwaitTest.cs @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class MixedUncontrolledTaskAwaitTest : BaseTest + { + public MixedUncontrolledTaskAwaitTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitSynchronousTask() + { + this.Test(async () => + { + async Task CallAsync() + { + await Task.CompletedTask; + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitSynchronousTaskInControlledTask() + { + this.Test(async () => + { + async ControlledTask CallAsync() + { + await Task.CompletedTask; + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitAsynchronousTask() + { + this.TestWithError(async () => + { + async Task CallAsync() + { + await Task.Delay(10); + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitAsynchronousTaskInControlledTask() + { + this.TestWithError(async () => + { + async ControlledTask CallAsync() + { + await Task.Delay(10); + } + + await CallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedSynchronousTask() + { + this.Test(async () => + { + async Task NestedCallAsync() + { + async Task CallAsync() + { + await Task.CompletedTask; + } + + await Task.CompletedTask; + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedSynchronousTaskInControlledTask() + { + this.Test(async () => + { + async Task NestedCallAsync() + { + async ControlledTask CallAsync() + { + await Task.CompletedTask; + } + + await Task.CompletedTask; + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedAsynchronousTask() + { + this.TestWithError(async () => + { + async Task NestedCallAsync() + { + async Task CallAsync() + { + await Task.Delay(10); + } + + await Task.Delay(10); + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedAsynchronousTaskInControlledTask() + { + this.TestWithError(async () => + { + async Task NestedCallAsync() + { + async ControlledTask CallAsync() + { + await Task.Delay(10); + } + + await Task.Delay(10); + await CallAsync(); + } + + await NestedCallAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitSynchronousTaskWithResult() + { + this.Test(async () => + { + async Task GetWriteResultAsync() + { + await Task.CompletedTask; + return 5; + } + + await GetWriteResultAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitSynchronousTaskInControlledTaskWithResult() + { + this.Test(async () => + { + async ControlledTask GetWriteResultAsync() + { + await Task.CompletedTask; + return 5; + } + + await GetWriteResultAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitAsynchronousTaskWithResult() + { + this.TestWithError(async () => + { + async Task GetWriteResultWithDelayAsync() + { + await Task.Delay(10); + return 5; + } + + await GetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitAsynchronousTaskInControlledTaskWithResult() + { + this.TestWithError(async () => + { + async ControlledTask GetWriteResultWithDelayAsync() + { + await Task.Delay(10); + return 5; + } + + await GetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedSynchronousTaskWithResult() + { + this.Test(async () => + { + async Task NestedGetWriteResultAsync() + { + async Task GetWriteResultAsync() + { + await Task.CompletedTask; + return 5; + } + + await Task.CompletedTask; + return await GetWriteResultAsync(); + } + + await NestedGetWriteResultAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedSynchronousTaskInControlledTaskWithResult() + { + this.Test(async () => + { + async Task NestedGetWriteResultAsync() + { + async ControlledTask GetWriteResultAsync() + { + await Task.CompletedTask; + return 5; + } + + await Task.CompletedTask; + return await GetWriteResultAsync(); + } + + await NestedGetWriteResultAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedAsynchronousTaskWithResult() + { + this.TestWithError(async () => + { + async Task NestedGetWriteResultWithDelayAsync() + { + async Task GetWriteResultWithDelayAsync() + { + await Task.Delay(10); + return 5; + } + + await Task.Delay(10); + return await GetWriteResultWithDelayAsync(); + } + + await NestedGetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestMixedUncontrolledAwaitNestedAsynchronousTaskInControlledTaskWithResult() + { + this.TestWithError(async () => + { + async Task NestedGetWriteResultWithDelayAsync() + { + async ControlledTask GetWriteResultWithDelayAsync() + { + await Task.Delay(10); + return 5; + } + + await Task.Delay(10); + return await GetWriteResultWithDelayAsync(); + } + + await NestedGetWriteResultWithDelayAsync(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Controlled task '' is trying to wait for an uncontrolled task or awaiter to complete. " + + "Please make sure to use Coyote APIs to express concurrency ().", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWaitAnyTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWaitAnyTest.cs new file mode 100644 index 000000000..153e1bd38 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWaitAnyTest.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskWaitAnyTest : BaseTest + { + public TaskWaitAnyTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestWaitAnyWithTwoSynchronousTasks() + { + this.TestWithError(() => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteAsync(entry, 5); + ControlledTask task2 = WriteAsync(entry, 3); + int index = ControlledTask.WaitAny(task1, task2); + Specification.Assert(index == 0 || index == 1, $"Index is {index}."); + Specification.Assert(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Specification.Assert((task1.IsCompleted && !task2.IsCompleted) || (!task1.IsCompleted && task2.IsCompleted), + "Both task have completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Both task have completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWaitAnyWithTwoAsynchronousTasks() + { + this.TestWithError(() => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteWithDelayAsync(entry, 3); + ControlledTask task2 = WriteWithDelayAsync(entry, 5); + int index = ControlledTask.WaitAny(task1, task2); + Specification.Assert(index == 0 || index == 1, $"Index is {index}."); + Specification.Assert(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWaitAnyWithTwoParallelTasks() + { + this.TestWithError(() => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 5); + }); + + int index = ControlledTask.WaitAny(task1, task2); + + Specification.Assert(index == 0 || index == 1, $"Index is {index}."); + Specification.Assert(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + entry.Value = value; + await ControlledTask.CompletedTask; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + entry.Value = value; + await ControlledTask.Delay(1); + return entry.Value; + } + + [Fact(Timeout = 5000)] + public void TestWaitAnyWithTwoSynchronousTaskWithResults() + { + this.TestWithError(() => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = GetWriteResultAsync(entry, 5); + ControlledTask task2 = GetWriteResultAsync(entry, 3); + int index = ControlledTask.WaitAny(task1, task2); + Task result = index == 0 ? task1.AwaiterTask : task2.AwaiterTask; + Specification.Assert(index == 0 || index == 1, $"Index is {index}."); + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert((task1.IsCompleted && !task2.IsCompleted) || (!task1.IsCompleted && task2.IsCompleted), + "Both task have completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Both task have completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWaitAnyWithTwoAsynchronousTaskWithResults() + { + this.TestWithError(() => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = GetWriteResultWithDelayAsync(entry, 5); + ControlledTask task2 = GetWriteResultWithDelayAsync(entry, 3); + int index = ControlledTask.WaitAny(task1, task2); + Task result = index == 0 ? task1.AwaiterTask : task2.AwaiterTask; + Specification.Assert(index == 0 || index == 1, $"Index is {index}."); + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWaitAnyWithTwoParallelSynchronousTaskWithResults() + { + this.TestWithError(() => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(entry, 5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(entry, 3); + }); + + int index = ControlledTask.WaitAny(task1, task2); + Task result = index == 0 ? task1.AwaiterTask : task2.AwaiterTask; + + Specification.Assert(index == 0 || index == 1, $"Index is {index}."); + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWaitAnyWithTwoParallelAsynchronousTaskWithResults() + { + this.TestWithError(() => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(entry, 5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(entry, 3); + }); + + int index = ControlledTask.WaitAny(task1, task2); + Task result = index == 0 ? task1.AwaiterTask : task2.AwaiterTask; + + Specification.Assert(index == 0 || index == 1, $"Index is {index}."); + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAllTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAllTest.cs new file mode 100644 index 000000000..eb6376757 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAllTest.cs @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskWhenAllTest : BaseTest + { + public TaskWhenAllTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestWhenAllWithTwoSynchronousTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteAsync(entry, 5); + ControlledTask task2 = WriteAsync(entry, 3); + await ControlledTask.WhenAll(task1, task2); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAllWithTwoAsynchronousTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteWithDelayAsync(entry, 3); + ControlledTask task2 = WriteWithDelayAsync(entry, 5); + await ControlledTask.WhenAll(task1, task2); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAllWithTwoParallelTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 5); + }); + + await ControlledTask.WhenAll(task1, task2); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + entry.Value = value; + await ControlledTask.CompletedTask; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + entry.Value = value; + await ControlledTask.Delay(1); + return entry.Value; + } + + [Fact(Timeout = 5000)] + public void TestWhenAllWithTwoSynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = GetWriteResultAsync(entry, 5); + ControlledTask task2 = GetWriteResultAsync(entry, 3); + int[] results = await ControlledTask.WhenAll(task1, task2); + Specification.Assert(results.Length == 2, "Result count is '{0}' instead of 2.", results.Length); + Specification.Assert(results[0] == 5 && results[1] == 3, "Found unexpected value."); + Specification.Assert(results[0] == results[1], "Results are not equal."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Results are not equal.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAllWithTwoAsynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = GetWriteResultWithDelayAsync(entry, 5); + ControlledTask task2 = GetWriteResultWithDelayAsync(entry, 3); + int[] results = await ControlledTask.WhenAll(task1, task2); + Specification.Assert(results.Length == 2, "Result count is '{0}' instead of 2.", results.Length); + Specification.Assert(results[0] == 5 && results[1] == 3, "Found unexpected value."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Found unexpected value.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAllWithTwoParallelSynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(entry, 5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(entry, 3); + }); + + int[] results = await ControlledTask.WhenAll(task1, task2); + + Specification.Assert(results.Length == 2, "Result count is '{0}' instead of 2.", results.Length); + Specification.Assert(results[0] == 5 && results[1] == 3, "Found unexpected value."); + Specification.Assert(results[0] == results[1], "Results are not equal."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Results are not equal.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAllWithTwoParallelAsynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(entry, 5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(entry, 3); + }); + + int[] results = await ControlledTask.WhenAll(task1, task2); + + Specification.Assert(results.Length == 2, "Result count is '{0}' instead of 2.", results.Length); + Specification.Assert(results[0] == 5 && results[1] == 3, "Found unexpected value."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Found unexpected value.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAnyTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAnyTest.cs new file mode 100644 index 000000000..117a45fb5 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/MultipleJoin/TaskWhenAnyTest.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskWhenAnyTest : BaseTest + { + public TaskWhenAnyTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestWhenAnyWithTwoSynchronousTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteAsync(entry, 5); + ControlledTask task2 = WriteAsync(entry, 3); + await ControlledTask.WhenAny(task1, task2); + Specification.Assert(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Specification.Assert((task1.IsCompleted && !task2.IsCompleted) || (!task1.IsCompleted && task2.IsCompleted), + "Both task have completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Both task have completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAnyWithTwoAsynchronousTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = WriteWithDelayAsync(entry, 3); + ControlledTask task2 = WriteWithDelayAsync(entry, 5); + await ControlledTask.WhenAny(task1, task2); + Specification.Assert(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAnyWithTwoParallelTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 5); + }); + + await ControlledTask.WhenAny(task1, task2); + + Specification.Assert(task1.IsCompleted || task2.IsCompleted, "No task has completed."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + entry.Value = value; + await ControlledTask.CompletedTask; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + entry.Value = value; + await ControlledTask.Delay(1); + return entry.Value; + } + + [Fact(Timeout = 5000)] + public void TestWhenAnyWithTwoSynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = GetWriteResultAsync(entry, 5); + ControlledTask task2 = GetWriteResultAsync(entry, 3); + Task result = await ControlledTask.WhenAny(task1, task2); + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert((task1.IsCompleted && !task2.IsCompleted) || (!task1.IsCompleted && task2.IsCompleted), + "Both task have completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Both task have completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAnyWithTwoAsynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + ControlledTask task1 = GetWriteResultWithDelayAsync(entry, 5); + ControlledTask task2 = GetWriteResultWithDelayAsync(entry, 3); + Task result = await ControlledTask.WhenAny(task1, task2); + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAnyWithTwoParallelSynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(entry, 5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultAsync(entry, 3); + }); + + Task result = await ControlledTask.WhenAny(task1, task2); + + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestWhenAnyWithTwoParallelAsynchronousTaskWithResults() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(entry, 5); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + return await GetWriteResultWithDelayAsync(entry, 3); + }); + + Task result = await ControlledTask.WhenAny(task1, task2); + + Specification.Assert(result.Result == 5 || result.Result == 3, "Found unexpected value."); + Specification.Assert(task1.IsCompleted && task2.IsCompleted, "One task has not completed."); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "One task has not completed.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/TaskAwaitTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/TaskAwaitTest.cs new file mode 100644 index 000000000..b23305a1e --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/TaskAwaitTest.cs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskAwaitTest : BaseTest + { + public TaskAwaitTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 5); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteAsync(entry, 3); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 5); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await WriteWithDelayAsync(entry, 3); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask NestedWriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + await WriteAsync(entry, value); + } + + private static async ControlledTask NestedWriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + await WriteWithDelayAsync(entry, value); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 5); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteAsync(entry, 3); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 5); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await NestedWriteWithDelayAsync(entry, 3); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask GetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + return entry.Value; + } + + private static async ControlledTask GetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + return entry.Value; + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 5); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultAsync(entry, 3); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 5); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await GetWriteResultWithDelayAsync(entry, 3); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + private static async ControlledTask NestedGetWriteResultAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + return await GetWriteResultAsync(entry, value); + } + + private static async ControlledTask NestedGetWriteResultWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + return await GetWriteResultWithDelayAsync(entry, value); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 5); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultAsync(entry, 3); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 5); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await NestedGetWriteResultWithDelayAsync(entry, 3); + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/TaskDelayTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/TaskDelayTest.cs new file mode 100644 index 000000000..b26555e77 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/TaskDelayTest.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskDelayTest : BaseTest + { + public TaskDelayTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value, int delay) + { + for (int i = 0; i < 2; i++) + { + entry.Value = value + i; + await ControlledTask.Delay(delay); + } + } + + [Fact(Timeout=5000)] + public void TestInterleavingsInLoopWithSynchronousDelays() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask[] tasks = new ControlledTask[2]; + for (int i = 0; i < 2; i++) + { + tasks[i] = WriteWithDelayAsync(entry, i, 0); + } + + await ControlledTask.WhenAll(tasks); + + Specification.Assert(entry.Value == 2, "Value is '{0}' instead of 2.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsInLoopWithAsynchronousDelays() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask[] tasks = new ControlledTask[2]; + for (int i = 0; i < 2; i++) + { + tasks[i] = WriteWithDelayAsync(entry, i, 1); + } + + await ControlledTask.WhenAll(tasks); + + Specification.Assert(entry.Value == 2, "Value is {0} instead of 2.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 1 instead of 2.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/TaskExceptionTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/TaskExceptionTest.cs new file mode 100644 index 000000000..9e1b2bbb3 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/TaskExceptionTest.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskExceptionTest : BaseTest + { + public TaskExceptionTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestNoSynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = WriteAsync(entry, 5); + await task; + + Specification.Assert(task.Status == TaskStatus.RanToCompletion, + $"Status is '{task.Status}' instead of 'RanToCompletion'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestNoAsynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = WriteWithDelayAsync(entry, 5); + await task; + + Specification.Assert(task.Status == TaskStatus.RanToCompletion, + $"Status is '{task.Status}' instead of 'RanToCompletion'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestNoParallelSynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(() => + { + entry.Value = 5; + }); + + await task; + + Specification.Assert(task.Status == TaskStatus.RanToCompletion, + $"Status is '{task.Status}' instead of 'RanToCompletion'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestNoParallelAsynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(async () => + { + entry.Value = 5; + await ControlledTask.Delay(1); + }); + await task; + + Specification.Assert(task.Status == TaskStatus.RanToCompletion, + $"Status is '{task.Status}' instead of 'RanToCompletion'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestNoParallelFuncTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + async ControlledTask func() + { + entry.Value = 5; + await ControlledTask.Delay(1); + } + + var task = ControlledTask.Run(func); + await task; + + Specification.Assert(task.Status == TaskStatus.RanToCompletion, + $"Status is '{task.Status}' instead of 'RanToCompletion'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + private static async ControlledTask WriteWithExceptionAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + throw new InvalidOperationException(); + } + + private static async ControlledTask WriteWithDelayedExceptionAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + throw new InvalidOperationException(); + } + + [Fact(Timeout = 5000)] + public void TestSynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = WriteWithExceptionAsync(entry, 5); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Specification.Assert(exception is InvalidOperationException, + $"Exception is not '{typeof(InvalidOperationException)}'."); + Specification.Assert(task.Status == TaskStatus.Faulted, + $"Status is '{task.Status}' instead of 'Faulted'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAsynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = WriteWithDelayedExceptionAsync(entry, 5); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Specification.Assert(exception is InvalidOperationException, + $"Exception is not '{typeof(InvalidOperationException)}'."); + Specification.Assert(task.Status == TaskStatus.Faulted, + $"Status is '{task.Status}' instead of 'Faulted'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestParallelSynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(() => + { + entry.Value = 5; + throw new InvalidOperationException(); + }); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Specification.Assert(exception is InvalidOperationException, + $"Exception is not '{typeof(InvalidOperationException)}'."); + Specification.Assert(task.Status == TaskStatus.Faulted, + $"Status is '{task.Status}' instead of 'Faulted'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestParallelAsynchronousTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + var task = ControlledTask.Run(async () => + { + entry.Value = 5; + await ControlledTask.Delay(1); + throw new InvalidOperationException(); + }); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Specification.Assert(exception is InvalidOperationException, + $"Exception is not '{typeof(InvalidOperationException)}'."); + Specification.Assert(task.Status == TaskStatus.Faulted, + $"Status is '{task.Status}' instead of 'Faulted'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestParallelFuncTaskExceptionStatus() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + async ControlledTask func() + { + entry.Value = 5; + await ControlledTask.Delay(1); + throw new InvalidOperationException(); + } + + var task = ControlledTask.Run(func); + + Exception exception = null; + try + { + await task; + } + catch (Exception ex) + { + exception = ex; + } + + Specification.Assert(exception is InvalidOperationException, + $"Exception is not '{typeof(InvalidOperationException)}'."); + Specification.Assert(task.Status == TaskStatus.Faulted, + $"Status is '{task.Status}' instead of 'Faulted'."); + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/TaskInterleavingsTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/TaskInterleavingsTest.cs new file mode 100644 index 000000000..e932cd60f --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/TaskInterleavingsTest.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskInterleavingsTest : BaseTest + { + public TaskInterleavingsTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + private static async ControlledTask WriteAsync(SharedEntry entry, int value) + { + await ControlledTask.CompletedTask; + entry.Value = value; + } + + private static async ControlledTask WriteWithDelayAsync(SharedEntry entry, int value) + { + await ControlledTask.Delay(1); + entry.Value = value; + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsWithOneSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task = WriteAsync(entry, 3); + entry.Value = 5; + await task; + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsWithOneAsynchronousTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task = WriteWithDelayAsync(entry, 3); + entry.Value = 5; + await task; + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsWithOneParallelTask() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task = ControlledTask.Run(async () => + { + await WriteAsync(entry, 3); + }); + + await WriteAsync(entry, 5); + await task; + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsWithTwoSynchronousTasks() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = WriteAsync(entry, 3); + ControlledTask task2 = WriteAsync(entry, 5); + + await task1; + await task2; + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsWithTwoAsynchronousTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = WriteWithDelayAsync(entry, 3); + ControlledTask task2 = WriteWithDelayAsync(entry, 5); + + await task1; + await task2; + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsWithTwoParallelTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 5); + }); + + await task1; + await task2; + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestInterleavingsWithNestedParallelTasks() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + + ControlledTask task1 = ControlledTask.Run(async () => + { + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(entry, 5); + }); + + await WriteAsync(entry, 3); + await task2; + }); + + await task1; + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/TaskRunTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/TaskRunTest.cs new file mode 100644 index 000000000..d444c1472 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/TaskRunTest.cs @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskRunTest : BaseTest + { + public TaskRunTest(ITestOutputHelper output) + : base(output) + { + } + + private class SharedEntry + { + public int Value = 0; + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 5; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(() => + { + entry.Value = 3; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 5; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 3; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + }); + + entry.Value = 5; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelSynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + }); + + entry.Value = 3; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelAsynchronousTask() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 3; + }); + + entry.Value = 5; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAwaitNestedParallelAsynchronousTaskFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + await ControlledTask.Run(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 5; + }); + + entry.Value = 3; + }); + + Specification.Assert(entry.Value == 5, "Value is {0} instead of 5.", entry.Value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 5; + return entry.Value; + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(() => + { + entry.Value = 3; + return entry.Value; + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + return entry.Value; + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 5; + return entry.Value; + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunParallelAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 3; + return entry.Value; + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 5; + return entry.Value; + }); + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelSynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.CompletedTask; + entry.Value = 3; + return entry.Value; + }); + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelAsynchronousTaskWithResult() + { + this.Test(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 5; + return entry.Value; + }); + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestRunNestedParallelAsynchronousTaskWithResultFailure() + { + this.TestWithError(async () => + { + SharedEntry entry = new SharedEntry(); + int value = await ControlledTask.Run(async () => + { + return await ControlledTask.Run(async () => + { + await ControlledTask.Delay(1); + entry.Value = 3; + return entry.Value; + }); + }); + + Specification.Assert(value == 5, "Value is {0} instead of 5.", value); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Threading/Tasks/TaskYieldTest.cs b/Tests/TestingServices.Tests/Threading/Tasks/TaskYieldTest.cs new file mode 100644 index 000000000..e394a90b6 --- /dev/null +++ b/Tests/TestingServices.Tests/Threading/Tasks/TaskYieldTest.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Specifications; +using Microsoft.Coyote.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TaskYieldTest : BaseTest + { + public TaskYieldTest(ITestOutputHelper output) + : base(output) + { + } + + [Fact(Timeout = 5000)] + public void TestTaskYield() + { + this.Test(async () => + { + await ControlledTask.Yield(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestAsynchronousTaskYield() + { + this.Test(async () => + { + await ControlledTask.Run(async () => + { + await ControlledTask.Yield(); + }); + + await ControlledTask.Yield(); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestParallelTaskYield() + { + this.Test(async () => + { + ControlledTask task = ControlledTask.Run(async () => + { + await ControlledTask.Yield(); + }); + + await ControlledTask.Yield(); + await task; + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestTwoParallelTasksYield() + { + this.Test(async () => + { + ControlledTask task1 = ControlledTask.Run(async () => + { + await ControlledTask.Yield(); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await ControlledTask.Yield(); + }); + + await ControlledTask.Yield(); + await ControlledTask.WhenAll(task1, task2); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestTwoParallelTasksWriteWithYield() + { + this.Test(async () => + { + int entry = 0; + + async ControlledTask WriteAsync(int value) + { + await ControlledTask.Yield(); + entry = value; + Specification.Assert(entry == value, "Value is {0} instead of '{1}'.", entry, value); + } + + ControlledTask task1 = ControlledTask.Run(async () => + { + await WriteAsync(3); + }); + + ControlledTask task2 = ControlledTask.Run(async () => + { + await WriteAsync(5); + }); + + await ControlledTask.Yield(); + await ControlledTask.WhenAll(task1, task2); + }, + configuration: GetConfiguration().WithNumberOfIterations(200)); + } + + [Fact(Timeout = 5000)] + public void TestTwoParallelTasksWriteWithYieldFail() + { + this.TestWithError(async () => + { + int entry = 0; + + async ControlledTask WriteAsync(int value) + { + entry = value; + await ControlledTask.Yield(); + Specification.Assert(entry == value, "Found unexpected value '{0}' after write.", entry); + } + + ControlledTask task1 = WriteAsync(3); + ControlledTask task2 = WriteAsync(5); + await ControlledTask.WhenAll(task1, task2); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Found unexpected value '' after write.", + replay: true); + } + + [Fact(Timeout = 5000)] + public void TestTwoAsynchronousTasksWriteWithYieldFail() + { + this.TestWithError(async () => + { + int entry = 0; + + async ControlledTask WriteAsync(int value) + { + await ControlledTask.Yield(); + entry = value; + } + + ControlledTask task1 = WriteAsync(3); + ControlledTask task2 = WriteAsync(5); + await ControlledTask.WhenAll(task1, task2); + Specification.Assert(entry == 5, "Value is {0} instead of 5.", entry); + }, + configuration: GetConfiguration().WithNumberOfIterations(200), + expectedError: "Value is 3 instead of 5.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Timers/BasicTimerTest.cs b/Tests/TestingServices.Tests/Timers/BasicTimerTest.cs new file mode 100644 index 000000000..239a390d7 --- /dev/null +++ b/Tests/TestingServices.Tests/Timers/BasicTimerTest.cs @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class BasicTimerTest : BaseTest + { + public BasicTimerTest(ITestOutputHelper output) + : base(output) + { + } + + private class T1 : Machine + { + private int Count; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Count = 0; + + // Start a regular timer. + this.StartTimer(TimeSpan.FromMilliseconds(10)); + } + + private void HandleTimeout() + { + this.Count++; + this.Assert(this.Count == 1); + } + } + + [Fact(Timeout=10000)] + public void TestBasicTimerOperation() + { + this.Test(r => + { + r.CreateMachine(typeof(T1)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200).WithMaxSteps(200)); + } + + private class T2 : Machine + { + private TimerInfo Timer; + private int Count; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Count = 0; + + // Start a periodic timer. + this.Timer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(10)); + } + + private void HandleTimeout() + { + this.Count++; + this.Assert(this.Count <= 10); + + if (this.Count == 10) + { + this.StopTimer(this.Timer); + } + } + } + + [Fact(Timeout=10000)] + public void TestBasicPeriodicTimerOperation() + { + this.Test(r => + { + r.CreateMachine(typeof(T2)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200)); + } + + private class T3 : Machine + { + private TimerInfo PingTimer; + private TimerInfo PongTimer; + + ///

+ /// Start the PingTimer and start handling the timeout events from it. + /// After handling 10 events, stop the timer and move to the Pong state. + /// + [Start] + [OnEntry(nameof(DoPing))] + [IgnoreEvents(typeof(TimerElapsedEvent))] + private class Ping : MachineState + { + } + + /// + /// Start the PongTimer and start handling the timeout events from it. + /// After handling 10 events, stop the timer and move to the Ping state. + /// + [OnEntry(nameof(DoPong))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Pong : MachineState + { + } + + private void DoPing() + { + this.PingTimer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(5), TimeSpan.FromMilliseconds(5)); + this.StopTimer(this.PingTimer); + + this.Goto(); + } + + private void DoPong() + { + this.PongTimer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(50)); + } + + private void HandleTimeout() + { + var timeout = this.ReceivedEvent as TimerElapsedEvent; + this.Assert(timeout.Info == this.PongTimer); + } + } + + [Fact(Timeout=10000)] + public void TestDropTimeoutsAfterTimerDisposal() + { + this.Test(r => + { + r.CreateMachine(typeof(T3)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200).WithMaxSteps(200)); + } + + private class T4 : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + private class Init : MachineState + { + } + + private void Initialize() + { + this.StartTimer(TimeSpan.FromSeconds(-1)); + } + } + + [Fact(Timeout=10000)] + public void TestIllegalDueTimeSpecification() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(T4)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200).WithMaxSteps(200), + expectedError: "Machine 'T4()' registered a timer with a negative due time.", + replay: true); + } + + private class T5 : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + private class Init : MachineState + { + } + + private void Initialize() + { + this.StartPeriodicTimer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(-1)); + } + } + + [Fact(Timeout=10000)] + public void TestIllegalPeriodSpecification() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(T5)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200).WithMaxSteps(200), + expectedError: "Machine 'T5()' registered a periodic timer with a negative period.", + replay: true); + } + + private class TransferTimerEvent : Event + { + public TimerInfo Timer; + + public TransferTimerEvent(TimerInfo timer) + { + this.Timer = timer; + } + } + + private class T6 : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + [IgnoreEvents(typeof(TimerElapsedEvent))] + private class Init : MachineState + { + } + + private void Initialize() + { + var timer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(10)); + this.CreateMachine(typeof(T7), new TransferTimerEvent(timer)); + } + } + + private class T7 : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + private class Init : MachineState + { + } + + private void Initialize() + { + var timer = (this.ReceivedEvent as TransferTimerEvent).Timer; + this.StopTimer(timer); + } + } + + [Fact(Timeout=10000)] + public void TestTimerDisposedByNonOwner() + { + this.TestWithError(r => + { + r.CreateMachine(typeof(T6)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200).WithMaxSteps(200), + expectedError: "Machine 'T7()' is not allowed to dispose timer '', which is owned by machine 'T6()'.", + replay: true); + } + + private class T8 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + [IgnoreEvents(typeof(TimerElapsedEvent))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + // Start a regular timer. + this.StartTimer(TimeSpan.FromMilliseconds(10)); + this.Goto(); + } + + [OnEntry(nameof(FinalOnEntry))] + [IgnoreEvents(typeof(TimerElapsedEvent))] + private class Final : MachineState + { + } + + private void FinalOnEntry() + { + this.Raise(new Halt()); + } + } + + [Fact(Timeout=10000)] + public void TestExplicitHaltWithTimer() + { + this.Test(r => + { + r.CreateMachine(typeof(T8)); + }, + configuration: Configuration.Create().WithNumberOfIterations(200).WithMaxSteps(200)); + } + } +} diff --git a/Tests/TestingServices.Tests/Timers/StartStopTimerTest.cs b/Tests/TestingServices.Tests/Timers/StartStopTimerTest.cs new file mode 100644 index 000000000..cf090be5b --- /dev/null +++ b/Tests/TestingServices.Tests/Timers/StartStopTimerTest.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class StartStopTimerTest : BaseTest + { + public StartStopTimerTest(ITestOutputHelper output) + : base(output) + { + } + + private class TimeoutReceivedEvent : Event + { + } + + private class Client : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void Initialize() + { + // Start a timer, and then stop it immediately. + var timer = this.StartPeriodicTimer(TimeSpan.FromMilliseconds(10), TimeSpan.FromMilliseconds(10)); + this.StopTimer(timer); + } + + private void HandleTimeout() + { + // Timeout in the interval between starting and disposing the timer. + this.Monitor(new TimeoutReceivedEvent()); + } + } + + private class LivenessMonitor : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(TimeoutReceivedEvent), typeof(TimeoutReceived))] + private class NoTimeoutReceived : MonitorState + { + } + + [Cold] + private class TimeoutReceived : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestStartStopTimer() + { + var configuration = GetConfiguration(); + configuration.LivenessTemperatureThreshold = 150; + configuration.MaxSchedulingSteps = 300; + configuration.SchedulingIterations = 1000; + + this.TestWithError(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(Client)); + }, + configuration: configuration, + expectedError: "Monitor 'LivenessMonitor' detected liveness bug in hot state " + + "'NoTimeoutReceived' at the end of program execution.", + replay: true); + } + } +} diff --git a/Tests/TestingServices.Tests/Timers/TimerLivenessTest.cs b/Tests/TestingServices.Tests/Timers/TimerLivenessTest.cs new file mode 100644 index 000000000..e5f887857 --- /dev/null +++ b/Tests/TestingServices.Tests/Timers/TimerLivenessTest.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Machines.Timers; +using Microsoft.Coyote.Specifications; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.TestingServices.Tests +{ + public class TimerLivenessTest : BaseTest + { + public TimerLivenessTest(ITestOutputHelper output) + : base(output) + { + } + + private class TimeoutReceivedEvent : Event + { + } + + private class Client : Machine + { + [Start] + [OnEntry(nameof(Initialize))] + [OnEventDoAction(typeof(TimerElapsedEvent), nameof(HandleTimeout))] + private class Init : MachineState + { + } + + private void Initialize() + { + this.StartTimer(TimeSpan.FromMilliseconds(10)); + } + + private void HandleTimeout() + { + this.Monitor(new TimeoutReceivedEvent()); + } + } + + private class LivenessMonitor : Monitor + { + [Start] + [Hot] + [OnEventGotoState(typeof(TimeoutReceivedEvent), typeof(TimeoutReceived))] + private class NoTimeoutReceived : MonitorState + { + } + + [Cold] + private class TimeoutReceived : MonitorState + { + } + } + + [Fact(Timeout=5000)] + public void TestTimerLiveness() + { + var configuration = GetConfiguration(); + configuration.LivenessTemperatureThreshold = 150; + configuration.MaxSchedulingSteps = 300; + configuration.SchedulingIterations = 1000; + + this.Test(r => + { + r.RegisterMonitor(typeof(LivenessMonitor)); + r.CreateMachine(typeof(Client)); + }, + configuration: configuration); + } + } +} diff --git a/Tests/Tests.Common/BaseTest.cs b/Tests/Tests.Common/BaseTest.cs new file mode 100644 index 000000000..00039cf1e --- /dev/null +++ b/Tests/Tests.Common/BaseTest.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Tests.Common +{ + public abstract class BaseTest + { + protected readonly ITestOutputHelper TestOutput; + + public BaseTest(ITestOutputHelper output) + { + this.TestOutput = output; + } + } +} diff --git a/Tests/Tests.Common/TestConsoleLogger.cs b/Tests/Tests.Common/TestConsoleLogger.cs new file mode 100644 index 000000000..1559447aa --- /dev/null +++ b/Tests/Tests.Common/TestConsoleLogger.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.IO; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Tests.Common +{ + /// + /// Logger that writes to the console. + /// + public sealed class TestConsoleLogger : ILogger, ITestOutputHelper + { + /// + /// If true, then messages are logged. The default value is false. + /// + public bool IsVerbose { get; set; } = false; + + /// + /// Initializes a new instance of the class. + /// + /// If true, then messages are logged. + public TestConsoleLogger(bool isVerbose) + { + this.IsVerbose = isVerbose; + } + + /// + /// Writes the specified string value. + /// + /// Text + public void Write(string value) + { + if (this.IsVerbose) + { + Console.Write(value); + } + } + + /// + /// Writes the text representation of the specified argument. + /// + public void Write(string format, object arg0) + { + if (this.IsVerbose) + { + Console.Write(format, arg0.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + Console.Write(format, arg0.ToString(), arg1.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + Console.Write(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + + /// + /// Writes the text representation of the specified array of objects. + /// + /// Text + /// Arguments + public void Write(string format, params object[] args) + { + if (this.IsVerbose) + { + Console.Write(format, args); + } + } + + /// + /// Writes the specified string value, followed by the + /// current line terminator. + /// + /// Text + public void WriteLine(string value) + { + if (this.IsVerbose) + { + Console.WriteLine(value); + } + } + + /// + /// Writes the text representation of the specified argument, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0) + { + if (this.IsVerbose) + { + Console.WriteLine(format, arg0.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + Console.WriteLine(format, arg0.ToString(), arg1.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + Console.WriteLine(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + + /// + /// Writes the text representation of the specified array of objects, + /// followed by the current line terminator. + /// + /// Text + /// Arguments + public void WriteLine(string format, params object[] args) + { + if (this.IsVerbose) + { + Console.WriteLine(format, args); + } + } + + /// + /// Disposes the logger. + /// + public void Dispose() + { + } + } +} diff --git a/Tests/Tests.Common/TestOutputLogger.cs b/Tests/Tests.Common/TestOutputLogger.cs new file mode 100644 index 000000000..77cb802d5 --- /dev/null +++ b/Tests/Tests.Common/TestOutputLogger.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.IO; +using Xunit.Abstractions; + +namespace Microsoft.Coyote.Tests.Common +{ + /// + /// Logger that writes to the test output. + /// + public sealed class TestOutputLogger : ILogger + { + /// + /// Underlying test output. + /// + private readonly ITestOutputHelper TestOutput; + + /// + /// If true, then messages are logged. The default value is false. + /// + public bool IsVerbose { get; set; } = false; + + /// + /// Initializes a new instance of the class. + /// + /// The test output helper. + /// If true, then messages are logged. The default value is false. + public TestOutputLogger(ITestOutputHelper output, bool isVerbose = false) + { + this.TestOutput = output; + this.IsVerbose = isVerbose; + } + + /// + /// Writes the specified string value. + /// + public void Write(string value) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(value); + } + } + + /// + /// Writes the text representation of the specified argument. + /// + public void Write(string format, object arg0) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, arg0.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, arg0.ToString(), arg1.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments. + /// + public void Write(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + + /// + /// Writes the text representation of the specified array of objects. + /// + /// Text + /// Arguments + public void Write(string format, params object[] args) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, args); + } + } + + /// + /// Writes the specified string value, followed by the + /// current line terminator. + /// + /// Text + public void WriteLine(string value) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(value); + } + } + + /// + /// Writes the text representation of the specified argument, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, arg0.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, arg0.ToString(), arg1.ToString()); + } + } + + /// + /// Writes the text representation of the specified arguments, followed by the + /// current line terminator. + /// + public void WriteLine(string format, object arg0, object arg1, object arg2) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, arg0.ToString(), arg1.ToString(), arg2.ToString()); + } + } + + /// + /// Writes the text representation of the specified array of objects, + /// followed by the current line terminator. + /// + /// Text + /// Arguments + public void WriteLine(string format, params object[] args) + { + if (this.IsVerbose) + { + this.TestOutput.WriteLine(format, args); + } + } + + /// + /// Returns the logged text as a string. + /// + public override string ToString() + { + return this.TestOutput.ToString(); + } + + /// + /// Disposes the logger. + /// + public void Dispose() + { + } + } +} diff --git a/Tests/Tests.Common/Tests.Common.csproj b/Tests/Tests.Common/Tests.Common.csproj new file mode 100644 index 000000000..b370b4c7f --- /dev/null +++ b/Tests/Tests.Common/Tests.Common.csproj @@ -0,0 +1,30 @@ + + + + + The Coyote test helpers. + Microsoft.Coyote.Tests.Common + Microsoft.Coyote.Tests.Common + ..\bin\ + + + netcoreapp2.1;net46;net47 + + + netcoreapp2.1 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tools/Benchmarking/CoyoteBenchmarkRunner/CoyoteBenchmarkRunner.csproj b/Tools/Benchmarking/CoyoteBenchmarkRunner/CoyoteBenchmarkRunner.csproj new file mode 100644 index 000000000..2a8cdd7a9 --- /dev/null +++ b/Tools/Benchmarking/CoyoteBenchmarkRunner/CoyoteBenchmarkRunner.csproj @@ -0,0 +1,23 @@ + + + + + The Coyote benchmark runner. + CoyoteBenchmarkRunner + Microsoft.Coyote.Benchmarking + Exe + ..\..\bin\ + + + netcoreapp2.0;net472 + + + netcoreapp2.0 + + + + + + + + \ No newline at end of file diff --git a/Tools/Benchmarking/CoyoteBenchmarkRunner/Program.cs b/Tools/Benchmarking/CoyoteBenchmarkRunner/Program.cs new file mode 100644 index 000000000..4d2cb6e28 --- /dev/null +++ b/Tools/Benchmarking/CoyoteBenchmarkRunner/Program.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using BenchmarkDotNet.Running; +using Microsoft.Coyote.Benchmarking.Creation; +using Microsoft.Coyote.Benchmarking.Messaging; + +namespace Microsoft.Coyote.Benchmarking +{ + /// + /// The Coyote performance benchmark runner. + /// + internal class Program + { +#pragma warning disable CA1801 // Parameter not used + private static void Main(string[] args) + { + // BenchmarkSwitcher.FromAssembly(typeof(Program).GetTypeInfo().Assembly).Run(args); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + BenchmarkRunner.Run(); + } +#pragma warning restore CA1801 // Parameter not used + } +} diff --git a/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Creation/MachineCreationThroughputBenchmark.cs b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Creation/MachineCreationThroughputBenchmark.cs new file mode 100644 index 000000000..fffb5547e --- /dev/null +++ b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Creation/MachineCreationThroughputBenchmark.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Benchmarking.Creation +{ + [ClrJob(baseline: true), CoreJob] + [MemoryDiagnoser] + [MinColumn, MaxColumn, MeanColumn, Q1Column, Q3Column, RankColumn] + [MarkdownExporter, HtmlExporter, CsvExporter, CsvMeasurementsExporter, RPlotExporter] + public class MachineCreationThroughputBenchmark + { + private class SetupEvent : Event + { + public TaskCompletionSource Tcs; + public int NumMachines; + public int Counter; + public bool DoHalt; + + public SetupEvent(TaskCompletionSource tcs, int numMachines, bool doHalt) + { + this.Tcs = tcs; + this.NumMachines = numMachines; + this.Counter = 0; + this.DoHalt = doHalt; + } + } + + private class M : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupEvent).Tcs; + var numMachines = (this.ReceivedEvent as SetupEvent).NumMachines; + var doHalt = (this.ReceivedEvent as SetupEvent).DoHalt; + + var counter = Interlocked.Increment(ref (this.ReceivedEvent as SetupEvent).Counter); + if (counter == numMachines) + { + tcs.TrySetResult(true); + } + + if (doHalt) + { + this.Raise(new Halt()); + } + } + } + + [Params(10000, 100000)] + public int NumMachines { get; set; } + + [Params(true, false)] + public bool DoHalt { get; set; } + + [Benchmark] + public void MeasureThroughputMachineCreation() + { + var configuration = Configuration.Create(); + var runtime = new ProductionRuntime(configuration); + + var tcs = new TaskCompletionSource(); + var e = new SetupEvent(tcs, this.NumMachines, this.DoHalt); + + for (int idx = 0; idx < this.NumMachines; idx++) + { + runtime.CreateMachine(typeof(M), null, e); + } + + tcs.Task.Wait(); + } + } +} diff --git a/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/DequeueEventThroughputBenchmark.cs b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/DequeueEventThroughputBenchmark.cs new file mode 100644 index 000000000..077255b76 --- /dev/null +++ b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/DequeueEventThroughputBenchmark.cs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Benchmarking.Messaging +{ + [ClrJob(baseline: true), CoreJob] + [MemoryDiagnoser] + [MinColumn, MaxColumn, MeanColumn, Q1Column, Q3Column, RankColumn] + [MarkdownExporter, HtmlExporter, CsvExporter, CsvMeasurementsExporter, RPlotExporter] + public class DequeueEventThroughputBenchmark + { + private class SetupProducerEvent : Event + { + public TaskCompletionSource TcsSetup; + public MachineId Consumer; + public long NumMessages; + + public SetupProducerEvent(TaskCompletionSource tcsSetup, MachineId consumer, long numMessages) + { + this.TcsSetup = tcsSetup; + this.Consumer = consumer; + this.NumMessages = numMessages; + } + } + + private class SetupConsumerEvent : Event + { + public TaskCompletionSource TcsExperiment; + public long NumMessages; + + internal SetupConsumerEvent(TaskCompletionSource tcsExperiment, long numMessages) + { + this.TcsExperiment = tcsExperiment; + this.NumMessages = numMessages; + } + } + + private class StartExperiment : Event + { + } + + private class Message : Event + { + } + + private class Ack : Event + { + } + + private class Producer : Machine + { + private TaskCompletionSource TcsSetup; + private MachineId Consumer; + private long NumMessages; + + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.TcsSetup = (this.ReceivedEvent as SetupProducerEvent).TcsSetup; + this.Consumer = (this.ReceivedEvent as SetupProducerEvent).Consumer; + this.NumMessages = (this.ReceivedEvent as SetupProducerEvent).NumMessages; + + this.TcsSetup.SetResult(true); + this.Goto(); + } + + [OnEventDoAction(typeof(StartExperiment), nameof(Run))] + private class Experiment : MachineState + { + } + + private void Run() + { + for (int i = 0; i < this.NumMessages; i++) + { + this.Send(this.Consumer, new Message()); + } + } + } + + private class Consumer : Machine + { + private TaskCompletionSource TcsExperiment; + private long NumMessages; + private long Counter = 0; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(Message), nameof(HandleMessage))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.TcsExperiment = (this.ReceivedEvent as SetupConsumerEvent).TcsExperiment; + this.NumMessages = (this.ReceivedEvent as SetupConsumerEvent).NumMessages; + } + + private void HandleMessage() + { + this.Counter++; + if (this.Counter == this.NumMessages) + { + this.TcsExperiment.SetResult(true); + } + } + } + + [Params(10, 100, 1000, 10000)] + public int NumProducers { get; set; } + + private static int NumMessages => 1000000; + + private ProductionRuntime Runtime; + private MachineId[] ProducerMachines; + private TaskCompletionSource ExperimentAwaiter; + + [IterationSetup] + public void IterationSetup() + { + var configuration = Configuration.Create(); + this.Runtime = new ProductionRuntime(configuration); + this.ExperimentAwaiter = new TaskCompletionSource(); + + var consumer = this.Runtime.CreateMachine(typeof(Consumer), null, + new SetupConsumerEvent(this.ExperimentAwaiter, NumMessages)); + + var tasks = new Task[this.NumProducers]; + this.ProducerMachines = new MachineId[this.NumProducers]; + for (int i = 0; i < this.NumProducers; i++) + { + var tcs = new TaskCompletionSource(); + this.ProducerMachines[i] = this.Runtime.CreateMachine(typeof(Producer), null, + new SetupProducerEvent(tcs, consumer, NumMessages / this.NumProducers)); + tasks[i] = tcs.Task; + } + + Task.WaitAll(tasks); + } + + [Benchmark] + public void MeasureEventDequeueingThroughput() + { + for (int i = 0; i < this.NumProducers; i++) + { + this.Runtime.SendEvent(this.ProducerMachines[i], new StartExperiment()); + } + + this.ExperimentAwaiter.Task.Wait(); + } + + [IterationCleanup] + public void IterationCleanup() + { + this.Runtime = null; + this.ProducerMachines = null; + this.ExperimentAwaiter = null; + } + } +} diff --git a/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/ExchangeEventLatencyBenchmark.cs b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/ExchangeEventLatencyBenchmark.cs new file mode 100644 index 000000000..b6ebc9286 --- /dev/null +++ b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/ExchangeEventLatencyBenchmark.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Benchmarking.Messaging +{ + [ClrJob(baseline: true), CoreJob] + [MemoryDiagnoser] + [MinColumn, MaxColumn, MeanColumn, Q1Column, Q3Column, RankColumn] + [MarkdownExporter, HtmlExporter, CsvExporter, CsvMeasurementsExporter, RPlotExporter] + public class ExchangeEventLatencyBenchmark + { + private class SetupTcsEvent : Event + { + public TaskCompletionSource Tcs; + public long NumMessages; + + public SetupTcsEvent(TaskCompletionSource tcs, long numMessages) + { + this.Tcs = tcs; + this.NumMessages = numMessages; + } + } + + private class SetupTargetEvent : Event + { + public MachineId Target; + public long NumMessages; + + internal SetupTargetEvent(MachineId target, long numMessages) + { + this.Target = target; + this.NumMessages = numMessages; + } + } + + private class Message : Event + { + } + + private class M1 : Machine + { + private TaskCompletionSource Tcs; + private MachineId Target; + private long NumMessages; + private long Counter = 0; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(Message), nameof(SendMessage))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Tcs = (this.ReceivedEvent as SetupTcsEvent).Tcs; + this.NumMessages = (this.ReceivedEvent as SetupTcsEvent).NumMessages; + this.Target = this.CreateMachine(typeof(M2), new SetupTargetEvent(this.Id, this.NumMessages)); + this.SendMessage(); + } + + private void SendMessage() + { + if (this.Counter == this.NumMessages) + { + this.Tcs.SetResult(true); + } + else + { + this.Counter++; + this.Send(this.Target, new Message()); + } + } + } + + private class M2 : Machine + { + private MachineId Target; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(Message), nameof(SendMessage))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Target = (this.ReceivedEvent as SetupTargetEvent).Target; + } + + private void SendMessage() + { + this.Send(this.Target, new Message()); + } + } + + private class M3 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var tcs = (this.ReceivedEvent as SetupTcsEvent).Tcs; + var numMessages = (this.ReceivedEvent as SetupTcsEvent).NumMessages; + var target = this.CreateMachine(typeof(M4), new SetupTargetEvent(this.Id, numMessages)); + + var counter = 0; + while (counter < numMessages) + { + counter++; + this.Send(target, new Message()); + await this.Receive(typeof(Message)); + } + + tcs.SetResult(true); + } + } + + private class M4 : Machine + { + [Start] + [OnEntry(nameof(InitOnEntry))] + private class Init : MachineState + { + } + + private async Task InitOnEntry() + { + var target = (this.ReceivedEvent as SetupTargetEvent).Target; + var numMessages = (this.ReceivedEvent as SetupTargetEvent).NumMessages; + + var counter = 0; + while (counter < numMessages) + { + counter++; + await this.Receive(typeof(Message)); + this.Send(target, new Message()); + } + } + } + + [Params(10000, 100000)] + public int NumMessages { get; set; } + + [Benchmark] + public void MeasureLatencyExchangeEvent() + { + var tcs = new TaskCompletionSource(); + + var configuration = Configuration.Create(); + var runtime = new ProductionRuntime(configuration); + runtime.CreateMachine(typeof(M1), null, + new SetupTcsEvent(tcs, this.NumMessages)); + + tcs.Task.Wait(); + } + + [Benchmark] + public void MeasureLatencyExchangeEventViaReceive() + { + var tcs = new TaskCompletionSource(); + + var configuration = Configuration.Create(); + var runtime = new ProductionRuntime(configuration); + runtime.CreateMachine(typeof(M3), null, + new SetupTcsEvent(tcs, this.NumMessages)); + + tcs.Task.Wait(); + } + } +} diff --git a/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/SendEventThroughputBenchmark.cs b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/SendEventThroughputBenchmark.cs new file mode 100644 index 000000000..8297bde06 --- /dev/null +++ b/Tools/Benchmarking/CoyoteBenchmarkRunner/Tests/Messaging/SendEventThroughputBenchmark.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Coyote.Machines; +using Microsoft.Coyote.Runtime; + +namespace Microsoft.Coyote.Benchmarking.Messaging +{ + [ClrJob(baseline: true), CoreJob] + [MemoryDiagnoser] + [MinColumn, MaxColumn, MeanColumn, Q1Column, Q3Column, RankColumn] + [MarkdownExporter, HtmlExporter, CsvExporter, CsvMeasurementsExporter, RPlotExporter] + public class SendEventThroughputBenchmark + { + private class SetupProducerEvent : Event + { + public TaskCompletionSource TcsSetup; + public TaskCompletionSource TcsExperiment; + public long NumConsumers; + public long NumMessages; + + public SetupProducerEvent(TaskCompletionSource tcsSetup, TaskCompletionSource tcsExperiment, + long numConsumers, long numMessages) + { + this.TcsSetup = tcsSetup; + this.TcsExperiment = tcsExperiment; + this.NumConsumers = numConsumers; + this.NumMessages = numMessages; + } + } + + private class SetupConsumerEvent : Event + { + public MachineId Producer; + public long NumMessages; + + internal SetupConsumerEvent(MachineId producer, long numMessages) + { + this.Producer = producer; + this.NumMessages = numMessages; + } + } + + private class StartExperiment : Event + { + } + + private class Message : Event + { + } + + private class Ack : Event + { + } + + private class Producer : Machine + { + private TaskCompletionSource TcsSetup; + private TaskCompletionSource TcsExperiment; + private MachineId[] Consumers; + private long NumConsumers; + private long NumMessages; + private long Counter; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(Ack), nameof(HandleCreationAck))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.TcsSetup = (this.ReceivedEvent as SetupProducerEvent).TcsSetup; + this.TcsExperiment = (this.ReceivedEvent as SetupProducerEvent).TcsExperiment; + this.NumConsumers = (this.ReceivedEvent as SetupProducerEvent).NumConsumers; + this.NumMessages = (this.ReceivedEvent as SetupProducerEvent).NumMessages; + + this.Consumers = new MachineId[this.NumConsumers]; + this.Counter = 0; + + for (int i = 0; i < this.NumConsumers; i++) + { + this.Consumers[i] = this.CreateMachine( + typeof(Consumer), + new SetupConsumerEvent(this.Id, this.NumMessages / this.NumConsumers)); + } + } + + private void HandleCreationAck() + { + this.Counter++; + if (this.Counter == this.NumConsumers) + { + this.TcsSetup.SetResult(true); + this.Goto(); + } + } + + [OnEventDoAction(typeof(StartExperiment), nameof(Run))] + [OnEventDoAction(typeof(Ack), nameof(HandleMessageAck))] + private class Experiment : MachineState + { + } + + private void Run() + { + this.Counter = 0; + for (int i = 0; i < this.NumMessages; i++) + { + this.Send(this.Consumers[i % this.NumConsumers], new Message()); + } + } + + private void HandleMessageAck() + { + this.Counter++; + if (this.Counter == this.NumConsumers) + { + this.TcsExperiment.SetResult(true); + } + } + } + + private class Consumer : Machine + { + private MachineId Producer; + private long NumMessages; + private long Counter = 0; + + [Start] + [OnEntry(nameof(InitOnEntry))] + [OnEventDoAction(typeof(Message), nameof(HandleMessage))] + private class Init : MachineState + { + } + + private void InitOnEntry() + { + this.Producer = (this.ReceivedEvent as SetupConsumerEvent).Producer; + this.NumMessages = (this.ReceivedEvent as SetupConsumerEvent).NumMessages; + this.Send(this.Producer, new Ack()); + } + + private void HandleMessage() + { + this.Counter++; + if (this.Counter == this.NumMessages) + { + this.Send(this.Producer, new Ack()); + } + } + } + + [Params(10, 100, 1000, 10000)] + public int NumConsumers { get; set; } + + private static int NumMessages => 1000000; + + private ProductionRuntime Runtime; + private MachineId ProducerMachine; + private TaskCompletionSource ExperimentAwaiter; + + [IterationSetup] + public void IterationSetup() + { + var configuration = Configuration.Create(); + this.Runtime = new ProductionRuntime(configuration); + this.ExperimentAwaiter = new TaskCompletionSource(); + + var tcs = new TaskCompletionSource(); + this.ProducerMachine = this.Runtime.CreateMachine(typeof(Producer), null, + new SetupProducerEvent(tcs, this.ExperimentAwaiter, this.NumConsumers, NumMessages)); + + tcs.Task.Wait(); + } + + [Benchmark] + public void MeasureEventSendingThroughput() + { + this.Runtime.SendEvent(this.ProducerMachine, new StartExperiment()); + this.ExperimentAwaiter.Task.Wait(); + } + + [IterationCleanup] + public void IterationCleanup() + { + this.Runtime = null; + this.ProducerMachine = null; + this.ExperimentAwaiter = null; + } + } +} diff --git a/Tools/Testing/CoverageReportMerger/CoverageReportMerger.csproj b/Tools/Testing/CoverageReportMerger/CoverageReportMerger.csproj new file mode 100644 index 000000000..39c5bace1 --- /dev/null +++ b/Tools/Testing/CoverageReportMerger/CoverageReportMerger.csproj @@ -0,0 +1,21 @@ + + + + + The Coyote coverage report merger. + CoyoteCoverageReportMerger + CoyoteCoverageReportMerger + coverage;merger;coyote + Exe + ..\..\..\bin\ + + + netcoreapp2.1;net46;net47 + + + netcoreapp2.1 + + + + + \ No newline at end of file diff --git a/Tools/Testing/CoverageReportMerger/Program.cs b/Tools/Testing/CoverageReportMerger/Program.cs new file mode 100644 index 000000000..095e46f6a --- /dev/null +++ b/Tools/Testing/CoverageReportMerger/Program.cs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization; +using System.Xml; +using Microsoft.Coyote.TestingServices.Coverage; + +namespace Microsoft.Coyote +{ + /// + /// The coverage report merger. + /// + internal class Program + { + /// + /// Input coverage info. + /// + private static List InputFiles; + + /// + /// Output file prefix. + /// + private static string OutputFilePrefix; + + private static void Main(string[] args) + { + if (!ParseArgs(args)) + { + return; + } + + if (InputFiles.Count == 0) + { + Console.WriteLine("Error: No input files provided"); + return; + } + + var cinfo = new CoverageInfo(); + foreach (var other in InputFiles) + { + cinfo.Merge(other); + } + + // Dump + string name = OutputFilePrefix; + string directoryPath = Environment.CurrentDirectory; + + var activityCoverageReporter = new ActivityCoverageReporter(cinfo); + + string[] graphFiles = Directory.GetFiles(directoryPath, name + "_*.dgml"); + string graphFilePath = Path.Combine(directoryPath, name + "_" + graphFiles.Length + ".dgml"); + + Console.WriteLine($"... Writing {graphFilePath}"); + activityCoverageReporter.EmitVisualizationGraph(graphFilePath); + + string[] coverageFiles = Directory.GetFiles(directoryPath, name + "_*.coverage.txt"); + string coverageFilePath = Path.Combine(directoryPath, name + "_" + coverageFiles.Length + ".coverage.txt"); + + Console.WriteLine($"... Writing {coverageFilePath}"); + activityCoverageReporter.EmitCoverageReport(coverageFilePath); + } + + /// + /// Parses the arguments. + /// + private static bool ParseArgs(string[] args) + { + InputFiles = new List(); + OutputFilePrefix = "merged"; + + if (args.Length == 0) + { + Console.WriteLine("Usage: CoyoteMergeCoverageReports.exe file1.sci file2.sci ... [/output:prefix]"); + return false; + } + + foreach (var arg in args) + { + if (arg.StartsWith("/output:")) + { + OutputFilePrefix = arg.Substring("/output:".Length); + continue; + } + else if (arg.StartsWith("/")) + { + Console.WriteLine("Error: Unknown flag {0}", arg); + return false; + } + else + { + // Check the suffix. + if (!arg.EndsWith(".sci")) + { + Console.WriteLine("Error: Only sci files accepted as input, got {0}", arg); + return false; + } + + // Check if the file exists? + if (!File.Exists(arg)) + { + Console.WriteLine("Error: File {0} not found", arg); + return false; + } + + try + { + using (var fs = new FileStream(arg, FileMode.Open)) + { + using (var reader = XmlDictionaryReader.CreateTextReader(fs, new XmlDictionaryReaderQuotas())) + { + var ser = new DataContractSerializer(typeof(CoverageInfo)); + var cinfo = (CoverageInfo)ser.ReadObject(reader, true); + InputFiles.Add(cinfo); + } + } + } + catch (Exception e) + { + Console.WriteLine("Error: got exception while trying to read input objects: {0}", e.Message); + return false; + } + } + } + + return true; + } + } +} diff --git a/Tools/Testing/Replayer/Program.cs b/Tools/Testing/Replayer/Program.cs new file mode 100644 index 000000000..62591fc2a --- /dev/null +++ b/Tools/Testing/Replayer/Program.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote +{ + /// + /// The Coyote trace replayer. + /// + internal class Program + { + private static void Main(string[] args) + { + AppDomain currentDomain = AppDomain.CurrentDomain; + currentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionHandler); + + // Parses the command line options to get the configuration. + var configuration = new ReplayerCommandLineOptions(args).Parse(); + + // Creates and starts a replaying process. + ReplayingProcess.Create(configuration).Start(); + + Console.WriteLine(". Done"); + } + + /// + /// Handler for unhandled exceptions. + /// + private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args) + { + var ex = (Exception)args.ExceptionObject; + Error.Report("[CoyoteReplayer] internal failure: {0}: {1}", ex.GetType().ToString(), ex.Message); + Console.WriteLine(ex.StackTrace); + Environment.Exit(1); + } + } +} diff --git a/Tools/Testing/Replayer/Replayer.csproj b/Tools/Testing/Replayer/Replayer.csproj new file mode 100644 index 000000000..e10f415db --- /dev/null +++ b/Tools/Testing/Replayer/Replayer.csproj @@ -0,0 +1,21 @@ + + + + + The Coyote bug-trace replayer. + CoyoteReplayer + CoyoteReplayer + bug;trace;replay;coyote + Exe + ..\..\..\bin\ + + + netcoreapp2.1;net46;net47 + + + netcoreapp2.1 + + + + + \ No newline at end of file diff --git a/Tools/Testing/Replayer/ReplayingProcess.cs b/Tools/Testing/Replayer/ReplayingProcess.cs new file mode 100644 index 000000000..936487763 --- /dev/null +++ b/Tools/Testing/Replayer/ReplayingProcess.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.TestingServices; + +namespace Microsoft.Coyote +{ + /// + /// A replaying process. + /// + internal sealed class ReplayingProcess + { + /// + /// Configuration. + /// + private readonly Configuration Configuration; + + /// + /// Creates a Coyote replaying process. + /// + public static ReplayingProcess Create(Configuration configuration) + { + return new ReplayingProcess(configuration); + } + + /// + /// Starts the Coyote replaying process. + /// + public void Start() + { + Console.WriteLine(". Reproducing trace in " + this.Configuration.AssemblyToBeAnalyzed); + + // Creates a new replay engine to reproduce a bug. + ITestingEngine engine = TestingEngineFactory.CreateReplayEngine(this.Configuration); + + engine.Run(); + Console.WriteLine(engine.GetReport()); + } + + /// + /// Initializes a new instance of the class. + /// + private ReplayingProcess(Configuration configuration) + { + configuration.EnableColoredConsoleOutput = true; + configuration.DisableEnvironmentExit = false; + this.Configuration = configuration; + } + } +} diff --git a/Tools/Testing/Replayer/Utilities/ReplayerCommandLineOptions.cs b/Tools/Testing/Replayer/Utilities/ReplayerCommandLineOptions.cs new file mode 100644 index 000000000..94ad0c7c6 --- /dev/null +++ b/Tools/Testing/Replayer/Utilities/ReplayerCommandLineOptions.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.Utilities +{ + public sealed class ReplayerCommandLineOptions : BaseCommandLineOptions + { + /// + /// Initializes a new instance of the class. + /// + public ReplayerCommandLineOptions(string[] args) + : base(args) + { + } + + /// + /// Parses the given option. + /// + protected override void ParseOption(string option) + { + if (IsMatch(option, @"^[\/|-]test:") && option.Length > 6) + { + this.Configuration.AssemblyToBeAnalyzed = option.Substring(6); + } + else if (IsMatch(option, @"^[\/|-]runtime:") && option.Length > 9) + { + this.Configuration.TestingRuntimeAssembly = option.Substring(9); + } + else if (IsMatch(option, @"^[\/|-]method:") && option.Length > 8) + { + this.Configuration.TestMethodName = option.Substring(8); + } + else if (IsMatch(option, @"^[\/|-]replay:") && option.Length > 8) + { + string extension = System.IO.Path.GetExtension(option.Substring(8)); + if (!extension.Equals(".schedule")) + { + Error.ReportAndExit("Please give a valid schedule file " + + "'-replay:[x]', where [x] has extension '.schedule'."); + } + + this.Configuration.ScheduleFile = option.Substring(8); + } + else if (IsMatch(option, @"^[\/|-]break$")) + { + this.Configuration.AttachDebugger = true; + } + else if (IsMatch(option, @"^[\/|-]attach-debugger$")) + { + this.Configuration.AttachDebugger = true; + } + else if (IsMatch(option, @"^[\/|-]cycle-detection$")) + { + this.Configuration.EnableCycleDetection = true; + } + else if (IsMatch(option, @"^[\/|-]custom-state-hashing$")) + { + this.Configuration.EnableUserDefinedStateHashing = true; + } + else + { + base.ParseOption(option); + } + } + + /// + /// Checks for parsing errors. + /// + protected override void CheckForParsingErrors() + { + if (string.IsNullOrEmpty(this.Configuration.AssemblyToBeAnalyzed)) + { + Error.ReportAndExit("Please give a valid path to a Coyote " + + "program's dll using '-test:[x]'."); + } + + if (string.IsNullOrEmpty(this.Configuration.ScheduleFile)) + { + Error.ReportAndExit("Please give a valid path to a Coyote schedule " + + "file using '-replay:[x]', where [x] has extension '.schedule'."); + } + } + + /// + /// Updates the configuration depending on the user specified options. + /// + protected override void UpdateConfiguration() + { + } + + /// + /// Shows help. + /// + protected override void ShowHelp() + { + string help = "\n"; + + help += " --------------"; + help += "\n Basic options:"; + help += "\n --------------"; + help += "\n -?\t\t Show this help menu"; + help += "\n -test:[x]\t Path to the Coyote program to test"; + help += "\n -timeout:[x]\t Timeout (default is no timeout)"; + help += "\n -v:[x]\t Enable verbose mode (values from '1' to '3')"; + + help += "\n\n ------------------"; + help += "\n Replaying options:"; + help += "\n ------------------"; + help += "\n -replay:[x]\t Schedule to replay"; + help += "\n -break:[x]\t Attach debugger and break at bug"; + + help += "\n"; + + Console.WriteLine(help); + } + } +} diff --git a/Tools/Testing/Tester/App.config b/Tools/Testing/Tester/App.config new file mode 100644 index 000000000..410628008 --- /dev/null +++ b/Tools/Testing/Tester/App.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Tools/Testing/Tester/Instrumentation/CodeCoverageInstrumentation.cs b/Tools/Testing/Tester/Instrumentation/CodeCoverageInstrumentation.cs new file mode 100644 index 000000000..83ff2d7eb --- /dev/null +++ b/Tools/Testing/Tester/Instrumentation/CodeCoverageInstrumentation.cs @@ -0,0 +1,286 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#if NET46 +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Diagnostics; +#endif +using System.IO; +#if NET46 +using System.Linq; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices.Utilities; +#endif + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Instruments a binary for code coverage. + /// + internal static class CodeCoverageInstrumentation + { + internal static string OutputDirectory = string.Empty; +#if NET46 + internal static List InstrumentedAssemblyNames = new List(); + + internal static void Instrument(Configuration configuration) + { + // HashSet in case of duplicate file specifications. + var assemblyNames = new HashSet(DependencyGraph.GetDependenciesToCoyote(configuration) + .Union(GetAdditionalAssemblies(configuration))); + InstrumentedAssemblyNames.Clear(); + + foreach (var assemblyName in assemblyNames) + { + if (!Instrument(assemblyName)) + { + Restore(); + Environment.Exit(1); + } + + InstrumentedAssemblyNames.Add(assemblyName); + } + } + + private static IEnumerable GetAdditionalAssemblies(Configuration configuration) + { + var testAssemblyPath = Path.GetDirectoryName(configuration.AssemblyToBeAnalyzed); + if (testAssemblyPath.Length == 0) + { + testAssemblyPath = "."; + } + + IEnumerable resolveFileSpec(string spec) + { + // If not rooted, the file path is relative to testAssemblyPath. + var gdn = Path.GetDirectoryName(spec); + var dir = Path.IsPathRooted(gdn) ? gdn : Path.Combine(testAssemblyPath, gdn); + var fullDir = Path.GetFullPath(dir); + var fileSpec = Path.GetFileName(spec); + var fullNames = Directory.GetFiles(fullDir, fileSpec); + foreach (var fullName in fullNames) + { + if (!File.Exists(fullName)) + { + Error.ReportAndExit($"Cannot find specified file for code-coverage instrumentation: '{fullName}'."); + } + + yield return fullName; + } + } + + IEnumerable resolveAdditionalFiles(KeyValuePair kvp) + { + if (!kvp.Value) + { + foreach (var file in resolveFileSpec(kvp.Key)) + { + yield return file; + } + + yield break; + } + + var dir = Path.GetDirectoryName(kvp.Key); + var fullDir = Path.GetFullPath(dir.Length > 0 ? dir : testAssemblyPath); + var listFile = Path.Combine(fullDir, Path.GetFileName(kvp.Key)); + if (!File.Exists(listFile)) + { + Error.ReportAndExit($"Cannot find specified list file for code-coverage instrumentation: '{kvp.Key}'."); + } + + foreach (var spec in File.ReadAllLines(listFile).Where(line => line.Length > 0).Select(line => line.Trim()) + .Where(line => !line.StartsWith("//"))) + { + foreach (var file in resolveFileSpec(spec)) + { + yield return file; + } + } + } + + // Note: Resolution has been deferred to here so that all empty path qualifiations, including to the list + // file, will resolve to testAssemblyPath (as config coverage parameters may be specified before /test). + // Return .ToList() to force iteration and return errors before we start instrumenting. + return configuration.AdditionalCodeCoverageAssemblies.SelectMany(kvp => resolveAdditionalFiles(kvp)).ToList(); + } + + private static bool Instrument(string assemblyName) + { + int exitCode; + string error; + Console.WriteLine($"Instrumenting {assemblyName}"); + + using (var instrProc = new Process()) + { + instrProc.StartInfo.FileName = GetToolPath("VSInstrToolPath", "VSInstr"); + instrProc.StartInfo.Arguments = $"/coverage {assemblyName}"; + instrProc.StartInfo.UseShellExecute = false; + instrProc.StartInfo.RedirectStandardOutput = true; + instrProc.StartInfo.RedirectStandardError = true; + instrProc.Start(); + + error = instrProc.StandardError.ReadToEnd(); + + instrProc.WaitForExit(); + exitCode = instrProc.ExitCode; + } + + // Exit code 0 means that the file was instrumented successfully. + // Exit code 4 means that the file was already instrumented. + if (exitCode != 0 && exitCode != 4) + { + Error.Report($"[CoyoteTester] 'VSInstr' failed to instrument '{assemblyName}'."); + IO.Debug.WriteLine(error); + return false; + } + + return true; + } + + internal static void Restore() + { + try + { + foreach (var assemblyName in InstrumentedAssemblyNames) + { + Restore(assemblyName); + } + } + finally + { + OutputDirectory = string.Empty; + InstrumentedAssemblyNames.Clear(); + } + } + + internal static void Restore(string assemblyName) + { + // VSInstr creates a backup of the uninstrumented .exe with the suffix ".exe.orig", and + // writes an instrumented .pdb with the suffix ".instr.pdb". We must restore the uninstrumented + // .exe after the coverage run, and viewing the coverage file requires the instrumented .exe, + // so move the instrumented files to the output directory and restore the uninstrumented .exe. + var origExe = $"{assemblyName}.orig"; + var origDir = Path.GetDirectoryName(assemblyName); + if (origDir.Length == 0) + { + origDir = "."; + } + + origDir += Path.DirectorySeparatorChar; + var instrExe = $"{OutputDirectory}{Path.GetFileName(assemblyName)}"; + var instrPdb = $"{Path.GetFileNameWithoutExtension(assemblyName)}.instr.pdb"; + try + { + if (!string.IsNullOrEmpty(OutputDirectory) && File.Exists(origExe)) + { + if (TestingProcessScheduler.ProcessCanceled) + { + File.Delete(assemblyName); + File.Delete(instrPdb); + Directory.Delete(OutputDirectory, true); + } + else + { + File.Move(assemblyName, instrExe); + File.Move($"{origDir}{instrPdb}", $"{OutputDirectory}{instrPdb}"); + } + + File.Move(origExe, assemblyName); + } + } + catch (IOException ex) + { + // Don't exit here as we're already shutting down the app, and we may have more assemblies to restore. + Error.Report($"[CoyoteTester] Failed to restore non-instrumented '{assemblyName}': {ex.Message}."); + } + } + + /// + /// Returns the tool path to the code coverage instrumentor. + /// + /// The name of the setting; also used to query the environment variables. + /// The name of the tool; used in messages only. + internal static string GetToolPath(string settingName, string toolName) + { + string toolPath = string.Empty; + try + { + toolPath = Environment.GetEnvironmentVariable(settingName); + if (string.IsNullOrEmpty(toolPath)) + { + toolPath = ConfigurationManager.AppSettings[settingName]; + } + else + { + Console.WriteLine($"{toolName} overriding app settings path with environment variable"); + } + } + catch (ConfigurationErrorsException) + { + Error.ReportAndExit($"[CoyoteTester] required '{settingName}' value is not set in configuration file."); + } + + if (!File.Exists(toolPath)) + { + Error.ReportAndExit($"[CoyoteTester] '{toolName}' tool '{toolPath}' not found."); + } + + return toolPath; + } +#endif + + /// + /// Set the to either the user-specified + /// or to a unique output directory name in the same directory as + /// and starting with its name. + /// + internal static void SetOutputDirectory(Configuration configuration, bool makeHistory) + { + if (OutputDirectory.Length > 0) + { + return; + } + + // Do not create the output directory yet if we have to scroll back the history first. + OutputDirectory = Reporter.GetOutputDirectory(configuration.OutputFilePath, configuration.AssemblyToBeAnalyzed, + "CoyoteTesterOutput", createDir: !makeHistory); + if (!makeHistory) + { + return; + } + + // The MaxHistory previous results are kept under the directory name with a suffix scrolling back from 0 to 9 (oldest). + const int MaxHistory = 10; + string makeHistoryDirName(int history) => OutputDirectory.Substring(0, OutputDirectory.Length - 1) + history; + var older = makeHistoryDirName(MaxHistory - 1); + + if (Directory.Exists(older)) + { + Directory.Delete(older, true); + } + + for (var history = MaxHistory - 2; history >= 0; --history) + { + var newer = makeHistoryDirName(history); + if (Directory.Exists(newer)) + { + Directory.Move(newer, older); + } + + older = newer; + } + + if (Directory.Exists(OutputDirectory)) + { + Directory.Move(OutputDirectory, older); + } + + // Now create the new directory. + Directory.CreateDirectory(OutputDirectory); + } + } +} diff --git a/Tools/Testing/Tester/Interfaces/ITestingProcess.cs b/Tools/Testing/Tester/Interfaces/ITestingProcess.cs new file mode 100644 index 000000000..30797c92e --- /dev/null +++ b/Tools/Testing/Tester/Interfaces/ITestingProcess.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------------------------------------------ + +#if NET46 +using System.ServiceModel; + +using Microsoft.Coyote.TestingServices.Coverage; +#endif + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Interface for a remote testing process. + /// +#if NET46 + [ServiceContract(Namespace = "Microsoft.Coyote")] + [ServiceKnownType(typeof(TestReport))] + [ServiceKnownType(typeof(CoverageInfo))] + [ServiceKnownType(typeof(Transition))] +#endif + internal interface ITestingProcess + { + /// + /// Returns the test report. + /// +#if NET46 + [OperationContract] +#endif + TestReport GetTestReport(); + + /// + /// Stops testing. + /// +#if NET46 + [OperationContract] +#endif + void Stop(); + } +} diff --git a/Tools/Testing/Tester/Interfaces/ITestingProcessScheduler.cs b/Tools/Testing/Tester/Interfaces/ITestingProcessScheduler.cs new file mode 100644 index 000000000..4d86c6b3d --- /dev/null +++ b/Tools/Testing/Tester/Interfaces/ITestingProcessScheduler.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See License.txt in the repo root for license information. +// ------------------------------------------------------------------------------------------------ + +#if NET46 +using System.ServiceModel; + +using Microsoft.Coyote.TestingServices.Coverage; +#endif + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Interface for a remote testing process scheduler. + /// +#if NET46 + [ServiceContract(Namespace = "Microsoft.Coyote")] + [ServiceKnownType(typeof(TestReport))] + [ServiceKnownType(typeof(CoverageInfo))] + [ServiceKnownType(typeof(Transition))] +#endif + internal interface ITestingProcessScheduler + { + /// + /// Notifies the testing process scheduler + /// that a bug was found. + /// +#if NET46 + [OperationContract] +#endif + void NotifyBugFound(uint processId); + + /// + /// Sets the test report from the specified process. + /// +#if NET46 + [OperationContract] +#endif + void SetTestReport(TestReport testReport, uint processId); + } +} diff --git a/Tools/Testing/Tester/Monitoring/CodeCoverageMonitor.cs b/Tools/Testing/Tester/Monitoring/CodeCoverageMonitor.cs new file mode 100644 index 000000000..52d05aabb --- /dev/null +++ b/Tools/Testing/Tester/Monitoring/CodeCoverageMonitor.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#if NET46 +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; + +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// Monitors the program being tested for code coverage. + /// + internal static class CodeCoverageMonitor + { + /// + /// Configuration. + /// + private static Configuration Configuration; + + /// + /// Monitoring process is running. + /// + internal static bool IsRunning; + + /// + /// Starts the code coverage monitor. + /// + /// Configuration + internal static void Start(Configuration configuration) + { + if (IsRunning) + { + throw new InvalidOperationException("Process has already started."); + } + + Configuration = configuration; + RunMonitorProcess(true); + IsRunning = true; + } + + /// + /// Stops the code coverage monitor. + /// + internal static void Stop() + { + if (Configuration is null) + { + throw new InvalidOperationException("Process has not been configured."); + } + + if (!IsRunning) + { + throw new InvalidOperationException("Process is not running."); + } + + RunMonitorProcess(false); + IsRunning = false; + } + + private static void RunMonitorProcess(bool isStarting) + { + var error = string.Empty; + var exitCode = 0; + var outputFile = GetOutputName(); + var arguments = isStarting ? $"/start:coverage /output:{outputFile}" : "/shutdown"; + var timedOut = false; + using (var monitorProc = new Process()) + { + monitorProc.StartInfo.FileName = CodeCoverageInstrumentation.GetToolPath("VSPerfCmdToolPath", "VSPerfCmd"); + monitorProc.StartInfo.Arguments = arguments; + monitorProc.StartInfo.UseShellExecute = false; + monitorProc.StartInfo.RedirectStandardOutput = true; + monitorProc.StartInfo.RedirectStandardError = true; + monitorProc.Start(); + + Console.WriteLine($"... {(isStarting ? "Starting" : "Shutting down")} code coverage monitor"); + + // timedOut can only become true on shutdown (non-infinite timeout value) + timedOut = !monitorProc.WaitForExit(isStarting ? Timeout.Infinite : 5000); + if (!timedOut) + { + exitCode = monitorProc.ExitCode; + if (exitCode != 0) + { + error = monitorProc.StandardError.ReadToEnd(); + } + } + } + + if (exitCode != 0 || error.Length > 0) + { + if (error.Length == 0) + { + error = ""; + } + + Console.WriteLine($"Warning: 'VSPerfCmd {arguments}' exit code {exitCode}: {error}"); + } + + if (!isStarting) + { + if (timedOut) + { + Console.WriteLine($"Warning: VsPerfCmd timed out on shutdown"); + } + + if (File.Exists(outputFile)) + { + var fileInfo = new FileInfo(outputFile); + Console.WriteLine($"..... Created {outputFile}"); + } + else + { + Console.WriteLine($"Warning: Code coverage output file {outputFile} was not created"); + } + } + } + + /// + /// Returns the output name. + /// + private static string GetOutputName() + { + string file = Path.GetFileNameWithoutExtension(Configuration.AssemblyToBeAnalyzed); + string directory = CodeCoverageInstrumentation.OutputDirectory; + return $"{directory}{file}.coverage"; + } + } +} +#endif diff --git a/Tools/Testing/Tester/Program.cs b/Tools/Testing/Tester/Program.cs new file mode 100644 index 000000000..3f6043c75 --- /dev/null +++ b/Tools/Testing/Tester/Program.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.TestingServices; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote +{ + /// + /// The Coyote tester. + /// + internal class Program + { + private static Configuration configuration; + + private static void Main(string[] args) + { + AppDomain currentDomain = AppDomain.CurrentDomain; + currentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionHandler); + + // Parses the command line options to get the configuration. + configuration = new TesterCommandLineOptions(args).Parse(); + Console.CancelKeyPress += (sender, eventArgs) => CancelProcess(); + +#if NET46 + if (configuration.RunAsParallelBugFindingTask) + { + // Creates and runs a testing process. + TestingProcess testingProcess = TestingProcess.Create(configuration); + testingProcess.Run(); + return; + } + + if (configuration.ReportCodeCoverage || configuration.ReportActivityCoverage) + { + // This has to be here because both forms of coverage require it. + CodeCoverageInstrumentation.SetOutputDirectory(configuration, makeHistory: true); + } + + if (configuration.ReportCodeCoverage) + { + // Instruments the program under test for code coverage. + CodeCoverageInstrumentation.Instrument(configuration); + + // Starts monitoring for code coverage. + CodeCoverageMonitor.Start(configuration); + } +#endif + + Console.WriteLine(". Testing " + configuration.AssemblyToBeAnalyzed); + if (!string.IsNullOrEmpty(configuration.TestMethodName)) + { + Console.WriteLine("... Method {0}", configuration.TestMethodName); + } + + // Creates and runs the testing process scheduler. + TestingProcessScheduler.Create(configuration).Run(); + Shutdown(); + + Console.WriteLine(". Done"); + } + + /// + /// Shutdowns any active monitors. + /// + private static void Shutdown() + { +#if NET46 + if (configuration != null && configuration.ReportCodeCoverage && CodeCoverageMonitor.IsRunning) + { + // Stops monitoring for code coverage. + CodeCoverageMonitor.Stop(); + CodeCoverageInstrumentation.Restore(); + } +#endif + } + + /// + /// Cancels the testing process. + /// + private static void CancelProcess() + { + if (TestingProcessScheduler.ProcessCanceled) + { + return; + } + + TestingProcessScheduler.ProcessCanceled = true; + +#if NET46 + var monitorMessage = CodeCoverageMonitor.IsRunning ? " Shutting down the code coverage monitor (this may take a few seconds)..." : string.Empty; +#else + var monitorMessage = string.Empty; +#endif + Console.WriteLine($". Process canceled by user.{monitorMessage}"); + Shutdown(); + } + + /// + /// Handler for unhandled exceptions. + /// + private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args) + { + var ex = (Exception)args.ExceptionObject; + Error.Report("[CoyoteTester] internal failure: {0}: {1}", ex.GetType().ToString(), ex.Message); + Console.WriteLine(ex.StackTrace); + Shutdown(); + Environment.Exit(1); + } + } +} diff --git a/Tools/Testing/Tester/Scheduling/TestingProcessScheduler.cs b/Tools/Testing/Tester/Scheduling/TestingProcessScheduler.cs new file mode 100644 index 000000000..82b040f95 --- /dev/null +++ b/Tools/Testing/Tester/Scheduling/TestingProcessScheduler.cs @@ -0,0 +1,489 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +#if NET46 +using System.ServiceModel; +using System.ServiceModel.Description; +#endif + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The Coyote testing process scheduler. + /// +#if NET46 + [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] +#endif + internal sealed class TestingProcessScheduler +#if NET46 + : ITestingProcessScheduler +#endif + { + /// + /// Configuration. + /// + private readonly Configuration Configuration; + +#if NET46 + /// + /// The notification listening service. + /// + private ServiceHost NotificationService; + + /// + /// Map from testing process ids to testing processes. + /// + private readonly Dictionary TestingProcesses; +#endif + + /// + /// Map from testing process ids to testing process channels. + /// + private readonly Dictionary TestingProcessChannels; + + /// + /// The test reports per process. + /// + private readonly ConcurrentDictionary TestReports; + + /// + /// The global test report, which contains merged information + /// from the test report of each testing process. + /// + private readonly TestReport GlobalTestReport; + + /// + /// The testing profiler. + /// + private readonly Profiler Profiler; + + /// + /// The scheduler lock. + /// + private readonly object SchedulerLock; + +#if NET46 + /// + /// The process id of the process that + /// discovered a bug, else null. + /// + private uint? BugFoundByProcess; +#endif + + /// + /// Set if ctrl-c or ctrl-break occurred. + /// + internal static bool ProcessCanceled; + +#if NET46 + /// + /// Set true if we have multiple parallel processes or are running code coverage. + /// + private readonly bool RunOutOfProcess; +#endif + + /// + /// Initializes a new instance of the class. + /// + private TestingProcessScheduler(Configuration configuration) + { +#if NET46 + this.TestingProcesses = new Dictionary(); +#endif + this.TestingProcessChannels = new Dictionary(); + this.TestReports = new ConcurrentDictionary(); + this.GlobalTestReport = new TestReport(configuration); + this.Profiler = new Profiler(); + this.SchedulerLock = new object(); +#if NET46 + this.BugFoundByProcess = null; + + // Code coverage should be run out-of-process; otherwise VSPerfMon won't shutdown correctly + // because an instrumented process (this one) is still running. + this.RunOutOfProcess = configuration.ParallelBugFindingTasks > 1 || configuration.ReportCodeCoverage; + + if (configuration.ParallelBugFindingTasks > 1) + { + configuration.IsVerbose = false; + } +#endif + + configuration.EnableColoredConsoleOutput = true; + + this.Configuration = configuration; + } + +#if NET46 + /// + /// Notifies the testing process scheduler that a bug was found. + /// + void ITestingProcessScheduler.NotifyBugFound(uint processId) + { + lock (this.SchedulerLock) + { + if (!this.Configuration.PerformFullExploration && this.BugFoundByProcess is null) + { + Console.WriteLine($"... Task {processId} found a bug."); + this.BugFoundByProcess = processId; + + foreach (var testingProcess in this.TestingProcesses) + { + if (testingProcess.Key != processId) + { + this.StopTestingProcess(testingProcess.Key); + + TestReport testReport = this.GetTestReport(testingProcess.Key); + if (testReport != null) + { + this.MergeTestReport(testReport, testingProcess.Key); + } + + try + { + this.TestingProcesses[testingProcess.Key].Kill(); + this.TestingProcesses[testingProcess.Key].Dispose(); + } + catch (InvalidOperationException) + { + IO.Debug.WriteLine("... Unable to terminate testing task " + + $"'{testingProcess.Key}'. Task has already terminated."); + } + } + } + } + } + } + + /// + /// Sets the test report from the specified process. + /// + void ITestingProcessScheduler.SetTestReport(TestReport testReport, uint processId) + { + lock (this.SchedulerLock) + { + this.MergeTestReport(testReport, processId); + } + } +#endif + + /// + /// Creates a new testing process scheduler. + /// + internal static TestingProcessScheduler Create(Configuration configuration) + { + return new TestingProcessScheduler(configuration); + } + + /// + /// Runs the Coyote testing scheduler. + /// + internal void Run() + { +#if NET46 + // Opens the remote notification listener. + this.OpenNotificationListener(); +#endif + + this.Profiler.StartMeasuringExecutionTime(); + +#if NET46 + if (this.RunOutOfProcess) + { + this.CreateParallelTestingProcesses(); + this.RunParallelTestingProcesses(); + } + else + { + this.CreateAndRunInMemoryTestingProcess(); + } +#else + this.CreateAndRunInMemoryTestingProcess(); +#endif + + this.Profiler.StopMeasuringExecutionTime(); + +#if NET46 + // Closes the remote notification listener. + this.CloseNotificationListener(); +#endif + + if (!ProcessCanceled) + { + // Merges and emits the test report. + this.EmitTestReport(); + } + } + +#if NET46 + /// + /// Creates the user specified number of parallel testing processes. + /// + private void CreateParallelTestingProcesses() + { + for (uint testId = 0; testId < this.Configuration.ParallelBugFindingTasks; testId++) + { + this.TestingProcesses.Add(testId, TestingProcessFactory.Create(testId, this.Configuration)); + this.TestingProcessChannels.Add(testId, this.CreateTestingProcessChannel(testId)); + } + + Console.WriteLine($"... Created '{this.Configuration.ParallelBugFindingTasks}' " + + "testing tasks."); + } + + /// + /// Runs the parallel testing processes. + /// + private void RunParallelTestingProcesses() + { + // Starts the testing processes. + for (uint testId = 0; testId < this.Configuration.ParallelBugFindingTasks; testId++) + { + this.TestingProcesses[testId].Start(); + } + + // Waits the testing processes to exit. + for (uint testId = 0; testId < this.Configuration.ParallelBugFindingTasks; testId++) + { + try + { + this.TestingProcesses[testId].WaitForExit(); + } + catch (InvalidOperationException) + { + IO.Debug.WriteLine($"... Unable to wait for testing task '{testId}' to " + + "terminate. Task has already terminated."); + } + } + } +#endif + + /// + /// Creates and runs an in-memory testing process. + /// + private void CreateAndRunInMemoryTestingProcess() + { + TestingProcess testingProcess = TestingProcess.Create(this.Configuration); + this.TestingProcessChannels.Add(0, testingProcess); + + Console.WriteLine($"... Created '1' testing task."); + + // Runs the testing process. + testingProcess.Run(); + + // Get and merge the test report. + TestReport testReport = this.GetTestReport(0); + if (testReport != null) + { + this.MergeTestReport(testReport, 0); + } + } + +#if NET46 + /// + /// Opens the remote notification listener. If there are + /// less than two parallel testing processes, then this + /// operation does nothing. + /// + private void OpenNotificationListener() + { + if (!this.RunOutOfProcess) + { + return; + } + + Uri address = new Uri("net.pipe://localhost/coyote/testing/scheduler/" + + $"{this.Configuration.TestingSchedulerEndPoint}"); + + NetNamedPipeBinding binding = new NetNamedPipeBinding(); + binding.MaxReceivedMessageSize = int.MaxValue; + + this.NotificationService = new ServiceHost(this); + this.NotificationService.AddServiceEndpoint(typeof(ITestingProcessScheduler), binding, address); + + ServiceDebugBehavior debug = this.NotificationService.Description.Behaviors.Find(); + debug.IncludeExceptionDetailInFaults = true; + + try + { + this.NotificationService.Open(); + } + catch (AddressAccessDeniedException) + { + Error.ReportAndExit("Your process does not have access " + + "rights to open the remote testing notification listener. " + + "Please run the process as administrator."); + } + } + + /// + /// Closes the remote notification listener. If there are + /// less than two parallel testing processes, then this + /// operation does nothing. + /// + private void CloseNotificationListener() + { + if (this.RunOutOfProcess && this.NotificationService.State == CommunicationState.Opened) + { + try + { + this.NotificationService.Close(); + } + catch (CommunicationException) + { + this.NotificationService.Abort(); + throw; + } + catch (TimeoutException) + { + this.NotificationService.Abort(); + throw; + } + catch (Exception) + { + this.NotificationService.Abort(); + throw; + } + } + } + + /// + /// Creates and returns a new testing process communication channel. + /// + private ITestingProcess CreateTestingProcessChannel(uint processId) + { + Uri address = new Uri("net.pipe://localhost/coyote/testing/process/" + + $"{processId}/{this.Configuration.TestingSchedulerEndPoint}"); + + NetNamedPipeBinding binding = new NetNamedPipeBinding(); + binding.MaxReceivedMessageSize = int.MaxValue; + + EndpointAddress endpoint = new EndpointAddress(address); + + return ChannelFactory.CreateChannel(binding, endpoint); + } + + /// + /// Stops the testing process. + /// + private void StopTestingProcess(uint processId) + { + try + { + this.TestingProcessChannels[processId].Stop(); + } + catch (CommunicationException ex) + { + IO.Debug.WriteLine("... Unable to communicate with testing task " + + $"'{processId}'. Task has already terminated."); + IO.Debug.WriteLine(ex.ToString()); + } + } +#endif + + /// + /// Gets the test report from the specified testing process. + /// + private TestReport GetTestReport(uint processId) + { + TestReport testReport = null; + +#if NET46 + try + { +#endif + testReport = this.TestingProcessChannels[processId].GetTestReport(); +#if NET46 + } + catch (CommunicationException ex) + { + IO.Debug.WriteLine("... Unable to communicate with testing task " + + $"'{processId}'. Task has already terminated."); + IO.Debug.WriteLine(ex.ToString()); + } +#endif + + return testReport; + } + + /// + /// Merges the test report from the specified process. + /// + private void MergeTestReport(TestReport testReport, uint processId) + { + if (this.TestReports.TryAdd(processId, testReport)) + { + // Merges the test report into the global report. + IO.Debug.WriteLine($"... Merging task {processId} test report."); + this.GlobalTestReport.Merge(testReport); + } + else + { + IO.Debug.WriteLine($"... Unable to merge test report from task '{processId}'. " + + " Report is already merged."); + } + } + + /// + /// Emits the test report. + /// + private void EmitTestReport() + { + var testReports = new List(this.TestReports.Values); +#if NET46 + foreach (var process in this.TestingProcesses) + { + if (!this.TestReports.ContainsKey(process.Key)) + { + Console.WriteLine($"... Task {process.Key} failed due to an internal error."); + } + } +#endif + + if (this.TestReports.Count == 0) + { + Environment.ExitCode = (int)ExitCode.InternalError; + return; + } + +#if NET46 + if (this.Configuration.ReportActivityCoverage) + { + Console.WriteLine($"... Emitting coverage reports:"); + Reporter.EmitTestingCoverageReport(this.GlobalTestReport); + } + + if (this.Configuration.DebugActivityCoverage) + { + Console.WriteLine($"... Emitting debug coverage reports:"); + foreach (var report in this.TestReports) + { + Reporter.EmitTestingCoverageReport(report.Value, report.Key, isDebug: true); + } + } +#endif + + Console.WriteLine(this.GlobalTestReport.GetText(this.Configuration, "...")); + Console.WriteLine($"... Elapsed {this.Profiler.Results()} sec."); + + if (this.GlobalTestReport.InternalErrors.Count > 0) + { + Environment.ExitCode = (int)ExitCode.InternalError; + } + else if (this.GlobalTestReport.NumOfFoundBugs > 0) + { + Environment.ExitCode = (int)ExitCode.BugFound; + } + else + { + Environment.ExitCode = (int)ExitCode.Success; + } + } + } +} diff --git a/Tools/Testing/Tester/Tester.csproj b/Tools/Testing/Tester/Tester.csproj new file mode 100644 index 000000000..24ed37eed --- /dev/null +++ b/Tools/Testing/Tester/Tester.csproj @@ -0,0 +1,29 @@ + + + + + The Coyote systematic tester. + CoyoteTester + CoyoteTester + systematic;tester;coyote + Exe + ..\..\..\bin\ + + + netcoreapp2.1;net46;net47 + + + netcoreapp2.1 + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tools/Testing/Tester/Testing/TestingPortfolio.cs b/Tools/Testing/Tester/Testing/TestingPortfolio.cs new file mode 100644 index 000000000..9770008c8 --- /dev/null +++ b/Tools/Testing/Tester/Testing/TestingPortfolio.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The Coyote testing portfolio. + /// + internal static class TestingPortfolio + { + /// + /// Configures the testing strategy for the current + /// testing process. + /// + /// Configuration + internal static void ConfigureStrategyForCurrentProcess(Configuration configuration) + { + // Random, PCT[1], ProbabilisticRandom[1], PCT[5], ProbabilisticRandom[2], PCT[10], etc. + if (configuration.TestingProcessId == 0) + { + configuration.SchedulingStrategy = SchedulingStrategy.Random; + } + else if (configuration.TestingProcessId % 2 == 0) + { + configuration.SchedulingStrategy = SchedulingStrategy.ProbabilisticRandom; + configuration.CoinFlipBound = (int)(configuration.TestingProcessId / 2); + } + else if (configuration.TestingProcessId == 1) + { + configuration.SchedulingStrategy = SchedulingStrategy.FairPCT; + configuration.PrioritySwitchBound = 1; + } + else + { + configuration.SchedulingStrategy = SchedulingStrategy.FairPCT; + configuration.PrioritySwitchBound = 5 * (int)((configuration.TestingProcessId + 1) / 2); + } + } + } +} diff --git a/Tools/Testing/Tester/Testing/TestingProcess.cs b/Tools/Testing/Tester/Testing/TestingProcess.cs new file mode 100644 index 000000000..86d48e9d1 --- /dev/null +++ b/Tools/Testing/Tester/Testing/TestingProcess.cs @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +#if NET46 +using System.ServiceModel; +using System.ServiceModel.Description; +#endif +using System.Timers; + +using Microsoft.Coyote.IO; +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// A testing process. + /// +#if NET46 + [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] +#endif + internal sealed class TestingProcess : ITestingProcess + { +#if NET46 + /// + /// The notification listening service. + /// + private ServiceHost NotificationService; +#endif + + /// + /// Configuration. + /// + private readonly Configuration Configuration; + + /// + /// The testing engine associated with + /// this testing process. + /// + private readonly ITestingEngine TestingEngine; + +#if NET46 + /// + /// The remote testing scheduler. + /// + private ITestingProcessScheduler TestingScheduler; +#endif + + /// + /// Returns the test report. + /// + TestReport ITestingProcess.GetTestReport() + { + return this.TestingEngine.TestReport.Clone(); + } + + /// + /// Stops testing. + /// + void ITestingProcess.Stop() + { + this.TestingEngine.Stop(); + } + + /// + /// Creates a Coyote testing process. + /// + internal static TestingProcess Create(Configuration configuration) + { + return new TestingProcess(configuration); + } + + /// + /// Runs the Coyote testing process. + /// + internal void Run() + { +#if NET46 + // Opens the remote notification listener. + this.OpenNotificationListener(); + + Timer timer = null; + if (this.Configuration.RunAsParallelBugFindingTask) + { + timer = this.CreateParentStatusMonitorTimer(); + timer.Start(); + } +#endif + + this.TestingEngine.Run(); + +#if NET46 + if (this.Configuration.RunAsParallelBugFindingTask) + { + if (this.TestingEngine.TestReport.InternalErrors.Count > 0) + { + Environment.ExitCode = (int)ExitCode.InternalError; + } + else if (this.TestingEngine.TestReport.NumOfFoundBugs > 0) + { + Environment.ExitCode = (int)ExitCode.BugFound; + this.NotifyBugFound(); + } + + this.SendTestReport(); + } +#endif + + if (!this.Configuration.PerformFullExploration) + { + if (this.TestingEngine.TestReport.NumOfFoundBugs > 0 && + !this.Configuration.RunAsParallelBugFindingTask) + { + Console.WriteLine($"... Task {this.Configuration.TestingProcessId} found a bug."); + } + + if (this.TestingEngine.TestReport.NumOfFoundBugs > 0) + { + this.EmitTraces(); + } + } + +#if NET46 + // Closes the remote notification listener. + this.CloseNotificationListener(); + + if (timer != null) + { + timer.Stop(); + } +#endif + } + + /// + /// Initializes a new instance of the class. + /// + private TestingProcess(Configuration configuration) + { + if (configuration.SchedulingStrategy == SchedulingStrategy.Portfolio) + { + TestingPortfolio.ConfigureStrategyForCurrentProcess(configuration); + } + + if (configuration.RandomSchedulingSeed != null) + { + configuration.RandomSchedulingSeed = (int)(configuration.RandomSchedulingSeed + (673 * configuration.TestingProcessId)); + } + + configuration.EnableColoredConsoleOutput = true; + + this.Configuration = configuration; + this.TestingEngine = TestingEngineFactory.CreateBugFindingEngine( + this.Configuration); + } + +#if NET46 + /// + /// Opens the remote notification listener. If this is + /// not a parallel testing process, then this operation + /// does nothing. + /// + private void OpenNotificationListener() + { + if (!this.Configuration.RunAsParallelBugFindingTask) + { + return; + } + + Uri address = new Uri("net.pipe://localhost/coyote/testing/process/" + + $"{this.Configuration.TestingProcessId}/" + + $"{this.Configuration.TestingSchedulerEndPoint}"); + + NetNamedPipeBinding binding = new NetNamedPipeBinding(); + binding.MaxReceivedMessageSize = int.MaxValue; + + this.NotificationService = new ServiceHost(this); + this.NotificationService.AddServiceEndpoint(typeof(ITestingProcess), binding, address); + + ServiceDebugBehavior debug = this.NotificationService.Description.Behaviors.Find(); + debug.IncludeExceptionDetailInFaults = true; + + try + { + this.NotificationService.Open(); + } + catch (AddressAccessDeniedException) + { + Error.ReportAndExit("Your process does not have access " + + "rights to open the remote testing notification listener. " + + "Please run the process as administrator."); + } + } + + /// + /// Closes the remote notification listener. If this is + /// not a parallel testing process, then this operation + /// does nothing. + /// + private void CloseNotificationListener() + { + if (this.Configuration.RunAsParallelBugFindingTask && + this.NotificationService.State == CommunicationState.Opened) + { + try + { + this.NotificationService.Close(); + } + catch (CommunicationException) + { + this.NotificationService.Abort(); + throw; + } + catch (TimeoutException) + { + this.NotificationService.Abort(); + throw; + } + catch (Exception) + { + this.NotificationService.Abort(); + throw; + } + } + } + + /// + /// Notifies the remote testing scheduler + /// about a discovered bug. + /// + private void NotifyBugFound() + { + Uri address = new Uri("net.pipe://localhost/coyote/testing/scheduler/" + + $"{this.Configuration.TestingSchedulerEndPoint}"); + + NetNamedPipeBinding binding = new NetNamedPipeBinding(); + binding.MaxReceivedMessageSize = int.MaxValue; + + EndpointAddress endpoint = new EndpointAddress(address); + + if (this.TestingScheduler is null) + { + this.TestingScheduler = ChannelFactory. + CreateChannel(binding, endpoint); + } + + this.TestingScheduler.NotifyBugFound(this.Configuration.TestingProcessId); + } + + /// + /// Sends the test report associated with this testing process. + /// + private void SendTestReport() + { + Uri address = new Uri("net.pipe://localhost/coyote/testing/scheduler/" + + $"{this.Configuration.TestingSchedulerEndPoint}"); + + NetNamedPipeBinding binding = new NetNamedPipeBinding(); + binding.MaxReceivedMessageSize = int.MaxValue; + + EndpointAddress endpoint = new EndpointAddress(address); + + if (this.TestingScheduler is null) + { + this.TestingScheduler = ChannelFactory. + CreateChannel(binding, endpoint); + } + + this.TestingScheduler.SetTestReport(this.TestingEngine.TestReport.Clone(), this.Configuration.TestingProcessId); + } +#endif + + /// + /// Emits the testing traces. + /// + private void EmitTraces() + { + string file = Path.GetFileNameWithoutExtension(this.Configuration.AssemblyToBeAnalyzed); + file += "_" + this.Configuration.TestingProcessId; + + // If this is a separate (sub-)process, CodeCoverageInstrumentation.OutputDirectory may not have been set up. + CodeCoverageInstrumentation.SetOutputDirectory(this.Configuration, makeHistory: false); + + Console.WriteLine($"... Emitting task {this.Configuration.TestingProcessId} traces:"); + this.TestingEngine.TryEmitTraces(CodeCoverageInstrumentation.OutputDirectory, file); + } + +#if NET46 + /// + /// Creates a timer that monitors the status of the parent process. + /// + private Timer CreateParentStatusMonitorTimer() + { + Timer timer = new Timer(5000); + timer.Elapsed += this.CheckParentStatus; + timer.AutoReset = true; + return timer; + } + + /// + /// Checks the status of the parent process. If the parent + /// process exits, then this process should also exit. + /// + private void CheckParentStatus(object sender, ElapsedEventArgs e) + { + Process parent = Process.GetProcesses().FirstOrDefault(val + => val.Id == this.Configuration.TestingSchedulerProcessId); + if (parent is null || !parent.ProcessName.Equals("CoyoteTester")) + { + Environment.Exit(1); + } + } +#endif + } +} diff --git a/Tools/Testing/Tester/Testing/TestingProcessFactory.cs b/Tools/Testing/Tester/Testing/TestingProcessFactory.cs new file mode 100644 index 000000000..f500ef69b --- /dev/null +++ b/Tools/Testing/Tester/Testing/TestingProcessFactory.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Reflection; +using System.Text; + +using Microsoft.Coyote.Utilities; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The Coyote testing process factory. + /// + internal static class TestingProcessFactory + { + /// + /// Creates a new testing process. + /// + public static Process Create(uint id, Configuration configuration) + { + ProcessStartInfo startInfo = new ProcessStartInfo( + Assembly.GetExecutingAssembly().Location, + CreateArgumentsFromConfiguration(id, configuration)); + startInfo.UseShellExecute = false; + + Process process = new Process(); + process.StartInfo = startInfo; + + return process; + } + + /// + /// Creates arguments from the specified configuration. + /// + private static string CreateArgumentsFromConfiguration(uint id, Configuration configuration) + { + StringBuilder arguments = new StringBuilder(); + + if (configuration.EnableDebugging) + { + arguments.Append($"/debug "); + } + + arguments.Append($"/test:{configuration.AssemblyToBeAnalyzed} "); + + if (!string.IsNullOrEmpty(configuration.TestingRuntimeAssembly)) + { + arguments.Append($"/runtime:{configuration.TestingRuntimeAssembly} "); + } + + if (!string.IsNullOrEmpty(configuration.TestMethodName)) + { + arguments.Append($"/method:{configuration.TestMethodName} "); + } + + arguments.Append($"/i:{configuration.SchedulingIterations} "); + arguments.Append($"/timeout:{configuration.Timeout} "); + + if (configuration.UserExplicitlySetMaxFairSchedulingSteps) + { + arguments.Append($"/max-steps:{configuration.MaxUnfairSchedulingSteps}:" + + $"{configuration.MaxFairSchedulingSteps} "); + } + else + { + arguments.Append($"/max-steps:{configuration.MaxUnfairSchedulingSteps} "); + } + + if (configuration.SchedulingStrategy == SchedulingStrategy.PCT || + configuration.SchedulingStrategy == SchedulingStrategy.FairPCT) + { + arguments.Append($"/sch:{configuration.SchedulingStrategy}:" + + $"{configuration.PrioritySwitchBound} "); + } + else if (configuration.SchedulingStrategy == SchedulingStrategy.ProbabilisticRandom) + { + arguments.Append($"/sch:probabilistic:{configuration.CoinFlipBound} "); + } + else if (configuration.SchedulingStrategy == SchedulingStrategy.Random || + configuration.SchedulingStrategy == SchedulingStrategy.Portfolio) + { + arguments.Append($"/sch:{configuration.SchedulingStrategy} "); + } + + if (configuration.RandomSchedulingSeed != null) + { + arguments.Append($"/sch-seed:{configuration.RandomSchedulingSeed} "); + } + + if (configuration.PerformFullExploration) + { + arguments.Append($"/explore "); + } + + arguments.Append($"/timeout-delay:{configuration.TimeoutDelay} "); + + if (configuration.ReportCodeCoverage && configuration.ReportActivityCoverage) + { + arguments.Append($"/coverage "); + } + else if (configuration.ReportCodeCoverage) + { + arguments.Append($"/coverage:code "); + } + else if (configuration.ReportActivityCoverage) + { + arguments.Append($"/coverage:activity "); + } + + if (configuration.EnableCycleDetection) + { + arguments.Append($"/cycle-detection "); + } + + if (configuration.OutputFilePath.Length > 0) + { + arguments.Append($"/o:{configuration.OutputFilePath} "); + } + + arguments.Append($"/run-as-parallel-testing-task "); + arguments.Append($"/testing-scheduler-endpoint:{configuration.TestingSchedulerEndPoint} "); + arguments.Append($"/testing-scheduler-process-id:{Process.GetCurrentProcess().Id} "); + arguments.Append($"/testing-process-id:{id}"); + + return arguments.ToString(); + } + } +} diff --git a/Tools/Testing/Tester/Utilities/DependencyGraph.cs b/Tools/Testing/Tester/Utilities/DependencyGraph.cs new file mode 100644 index 000000000..249ce6b35 --- /dev/null +++ b/Tools/Testing/Tester/Utilities/DependencyGraph.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#if NET46 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Coyote.TestingServices.Utilities +{ + internal static class DependencyGraph + { + private class DependencyNode + { + internal string Name { get; private set; } + + internal DependencyNode Next { get; private set; } + + internal DependencyNode(string name) + { + this.Name = name; + } + + internal DependencyNode Append(string name) + { + return new DependencyNode(name) { Next = this }; + } + + internal void Process(Action action) + { + action(this.Name); + for (var node = this.Next; node != null; node = node.Next) + { + node.Process(action); + } + } + } + + internal static string[] GetDependenciesToCoyote(Configuration configuration) + { + var domain = AppDomain.CreateDomain("DependentAssemblyLoading"); + try + { + // We need to do this in a separate AppDomain so we can unload the assemblies to allow instrumentation. + var loader = (DependentAssemblyLoader)domain.CreateInstanceAndUnwrap( + Assembly.GetExecutingAssembly().FullName, typeof(DependentAssemblyLoader).FullName); + return loader.GetDependenciesToCoyote(configuration.AssemblyToBeAnalyzed); + } + finally + { + AppDomain.Unload(domain); + } + } + + internal static string[] GetDependenciesToTarget(string source, HashSet allNames, + Func dependenciesFunc, Func isTargetFunc) + { + var known = new Dictionary(); + var queue = new Queue(); + + void evaluate() + { + // Adding it initially handles circular dependencies. + var node = queue.Dequeue(); + known[node.Name] = false; + + // If this has hit the target it may still have other dependencies we want. + var dependencies = dependenciesFunc(node.Name); + var isTarget = dependencies.Any(dep => isTargetFunc(dep)); + if (isTarget) + { + node.Process(n => known[n] = true); + } + + foreach (var name in dependencies.Where(n => allNames.Contains(n) && !known.ContainsKey(n))) + { + queue.Enqueue(isTarget ? new DependencyNode(name) : node.Append(name)); + } + } + + for (queue.Enqueue(new DependencyNode(source)); queue.Count > 0; /* queue adjusted in loop */) + { + evaluate(); + } + + known[source] = true; // Always return the source + return known.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToArray(); + } + +#if false + internal void Test() + { + string[] getDependencies(string key) + { + switch (key) + { + case "A": + return "B C D".Split(); + case "B": + return "C E f".Split(); + case "C": + return "c d e".Split(); + case "D": + return "F X Z".Split(); + case "E": + return "Y Z".Split(); + case "F": + return "Z".Split(); + } + return new string[0]; + } + + bool isTarget(string key) + { + return key == "Z"; + } + + string stringify(string[] strings) + { + var list = strings.ToList(); + list.Sort(); + return string.Join(" ", list); + } + + void compare(string[] expected, string[] actual) + { + var exp = stringify(expected); + var act = stringify(actual); + var cmp = exp == act ? "Pass" : "Fail"; + Console.WriteLine($"{exp} | {act} : {cmp}"); + } + + var allNames = new HashSet("A B C D E F G H I X Y Z".Split()); + var deps = GetDependenciesToTarget("A", allNames, name => getDependencies(name), name => isTarget(name)); + compare("A B E D F".Split(), deps); + deps = GetDependenciesToTarget("B", allNames, name => getDependencies(name), name => isTarget(name)); + compare("B E".Split(), deps); + deps = GetDependenciesToTarget("C", allNames, name => getDependencies(name), name => isTarget(name)); + compare("C".Split(), deps); + deps = GetDependenciesToTarget("D", allNames, name => getDependencies(name), name => isTarget(name)); + compare("D F".Split(), deps); + deps = GetDependenciesToTarget("F", allNames, name => getDependencies(name), name => isTarget(name)); + compare("F".Split(), deps); + + var numbers = Enumerable.Range(0, 10).Select(i => i.ToString()).ToArray(); + allNames = new HashSet(numbers); + deps = GetDependenciesToTarget("0", allNames, name => new[] { (int.Parse(name) + 1).ToString() }, name => name == (numbers.Length - 1).ToString()); + compare(Enumerable.Range(0, 9).Select(i => i.ToString()).ToArray(), deps); + deps = GetDependenciesToTarget("0", allNames, name => new[] { (int.Parse(name) + 1).ToString() }, name => false); + compare("0".Split(), deps); + + // Sanity check for recursion depth + numbers = Enumerable.Range(0, 100000).Select(i => i.ToString()).ToArray(); + allNames = new HashSet(numbers); + deps = GetDependenciesToTarget("0", allNames, name => new[] { (int.Parse(name) + 1).ToString() }, name => false); + compare("0".Split(), deps); + } +#endif + } +} +#endif diff --git a/Tools/Testing/Tester/Utilities/DependentAssemblyLoader.cs b/Tools/Testing/Tester/Utilities/DependentAssemblyLoader.cs new file mode 100644 index 000000000..fb1d7bcb6 --- /dev/null +++ b/Tools/Testing/Tester/Utilities/DependentAssemblyLoader.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#if NET46 +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Coyote.TestingServices.Utilities +{ + public sealed class DependentAssemblyLoader : MarshalByRefObject + { +#pragma warning disable CA1822 // Mark members as static + public string[] GetDependenciesToCoyote(string assemblyUnderTest) + { + // Get the case-normalized directory name + var fullTestAssemblyName = Path.GetFullPath(assemblyUnderTest); + var dir = Path.GetDirectoryName(Assembly.ReflectionOnlyLoadFrom(fullTestAssemblyName).Location); + var allNames = new HashSet(Directory.GetFiles(dir, "*.dll")); + + // Because Assembly.GetReferencedAssemblies does not yet have the path (assembly resolution is complex), we will assume that + // any assembly that matches a name in the executing directory is the referenced assembly. + var assemblyNameToFullPathMap = allNames.ToDictionary(name => Path.GetFileNameWithoutExtension(name), name => name); + + string getAssemblyFullPath(AssemblyName assemblyName) => + assemblyNameToFullPathMap.ContainsKey(assemblyName.Name) ? assemblyNameToFullPathMap[assemblyName.Name] : string.Empty; + + string[] getDependencies(string fullPath) + { + var assembly = Assembly.ReflectionOnlyLoadFrom(fullPath); + return assembly.GetReferencedAssemblies().Select(getAssemblyFullPath).Where(x => x.Length > 0).ToArray(); + } + + bool isTarget(string fullPath) => Path.GetFileName(fullPath).StartsWith("Microsoft.Coyote."); + + return DependencyGraph.GetDependenciesToTarget(fullTestAssemblyName, allNames, name => getDependencies(name), name => isTarget(name)); + } +#pragma warning restore CA1822 // Mark members as static + } +} +#endif diff --git a/Tools/Testing/Tester/Utilities/ExitCode.cs b/Tools/Testing/Tester/Utilities/ExitCode.cs new file mode 100644 index 000000000..79a99fc67 --- /dev/null +++ b/Tools/Testing/Tester/Utilities/ExitCode.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The exit code returned by the tester. + /// + public enum ExitCode + { + /// + /// Indicates that no bugs were found. + /// + Success = 0, + + /// + /// Indicates that a bug was found. + /// + BugFound = 1, + + /// + /// Indicates that an internal exception was thrown. + /// + InternalError = 2 + } +} diff --git a/Tools/Testing/Tester/Utilities/Reporter.cs b/Tools/Testing/Tester/Utilities/Reporter.cs new file mode 100644 index 000000000..7203471ee --- /dev/null +++ b/Tools/Testing/Tester/Utilities/Reporter.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Runtime.Serialization; +using Microsoft.Coyote.TestingServices.Coverage; + +namespace Microsoft.Coyote.TestingServices +{ + /// + /// The Coyote testing reporter. + /// + internal sealed class Reporter + { +#if NET46 + /// + /// Emits the testing coverage report. + /// + /// TestReport + /// Optional process id that produced the report + /// Is a debug report + internal static void EmitTestingCoverageReport(TestReport report, uint? processId = null, bool isDebug = false) + { + string file = Path.GetFileNameWithoutExtension(report.Configuration.AssemblyToBeAnalyzed); + if (isDebug && processId != null) + { + file += "_" + processId; + } + + string directory = CodeCoverageInstrumentation.OutputDirectory; + if (isDebug) + { + directory += $"Debug{Path.DirectorySeparatorChar}"; + Directory.CreateDirectory(directory); + } + + EmitTestingCoverageOutputFiles(report, directory, file); + } +#endif + + /// + /// Returns (and creates if it does not exist) the output directory with an optional suffix. + /// + internal static string GetOutputDirectory(string userOutputDir, string assemblyPath, string suffix = "", bool createDir = true) + { + string directoryPath; + + if (!string.IsNullOrEmpty(userOutputDir)) + { + directoryPath = userOutputDir + Path.DirectorySeparatorChar; + } + else + { + var subpath = Path.GetDirectoryName(assemblyPath); + if (subpath.Length == 0) + { + subpath = "."; + } + + directoryPath = subpath + + Path.DirectorySeparatorChar + "Output" + Path.DirectorySeparatorChar + + Path.GetFileName(assemblyPath) + Path.DirectorySeparatorChar; + } + + if (suffix.Length > 0) + { + directoryPath += suffix + Path.DirectorySeparatorChar; + } + + if (createDir) + { + Directory.CreateDirectory(directoryPath); + } + + return directoryPath; + } + + /// + /// Emits all the testing coverage related output files. + /// + /// TestReport containing CoverageInfo + /// Output directory name, unique for this run + /// Output file name + private static void EmitTestingCoverageOutputFiles(TestReport report, string directory, string file) + { + var codeCoverageReporter = new ActivityCoverageReporter(report.CoverageInfo); + var filePath = $"{directory}{file}"; + + string graphFilePath = $"{filePath}.dgml"; + Console.WriteLine($"..... Writing {graphFilePath}"); + codeCoverageReporter.EmitVisualizationGraph(graphFilePath); + + string coverageFilePath = $"{filePath}.coverage.txt"; + Console.WriteLine($"..... Writing {coverageFilePath}"); + codeCoverageReporter.EmitCoverageReport(coverageFilePath); + + string serFilePath = $"{filePath}.sci"; + Console.WriteLine($"..... Writing {serFilePath}"); + using (var fs = new FileStream(serFilePath, FileMode.Create)) + { + var serializer = new DataContractSerializer(typeof(CoverageInfo)); + serializer.WriteObject(fs, report.CoverageInfo); + } + } + } +} diff --git a/Tools/Testing/Tester/Utilities/TesterCommandLineOptions.cs b/Tools/Testing/Tester/Utilities/TesterCommandLineOptions.cs new file mode 100644 index 000000000..0cf000c19 --- /dev/null +++ b/Tools/Testing/Tester/Utilities/TesterCommandLineOptions.cs @@ -0,0 +1,446 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.Coyote.IO; + +namespace Microsoft.Coyote.Utilities +{ + public sealed class TesterCommandLineOptions : BaseCommandLineOptions + { + /// + /// Initializes a new instance of the class. + /// + public TesterCommandLineOptions(string[] args) + : base(args) + { + } + + /// + /// Parses the given option. + /// + protected override void ParseOption(string option) + { + if (IsMatch(option, @"^[\/|-]test:") && option.Length > 6) + { + this.Configuration.AssemblyToBeAnalyzed = option.Substring(6); + } + else if (IsMatch(option, @"^[\/|-]runtime:") && option.Length > 9) + { + this.Configuration.TestingRuntimeAssembly = option.Substring(9); + } + else if (IsMatch(option, @"^[\/|-]method:") && option.Length > 8) + { + this.Configuration.TestMethodName = option.Substring(8); + } + else if (IsMatch(option, @"^[\/|-]interactive$")) + { + this.Configuration.SchedulingStrategy = SchedulingStrategy.Interactive; + } + else if (IsMatch(option, @"^[\/|-]sch:")) + { + string scheduler = option.Substring(5); + if (IsMatch(scheduler, @"^portfolio$")) + { + this.Configuration.SchedulingStrategy = SchedulingStrategy.Portfolio; + } + else if (IsMatch(scheduler, @"^random$")) + { + this.Configuration.SchedulingStrategy = SchedulingStrategy.Random; + } + else if (IsMatch(scheduler, @"^probabilistic")) + { + int i = 0; + if (IsMatch(scheduler, @"^probabilistic$") || + (!int.TryParse(scheduler.Substring(14), out i) && i >= 0)) + { + Error.ReportAndExit("Please give a valid number of coin " + + "flip bound '-sch:probabilistic:[bound]', where [bound] >= 0."); + } + + this.Configuration.SchedulingStrategy = SchedulingStrategy.ProbabilisticRandom; + this.Configuration.CoinFlipBound = i; + } + else if (IsMatch(scheduler, @"^pct")) + { + int i = 0; + if (IsMatch(scheduler, @"^pct$") || + (!int.TryParse(scheduler.Substring(4), out i) && i >= 0)) + { + Error.ReportAndExit("Please give a valid number of priority " + + "switch bound '-sch:pct:[bound]', where [bound] >= 0."); + } + + this.Configuration.SchedulingStrategy = SchedulingStrategy.PCT; + this.Configuration.PrioritySwitchBound = i; + } + else if (IsMatch(scheduler, @"^fairpct")) + { + int i = 0; + if (IsMatch(scheduler, @"^fairpct$") || + (!int.TryParse(scheduler.Substring("fairpct:".Length), out i) && i >= 0)) + { + Error.ReportAndExit("Please give a valid number of priority " + + "switch bound '-sch:fairpct:[bound]', where [bound] >= 0."); + } + + this.Configuration.SchedulingStrategy = SchedulingStrategy.FairPCT; + this.Configuration.PrioritySwitchBound = i; + } + else if (IsMatch(scheduler, @"^dfs$")) + { + this.Configuration.SchedulingStrategy = SchedulingStrategy.DFS; + } + else if (IsMatch(scheduler, @"^iddfs$")) + { + this.Configuration.SchedulingStrategy = SchedulingStrategy.IDDFS; + } + else if (IsMatch(scheduler, @"^db")) + { + int i = 0; + if (IsMatch(scheduler, @"^db$") || + (!int.TryParse(scheduler.Substring(3), out i) && i >= 0)) + { + Error.ReportAndExit("Please give a valid delay " + + "bound '-sch:db:[bound]', where [bound] >= 0."); + } + + this.Configuration.SchedulingStrategy = SchedulingStrategy.DelayBounding; + this.Configuration.DelayBound = i; + } + else if (IsMatch(scheduler, @"^rdb")) + { + int i = 0; + if (IsMatch(scheduler, @"^rdb$") || + (!int.TryParse(scheduler.Substring(4), out i) && i >= 0)) + { + Error.ReportAndExit("Please give a valid delay " + + "bound '-sch:rdb:[bound]', where [bound] >= 0."); + } + + this.Configuration.SchedulingStrategy = SchedulingStrategy.RandomDelayBounding; + this.Configuration.DelayBound = i; + } + else + { + Error.ReportAndExit("Please give a valid scheduling strategy " + + "'-sch:[x]', where [x] is 'random', 'pct' or 'dfs' (other " + + "experimental strategies also exist, but are not listed here)."); + } + } + else if (IsMatch(option, @"^[\/|-]replay:") && option.Length > 8) + { + string extension = System.IO.Path.GetExtension(option.Substring(8)); + if (!extension.Equals(".schedule")) + { + Error.ReportAndExit("Please give a valid schedule file " + + "'-replay:[x]', where [x] has extension '.schedule'."); + } + + this.Configuration.ScheduleFile = option.Substring(8); + } + else if (IsMatch(option, @"^[\/|-]i:") && option.Length > 3) + { + if (!int.TryParse(option.Substring(3), out int i) && i > 0) + { + Error.ReportAndExit("Please give a valid number of " + + "iterations '-i:[x]', where [x] > 0."); + } + + this.Configuration.SchedulingIterations = i; + } + else if (IsMatch(option, @"^[\/|-]parallel:") && option.Length > 10) + { + if (!uint.TryParse(option.Substring(10), out uint i) || i <= 1) + { + Error.ReportAndExit("Please give a valid number of " + + "parallel tasks '-parallel:[x]', where [x] > 1."); + } + + this.Configuration.ParallelBugFindingTasks = i; + } + else if (IsMatch(option, @"^[\/|-]run-as-parallel-testing-task$")) + { + this.Configuration.RunAsParallelBugFindingTask = true; + } + else if (IsMatch(option, @"^[\/|-]testing-scheduler-endpoint:") && option.Length > 28) + { + string endpoint = option.Substring(28); + if (endpoint.Length != 36) + { + Error.ReportAndExit("Please give a valid testing scheduler endpoint " + + "'-testing-scheduler-endpoint:[x]', where [x] is a unique GUID."); + } + + this.Configuration.TestingSchedulerEndPoint = endpoint; + } + else if (IsMatch(option, @"^[\/|-]testing-scheduler-process-id:") && option.Length > 30) + { + if (!int.TryParse(option.Substring(30), out int i) && i >= 0) + { + Error.ReportAndExit("Please give a valid testing scheduler " + + "process id '-testing-scheduler-process-id:[x]', where [x] >= 0."); + } + + this.Configuration.TestingSchedulerProcessId = i; + } + else if (IsMatch(option, @"^[\/|-]testing-process-id:") && option.Length > 20) + { + if (!uint.TryParse(option.Substring(20), out uint i) && i >= 0) + { + Error.ReportAndExit("Please give a valid testing " + + "process id '-testing-process-id:[x]', where [x] >= 0."); + } + + this.Configuration.TestingProcessId = i; + } + else if (IsMatch(option, @"^[\/|-]explore$")) + { + this.Configuration.PerformFullExploration = true; + } + else if (IsMatch(option, @"^[\/|-]coverage$")) + { + this.Configuration.ReportCodeCoverage = true; + this.Configuration.ReportActivityCoverage = true; + } + else if (IsMatch(option, @"^[\/|-]coverage:code$")) + { + this.Configuration.ReportCodeCoverage = true; + } + else if (IsMatch(option, @"^[\/|-]coverage:activity$")) + { + this.Configuration.ReportActivityCoverage = true; + } + else if (IsMatch(option, @"^[\/|-]coverage:activity-debug$")) + { + this.Configuration.ReportActivityCoverage = true; + this.Configuration.DebugActivityCoverage = true; + } + else if (IsMatch(option, @"^[\/|-]instr:")) + { + this.Configuration.AdditionalCodeCoverageAssemblies[option.Substring(7)] = false; + } + else if (IsMatch(option, @"^[\/|-]instr-list:")) + { + this.Configuration.AdditionalCodeCoverageAssemblies[option.Substring(12)] = true; + } + else if (IsMatch(option, @"^[\/|-]timeout-delay:") && option.Length > 15) + { + if (!uint.TryParse(option.Substring(15), out uint timeoutDelay) && timeoutDelay >= 0) + { + Error.ReportAndExit("Please give a valid timeout delay '-timeout-delay:[x]', where [x] >= 0."); + } + + this.Configuration.TimeoutDelay = timeoutDelay; + } + else if (IsMatch(option, @"^[\/|-]sch-seed:") && option.Length > 10) + { + if (!int.TryParse(option.Substring(10), out int seed)) + { + Error.ReportAndExit("Please give a valid random scheduling " + + "seed '-sch-seed:[x]', where [x] is a signed 32-bit integer."); + } + + this.Configuration.RandomSchedulingSeed = seed; + } + else if (IsMatch(option, @"^[\/|-]max-steps:") && option.Length > 11) + { + int i = 0; + var tokens = option.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + if (tokens.Length > 3 || tokens.Length <= 1) + { + Error.ReportAndExit("Invalid number of options supplied via '-max-steps'."); + } + + if (tokens.Length >= 2) + { + if (!int.TryParse(tokens[1], out i) && i >= 0) + { + Error.ReportAndExit("Please give a valid number of max scheduling " + + " steps to explore '-max-steps:[x]', where [x] >= 0."); + } + } + + int j; + if (tokens.Length == 3) + { + if (!int.TryParse(tokens[2], out j) && j >= 0) + { + Error.ReportAndExit("Please give a valid number of max scheduling " + + " steps to explore '-max-steps:[x]:[y]', where [y] >= 0."); + } + + this.Configuration.UserExplicitlySetMaxFairSchedulingSteps = true; + } + else + { + j = 10 * i; + } + + this.Configuration.MaxUnfairSchedulingSteps = i; + this.Configuration.MaxFairSchedulingSteps = j; + } + else if (IsMatch(option, @"^[\/|-]depth-bound-bug$")) + { + this.Configuration.ConsiderDepthBoundHitAsBug = true; + } + else if (IsMatch(option, @"^[\/|-]prefix:") && option.Length > 8) + { + if (!int.TryParse(option.Substring(8), out int i) && i >= 0) + { + Error.ReportAndExit("Please give a valid safety prefix " + + "bound '-prefix:[x]', where [x] >= 0."); + } + + this.Configuration.SafetyPrefixBound = i; + } + else if (IsMatch(option, @"^[\/|-]liveness-temperature-threshold:") && option.Length > 32) + { + if (!int.TryParse(option.Substring(32), out int i) && i >= 0) + { + Error.ReportAndExit("Please give a valid liveness temperature threshold " + + "'-liveness-temperature-threshold:[x]', where [x] >= 0."); + } + + this.Configuration.LivenessTemperatureThreshold = i; + } + else if (IsMatch(option, @"^[\/|-]cycle-detection$")) + { + this.Configuration.EnableCycleDetection = true; + } + else if (IsMatch(option, @"^[\/|-]custom-state-hashing$")) + { + this.Configuration.EnableUserDefinedStateHashing = true; + } + else + { + base.ParseOption(option); + } + } + + /// + /// Checks for parsing errors. + /// + protected override void CheckForParsingErrors() + { + if (string.IsNullOrEmpty(this.Configuration.AssemblyToBeAnalyzed)) + { + Error.ReportAndExit("Please give a valid path to a Coyote program's dll using '-test:[x]'."); + } + + if (this.Configuration.SchedulingStrategy != SchedulingStrategy.Interactive && + this.Configuration.SchedulingStrategy != SchedulingStrategy.Portfolio && + this.Configuration.SchedulingStrategy != SchedulingStrategy.Random && + this.Configuration.SchedulingStrategy != SchedulingStrategy.ProbabilisticRandom && + this.Configuration.SchedulingStrategy != SchedulingStrategy.PCT && + this.Configuration.SchedulingStrategy != SchedulingStrategy.FairPCT && + this.Configuration.SchedulingStrategy != SchedulingStrategy.DFS && + this.Configuration.SchedulingStrategy != SchedulingStrategy.IDDFS && + this.Configuration.SchedulingStrategy != SchedulingStrategy.DelayBounding && + this.Configuration.SchedulingStrategy != SchedulingStrategy.RandomDelayBounding) + { + Error.ReportAndExit("Please give a valid scheduling strategy " + + "'-sch:[x]', where [x] is 'random' or 'pct' (other experimental " + + "strategies also exist, but are not listed here)."); + } + + if (this.Configuration.MaxFairSchedulingSteps < this.Configuration.MaxUnfairSchedulingSteps) + { + Error.ReportAndExit("For the option '-max-steps:[N]:[M]', please make sure that [M] >= [N]."); + } + + if (this.Configuration.SafetyPrefixBound > 0 && + this.Configuration.SafetyPrefixBound >= this.Configuration.MaxUnfairSchedulingSteps) + { + Error.ReportAndExit("Please give a safety prefix bound that is less than the " + + "max scheduling steps bound."); + } + + if (this.Configuration.SchedulingStrategy.Equals("iddfs") && + this.Configuration.MaxUnfairSchedulingSteps == 0) + { + Error.ReportAndExit("The Iterative Deepening DFS scheduler ('iddfs') " + + "must have a max scheduling steps bound, which can be given using " + + "'-max-steps:[bound]', where [bound] > 0."); + } + +#if NETCOREAPP2_1 + if (this.Configuration.ParallelBugFindingTasks > 1) + { + Error.ReportAndExit("We do not yet support parallel testing when using the .NET Core runtime."); + } + + if (this.Configuration.ReportCodeCoverage || this.Configuration.ReportActivityCoverage) + { + Error.ReportAndExit("We do not yet support coverage reports when using the .NET Core runtime."); + } +#endif + } + + /// + /// Updates the configuration depending on the user specified options. + /// + protected override void UpdateConfiguration() + { + if (this.Configuration.LivenessTemperatureThreshold == 0) + { + if (this.Configuration.EnableCycleDetection) + { + this.Configuration.LivenessTemperatureThreshold = 100; + } + else if (this.Configuration.MaxFairSchedulingSteps > 0) + { + this.Configuration.LivenessTemperatureThreshold = + this.Configuration.MaxFairSchedulingSteps / 2; + } + } + + if (this.Configuration.RandomSchedulingSeed is null) + { + this.Configuration.RandomSchedulingSeed = DateTime.Now.Millisecond; + } + } + + /// + /// Shows help. + /// + protected override void ShowHelp() + { + string help = "\n"; + + help += " --------------"; + help += "\n Basic options:"; + help += "\n --------------"; + help += "\n -?\t\t Show this help menu"; + help += "\n -test:[x]\t Path to the Coyote program to test"; + help += "\n -method:[x]\t Suffix of the test method to execute"; + help += "\n -timeout:[x]\t Timeout in seconds (disabled by default)"; + help += "\n -v:[x]\t Enable verbose mode (values from '1' to '3')"; + help += "\n -o:[x]\t Dump output to directory x (absolute path or relative to current directory)"; + + help += "\n\n ---------------------------"; + help += "\n Systematic testing options:"; + help += "\n ---------------------------"; + help += "\n -i:[x]\t\t Number of schedules to explore for bugs"; + help += "\n -parallel:[x]\t\t Number of parallel testing tasks ('1' by default)"; + help += "\n -sch:[x]\t\t Choose a systematic testing strategy ('random' by default)"; + help += "\n -max-steps:[x]\t Max scheduling steps to be explored (disabled by default)"; + help += "\n -replay:[x]\t Tries to replay the schedule, and then switches to the specified strategy"; + + help += "\n\n ---------------------------"; + help += "\n Testing code coverage options:"; + help += "\n ---------------------------"; + help += "\n -coverage:code\t Generate code coverage statistics (via VS instrumentation)"; + help += "\n -coverage:activity\t Generate activity (machine, event, etc.) coverage statistics"; + help += "\n -coverage\t Generate both code and activity coverage statistics"; + help += "\n -coverage:activity-debug\t Print activity coverage statistics with debug info"; + help += "\n -instr:[filespec]\t Additional file spec(s) to instrument for -coverage:code; wildcards supported"; + help += "\n -instr-list:[listfilename]\t File containing the names of additional file(s), one per line,"; + help += "\n wildcards supported, to instrument for -coverage:code; lines starting with '//' are skipped"; + + help += "\n"; + + Console.WriteLine(help); + } + } +} diff --git a/Versioning.md b/Versioning.md new file mode 100644 index 000000000..ee76071a8 --- /dev/null +++ b/Versioning.md @@ -0,0 +1,55 @@ +# Guidance on Coyote versioning +#### v1.0.0 + +The Coyote framework versioning follows [Semantic Versioning 2.0.0](https://semver.org/) and has the pattern of MAJOR.MINOR.PATCH. + +1) MAJOR version when you make incompatible API changes, +2) MINOR version when you add functionality in a backwards compatible manner, and +3) PATCH version when you make backwards compatible bug fixes. + +We adopt everything listed in [Semantic Versioning 2.0.0](https://semver.org/) +but to summarize the major points here: + +**Incrementing the MAJOR version number** should be done when: + * a major new feature is added to the Coyote framework or tools and is showcased by a new tutorial demonstrating this feature + * a breaking change has been made to Coyote API or serialization format, or + tool command line arguments. + +**Incrementing the MINOR version number** should be done when: + * new features are added to the Coyote API or tools that are backwards + compatible + +**Incrementing the PATCH version number** should be done when + * anything else changes in the the Coyote framework. + +Not all changes to the repository warrant a version change. For example, + * test code changes + * documentation only fixing typos and grammar + * automation script updates + * reformatting of code for styling changes + +## Process + +Developers maintain `History.md` with each checkin, adding bullet points to the +top version number listed with an asterix. For example, it might look like this: + +``` +## v2.4.5* +- Fix some concurrency bugs in the framis checker. +``` + +The asterix means this version has not yet reached the master branch on github. Each developer +modifies this the new version number in `History.md` according to the above rules. For example, one +developer might fix a bug and bump the **patch** version number from "v2.4.5*" to "v2.4.6*". Another +might then add a big new feature that warrants a major version change, so they will change the +top record in `History.md` from "v2.4.6*" to "v3.0.0*". All other changes made from there will leave +the number at "v3.0.0" until these bits are pushed to the master branch in github. + +When all this is pushed to github in a Pull Request, `Common\version.props` and +`Scripts\NuGet\Coyote.nuspec` are updated with the new version number listed in `History.md` and the +asterix is removed, indicating this version number is now locked. The next person to change the repo will then start a new version number record in `History.md` and add the asterix to indicate to everyone else +that this new number is not yet locked. + +The contents in `History.md` is then copied to the github release page, which makes it easy to do +a release. There is no need to go searching through the change log to come up with the right +release summary. Code reviews are used to ensure folks remember to update this `History.md` file. \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 000000000..7f8560efd --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "2.2.400" + } +} \ No newline at end of file