#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
}
}