1 #region Copyright (c) 2002-2003, James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole, Philip A. Craig
2 /************************************************************************************
4 ' Copyright 2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole
5 ' Copyright 2000-2002 Philip A. Craig
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
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:
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.
19 ' Portions Copyright 2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole
20 ' or Copyright 2000-2002 Philip A. Craig
22 ' 2. Altered source versions must be plainly marked as such, and must not be
23 ' misrepresented as being the original software.
25 ' 3. This notice may not be removed or altered from any source distribution.
27 '***********************************************************************************/
34 using System.Collections;
35 using System.Configuration;
36 using System.Threading;
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.
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.
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.
54 public class TestLoader : LongLivingMarshalByRefObject, NUnit.Core.EventListener, ITestLoader
56 #region Instance Variables
59 /// StdOut stream for use by the TestRunner
61 private TextWriter stdOutWriter;
64 /// StdErr stream for use by the TestRunner
66 private TextWriter stdErrWriter;
69 /// Our event dispatiching helper object
71 private ProjectEventDispatcher events;
74 /// Loads and executes tests. Non-null when
75 /// we have loaded a test.
77 private TestDomain testDomain = null;
80 /// Our current test project, if we have one.
82 private NUnitProject testProject = null;
85 /// The currently loaded test, returned by the testrunner
87 private Test loadedTest = null;
90 /// The test name that was specified when loading
92 private string loadedTestName = null;
95 /// The tests that are running
97 private ITest[] runningTests = null;
100 /// Result of the last test run
102 private TestResult[] results = null;
105 /// The last exception received when trying to load, unload or run a test
107 private Exception lastException = null;
110 /// Watcher fires when the assembly changes
112 private AssemblyWatcher watcher;
115 /// Assembly changed during a test and
116 /// needs to be reloaded later
118 private bool reloadPending = false;
121 /// Indicates whether to watch for changes
122 /// and reload the tests when a change occurs.
124 private bool reloadOnChange = false;
127 /// Indicates whether to reload the tests
130 private bool reloadOnRun = false;
132 private IFilter filter;
138 public TestLoader(TextWriter stdOutWriter, TextWriter stdErrWriter )
140 this.stdOutWriter = stdOutWriter;
141 this.stdErrWriter = stdErrWriter;
142 this.events = new ProjectEventDispatcher();
149 public bool IsProjectLoaded
151 get { return testProject != null; }
154 public bool IsTestLoaded
156 get { return loadedTest != null; }
159 public bool IsTestRunning
161 get { return runningTests != null; }
164 public NUnitProject TestProject
166 get { return testProject; }
167 set { OnProjectLoad( value ); }
170 public IProjectEvents Events
172 get { return events; }
175 public string TestFileName
177 get { return testProject.ProjectPath; }
180 public TestResult[] Results
182 get { return results; }
185 public Exception LastException
187 get { return lastException; }
190 public bool ReloadOnChange
192 get { return reloadOnChange; }
193 set { reloadOnChange = value; }
196 public bool ReloadOnRun
198 get { return reloadOnRun; }
199 set { reloadOnRun = value; }
202 public Version FrameworkVersion
204 get { return this.testDomain.FrameworkVersion; }
209 #region EventListener Handlers
211 void EventListener.RunStarted(Test[] tests)
214 foreach( Test test in tests )
215 count += filter == null ? test.CountTestCases() : test.CountTestCases( filter );
217 events.FireRunStarting( tests, count );
220 void EventListener.RunFinished(NUnit.Core.TestResult[] results)
222 this.results = results;
223 events.FireRunFinished( results );
227 void EventListener.RunFinished(Exception exception)
229 this.lastException = exception;
230 events.FireRunFinished( exception );
235 /// Trigger event when each test starts
237 /// <param name="testCase">TestCase that is starting</param>
238 void EventListener.TestStarted(NUnit.Core.TestCase testCase)
240 events.FireTestStarting( testCase );
244 /// Trigger event when each test finishes
246 /// <param name="result">Result of the case that finished</param>
247 void EventListener.TestFinished(TestCaseResult result)
249 events.FireTestFinished( result );
253 /// Trigger event when each suite starts
255 /// <param name="suite">Suite that is starting</param>
256 void EventListener.SuiteStarted(TestSuite suite)
258 events.FireSuiteStarting( suite );
262 /// Trigger event when each suite finishes
264 /// <param name="result">Result of the suite that finished</param>
265 void EventListener.SuiteFinished(TestSuiteResult result)
267 events.FireSuiteFinished( result );
271 /// Trigger event when an unhandled exception occurs during a test
273 /// <param name="exception">The unhandled exception</param>
274 void EventListener.UnhandledException(Exception exception)
276 events.FireTestException( exception );
281 #region Methods for Loading and Unloading Projects
284 /// Create a new project with default naming
286 public void NewProject()
290 events.FireProjectLoading( "New Project" );
292 OnProjectLoad( NUnitProject.NewProject() );
294 catch( Exception exception )
296 lastException = exception;
297 events.FireProjectLoadFailed( "New Project", exception );
302 /// Create a new project using a given path
304 public void NewProject( string filePath )
308 events.FireProjectLoading( filePath );
310 NUnitProject project = new NUnitProject( filePath );
312 project.Configs.Add( "Debug" );
313 project.Configs.Add( "Release" );
314 project.IsDirty = false;
316 OnProjectLoad( project );
318 catch( Exception exception )
320 lastException = exception;
321 events.FireProjectLoadFailed( filePath, exception );
326 /// Load a new project, optionally selecting the config and fire events
328 public void LoadProject( string filePath, string configName )
332 events.FireProjectLoading( filePath );
334 NUnitProject newProject = NUnitProject.LoadProject( filePath );
335 if ( configName != null )
337 newProject.SetActiveConfig( configName );
338 newProject.IsDirty = false;
341 OnProjectLoad( newProject );
345 catch( Exception exception )
347 lastException = exception;
348 events.FireProjectLoadFailed( filePath, exception );
355 /// Load a new project using the default config and fire events
357 public void LoadProject( string filePath )
359 LoadProject( filePath, null );
363 /// Load a project from a list of assemblies and fire events
365 public void LoadProject( string[] assemblies )
369 events.FireProjectLoading( "New Project" );
371 NUnitProject newProject = NUnitProject.FromAssemblies( assemblies );
373 OnProjectLoad( newProject );
377 catch( Exception exception )
379 lastException = exception;
380 events.FireProjectLoadFailed( "New Project", exception );
387 /// Unload the current project and fire events
389 public void UnloadProject()
391 string testFileName = TestFileName;
395 events.FireProjectUnloading( testFileName );
397 // if ( testFileName != null && File.Exists( testFileName ) )
398 // UserSettings.RecentProjects.RecentFile = testFileName;
403 testProject.Changed -= new ProjectEventHandler( OnProjectChanged );
406 events.FireProjectUnloaded( testFileName );
408 catch (Exception exception )
410 lastException = exception;
411 events.FireProjectUnloadFailed( testFileName, exception );
417 /// Common operations done each time a project is loaded
419 /// <param name="testProject">The newly loaded project</param>
420 private void OnProjectLoad( NUnitProject testProject )
422 if ( IsProjectLoaded )
425 this.testProject = testProject;
426 testProject.Changed += new ProjectEventHandler( OnProjectChanged );
428 events.FireProjectLoaded( TestFileName );
431 private void OnProjectChanged( object sender, ProjectEventArgs e )
435 case ProjectChangeType.ActiveConfig:
436 if( TestProject.IsLoadable )
440 case ProjectChangeType.AddConfig:
441 case ProjectChangeType.UpdateConfig:
442 if ( e.configName == TestProject.ActiveConfigName && TestProject.IsLoadable )
446 case ProjectChangeType.RemoveConfig:
447 if ( IsTestLoaded && TestProject.Configs.Count == 0 )
458 #region Methods for Loading and Unloading Tests
460 public void LoadTest()
465 public void LoadTest( string testName )
469 events.FireTestLoading( TestFileName );
471 testDomain = new TestDomain( stdOutWriter, stdErrWriter );
472 Test test = testDomain.Load( TestProject, testName );
474 TestSuite suite = test as TestSuite;
479 loadedTestName = testName;
481 reloadPending = false;
483 if ( ReloadOnChange )
487 events.FireTestLoaded( TestFileName, this.loadedTest );
490 lastException = new ApplicationException( string.Format ( "Unable to find test {0} in assembly", testName ) );
491 events.FireTestLoadFailed( TestFileName, lastException );
494 catch( FileNotFoundException exception )
496 lastException = exception;
498 foreach( string assembly in TestProject.ActiveConfig.AbsolutePaths )
500 if ( Path.GetFileNameWithoutExtension( assembly ) == exception.FileName &&
501 !ProjectPath.SamePathOrUnder( testProject.ActiveConfig.BasePath, assembly ) )
503 lastException = new ApplicationException( string.Format( "Unable to load {0} because it is not located under the AppBase", exception.FileName ), exception );
508 events.FireTestLoadFailed( TestFileName, lastException );
510 catch( Exception exception )
512 lastException = exception;
513 events.FireTestLoadFailed( TestFileName, exception );
518 /// Unload the current test suite and fire the Unloaded event
520 public void UnloadTest( )
524 // Hold the name for notifications after unload
525 string fileName = TestFileName;
529 events.FireTestUnloading( TestFileName, this.loadedTest );
538 loadedTestName = null;
540 reloadPending = false;
542 events.FireTestUnloaded( fileName, this.loadedTest );
544 catch( Exception exception )
546 lastException = exception;
547 events.FireTestUnloadFailed( fileName, exception );
553 /// Reload the current test on command
555 public void ReloadTest()
557 OnTestChanged( TestFileName );
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.
567 /// <param name="assemblyFileName">Assembly file that changed</param>
568 public void OnTestChanged( string testFileName )
571 reloadPending = true;
575 events.FireTestReloading( testFileName, this.loadedTest );
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;
587 testDomain = newDomain;
588 loadedTest = newTest;
589 reloadPending = false;
591 events.FireTestReloaded( testFileName, newTest );
593 catch( Exception exception )
595 lastException = exception;
596 events.FireTestReloadFailed( testFileName, exception );
602 #region Methods for Running Tests
604 public void SetFilter( IFilter filter )
606 this.filter = filter;
610 /// Run the currently loaded top level test suite
612 public void RunLoadedTest()
614 RunTest( loadedTest );
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.
623 /// <param name="testName">The test to be run</param>
624 public void RunTest( ITest test )
626 RunTests( new ITest[] { test } );
629 public void RunTests( ITest[] tests )
631 if ( !IsTestRunning )
633 if ( reloadPending || ReloadOnRun )
636 runningTests = tests;
639 string[] testNames = new string[ runningTests.Length ];
641 foreach (ITest node in runningTests)
642 testNames[index++] = node.UniqueName;
644 testDomain.SetFilter( filter );
645 // testDomain.DisplayTestLabels = UserSettings.Options.TestLabels;
646 testDomain.RunTest( this, testNames );
651 /// Cancel the currently running test.
652 /// Fail silently if there is none to
653 /// allow for latency in the UI.
655 public void CancelTestRun()
658 testDomain.CancelRun();
661 public IList GetCategories()
663 ArrayList list = new ArrayList();
664 list.AddRange(testDomain.GetCategories());
670 #region Helper Methods
673 /// Install our watcher object so as to get notifications
674 /// about changes to a test.
676 /// <param name="assemblyFileName">Full path of the assembly to watch</param>
677 private void InstallWatcher()
679 if(watcher!=null) watcher.Stop();
681 watcher = new AssemblyWatcher( 1000, TestProject.ActiveConfig.AbsolutePaths );
682 watcher.AssemblyChangedEvent += new AssemblyWatcher.AssemblyChangedHandler( OnTestChanged );
687 /// Stop and remove our current watcher object.
689 private void RemoveWatcher()
691 if ( watcher != null )