This commit was manufactured by cvs2svn to create branch 'mono-1-0'.
[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 !MONO
398                                 if ( testFileName != null && File.Exists( testFileName ) )
399                                         UserSettings.RecentProjects.RecentFile = testFileName;
400 #endif
401
402                                 if ( IsTestLoaded )
403                                         UnloadTest();
404
405                                 testProject.Changed -= new ProjectEventHandler( OnProjectChanged );
406                                 testProject = null;
407
408                                 events.FireProjectUnloaded( testFileName );
409                         }
410                         catch (Exception exception )
411                         {
412                                 lastException = exception;
413                                 events.FireProjectUnloadFailed( testFileName, exception );
414                         }
415
416                 }
417
418                 /// <summary>
419                 /// Common operations done each time a project is loaded
420                 /// </summary>
421                 /// <param name="testProject">The newly loaded project</param>
422                 private void OnProjectLoad( NUnitProject testProject )
423                 {
424                         if ( IsProjectLoaded )
425                                 UnloadProject();
426
427                         this.testProject = testProject;
428                         testProject.Changed += new ProjectEventHandler( OnProjectChanged );
429
430                         events.FireProjectLoaded( TestFileName );
431                 }
432
433                 private void OnProjectChanged( object sender, ProjectEventArgs e )
434                 {
435                         switch ( e.type )
436                         {
437                                 case ProjectChangeType.ActiveConfig:
438                                         if( TestProject.IsLoadable )
439                                                 LoadTest();
440                                         break;
441
442                                 case ProjectChangeType.AddConfig:
443                                 case ProjectChangeType.UpdateConfig:
444                                         if ( e.configName == TestProject.ActiveConfigName && TestProject.IsLoadable )
445                                                 LoadTest();
446                                         break;
447
448                                 case ProjectChangeType.RemoveConfig:
449                                         if ( IsTestLoaded && TestProject.Configs.Count == 0 )
450                                                 UnloadTest();
451                                         break;
452
453                                 default:
454                                         break;
455                         }
456                 }
457
458                 #endregion
459
460                 #region Methods for Loading and Unloading Tests
461
462                 public void LoadTest()
463                 {
464                         LoadTest( null );
465                 }
466                 
467                 public void LoadTest( string testName )
468                 {
469                         try
470                         {
471                                 events.FireTestLoading( TestFileName );
472
473                                 testDomain = new TestDomain( stdOutWriter, stdErrWriter );              
474                                 Test test = testDomain.Load( TestProject, testName );
475
476                                 TestSuite suite = test as TestSuite;
477                                 if ( suite != null )
478                                         suite.Sort();
479                         
480                                 loadedTest = test;
481                                 loadedTestName = testName;
482                                 results = null;
483                                 reloadPending = false;
484                         
485                                 if ( ReloadOnChange )
486                                         InstallWatcher( );
487
488                                 events.FireTestLoaded( TestFileName, this.loadedTest );
489                         }
490                         catch( FileNotFoundException exception )
491                         {
492                                 lastException = exception;
493
494                                 foreach( string assembly in TestProject.ActiveConfig.AbsolutePaths )
495                                 {
496                                         if ( Path.GetFileNameWithoutExtension( assembly ) == exception.FileName &&
497                                                 !ProjectPath.SamePathOrUnder( testProject.ActiveConfig.BasePath, assembly ) )
498                                         {
499                                                 lastException = new ApplicationException( string.Format( "Unable to load {0} because it is not located under the AppBase", exception.FileName ), exception );
500                                                 break;
501                                         }
502                                 }
503
504                                 events.FireTestLoadFailed( TestFileName, lastException );
505                         }
506                         catch( Exception exception )
507                         {
508                                 lastException = exception;
509                                 events.FireTestLoadFailed( TestFileName, exception );
510                         }
511                 }
512
513                 /// <summary>
514                 /// Unload the current test suite and fire the Unloaded event
515                 /// </summary>
516                 public void UnloadTest( )
517                 {
518                         if( IsTestLoaded )
519                         {
520                                 // Hold the name for notifications after unload
521                                 string fileName = TestFileName;
522
523                                 try
524                                 {
525                                         events.FireTestUnloading( TestFileName, this.loadedTest );
526
527                                         RemoveWatcher();
528
529                                         testDomain.Unload();
530
531                                         testDomain = null;
532
533                                         loadedTest = null;
534                                         loadedTestName = null;
535                                         results = null;
536                                         reloadPending = false;
537
538                                         events.FireTestUnloaded( fileName, this.loadedTest );
539                                 }
540                                 catch( Exception exception )
541                                 {
542                                         lastException = exception;
543                                         events.FireTestUnloadFailed( fileName, exception );
544                                 }
545                         }
546                 }
547
548                 /// <summary>
549                 /// Reload the current test on command
550                 /// </summary>
551                 public void ReloadTest()
552                 {
553                         OnTestChanged( TestFileName );
554                 }
555
556                 /// <summary>
557                 /// Handle watcher event that signals when the loaded assembly
558                 /// file has changed. Make sure it's a real change before
559                 /// firing the SuiteChangedEvent. Since this all happens
560                 /// asynchronously, we use an event to let ui components
561                 /// know that the failure happened.
562                 /// </summary>
563                 /// <param name="assemblyFileName">Assembly file that changed</param>
564                 public void OnTestChanged( string testFileName )
565                 {
566                         if ( IsTestRunning )
567                                 reloadPending = true;
568                         else 
569                                 try
570                                 {
571                                         events.FireTestReloading( testFileName, this.loadedTest );
572
573                                         // Don't unload the old domain till after the event
574                                         // handlers get a chance to compare the trees.
575                                         TestDomain newDomain = new TestDomain( stdOutWriter, stdErrWriter );
576                                         Test newTest = newDomain.Load( testProject, loadedTestName );
577                                         TestSuite suite = newTest as TestSuite;
578                                         if ( suite != null )
579                                                 suite.Sort();
580
581                                         testDomain.Unload();
582
583                                         testDomain = newDomain;
584                                         loadedTest = newTest;
585                                         reloadPending = false;
586
587                                         events.FireTestReloaded( testFileName, newTest );                               
588                                 }
589                                 catch( Exception exception )
590                                 {
591                                         lastException = exception;
592                                         events.FireTestReloadFailed( testFileName, exception );
593                                 }
594                 }
595
596                 #endregion
597
598                 #region Methods for Running Tests
599
600                 public void SetFilter( IFilter filter )
601                 {
602                         this.filter = filter;
603                 }
604
605                 /// <summary>
606                 /// Run the currently loaded top level test suite
607                 /// </summary>
608                 public void RunLoadedTest()
609                 {
610                         RunTest( loadedTest );
611                 }
612
613                 /// <summary>
614                 /// Run a testcase or testsuite from the currrent tree
615                 /// firing the RunStarting and RunFinished events.
616                 /// Silently ignore the call if a test is running
617                 /// to allow for latency in the UI.
618                 /// </summary>
619                 /// <param name="testName">The test to be run</param>
620                 public void RunTest( ITest test )
621                 {
622                         RunTests( new ITest[] { test } );
623                 }
624
625                 public void RunTests( ITest[] tests )
626                 {
627                         if ( !IsTestRunning )
628                         {
629                                 if ( reloadPending || ReloadOnRun )
630                                         ReloadTest();
631
632                                 runningTests = tests;
633
634                                 //kind of silly
635                                 string[] testNames = new string[ runningTests.Length ];
636                                 int index = 0; 
637                                 foreach (ITest node in runningTests) 
638                                         testNames[index++] = node.UniqueName;
639
640                                 testDomain.SetFilter( filter );
641 #if !MONO
642                                 testDomain.DisplayTestLabels = UserSettings.Options.TestLabels;
643 #endif
644                                 testDomain.RunTest( this, testNames );
645                         }
646                 }
647
648                 /// <summary>
649                 /// Cancel the currently running test.
650                 /// Fail silently if there is none to
651                 /// allow for latency in the UI.
652                 /// </summary>
653                 public void CancelTestRun()
654                 {
655                         if ( IsTestRunning )
656                                 testDomain.CancelRun();
657                 }
658
659                 public IList GetCategories() 
660                 {
661                         ArrayList list = new ArrayList();
662                         list.AddRange(testDomain.GetCategories());
663                         return list;
664                 }
665
666                 #endregion
667
668                 #region Helper Methods
669
670                 /// <summary>
671                 /// Install our watcher object so as to get notifications
672                 /// about changes to a test.
673                 /// </summary>
674                 /// <param name="assemblyFileName">Full path of the assembly to watch</param>
675                 private void InstallWatcher()
676                 {
677                         if(watcher!=null) watcher.Stop();
678
679                         watcher = new AssemblyWatcher( 1000, TestProject.ActiveConfig.AbsolutePaths );
680                         watcher.AssemblyChangedEvent += new AssemblyWatcher.AssemblyChangedHandler( OnTestChanged );
681                         watcher.Start();
682                 }
683
684                 /// <summary>
685                 /// Stop and remove our current watcher object.
686                 /// </summary>
687                 private void RemoveWatcher()
688                 {
689                         if ( watcher != null )
690                         {
691                                 watcher.Stop();
692                                 watcher = null;
693                         }
694                 }
695
696                 #endregion
697         }
698 }