#region Copyright (c) 2002-2003, James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole, Philip A. Craig /************************************************************************************ ' ' Copyright 2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole ' Copyright 2000-2002 Philip A. Craig ' ' This software is provided 'as-is', without any express or implied warranty. In no ' event will the authors be held liable for any damages arising from the use of this ' software. ' ' Permission is granted to anyone to use this software for any purpose, including ' commercial applications, and to alter it and redistribute it freely, subject to the ' following restrictions: ' ' 1. The origin of this software must not be misrepresented; you must not claim that ' you wrote the original software. If you use this software in a product, an ' acknowledgment (see the following) in the product documentation is required. ' ' Portions Copyright 2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole ' or Copyright 2000-2002 Philip A. Craig ' ' 2. Altered source versions must be plainly marked as such, and must not be ' misrepresented as being the original software. ' ' 3. This notice may not be removed or altered from any source distribution. ' '***********************************************************************************/ #endregion namespace NUnit.Util { using System; using System.IO; using System.Collections; using System.Configuration; using System.Threading; using NUnit.Core; /// /// TestLoader handles interactions between a test runner and a /// client program - typically the user interface - for the /// purpose of loading, unloading and running tests. /// /// It implemements the EventListener interface which is used by /// the test runner and repackages those events, along with /// others as individual events that clients may subscribe to /// in collaboration with a TestEventDispatcher helper object. /// /// TestLoader is quite handy for use with a gui client because /// of the large number of events it supports. However, it has /// no dependencies on ui components and can be used independently. /// public class TestLoader : LongLivingMarshalByRefObject, NUnit.Core.EventListener, ITestLoader { #region Instance Variables /// /// StdOut stream for use by the TestRunner /// private TextWriter stdOutWriter; /// /// StdErr stream for use by the TestRunner /// private TextWriter stdErrWriter; /// /// Our event dispatiching helper object /// private ProjectEventDispatcher events; /// /// Loads and executes tests. Non-null when /// we have loaded a test. /// private TestDomain testDomain = null; /// /// Our current test project, if we have one. /// private NUnitProject testProject = null; /// /// The currently loaded test, returned by the testrunner /// private Test loadedTest = null; /// /// The test name that was specified when loading /// private string loadedTestName = null; /// /// The tests that are running /// private ITest[] runningTests = null; /// /// Result of the last test run /// private TestResult[] results = null; /// /// The last exception received when trying to load, unload or run a test /// private Exception lastException = null; /// /// Watcher fires when the assembly changes /// private AssemblyWatcher watcher; /// /// Assembly changed during a test and /// needs to be reloaded later /// private bool reloadPending = false; /// /// Indicates whether to watch for changes /// and reload the tests when a change occurs. /// private bool reloadOnChange = false; /// /// Indicates whether to reload the tests /// before each run. /// private bool reloadOnRun = false; private IFilter filter; #endregion #region Constructor public TestLoader(TextWriter stdOutWriter, TextWriter stdErrWriter ) { this.stdOutWriter = stdOutWriter; this.stdErrWriter = stdErrWriter; this.events = new ProjectEventDispatcher(); } #endregion #region Properties public bool IsProjectLoaded { get { return testProject != null; } } public bool IsTestLoaded { get { return loadedTest != null; } } public bool IsTestRunning { get { return runningTests != null; } } public NUnitProject TestProject { get { return testProject; } set { OnProjectLoad( value ); } } public IProjectEvents Events { get { return events; } } public string TestFileName { get { return testProject.ProjectPath; } } public TestResult[] Results { get { return results; } } public Exception LastException { get { return lastException; } } public bool ReloadOnChange { get { return reloadOnChange; } set { reloadOnChange = value; } } public bool ReloadOnRun { get { return reloadOnRun; } set { reloadOnRun = value; } } public Version FrameworkVersion { get { return this.testDomain.FrameworkVersion; } } #endregion #region EventListener Handlers void EventListener.RunStarted(Test[] tests) { int count = 0; foreach( Test test in tests ) count += filter == null ? test.CountTestCases() : test.CountTestCases( filter ); events.FireRunStarting( tests, count ); } void EventListener.RunFinished(NUnit.Core.TestResult[] results) { this.results = results; events.FireRunFinished( results ); runningTests = null; } void EventListener.RunFinished(Exception exception) { this.lastException = exception; events.FireRunFinished( exception ); runningTests = null; } /// /// Trigger event when each test starts /// /// TestCase that is starting void EventListener.TestStarted(NUnit.Core.TestCase testCase) { events.FireTestStarting( testCase ); } /// /// Trigger event when each test finishes /// /// Result of the case that finished void EventListener.TestFinished(TestCaseResult result) { events.FireTestFinished( result ); } /// /// Trigger event when each suite starts /// /// Suite that is starting void EventListener.SuiteStarted(TestSuite suite) { events.FireSuiteStarting( suite ); } /// /// Trigger event when each suite finishes /// /// Result of the suite that finished void EventListener.SuiteFinished(TestSuiteResult result) { events.FireSuiteFinished( result ); } /// /// Trigger event when an unhandled exception occurs during a test /// /// The unhandled exception void EventListener.UnhandledException(Exception exception) { events.FireTestException( exception ); } #endregion #region Methods for Loading and Unloading Projects /// /// Create a new project with default naming /// public void NewProject() { try { events.FireProjectLoading( "New Project" ); OnProjectLoad( NUnitProject.NewProject() ); } catch( Exception exception ) { lastException = exception; events.FireProjectLoadFailed( "New Project", exception ); } } /// /// Create a new project using a given path /// public void NewProject( string filePath ) { try { events.FireProjectLoading( filePath ); NUnitProject project = new NUnitProject( filePath ); project.Configs.Add( "Debug" ); project.Configs.Add( "Release" ); project.IsDirty = false; OnProjectLoad( project ); } catch( Exception exception ) { lastException = exception; events.FireProjectLoadFailed( filePath, exception ); } } /// /// Load a new project, optionally selecting the config and fire events /// public void LoadProject( string filePath, string configName ) { try { events.FireProjectLoading( filePath ); NUnitProject newProject = NUnitProject.LoadProject( filePath ); if ( configName != null ) { newProject.SetActiveConfig( configName ); newProject.IsDirty = false; } OnProjectLoad( newProject ); // return true; } catch( Exception exception ) { lastException = exception; events.FireProjectLoadFailed( filePath, exception ); // return false; } } /// /// Load a new project using the default config and fire events /// public void LoadProject( string filePath ) { LoadProject( filePath, null ); } /// /// Load a project from a list of assemblies and fire events /// public void LoadProject( string[] assemblies ) { try { events.FireProjectLoading( "New Project" ); NUnitProject newProject = NUnitProject.FromAssemblies( assemblies ); OnProjectLoad( newProject ); // return true; } catch( Exception exception ) { lastException = exception; events.FireProjectLoadFailed( "New Project", exception ); // return false; } } /// /// Unload the current project and fire events /// public void UnloadProject() { string testFileName = TestFileName; try { events.FireProjectUnloading( testFileName ); // if ( testFileName != null && File.Exists( testFileName ) ) // UserSettings.RecentProjects.RecentFile = testFileName; if ( IsTestLoaded ) UnloadTest(); testProject.Changed -= new ProjectEventHandler( OnProjectChanged ); testProject = null; events.FireProjectUnloaded( testFileName ); } catch (Exception exception ) { lastException = exception; events.FireProjectUnloadFailed( testFileName, exception ); } } /// /// Common operations done each time a project is loaded /// /// The newly loaded project private void OnProjectLoad( NUnitProject testProject ) { if ( IsProjectLoaded ) UnloadProject(); this.testProject = testProject; testProject.Changed += new ProjectEventHandler( OnProjectChanged ); events.FireProjectLoaded( TestFileName ); } private void OnProjectChanged( object sender, ProjectEventArgs e ) { switch ( e.type ) { case ProjectChangeType.ActiveConfig: if( TestProject.IsLoadable ) LoadTest(); break; case ProjectChangeType.AddConfig: case ProjectChangeType.UpdateConfig: if ( e.configName == TestProject.ActiveConfigName && TestProject.IsLoadable ) LoadTest(); break; case ProjectChangeType.RemoveConfig: if ( IsTestLoaded && TestProject.Configs.Count == 0 ) UnloadTest(); break; default: break; } } #endregion #region Methods for Loading and Unloading Tests public void LoadTest() { LoadTest( null ); } public void LoadTest( string testName ) { try { events.FireTestLoading( TestFileName ); testDomain = new TestDomain( stdOutWriter, stdErrWriter ); Test test = testDomain.Load( TestProject, testName ); TestSuite suite = test as TestSuite; if ( suite != null ) suite.Sort(); loadedTest = test; loadedTestName = testName; results = null; reloadPending = false; if ( ReloadOnChange ) InstallWatcher( ); if ( suite != null ) events.FireTestLoaded( TestFileName, this.loadedTest ); else { lastException = new ApplicationException( string.Format ( "Unable to find test {0} in assembly", testName ) ); events.FireTestLoadFailed( TestFileName, lastException ); } } catch( FileNotFoundException exception ) { lastException = exception; foreach( string assembly in TestProject.ActiveConfig.AbsolutePaths ) { if ( Path.GetFileNameWithoutExtension( assembly ) == exception.FileName && !ProjectPath.SamePathOrUnder( testProject.ActiveConfig.BasePath, assembly ) ) { lastException = new ApplicationException( string.Format( "Unable to load {0} because it is not located under the AppBase", exception.FileName ), exception ); break; } } events.FireTestLoadFailed( TestFileName, lastException ); } catch( Exception exception ) { lastException = exception; events.FireTestLoadFailed( TestFileName, exception ); } } /// /// Unload the current test suite and fire the Unloaded event /// public void UnloadTest( ) { if( IsTestLoaded ) { // Hold the name for notifications after unload string fileName = TestFileName; try { events.FireTestUnloading( TestFileName, this.loadedTest ); RemoveWatcher(); testDomain.Unload(); testDomain = null; loadedTest = null; loadedTestName = null; results = null; reloadPending = false; events.FireTestUnloaded( fileName, this.loadedTest ); } catch( Exception exception ) { lastException = exception; events.FireTestUnloadFailed( fileName, exception ); } } } /// /// Reload the current test on command /// public void ReloadTest() { OnTestChanged( TestFileName ); } /// /// Handle watcher event that signals when the loaded assembly /// file has changed. Make sure it's a real change before /// firing the SuiteChangedEvent. Since this all happens /// asynchronously, we use an event to let ui components /// know that the failure happened. /// /// Assembly file that changed public void OnTestChanged( string testFileName ) { if ( IsTestRunning ) reloadPending = true; else try { events.FireTestReloading( testFileName, this.loadedTest ); // Don't unload the old domain till after the event // handlers get a chance to compare the trees. TestDomain newDomain = new TestDomain( stdOutWriter, stdErrWriter ); Test newTest = newDomain.Load( testProject, loadedTestName ); TestSuite suite = newTest as TestSuite; if ( suite != null ) suite.Sort(); testDomain.Unload(); testDomain = newDomain; loadedTest = newTest; reloadPending = false; events.FireTestReloaded( testFileName, newTest ); } catch( Exception exception ) { lastException = exception; events.FireTestReloadFailed( testFileName, exception ); } } #endregion #region Methods for Running Tests public void SetFilter( IFilter filter ) { this.filter = filter; } /// /// Run the currently loaded top level test suite /// public void RunLoadedTest() { RunTest( loadedTest ); } /// /// Run a testcase or testsuite from the currrent tree /// firing the RunStarting and RunFinished events. /// Silently ignore the call if a test is running /// to allow for latency in the UI. /// /// The test to be run public void RunTest( ITest test ) { RunTests( new ITest[] { test } ); } public void RunTests( ITest[] tests ) { if ( !IsTestRunning ) { if ( reloadPending || ReloadOnRun ) ReloadTest(); runningTests = tests; //kind of silly string[] testNames = new string[ runningTests.Length ]; int index = 0; foreach (ITest node in runningTests) testNames[index++] = node.UniqueName; testDomain.SetFilter( filter ); // testDomain.DisplayTestLabels = UserSettings.Options.TestLabels; testDomain.RunTest( this, testNames ); } } /// /// Cancel the currently running test. /// Fail silently if there is none to /// allow for latency in the UI. /// public void CancelTestRun() { if ( IsTestRunning ) testDomain.CancelRun(); } public IList GetCategories() { ArrayList list = new ArrayList(); list.AddRange(testDomain.GetCategories()); return list; } #endregion #region Helper Methods /// /// Install our watcher object so as to get notifications /// about changes to a test. /// /// Full path of the assembly to watch private void InstallWatcher() { if(watcher!=null) watcher.Stop(); watcher = new AssemblyWatcher( 1000, TestProject.ActiveConfig.AbsolutePaths ); watcher.AssemblyChangedEvent += new AssemblyWatcher.AssemblyChangedHandler( OnTestChanged ); watcher.Start(); } /// /// Stop and remove our current watcher object. /// private void RemoveWatcher() { if ( watcher != null ) { watcher.Stop(); watcher = null; } } #endregion } }