Parallel Thread Testing

    In the debugger, you can easily catch the execution flow at the correct point, and then, after analysis, restart it. In automated tests, these operations look insanely complicated.

    And why do you need it?

    Building parallel systems is not the easiest thing. A balance must be maintained between parallelism and synchronism. If you do not synchronize, you will lose stability. Resynchronize - you get a consistent system.

    Refactoring is generally a walk through a minefield.

    If not for the complexity of the implementation, automated testing could help a lot. In my opinion, in a parallel system, it would be nice to automatically check the simultaneous operation of individual conflicting sections of code - the fact and the execution order.

    So I finally got to the suspension and resumption of threads. As already mentioned, such a non-automatic mechanism exists. These are breakpoints. We will not invent a new terminology - breakpoint, which means breakpoint.

    publicclassBreakpoint
    {
    	[Conditional("DEBUG")]
    	publicstaticvoidDefine(string name){…}
    }
    publicclassBreakCtrl : IDisposable
    {
    	publicstring Name { get; privateset; }
    	publicBreakCtrl(string name) {…}
    	public BreakCtrl From(params Thread[] threads) {…}
    	publicvoidDispose(){…}
    	publicvoidRun(Thread thread){…}
    	publicvoidWait(Thread thread){…}
    	publicboolIsCapture(Thread thread){…}
    	public Thread AnyCapture(){…}
    }
    


    Properties of automatic breakpoints:
    1. They work only in debug mode (with a specific macro DEBUG). We should not think that additional code will affect the operation of the system at the end user.
    2. Breakpoint only works if its controller is defined. Breakpoints unnecessary in a particular test should not lead the system (and complicate the tests).
    3. The controller knows what state the breakpoint is in - does it hold the stream.
    4. The controller is able to force the breakpoint to release the stream.
    5. And optional binding to a specific thread. We want to control a specific flow, we want everyone at once.


    [TestMethod]
    publicvoidStopStartThreadsTest_exemple1()
    {
    	var log = new List<string>();
    	ThreadStart act1 = () =>
    		{
    			Breakpoint.Define("empty");
    			Breakpoint.Define("start1");
    			log.Add("after start1");
    			Breakpoint.Define("step act1");
    			log.Add("after step act1");
    			Breakpoint.Define("finish1");
    		};
    	ThreadStart act2 = () =>
    		{
    			Breakpoint.Define("start2");
    			log.Add("after start2");
    			Breakpoint.Define("step act2");
    			log.Add("after step act2");
    			Breakpoint.Define("finish2");
    		};
    	using (var start1 = new BreakCtrl("start1"))
    	using (var step_act1 = new BreakCtrl("step act1"))
    	using (var finish1 = new BreakCtrl("finish1"))
    	using (var start2 = new BreakCtrl("start2"))
    	using (var step_act2 = new BreakCtrl("step act2"))
    	using (var finish2 = new BreakCtrl("finish2"))
    	{
    		var thr1 = new Thread(act1);
    		thr1.Start();
    		var thr2 = new Thread(act2);
    		thr2.Start();
    		start1.Wait(thr1);
    		start2.Wait(thr2);
    		start1.Run(thr1);
    		step_act1.Wait(thr1);
    		step_act1.Run(thr1);
    		finish1.Wait(thr1);
    		start2.Run(thr2);
    		step_act2.Wait(thr2);
    		step_act2.Run(thr2);
    		finish2.Wait(thr2);
    		finish1.Run(thr1);
    		finish2.Run(thr2);
    		thr1.Join();
    		thr2.Join();
    	}
    	Assert.AreEqual(4, log.Count);
    	Assert.AreEqual("after start1", log[0]);
    	Assert.AreEqual("after step act1", log[1]);
    	Assert.AreEqual("after start2", log[2]);
    	Assert.AreEqual("after step act2", log[3]);
    }
    


    Really uncomfortable? But you have to put up with it - after all, refactoring is not possible without testing. You always have to go over yourself first ... blah blah blah. Even I soon realized that it was impossible to use it. I understood in the area of ​​the second ten written tests. Tests are beloved and complex. But ...

    Difficult is good. After all, I can not do anything other than solving complexity. With a little effort, we got this solution:

    publicclassThreadTestManager
    {
    	publicThreadTestManager(TimeSpan timeout, params Action[] threads){…}
    	publicvoidRun(params BreakMark[] breaks){…}
    }
    publicclassBreakMark
    {
    	publicstring Name { get; privateset; }
    	public Action ThreadActor { get; privateset; }
    	publicbool Timeout { get; set; }
    	publicBreakMark(string breakName){…}
    	publicBreakMark(Action threadActor, string breakName){…}
    	publicstaticimplicitoperatorBreakMark(string breakName){…}
    }
    


    When using it, the previous test would look like this:

    [TestMethod]
    publicvoidStopStartThreadsTest_exemple2()
    {
    	var log = new List<string>();
    	Action act1 = () =>
    	{
    		Breakpoint.Define("before start1");
    		Breakpoint.Define("start1");
    		log.Add("after start1");
    		Breakpoint.Define("step act1");
    		log.Add("after step act1");
    		Breakpoint.Define("finish1");
    	};
    	Action act2 = () =>
    	{
    		Breakpoint.Define("before start2");
    		Breakpoint.Define("start2");
    		log.Add("after start2");
    		Breakpoint.Define("step act2");
    		log.Add("after step act2");
    		Breakpoint.Define("finish2");
    	};
    	new ThreadTestManager(TimeSpan.FromSeconds(1), act1, act2).Run(
    		"before start1", "before start2",
    		"start1", "step act1", "finish1",
    		"start2", "step act2", "finish2");
    	Assert.AreEqual(4, log.Count);
    	Assert.AreEqual("after start1", log[0]);
    	Assert.AreEqual("after step act1", log[1]);
    	Assert.AreEqual("after start2", log[2]);
    	Assert.AreEqual("after step act2", log[3]);
    }
    


    Manager Properties:
    1. All delegates are launched at startup in their thread.
    2. Breakpoint markers determine how to resume work. Not the entrance, but the exit from the breakpoints. Perhaps this is just the cost of implementing the breakpoint abstraction. But there is a property and it is sometimes necessary to remember about it.
    3. All controllers for the corresponding breakpoint markers are defined throughout the dispatcher's work.
    4. With a breakpoint marker, you can specify the thread (delegate) with which it will work. By default it works with everyone.

      [TestMethod]
      publicvoidThreadMarkBreakpointTest_exemple3()
      {
      	var log = new List<string>();
      	Action<string> act = name =>
      	{
      		Breakpoint.Define("start");
      		log.Add(name);
      		Breakpoint.Define("finish");
      	};
      	Action act0 = () => act("act0");
      	Action act1 = () => act("act1");
      	new ThreadTestManager(TimeSpan.FromSeconds(1), act0, act1).Run(
      		new BreakMark(act0, "finish"),
      		new BreakMark(act1, "start"),
      		new BreakMark(act1, "finish"));
      	Assert.AreEqual(2, log.Count);
      	Assert.AreEqual("act0", log[0]);
      	Assert.AreEqual("act1", log[1]);
      }
      
    5. Defined time during which all operations should be performed - timeout. When exceeded - all threads stop roughly and mercilessly (abort).
    6. To the breakpoint marker, you can add a sign of inaccessibility, if you do not get here, the system will exit on timeout. Triggering a breakpoint will result in failure of the test. This mechanism is used to verify the fact of blocking.

      [TestMethod]
      publicvoidTimeout_exemple4()
      {
      	var log = new List<string>();
      	Action act = () =>
      	{
      		try
      		{
      			while (true) ;
      		}
      		catch (ThreadAbortException)
      		{
      			log.Add("timeout");
      		}
      		Breakpoint.Define("don't work");
      	};
      	new ThreadTestManager(TimeSpan.FromSeconds(1), act).Run(
      		new BreakMark("don't work") { Timeout = true });
      	Assert.AreEqual("timeout", log.Single());
      }
      
    7. If you want to stop the flow and not continue, you must specify the appropriate breakpoint marker after the marker with timeout.

      [TestMethod]
      publicvoidCatchThread_exemple5()
      {
      	var log = new List<string>();
      	Action act0 = () =>
      	{
      		bool a = true;
      		while (a) ;
      		Breakpoint.Define("act0");
      	};
      	Action act1 = () =>
      	{
      		Breakpoint.Define("act1");
      		log.Add("after act1");
      	};
      	new ThreadTestManager(TimeSpan.FromSeconds(1), act0, act1).Run(
      		new BreakMark("act0") { Timeout = true },
      		"act1");
      	Assert.IsFalse(log.Any());
      }
      


    PS: Solution

    Also popular now: