#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-2003 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 © 2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole ' or Copyright © 2000-2003 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 using System; namespace NUnit.Util { using System.Runtime.Remoting; using System.Security.Policy; using System.Reflection; using System.Collections; using System.Collections.Specialized; using System.Configuration; using System.IO; using NUnit.Core; public class TestDomain : TestRunner { #region Instance Variables /// /// The appdomain used to load tests /// private AppDomain domain; /// /// The path to our cache /// private string cachePath; /// /// The remote runner loaded in the test appdomain /// private TestRunner testRunner; /// /// Writer for console standard output /// private TextWriter outWriter; /// /// Writer for console error output /// private TextWriter errorWriter; /// /// Holds the event listener while we are running /// private EventListener listener; /// /// Indicate whether files should be shadow copied /// private bool shadowCopyFiles = true; #endregion #region Properties public AppDomain AppDomain { get { return domain; } } public TextWriter Out { get { return outWriter; } set { outWriter = value; } } public TextWriter Error { get { return errorWriter; } set { errorWriter = value; } } private TestRunner Runner { get { if ( testRunner == null ) testRunner = MakeRemoteTestRunner( domain ); return testRunner; } } public bool DisplayTestLabels { get { return Runner.DisplayTestLabels; } set { Runner.DisplayTestLabels = value; } } private TestRunner MakeRemoteTestRunner( AppDomain runnerDomain ) { #if !TARGET_JVM object obj = runnerDomain.CreateInstanceAndUnwrap( typeof(RemoteTestRunner).Assembly.FullName, typeof(RemoteTestRunner).FullName, false, BindingFlags.Default,null,null,null,null,null); RemoteTestRunner runner = (RemoteTestRunner) obj; #else RemoteTestRunner runner = new RemoteTestRunner(); #endif runner.Out = this.outWriter; runner.Error = this.errorWriter; return runner; } public Version FrameworkVersion { get { return Runner.FrameworkVersion; } } public bool ShadowCopyFiles { get { return shadowCopyFiles; } set { if ( this.domain != null ) throw new ArgumentException( "ShadowCopyFiles may not be set after domain is created" ); shadowCopyFiles = value; } } public TestResult[] Results { get { return Runner.Results; } } public TestResult Result { get { return Runner.Result; } } #endregion #region Constructors public TestDomain( TextWriter outWriter, TextWriter errorWriter ) { this.outWriter = outWriter; this.errorWriter = errorWriter; } public TestDomain() : this( TextWriter.Null, TextWriter.Null ) { } #endregion #region Loading and Unloading Tests public Test Load( string assemblyFileName ) { return Load( assemblyFileName, string.Empty ); } public Test Load(string assemblyFileName, string testFixture) { Unload(); try { CreateDomain( assemblyFileName ); string assemblyPath = Path.GetFullPath( assemblyFileName ); if ( testFixture != null && testFixture != string.Empty ) return Runner.Load( assemblyPath, testFixture ); else return Runner.Load( assemblyPath ); } catch { Unload(); throw; } } public Test Load( string testFileName, string[] assemblies ) { return Load( testFileName, assemblies, null ); } public Test Load( string testFileName, string[] assemblies, string testFixture ) { FileInfo testFile = new FileInfo( testFileName ); return Load( testFileName, testFile.DirectoryName, testFile.FullName + ".config", GetBinPath(assemblies), assemblies, testFixture ); } public Test Load( string testFileName, string appBase, string configFile, string binPath, string[] assemblies, string testFixture ) { Unload(); try { CreateDomain( testFileName, appBase, configFile, binPath, assemblies ); if ( testFixture != null ) return Runner.Load( testFileName, assemblies, testFixture ); else return Runner.Load( testFileName, assemblies ); } catch { Unload(); throw; } } public Test Load( NUnitProject project ) { return Load( project, null ); } public Test Load( NUnitProject project, string testFixture ) { ProjectConfig cfg = project.ActiveConfig; if ( project.IsAssemblyWrapper ) return Load( cfg.Assemblies[0].FullPath, testFixture ); else return Load( project.ProjectPath, cfg.BasePath, cfg.ConfigurationFile, cfg.PrivateBinPath, cfg.TestAssemblies, testFixture ); } public void Unload() { testRunner = null; #if !TARGET_JVM if(domain != null) { try { AppDomain.Unload(domain); if ( this.ShadowCopyFiles ) DeleteCacheDir( new DirectoryInfo( cachePath ) ); } catch( CannotUnloadAppDomainException ) { // TODO: Do something useful. For now we just // leave the orphaned AppDomain "out there" // rather than aborting the application. } finally { domain = null; } } #endif } public static string GetBinPath( string[] assemblies ) { ArrayList dirs = new ArrayList(); string binPath = null; foreach( string path in assemblies ) { string dir = Path.GetDirectoryName( Path.GetFullPath( path ) ); if ( !dirs.Contains( dir ) ) { dirs.Add( dir ); if ( binPath == null ) binPath = dir; else binPath = binPath + ";" + dir; } } return binPath; } #endregion #region Counting Tests public int CountTestCases() { return Runner.CountTestCases(); } public int CountTestCases( string testName ) { return Runner.CountTestCases( testName ); } public int CountTestCases( string[] testNames ) { return Runner.CountTestCases( testNames ); } #endregion public ICollection GetCategories() { return Runner.GetCategories(); } #region Running Tests // public TestResult Run(NUnit.Core.EventListener listener, IFilter filter) // { // return Runner.Run( listener, filter ); // } public void SetFilter( IFilter filter ) { Runner.SetFilter( filter ); } public TestResult Run(NUnit.Core.EventListener listener) { using( new TestExceptionHandler( new UnhandledExceptionEventHandler( OnUnhandledException ) ) ) { this.listener = listener; return Runner.Run( listener ); } } public TestResult Run(NUnit.Core.EventListener listener, string testName) { using( new TestExceptionHandler( new UnhandledExceptionEventHandler( OnUnhandledException ) ) ) { this.listener = listener; return Runner.Run( listener, testName ); } } public TestResult[] Run(NUnit.Core.EventListener listener, string[] testNames) { using( new TestExceptionHandler( new UnhandledExceptionEventHandler( OnUnhandledException ) ) ) { this.listener = listener; return Runner.Run( listener, testNames ); } } public void RunTest(NUnit.Core.EventListener listener ) { using( new TestExceptionHandler( new UnhandledExceptionEventHandler( OnUnhandledException ) ) ) { this.listener = listener; Runner.RunTest( listener ); } } public void RunTest(NUnit.Core.EventListener listener, string testName ) { using( new TestExceptionHandler( new UnhandledExceptionEventHandler( OnUnhandledException ) ) ) { this.listener = listener; Runner.RunTest( listener, testName ); } } public void RunTest(NUnit.Core.EventListener listener, string[] testNames) { using( new TestExceptionHandler( new UnhandledExceptionEventHandler( OnUnhandledException ) ) ) { this.listener = listener; Runner.RunTest( listener, testNames ); } } public void CancelRun() { Runner.CancelRun(); } public void Wait() { Runner.Wait(); } // For now, just publish any unhandled exceptions and let the listener // figure out what to do with them. private void OnUnhandledException( object sender, UnhandledExceptionEventArgs e ) { this.listener.UnhandledException( (Exception)e.ExceptionObject ); } #endregion #region Helpers Used in AppDomain Creation and Removal /// /// Construct an application domain for testing a single assembly /// /// The assembly file name private void CreateDomain( string assemblyFileName ) { FileInfo testFile = new FileInfo( assemblyFileName ); string assemblyPath = Path.GetFullPath( assemblyFileName ); string domainName = string.Format( "domain-{0}", Path.GetFileName( assemblyFileName ) ); domain = MakeAppDomain( domainName, testFile.DirectoryName, testFile.FullName + ".config", testFile.DirectoryName ); } /// /// Construct an application domain for testing multiple assemblies /// /// The file name of the project file /// The application base path /// The configuration file to use /// The private bin path /// A collection of assemblies to load private void CreateDomain( string testFileName, string appBase, string configFile, string binPath, string[] assemblies ) { string domainName = string.Format( "domain-{0}", Path.GetFileName( testFileName ) ); domain = MakeAppDomain( testFileName, appBase, configFile, binPath ); } #if !TARGET_JVM /// /// This method creates appDomains for the framework. /// /// Name of the domain /// ApplicationBase for the domain /// ConfigurationFile for the domain /// PrivateBinPath for the domain /// private AppDomain MakeAppDomain( string domainName, string appBase, string configFile, string binPath ) { Evidence baseEvidence = AppDomain.CurrentDomain.Evidence; Evidence evidence = new Evidence(baseEvidence); AppDomainSetup setup = new AppDomainSetup(); // We always use the same application name setup.ApplicationName = "Tests"; // Note that we do NOT // set ShadowCopyDirectories because we rely on the default // setting of ApplicationBase plus PrivateBinPath if ( this.ShadowCopyFiles ) { setup.ShadowCopyFiles = "true"; setup.ShadowCopyDirectories = appBase; } else { setup.ShadowCopyFiles = "false"; } setup.ApplicationBase = appBase; setup.ConfigurationFile = configFile; setup.PrivateBinPath = binPath; AppDomain runnerDomain = AppDomain.CreateDomain(domainName, evidence, setup); if ( this.ShadowCopyFiles ) ConfigureCachePath(runnerDomain); return runnerDomain; } #else private AppDomain MakeAppDomain( string domainName, string appBase, string configFile, string binPath ) { return AppDomain.CurrentDomain; } #endif /// /// Set the location for caching and delete any old cache info /// /// Our domain private void ConfigureCachePath(AppDomain domain) { cachePath = String.Format(@"{0}\{1}", ConfigurationSettings.AppSettings["shadowfiles.path"], DateTime.Now.Ticks); cachePath = Environment.ExpandEnvironmentVariables(cachePath); DirectoryInfo dir = new DirectoryInfo(cachePath); if(dir.Exists) dir.Delete(true); domain.SetCachePath(cachePath); return; } /// /// Helper method to delete the cache dir. This method deals /// with a bug that occurs when pdb files are marked read-only. /// /// private void DeleteCacheDir( DirectoryInfo cacheDir ) { if(cacheDir.Exists) { foreach( DirectoryInfo dirInfo in cacheDir.GetDirectories() ) { dirInfo.Attributes &= ~FileAttributes.ReadOnly; DeleteCacheDir( dirInfo ); } foreach( FileInfo fileInfo in cacheDir.GetFiles() ) { fileInfo.Attributes &= ~FileAttributes.ReadOnly; } cacheDir.Delete(true); } } #endregion } }