// **************************************************************** // Copyright 2002-2003, Charlie Poole // This is free software licensed under the NUnit license. You may // obtain a copy of the license at http://nunit.org/?p=license&r=2.4 // **************************************************************** namespace NUnit.Util { using System; using System.IO; using System.Collections; using System.Threading; using System.Configuration; using NUnit.Core; using NUnit.Core.Filters; /// /// 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 : MarshalByRefObject, NUnit.Core.EventListener, ITestLoader, IService { #region Instance Variables /// /// Our event dispatching helper object /// private TestEventDispatcher events; /// /// Use MuiltipleTestDomainRunner if true /// private bool multiDomain; /// /// Merge namespaces across multiple assemblies /// private bool mergeAssemblies; /// /// Generate suites for each level of namespace containing tests /// private bool autoNamespaceSuites; private bool shadowCopyFiles; /// /// Loads and executes tests. Non-null when /// we have loaded a test. /// private TestRunner testRunner = null; /// /// Our current test project, if we have one. /// private NUnitProject testProject = null; /// /// The currently loaded test, returned by the testrunner /// private ITest loadedTest = null; /// /// The test name that was specified when loading /// private string loadedTestName = null; /// /// The currently executing test /// private string currentTestName; /// /// Result of the last test run /// private TestResult testResult = 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 automatically rerun /// the tests when a change occurs. /// private bool rerunOnChange = false; /// /// The last filter used for a run - used to /// rerun tests when a change occurs /// private ITestFilter lastFilter; /// /// Indicates whether to reload the tests /// before each run. /// private bool reloadOnRun = false; #endregion #region Constructors public TestLoader() : this( new TestEventDispatcher() ) { } public TestLoader(TestEventDispatcher eventDispatcher ) { this.events = eventDispatcher; ISettings settings = Services.UserSettings; this.ReloadOnRun = settings.GetSetting( "Options.TestLoader.ReloadOnRun", true ); this.ReloadOnChange = settings.GetSetting( "Options.TestLoader.ReloadOnChange", true ); this.RerunOnChange = settings.GetSetting( "Options.TestLoader.RerunOnChange", false ); this.MultiDomain = settings.GetSetting( "Options.TestLoader.MultiDomain", false ); this.MergeAssemblies = settings.GetSetting( "Options.TestLoader.MergeAssemblies", false ); this.AutoNamespaceSuites = settings.GetSetting( "Options.TestLoader.AutoNamespaceSuites", true ); this.ShadowCopyFiles = settings.GetSetting( "Options.TestLoader.ShadowCopyFiles", true ); AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler( OnUnhandledException ); } #endregion #region Properties public bool IsProjectLoaded { get { return testProject != null; } } public bool IsTestLoaded { get { return loadedTest != null; } } public bool Running { get { return testRunner != null && testRunner.Running; } } public NUnitProject TestProject { get { return testProject; } set { OnProjectLoad( value ); } } public ITestEvents Events { get { return events; } } public string TestFileName { get { return testProject.ProjectPath; } } public TestResult TestResult { get { return testResult; } } public Exception LastException { get { return lastException; } } public bool ReloadOnChange { get { return reloadOnChange; } set { reloadOnChange = value; } } public bool RerunOnChange { get { return rerunOnChange; } set { rerunOnChange = value; } } public bool ReloadOnRun { get { return reloadOnRun; } set { reloadOnRun = value; } } public bool MultiDomain { get { return multiDomain; } set { multiDomain = value; } } public bool MergeAssemblies { get { return mergeAssemblies; } set { mergeAssemblies = value; } } public bool AutoNamespaceSuites { get { return autoNamespaceSuites; } set { autoNamespaceSuites = value; } } public bool ShadowCopyFiles { get { return shadowCopyFiles; } set { shadowCopyFiles = value; } } public IList AssemblyInfo { get { return testRunner == null ? null : testRunner.AssemblyInfo; } } public int TestCount { get { return loadedTest == null ? 0 : loadedTest.TestCount; } } #endregion #region EventListener Handlers void EventListener.RunStarted(string name, int testCount) { events.FireRunStarting( name, testCount ); } void EventListener.RunFinished(NUnit.Core.TestResult testResult) { this.testResult = testResult; try { this.SaveLastResult( Path.Combine( Path.GetDirectoryName( this.TestFileName ), "TestResult.xml" ) ); events.FireRunFinished( testResult ); } catch( Exception ex ) { this.lastException = ex; events.FireRunFinished( ex ); } } void EventListener.RunFinished(Exception exception) { this.lastException = exception; events.FireRunFinished( exception ); } /// /// Trigger event when each test starts /// /// TestCase that is starting void EventListener.TestStarted(TestName testName) { this.currentTestName = testName.FullName; events.FireTestStarting( testName ); } /// /// 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(TestName suiteName) { events.FireSuiteStarting( suiteName ); } /// /// 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 (other than ThreadAbordException) occurs during a test /// /// The unhandled exception void EventListener.UnhandledException(Exception exception) { events.FireTestException( this.currentTestName, exception ); } void OnUnhandledException( object sender, UnhandledExceptionEventArgs args ) { switch( args.ExceptionObject.GetType().FullName ) { case "System.Threading.ThreadAbortException": break; case "NUnit.Framework.AssertionException": default: events.FireTestException( this.currentTestName, (Exception)args.ExceptionObject ); break; } } /// /// Trigger event when output occurs during a test /// /// The test output void EventListener.TestOutput(TestOutput testOutput) { events.FireTestOutput( testOutput ); } #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 ); } catch( Exception exception ) { lastException = exception; events.FireProjectLoadFailed( filePath, exception ); } } /// /// 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 ); } catch( Exception exception ) { lastException = exception; events.FireProjectLoadFailed( "New Project", exception ); } } /// /// Unload the current project and fire events /// public void UnloadProject() { string testFileName = TestFileName; try { events.FireProjectUnloading( 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: case ProjectChangeType.Other: if( TestProject.IsLoadable ) TryToLoadOrReloadTest(); break; case ProjectChangeType.AddConfig: case ProjectChangeType.UpdateConfig: if ( e.configName == TestProject.ActiveConfigName && TestProject.IsLoadable ) TryToLoadOrReloadTest(); break; case ProjectChangeType.RemoveConfig: if ( IsTestLoaded && TestProject.Configs.Count == 0 ) UnloadTest(); break; default: break; } } private void TryToLoadOrReloadTest() { if ( IsTestLoaded ) ReloadTest(); else LoadTest(); } #endregion #region Methods for Loading and Unloading Tests public void LoadTest() { LoadTest( null ); } public void LoadTest( string testName ) { long startTime = DateTime.Now.Ticks; try { events.FireTestLoading( TestFileName ); testRunner = CreateRunner(); bool loaded = testRunner.Load( MakeTestPackage( testName ) ); loadedTest = testRunner.Test; loadedTestName = testName; testResult = null; reloadPending = false; if ( ReloadOnChange ) InstallWatcher( ); if ( loaded ) events.FireTestLoaded( TestFileName, 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.Assemblies ) { if ( Path.GetFileNameWithoutExtension( assembly ) == exception.FileName && !PathUtils.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 ); } double loadTime = (double)(DateTime.Now.Ticks - startTime) / (double)TimeSpan.TicksPerSecond; System.Diagnostics.Trace.WriteLine(string.Format("TestLoader: Loaded in {0} seconds", loadTime)); } /// /// 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( fileName ); RemoveWatcher(); testRunner.Unload(); testRunner = null; loadedTest = null; loadedTestName = null; testResult = null; reloadPending = false; events.FireTestUnloaded( fileName ); } catch( Exception exception ) { lastException = exception; events.FireTestUnloadFailed( fileName, exception ); } } } /// /// Reload the current test on command /// public void ReloadTest() { try { events.FireTestReloading( TestFileName ); testRunner.Load( MakeTestPackage( loadedTestName ) ); loadedTest = testRunner.Test; reloadPending = false; events.FireTestReloaded( TestFileName, loadedTest ); } catch( Exception exception ) { lastException = exception; events.FireTestReloadFailed( TestFileName, exception ); } } /// /// 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. /// public void OnTestChanged( string testFileName ) { if ( Running ) reloadPending = true; else { ReloadTest(); if ( rerunOnChange && lastFilter != null ) testRunner.BeginRun( this, lastFilter ); } } #endregion #region Methods for Running Tests /// /// Run all the tests /// public void RunTests() { RunTests( TestFilter.Empty ); } /// /// Run selected tests using a filter /// /// The filter to be used public void RunTests( ITestFilter filter ) { if ( !Running ) { if ( reloadPending || ReloadOnRun ) ReloadTest(); this.lastFilter = filter; testRunner.BeginRun( this, filter ); } } /// /// Cancel the currently running test. /// Fail silently if there is none to /// allow for latency in the UI. /// public void CancelTestRun() { if ( Running ) testRunner.CancelRun(); } public IList GetCategories() { CategoryManager categoryManager = new CategoryManager(); categoryManager.AddAllCategories( this.loadedTest ); ArrayList list = new ArrayList( categoryManager.Categories ); list.Sort(); return list; } #endregion public void SaveLastResult( string fileName ) { XmlResultVisitor resultVisitor = new XmlResultVisitor( fileName, this.testResult ); this.testResult.Accept(resultVisitor); resultVisitor.Write(); } #region Helper Methods /// /// Install our watcher object so as to get notifications /// about changes to a test. /// private void InstallWatcher() { if(watcher!=null) watcher.Stop(); watcher = new AssemblyWatcher( 1000, TestProject.ActiveConfig.Assemblies.ToArray() ); 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; } } private TestRunner CreateRunner() { TestRunner runner = multiDomain ? (TestRunner)new MultipleTestDomainRunner() : (TestRunner)new TestDomain(); return runner; } private TestPackage MakeTestPackage( string testName ) { TestPackage package = TestProject.ActiveConfig.MakeTestPackage(); package.TestName = testName; package.Settings["MergeAssemblies"] = mergeAssemblies; package.Settings["AutoNamespaceSuites"] = autoNamespaceSuites; package.Settings["ShadowCopyFiles"] = shadowCopyFiles; return package; } #endregion #region InitializeLifetimeService Override public override object InitializeLifetimeService() { return null; } #endregion #region IService Members public void UnloadService() { // TODO: Add TestLoader.UnloadService implementation } public void InitializeService() { // TODO: Add TestLoader.InitializeService implementation } #endregion } }