/// <summary> /// Called before starting a new test run to clear all the transient values to its initial values. /// </summary> public void Reset() { lock (this.stateLock) { // reset run-time objects this.randomizer = new Helpers.SeededRandomizer(this.Meta.schedulingSeed); this.counter = 0; this.programState = new ProgramState(); this.SessionFinished = new TaskCompletionSource <SessionRecord>(); this.pendingRequests = new HashSet <RemoteMethodInvocation>(); this.testTrace = new List <DecisionTrace>(); this.runtimeLog = new List <string>(); this.IsFinished.Value = false; this.timeout = new Timer(_ => { string currentTask = this.programState.currentTask.ToString(); var errorInfo = $"No activity for {(Meta.timeoutMs / 1000).ToString()} seconds!\n Program State: [ {this.programState.GetCurrentStateString()} ]\n Possible reasons:\n\t- Not calling EndTask({currentTask})\n\t- Calling ContextSwitch from an undeclared Task\n\t- Some Tasks not being modelled"; this.Finish(TestResult.InactivityTimeout, errorInfo); }, null, Meta.timeoutMs, Timeout.Infinite); this.currentRecord = new SessionRecord(this.Id, this.Meta.schedulingSeed); this.currentRecord.RecordBegin(); } // create a continuation callback that will notify the client once the test is finished this.SessionFinished.Task.ContinueWith(prev => { //Console.WriteLine("\n[TestingSession.SessionFinished] was settled"); #if DEBUG Console.WriteLine(Profiler.ToString()); #endif // emit onComplete event this.OnComplete(this, prev.Result); }); }
/// <summary> /// Called internally to end the test. This method asynchronously resolves the <see cref="SessionFinished"/> /// TaskCompletionSource and returns immediately to the caller. /// If the test run has encountered a bug (i.e., AssertionFailure is thrown), it drops all pending requests. /// This method is idempotent with respect to a single test run -- only the first call will move the object's /// state to IsFinished, and any subsequent calls potentially made by concurrent requests will not do anything. /// </summary> /// <param name="result">The result of the test run</param> /// <param name="reason">Any description of the result</param> private void Finish(TestResult result, string reason = "") { // Ensure the following is called at most once if (!this.IsFinished.Value) { this.IsFinished.Value = true; // Drop all pending tasks if finished due to error if (result != TestResult.Pass) { // Console.WriteLine(" Dropping {0} tasks", this.programState.taskToTcs.Count); lock (programState) { foreach (var item in this.programState.taskToTcs) { #if DEBUG Console.WriteLine(" ... dropping Session {2} Task {0} ({1})", item.Key, item.Value.Task.Status.ToString(), this.Id); #endif item.Value.TrySetException(new TestFailedException(reason)); this.programState.taskToTcs.Remove(item.Key); } } } // The following procedure needs to be done asynchronously // because this call to Finish must return to the caller Task.Run(() => { SessionRecord record = this.currentRecord; // Wait for other threads to return first // if there is some thread that is not returning, then there is something wrong with the user program. // One last pending request is the current request: WaitForMainTask bool userProgramFaulty = false; string userProgramFaultyReason = ""; int wait = 2000; while (this.pendingRequests.Count > 0) { #if DEBUG lock (this.pendingRequests) { Console.WriteLine("Waiting for {0} requests to complete...\t({2} Tasks still in queue){1}", this.pendingRequests.Count, String.Join("", this.pendingRequests.Select(item => "\n\t... " + item.ToString())), programState.taskToTcs.Count); } #endif Thread.Sleep(50); wait -= 50; if (wait <= 0) { userProgramFaulty = true; var pendingList = String.Join("", this.pendingRequests.Select(req => "\n\t - " + req.ToString())); userProgramFaultyReason = $"Could not resolve {this.pendingRequests.Count} pending requests!\n\t waiting on:{pendingList}.\n\nPossible reasons:\n\t- Some Tasks not being modelled\n\t- Calling ContextSwitch in a Task that is not declared"; break; } } // Clear timer this.timeout.Change(Timeout.Infinite, Timeout.Infinite); this.timeout.Dispose(); #if DEBUG Console.WriteLine("\n\nOnly {0} last request to complete...", this.pendingRequests.Count); #endif record.RecordEnd(); record.numDecisions = testTrace.Count; if (PrintVerbosity > 1) { Console.WriteLine("\n===[ Decision Trace ({0} decision points) ]=====\n", testTrace.Count); PrintTrace(testTrace); } // TODO: Uncomment while doing for Traces /* if (testTrace.Count == 0) * { * userProgramFaulty = true; * userProgramFaultyReason = "There seems to be no concurrency modeled in the user program - 0 scheduling decisions were made"; * } */ // if result is set, then there was something wrong with the user program so we do not record the trace if (userProgramFaulty) { // var pendingList = String.Join("", this.pendingRequests.Select(req => "\n\t - " + req.ToString())); // var errorInfo = $"Could not resolve {this.pendingRequests.Count} pending requests!\n\t waiting on:{pendingList}.\n\nPossible reasons:\n\t- Some Tasks not being modelled\n\t- Calling ContextSwitch in a Task that is not declared"; record.RecordResult(TestResult.FaultyProgram, userProgramFaultyReason); } else { bool reproducible = false; // Write trace only if this is the first time running this session if (!this.IsReplayMode) { record.RecordResult(result, reason); this.traceFile.Write(String.Join("\n", this.testTrace.Select(step => step.ToString()))); this.traceFile.Close(); this.logFile.Write(String.Join("\n", this.runtimeLog)); this.logFile.Close(); } // If it is in replay mode, compare with previous trace /* else * { * // if the results are different, then something is wrong with the user program * if (this.LastRecord.result != result || this.LastRecord.reason != reason) * { * var errorInfo = $"Could not reproduce the trace for Session {this.Id}.\nPossible reasons:\n\t- Some Tasks not being modelled"; * record.RecordResult(TestResult.FaultyProgram, errorInfo); * Console.WriteLine("\n\n!!! Result Mismatch:\n Last: {0} {1},\n Now: {2} {3} \n", this.LastRecord.result.ToString(), this.LastRecord.reason, result.ToString(), reason); * } * else * { * var traceText = File.ReadAllText(this.traceFilePath); * List<DecisionTrace> previousTrace = traceText.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(line => DecisionTrace.FromString(line)).ToList(); * * //PrintTrace(previousTrace); * * // if the traces are different, then something is wrong with the user program * if (this.testTrace.Count != previousTrace.Count) * { * var errorInfo = $"Could not reproduce the trace for Session {this.Id}.\nPossible reasons:\n\t- Some Tasks not being modelled"; * record.RecordResult(TestResult.FaultyProgram, errorInfo); * * Console.WriteLine("\n\n!!! Trace Length Mismatch: Last = {0} vs Now = {1}\n", previousTrace.Count.ToString(), this.testTrace.Count.ToString()); * PrintTrace(previousTrace); * Console.WriteLine("----- versus -----"); * PrintTrace(this.testTrace); * } * else * { * bool match = this.testTrace.Select((t, i) => previousTrace[i] == t).Aggregate(true, (acc, b) => acc && b); * * // if the traces are different, then something is wrong with the user program * if (!match) * { * var errorInfo = $"Could not reproduce the trace for Session {this.Id}.\nPossible reasons:\n\t- Some Tasks not being modelled"; * record.RecordResult(TestResult.FaultyProgram, errorInfo); * * Console.WriteLine("\n\n!!! Trace Mismatch \n"); * PrintTrace(previousTrace); * Console.WriteLine("----- versus -----"); * PrintTrace(this.testTrace); * } * else * { * // Console.WriteLine("\n\t... Decision trace was reproduced successfully"); * record.RecordResult(result, reason); * reproducible = true; * } * } * } * } */ lock (this.records) { if (!this.IsReplayMode || reproducible) { this.records.Add(record); } } } #if DEBUG //Console.WriteLine(Profiler.ToString()); #endif Console.WriteLine("<<< Ending Session {0} : {1} >>>", this.Id, result.ToString()); // signal the end of the test, so that WaitForMainTask can return this.SessionFinished.SetResult(record); }); } }