* roottypes.cs: Rename from tree.cs.
[mono.git] / mcs / nunit20 / util / TestLoader.cs
1 #region Copyright (c) 2002-2003, James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole, Philip A. Craig
2 /************************************************************************************
3 '
4 ' Copyright  2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole
5 ' Copyright  2000-2002 Philip A. Craig
6 '
7 ' This software is provided 'as-is', without any express or implied warranty. In no 
8 ' event will the authors be held liable for any damages arising from the use of this 
9 ' software.
10
11 ' Permission is granted to anyone to use this software for any purpose, including 
12 ' commercial applications, and to alter it and redistribute it freely, subject to the 
13 ' following restrictions:
14 '
15 ' 1. The origin of this software must not be misrepresented; you must not claim that 
16 ' you wrote the original software. If you use this software in a product, an 
17 ' acknowledgment (see the following) in the product documentation is required.
18 '
19 ' Portions Copyright  2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole
20 ' or Copyright  2000-2002 Philip A. Craig
21 '
22 ' 2. Altered source versions must be plainly marked as such, and must not be 
23 ' misrepresented as being the original software.
24 '
25 ' 3. This notice may not be removed or altered from any source distribution.
26 '
27 '***********************************************************************************/
28 #endregion
29
30 namespace NUnit.Util
31 {
32         using System;
33         using System.IO;
34         using System.Collections;
35         using System.Configuration;
36         using System.Threading;
37         using NUnit.Core;
38
39
40         /// <summary>
41         /// TestLoader handles interactions between a test runner and a 
42         /// client program - typically the user interface - for the 
43         /// purpose of loading, unloading and running tests.
44         /// 
45         /// It implemements the EventListener interface which is used by 
46         /// the test runner and repackages those events, along with
47         /// others as individual events that clients may subscribe to
48         /// in collaboration with a TestEventDispatcher helper object.
49         /// 
50         /// TestLoader is quite handy for use with a gui client because
51         /// of the large number of events it supports. However, it has
52         /// no dependencies on ui components and can be used independently.
53         /// </summary>
54         public class TestLoader : LongLivingMarshalByRefObject, NUnit.Core.EventListener, ITestLoader
55         {
56                 #region Instance Variables
57
58                 /// <summary>
59                 /// StdOut stream for use by the TestRunner
60                 /// </summary>
61                 private TextWriter stdOutWriter;
62
63                 /// <summary>
64                 /// StdErr stream for use by the TestRunner
65                 /// </summary>
66                 private TextWriter stdErrWriter;
67
68                 /// <summary>
69                 /// Our event dispatiching helper object
70                 /// </summary>
71                 private ProjectEventDispatcher events;
72
73                 /// <summary>
74                 /// Loads and executes tests. Non-null when
75                 /// we have loaded a test.
76                 /// </summary>
77                 private TestDomain testDomain = null;
78
79                 /// <summary>
80                 /// Our current test project, if we have one.
81                 /// </summary>
82                 private NUnitProject testProject = null;
83
84                 /// <summary>
85                 /// The currently loaded test, returned by the testrunner
86                 /// </summary>
87                 private Test loadedTest = null;
88
89                 /// <summary>
90                 /// The test name that was specified when loading
91                 /// </summary>
92                 private string loadedTestName = null;
93
94                 /// <summary>
95                 /// The tests that are running
96                 /// </summary>
97                 private ITest[] runningTests = null;
98
99                 /// <summary>
100                 /// Result of the last test run
101                 /// </summary>
102                 private TestResult[] results = null;
103
104                 /// <summary>
105                 /// The last exception received when trying to load, unload or run a test
106                 /// </summary>
107                 private Exception lastException = null;
108
109                 /// <summary>
110                 /// Watcher fires when the assembly changes
111                 /// </summary>
112                 private AssemblyWatcher watcher;
113
114                 /// <summary>
115                 /// Assembly changed during a test and
116                 /// needs to be reloaded later
117                 /// </summary>
118                 private bool reloadPending = false;
119
120                 /// <summary>
121                 /// Indicates whether to watch for changes
122                 /// and reload the tests when a change occurs.
123                 /// </summary>
124                 private bool reloadOnChange = false;
125
126                 /// <summary>
127                 /// Indicates whether to reload the tests
128                 /// before each run.
129                 /// </summary>
130                 private bool reloadOnRun = false;
131
132                 private IFilter filter;
133
134                 #endregion
135
136                 #region Constructor
137
138                 public TestLoader(TextWriter stdOutWriter, TextWriter stdErrWriter )
139                 {
140                         this.stdOutWriter = stdOutWriter;
141                         this.stdErrWriter = stdErrWriter;
142                         this.events = new ProjectEventDispatcher();
143                 }
144
145                 #endregion
146
147                 #region Properties
148
149                 public bool IsProjectLoaded
150                 {
151                         get { return testProject != null; }
152                 }
153
154                 public bool IsTestLoaded
155                 {
156                         get { return loadedTest != null; }
157                 }
158
159                 public bool IsTestRunning
160                 {
161                         get { return runningTests != null; }
162                 }
163
164                 public NUnitProject TestProject
165                 {
166                         get { return testProject; }
167                         set     { OnProjectLoad( value ); }
168                 }
169
170                 public IProjectEvents Events
171                 {
172                         get { return events; }
173                 }
174
175                 public string TestFileName
176                 {
177                         get { return testProject.ProjectPath; }
178                 }
179
180                 public TestResult[] Results
181                 {
182                         get { return results; }
183                 }
184
185                 public Exception LastException
186                 {
187                         get { return lastException; }
188                 }
189
190                 public bool ReloadOnChange
191                 {
192                         get { return reloadOnChange; }
193                         set { reloadOnChange = value; }
194                 }
195
196                 public bool ReloadOnRun
197                 {
198                         get { return reloadOnRun; }
199                         set { reloadOnRun = value; }
200                 }
201
202                 public Version FrameworkVersion
203                 {
204                         get { return this.testDomain.FrameworkVersion; }
205                 }
206
207                 #endregion
208
209                 #region EventListener Handlers
210
211                 void EventListener.RunStarted(Test[] tests)
212                 {
213                         int count = 0;
214                         foreach( Test test in tests )
215                                 count += filter == null ? test.CountTestCases() : test.CountTestCases( filter );
216
217                         events.FireRunStarting( tests, count );
218                 }
219
220                 void EventListener.RunFinished(NUnit.Core.TestResult[] results)
221                 {
222                         this.results = results;
223                         events.FireRunFinished( results );
224                         runningTests = null;
225                 }
226
227                 void EventListener.RunFinished(Exception exception)
228                 {
229                         this.lastException = exception;
230                         events.FireRunFinished( exception );
231                         runningTests = null;
232                 }
233
234                 /// <summary>
235                 /// Trigger event when each test starts
236                 /// </summary>
237                 /// <param name="testCase">TestCase that is starting</param>
238                 void EventListener.TestStarted(NUnit.Core.TestCase testCase)
239                 {
240                         events.FireTestStarting( testCase );
241                 }
242
243                 /// <summary>
244                 /// Trigger event when each test finishes
245                 /// </summary>
246                 /// <param name="result">Result of the case that finished</param>
247                 void EventListener.TestFinished(TestCaseResult result)
248                 {
249                         events.FireTestFinished( result );
250                 }
251
252                 /// <summary>
253                 /// Trigger event when each suite starts
254                 /// </summary>
255                 /// <param name="suite">Suite that is starting</param>
256                 void EventListener.SuiteStarted(TestSuite suite)
257                 {
258                         events.FireSuiteStarting( suite );
259                 }
260
261                 /// <summary>
262                 /// Trigger event when each suite finishes
263                 /// </summary>
264                 /// <param name="result">Result of the suite that finished</param>
265                 void EventListener.SuiteFinished(TestSuiteResult result)
266                 {
267                         events.FireSuiteFinished( result );
268                 }
269
270                 /// <summary>
271                 /// Trigger event when an unhandled exception occurs during a test
272                 /// </summary>
273                 /// <param name="exception">The unhandled exception</param>
274                 void EventListener.UnhandledException(Exception exception)
275                 {
276                         events.FireTestException( exception );
277                 }
278
279                 #endregion
280
281                 #region Methods for Loading and Unloading Projects
282                 
283                 /// <summary>
284                 /// Create a new project with default naming
285                 /// </summary>
286                 public void NewProject()
287                 {
288                         try
289                         {
290                                 events.FireProjectLoading( "New Project" );
291
292                                 OnProjectLoad( NUnitProject.NewProject() );
293                         }
294                         catch( Exception exception )
295                         {
296                                 lastException = exception;
297                                 events.FireProjectLoadFailed( "New Project", exception );
298                         }
299                 }
300
301                 /// <summary>
302                 /// Create a new project using a given path
303                 /// </summary>
304                 public void NewProject( string filePath )
305                 {
306                         try
307                         {
308                                 events.FireProjectLoading( filePath );
309
310                                 NUnitProject project = new NUnitProject( filePath );
311
312                                 project.Configs.Add( "Debug" );
313                                 project.Configs.Add( "Release" );                       
314                                 project.IsDirty = false;
315
316                                 OnProjectLoad( project );
317                         }
318                         catch( Exception exception )
319                         {
320                                 lastException = exception;
321                                 events.FireProjectLoadFailed( filePath, exception );
322                         }
323                 }
324
325                 /// <summary>
326                 /// Load a new project, optionally selecting the config and fire events
327                 /// </summary>
328                 public void LoadProject( string filePath, string configName )
329                 {
330                         try
331                         {
332                                 events.FireProjectLoading( filePath );
333
334                                 NUnitProject newProject = NUnitProject.LoadProject( filePath );
335                                 if ( configName != null ) 
336                                 {
337                                         newProject.SetActiveConfig( configName );
338                                         newProject.IsDirty = false;
339                                 }
340
341                                 OnProjectLoad( newProject );
342
343 //                              return true;
344                         }
345                         catch( Exception exception )
346                         {
347                                 lastException = exception;
348                                 events.FireProjectLoadFailed( filePath, exception );
349
350 //                              return false;
351                         }
352                 }
353
354                 /// <summary>
355                 /// Load a new project using the default config and fire events
356                 /// </summary>
357                 public void LoadProject( string filePath )
358                 {
359                         LoadProject( filePath, null );
360                 }
361
362                 /// <summary>
363                 /// Load a project from a list of assemblies and fire events
364                 /// </summary>
365                 public void LoadProject( string[] assemblies )
366                 {
367                         try
368                         {
369                                 events.FireProjectLoading( "New Project" );
370
371                                 NUnitProject newProject = NUnitProject.FromAssemblies( assemblies );
372
373                                 OnProjectLoad( newProject );
374
375 //                              return true;
376                         }
377                         catch( Exception exception )
378                         {
379                                 lastException = exception;
380                                 events.FireProjectLoadFailed( "New Project", exception );
381
382 //                              return false;
383                         }
384                 }
385
386                 /// <summary>
387                 /// Unload the current project and fire events
388                 /// </summary>
389                 public void UnloadProject()
390                 {
391                         string testFileName = TestFileName;
392
393                         try
394                         {
395                                 events.FireProjectUnloading( testFileName );
396
397 //                              if ( testFileName != null && File.Exists( testFileName ) )
398 //                                      UserSettings.RecentProjects.RecentFile = testFileName;
399
400                                 if ( IsTestLoaded )
401                                         UnloadTest();
402
403                                 testProject.Changed -= new ProjectEventHandler( OnProjectChanged );
404                                 testProject = null;
405
406                                 events.FireProjectUnloaded( testFileName );
407                         }
408                         catch (Exception exception )
409                         {
410                                 lastException = exception;
411                                 events.FireProjectUnloadFailed( testFileName, exception );
412                         }
413
414                 }
415
416                 /// <summary>
417                 /// Common operations done each time a project is loaded
418                 /// </summary>
419                 /// <param name="testProject">The newly loaded project</param>
420                 private void OnProjectLoad( NUnitProject testProject )
421                 {
422                         if ( IsProjectLoaded )
423                                 UnloadProject();
424
425                         this.testProject = testProject;
426                         testProject.Changed += new ProjectEventHandler( OnProjectChanged );
427
428                         events.FireProjectLoaded( TestFileName );
429                 }
430
431                 private void OnProjectChanged( object sender, ProjectEventArgs e )
432                 {
433                         switch ( e.type )
434                         {
435                                 case ProjectChangeType.ActiveConfig:
436                                         if( TestProject.IsLoadable )
437                                                 LoadTest();
438                                         break;
439
440                                 case ProjectChangeType.AddConfig:
441                                 case ProjectChangeType.UpdateConfig:
442                                         if ( e.configName == TestProject.ActiveConfigName && TestProject.IsLoadable )
443                                                 LoadTest();
444                                         break;
445
446                                 case ProjectChangeType.RemoveConfig:
447                                         if ( IsTestLoaded && TestProject.Configs.Count == 0 )
448                                                 UnloadTest();
449                                         break;
450
451                                 default:
452                                         break;
453                         }
454                 }
455
456                 #endregion
457
458                 #region Methods for Loading and Unloading Tests
459
460                 public void LoadTest()
461                 {
462                         LoadTest( null );
463                 }
464                 
465                 public void LoadTest( string testName )
466                 {
467                         try
468                         {
469                                 events.FireTestLoading( TestFileName );
470
471                                 testDomain = new TestDomain( stdOutWriter, stdErrWriter );              
472                                 Test test = testDomain.Load( TestProject, testName );
473
474                                 TestSuite suite = test as TestSuite;
475                                 if ( suite != null )
476                                         suite.Sort();
477                         
478                                 loadedTest = test;
479                                 loadedTestName = testName;
480                                 results = null;
481                                 reloadPending = false;
482                         
483                                 if ( ReloadOnChange )
484                                         InstallWatcher( );
485
486                                 if ( suite != null )
487                                         events.FireTestLoaded( TestFileName, this.loadedTest );
488                                 else
489                                 {
490                                         lastException = new ApplicationException( string.Format ( "Unable to find test {0} in assembly", testName ) );
491                                         events.FireTestLoadFailed( TestFileName, lastException );
492                                 }
493                         }
494                         catch( FileNotFoundException exception )
495                         {
496                                 lastException = exception;
497
498                                 foreach( string assembly in TestProject.ActiveConfig.AbsolutePaths )
499                                 {
500                                         if ( Path.GetFileNameWithoutExtension( assembly ) == exception.FileName &&
501                                                 !ProjectPath.SamePathOrUnder( testProject.ActiveConfig.BasePath, assembly ) )
502                                         {
503                                                 lastException = new ApplicationException( string.Format( "Unable to load {0} because it is not located under the AppBase", exception.FileName ), exception );
504                                                 break;
505                                         }
506                                 }
507
508                                 events.FireTestLoadFailed( TestFileName, lastException );
509                         }
510                         catch( Exception exception )
511                         {
512                                 lastException = exception;
513                                 events.FireTestLoadFailed( TestFileName, exception );
514                         }
515                 }
516
517                 /// <summary>
518                 /// Unload the current test suite and fire the Unloaded event
519                 /// </summary>
520                 public void UnloadTest( )
521                 {
522                         if( IsTestLoaded )
523                         {
524                                 // Hold the name for notifications after unload
525                                 string fileName = TestFileName;
526
527                                 try
528                                 {
529                                         events.FireTestUnloading( TestFileName, this.loadedTest );
530
531                                         RemoveWatcher();
532
533                                         testDomain.Unload();
534
535                                         testDomain = null;
536
537                                         loadedTest = null;
538                                         loadedTestName = null;
539                                         results = null;
540                                         reloadPending = false;
541
542                                         events.FireTestUnloaded( fileName, this.loadedTest );
543                                 }
544                                 catch( Exception exception )
545                                 {
546                                         lastException = exception;
547                                         events.FireTestUnloadFailed( fileName, exception );
548                                 }
549                         }
550                 }
551
552                 /// <summary>
553                 /// Reload the current test on command
554                 /// </summary>
555                 public void ReloadTest()
556                 {
557                         OnTestChanged( TestFileName );
558                 }
559
560                 /// <summary>
561                 /// Handle watcher event that signals when the loaded assembly
562                 /// file has changed. Make sure it's a real change before
563                 /// firing the SuiteChangedEvent. Since this all happens
564                 /// asynchronously, we use an event to let ui components
565                 /// know that the failure happened.
566                 /// </summary>
567                 /// <param name="assemblyFileName">Assembly file that changed</param>
568                 public void OnTestChanged( string testFileName )
569                 {
570                         if ( IsTestRunning )
571                                 reloadPending = true;
572                         else 
573                                 try
574                                 {
575                                         events.FireTestReloading( testFileName, this.loadedTest );
576
577                                         // Don't unload the old domain till after the event
578                                         // handlers get a chance to compare the trees.
579                                         TestDomain newDomain = new TestDomain( stdOutWriter, stdErrWriter );
580                                         Test newTest = newDomain.Load( testProject, loadedTestName );
581                                         TestSuite suite = newTest as TestSuite;
582                                         if ( suite != null )
583                                                 suite.Sort();
584
585                                         testDomain.Unload();
586
587                                         testDomain = newDomain;
588                                         loadedTest = newTest;
589                                         reloadPending = false;
590
591                                         events.FireTestReloaded( testFileName, newTest );                               
592                                 }
593                                 catch( Exception exception )
594                                 {
595                                         lastException = exception;
596                                         events.FireTestReloadFailed( testFileName, exception );
597                                 }
598                 }
599
600                 #endregion
601
602                 #region Methods for Running Tests
603
604                 public void SetFilter( IFilter filter )
605                 {
606                         this.filter = filter;
607                 }
608
609                 /// <summary>
610                 /// Run the currently loaded top level test suite
611                 /// </summary>
612                 public void RunLoadedTest()
613                 {
614                         RunTest( loadedTest );
615                 }
616
617                 /// <summary>
618                 /// Run a testcase or testsuite from the currrent tree
619                 /// firing the RunStarting and RunFinished events.
620                 /// Silently ignore the call if a test is running
621                 /// to allow for latency in the UI.
622                 /// </summary>
623                 /// <param name="testName">The test to be run</param>
624                 public void RunTest( ITest test )
625                 {
626                         RunTests( new ITest[] { test } );
627                 }
628
629                 public void RunTests( ITest[] tests )
630                 {
631                         if ( !IsTestRunning )
632                         {
633                                 if ( reloadPending || ReloadOnRun )
634                                         ReloadTest();
635
636                                 runningTests = tests;
637
638                                 //kind of silly
639                                 string[] testNames = new string[ runningTests.Length ];
640                                 int index = 0; 
641                                 foreach (ITest node in runningTests) 
642                                         testNames[index++] = node.UniqueName;
643
644                                 testDomain.SetFilter( filter );
645 //                              testDomain.DisplayTestLabels = UserSettings.Options.TestLabels;
646                                 testDomain.RunTest( this, testNames );
647                         }
648                 }
649
650                 /// <summary>
651                 /// Cancel the currently running test.
652                 /// Fail silently if there is none to
653                 /// allow for latency in the UI.
654                 /// </summary>
655                 public void CancelTestRun()
656                 {
657                         if ( IsTestRunning )
658                                 testDomain.CancelRun();
659                 }
660
661                 public IList GetCategories() 
662                 {
663                         ArrayList list = new ArrayList();
664                         list.AddRange(testDomain.GetCategories());
665                         return list;
666                 }
667
668                 #endregion
669
670                 #region Helper Methods
671
672                 /// <summary>
673                 /// Install our watcher object so as to get notifications
674                 /// about changes to a test.
675                 /// </summary>
676                 /// <param name="assemblyFileName">Full path of the assembly to watch</param>
677                 private void InstallWatcher()
678                 {
679                         if(watcher!=null) watcher.Stop();
680
681                         watcher = new AssemblyWatcher( 1000, TestProject.ActiveConfig.AbsolutePaths );
682                         watcher.AssemblyChangedEvent += new AssemblyWatcher.AssemblyChangedHandler( OnTestChanged );
683                         watcher.Start();
684                 }
685
686                 /// <summary>
687                 /// Stop and remove our current watcher object.
688                 /// </summary>
689                 private void RemoveWatcher()
690                 {
691                         if ( watcher != null )
692                         {
693                                 watcher.Stop();
694                                 watcher = null;
695                         }
696                 }
697
698                 #endregion
699         }
700 }