// **************************************************************** // This is free software licensed under the NUnit license. You // may obtain a copy of the license as well as information regarding // copyright ownership at http://nunit.org/?p=license&r=2.4. // **************************************************************** using System; using System.Collections; using System.Xml; using System.Xml.Schema; using System.IO; using System.Threading; using NUnit.Core; namespace NUnit.Util { /// /// Types of changes that may occur to a config /// public enum ProjectChangeType { ActiveConfig, AddConfig, RemoveConfig, UpdateConfig, Other } /// /// Arguments for a project event /// public class ProjectEventArgs : EventArgs { public ProjectChangeType type; public string configName; public ProjectEventArgs( ProjectChangeType type, string configName ) { this.type = type; this.configName = configName; } } /// /// Delegate to be used to handle project events /// public delegate void ProjectEventHandler( object sender, ProjectEventArgs e ); /// /// Class that represents an NUnit test project /// public class NUnitProject { #region Static and instance variables /// /// Used to generate default names for projects /// private static int projectSeed = 0; /// /// The extension used for test projects /// private static readonly string nunitExtension = ".nunit"; /// /// Path to the file storing this project /// private string projectPath; /// /// Application Base for the project. Since this /// can be null, always fetch from the property /// rather than using the field directly. /// private string basePath; /// /// Whether the project is dirty /// private bool isDirty = false; /// /// Collection of configs for the project /// private ProjectConfigCollection configs; /// /// The currently active configuration /// private ProjectConfig activeConfig; /// /// Flag indicating that this project is a /// temporary wrapper for an assembly. /// private bool isAssemblyWrapper = false; #endregion #region Constructor public NUnitProject( string projectPath ) { this.projectPath = Path.GetFullPath( projectPath ); configs = new ProjectConfigCollection( this ); } #endregion #region Static Methods // True if it's one of our project types public static bool IsProjectFile( string path ) { return Path.GetExtension( path ) == nunitExtension; } // True if it's ours or one we can load public static bool CanLoadAsProject( string path ) { return IsProjectFile( path ) || VSProject.IsProjectFile( path ) || VSProject.IsSolutionFile( path ); } public static string GenerateProjectName() { return string.Format( "Project{0}", ++projectSeed ); } public static NUnitProject EmptyProject() { return new NUnitProject( GenerateProjectName() ); } public static NUnitProject NewProject() { NUnitProject project = EmptyProject(); project.Configs.Add( "Debug" ); project.Configs.Add( "Release" ); project.IsDirty = false; return project; } /// /// Return a test project by either loading it from /// the supplied path, creating one from a VS file /// or wrapping an assembly. /// public static NUnitProject LoadProject( string path ) { if ( NUnitProject.IsProjectFile( path ) ) { NUnitProject project = new NUnitProject( path ); project.Load(); return project; } else if ( VSProject.IsProjectFile( path ) ) return NUnitProject.FromVSProject( path ); else if ( VSProject.IsSolutionFile( path ) ) return NUnitProject.FromVSSolution( path ); else return NUnitProject.FromAssembly( path ); } /// /// Creates a project to wrap a list of assemblies /// public static NUnitProject FromAssemblies( string[] assemblies ) { // if only one assembly is passed in then the configuration file // should follow the name of the assembly. This will only happen // if the LoadAssembly method is called. Currently the console ui // does not differentiate between having one or multiple assemblies // passed in. if ( assemblies.Length == 1) return NUnitProject.FromAssembly(assemblies[0]); NUnitProject project = NUnitProject.EmptyProject(); ProjectConfig config = new ProjectConfig( "Default" ); foreach( string assembly in assemblies ) { string fullPath = Path.GetFullPath( assembly ); if ( !File.Exists( fullPath ) ) throw new FileNotFoundException( string.Format( "Assembly not found: {0}", fullPath ) ); config.Assemblies.Add( fullPath ); } project.Configs.Add( config ); // TODO: Deduce application base, and provide a // better value for loadpath and project path // analagous to how new projects are handled string basePath = Path.GetDirectoryName( Path.GetFullPath( assemblies[0] ) ); project.projectPath = Path.Combine( basePath, project.Name + ".nunit" ); project.IsDirty = true; return project; } /// /// Creates a project to wrap an assembly /// public static NUnitProject FromAssembly( string assemblyPath ) { if ( !File.Exists( assemblyPath ) ) throw new FileNotFoundException( string.Format( "Assembly not found: {0}", assemblyPath ) ); string fullPath = Path.GetFullPath( assemblyPath ); NUnitProject project = new NUnitProject( fullPath ); ProjectConfig config = new ProjectConfig( "Default" ); config.Assemblies.Add( fullPath ); project.Configs.Add( config ); project.isAssemblyWrapper = true; project.IsDirty = false; return project; } public static NUnitProject FromVSProject( string vsProjectPath ) { NUnitProject project = new NUnitProject( Path.GetFullPath( vsProjectPath ) ); VSProject vsProject = new VSProject( vsProjectPath ); project.Add( vsProject ); project.isDirty = false; return project; } public static NUnitProject FromVSSolution( string solutionPath ) { NUnitProject project = new NUnitProject( Path.GetFullPath( solutionPath ) ); string solutionDirectory = Path.GetDirectoryName( solutionPath ); using(StreamReader reader = new StreamReader( solutionPath )) { char[] delims = { '=', ',' }; char[] trimchars = { ' ', '"' }; string line = reader.ReadLine(); while ( line != null ) { if ( line.StartsWith( "Project" ) ) { string[] parts = line.Split( delims ); string vsProjectPath = parts[2].Trim(trimchars); if ( VSProject.IsProjectFile( vsProjectPath ) ) project.Add( new VSProject( Path.Combine( solutionDirectory, vsProjectPath ) ) ); } line = reader.ReadLine(); } } project.isDirty = false; return project; } /// /// Figure out the proper name to be used when saving a file. /// public static string ProjectPathFromFile( string path ) { string fileName = Path.GetFileNameWithoutExtension( path ) + nunitExtension; return Path.Combine( Path.GetDirectoryName( path ), fileName ); } #endregion #region Properties and Events public static int ProjectSeed { get { return projectSeed; } set { projectSeed = value; } } /// /// The path to which a project will be saved. /// public string ProjectPath { get { return projectPath; } set { projectPath = Path.GetFullPath( value ); isDirty = true; } } public string DefaultBasePath { get { return Path.GetDirectoryName( projectPath ); } } /// /// Indicates whether a base path was specified for the project /// public bool BasePathSpecified { get { return basePath != null && basePath != string.Empty; } } /// /// The base path for the project. Constructor sets /// it to the directory part of the project path. /// public string BasePath { get { if ( !BasePathSpecified ) return DefaultBasePath; return basePath; } set { basePath = value; if ( basePath != null && basePath != string.Empty && !Path.IsPathRooted( basePath ) ) { basePath = Path.Combine( DefaultBasePath, basePath ); } basePath = PathUtils.Canonicalize( basePath ); OnProjectChange( ProjectChangeType.Other, null ); } } /// /// The name of the project. /// public string Name { get { return Path.GetFileNameWithoutExtension( projectPath ); } } public ProjectConfig ActiveConfig { get { // In case the previous active config was removed if ( activeConfig != null && !configs.Contains( activeConfig ) ) activeConfig = null; // In case no active config is set or it was removed if ( activeConfig == null && configs.Count > 0 ) activeConfig = configs[0]; return activeConfig; } } // Safe access to name of the active config public string ActiveConfigName { get { ProjectConfig config = ActiveConfig; return config == null ? null : config.Name; } } public bool IsLoadable { get { return ActiveConfig != null && ActiveConfig.Assemblies.Count > 0; } } // A project made from a single assembly is treated // as a transparent wrapper for some purposes until // a change is made to it. public bool IsAssemblyWrapper { get { return isAssemblyWrapper; } } public string ConfigurationFile { get { // TODO: Check this return isAssemblyWrapper ? Path.GetFileName( projectPath ) + ".config" : Path.GetFileNameWithoutExtension( projectPath ) + ".config"; } } public bool IsDirty { get { return isDirty; } set { isDirty = value; } } public ProjectConfigCollection Configs { get { return configs; } } public event ProjectEventHandler Changed; #endregion #region Instance Methods public void SetActiveConfig( int index ) { activeConfig = configs[index]; OnProjectChange( ProjectChangeType.ActiveConfig, activeConfig.Name ); } public void SetActiveConfig( string name ) { foreach( ProjectConfig config in configs ) { if ( config.Name == name ) { activeConfig = config; OnProjectChange( ProjectChangeType.ActiveConfig, activeConfig.Name ); break; } } } // public int IndexOf( string name ) // { // for( int index = 0; index < configs.Count; index++ ) // if( configs[index].Name == name ) // return index; // // return -1; // } public void OnProjectChange( ProjectChangeType type, string configName ) { isDirty = true; if ( isAssemblyWrapper ) { projectPath = Path.ChangeExtension( projectPath, ".nunit" ); isAssemblyWrapper = false; } if ( Changed != null ) Changed( this, new ProjectEventArgs( type, configName ) ); if ( type == ProjectChangeType.RemoveConfig ) { if ( activeConfig == null || activeConfig.Name == configName ) { if ( configs.Count > 0 ) SetActiveConfig( 0 ); } } } public void Add( VSProject vsProject ) { foreach( VSProjectConfig vsConfig in vsProject.Configs ) { string name = vsConfig.Name; if ( !configs.Contains( name ) ) configs.Add( name ); ProjectConfig config = this.Configs[name]; foreach ( string assembly in vsConfig.Assemblies ) config.Assemblies.Add( assembly ); } } public void Load() { XmlTextReader reader = new XmlTextReader( projectPath ); string activeConfigName = null; ProjectConfig currentConfig = null; try { reader.MoveToContent(); if ( reader.NodeType != XmlNodeType.Element || reader.Name != "NUnitProject" ) throw new ProjectFormatException( "Invalid project format: expected.", reader.LineNumber, reader.LinePosition ); while( reader.Read() ) if ( reader.NodeType == XmlNodeType.Element ) switch( reader.Name ) { case "Settings": if ( reader.NodeType == XmlNodeType.Element ) { activeConfigName = reader.GetAttribute( "activeconfig" ); if (activeConfigName == "NUnitAutoConfig") activeConfigName = NUnitFramework.BuildConfiguration; string appbase = reader.GetAttribute( "appbase" ); if ( appbase != null ) this.BasePath = appbase; } break; case "Config": if ( reader.NodeType == XmlNodeType.Element ) { string configName = reader.GetAttribute( "name" ); currentConfig = new ProjectConfig( configName ); currentConfig.BasePath = reader.GetAttribute( "appbase" ); currentConfig.ConfigurationFile = reader.GetAttribute( "configfile" ); string binpath = reader.GetAttribute( "binpath" ); currentConfig.PrivateBinPath = binpath; string type = reader.GetAttribute( "binpathtype" ); if ( type == null ) if ( binpath == null ) currentConfig.BinPathType = BinPathType.Auto; else currentConfig.BinPathType = BinPathType.Manual; else currentConfig.BinPathType = (BinPathType)Enum.Parse( typeof( BinPathType ), type, true ); Configs.Add( currentConfig ); if ( configName == activeConfigName ) activeConfig = currentConfig; } else if ( reader.NodeType == XmlNodeType.EndElement ) currentConfig = null; break; case "assembly": if ( reader.NodeType == XmlNodeType.Element && currentConfig != null ) { string path = reader.GetAttribute( "path" ); currentConfig.Assemblies.Add( Path.Combine( currentConfig.BasePath, path ) ); } break; default: break; } this.IsDirty = false; } catch( FileNotFoundException ) { throw; } catch( XmlException e ) { throw new ProjectFormatException( string.Format( "Invalid project format: {0}", e.Message ), e.LineNumber, e.LinePosition ); } catch( Exception e ) { throw new ProjectFormatException( string.Format( "Invalid project format: {0} Line {1}, Position {2}", e.Message, reader.LineNumber, reader.LinePosition ), reader.LineNumber, reader.LinePosition ); } finally { reader.Close(); } } public void Save() { projectPath = ProjectPathFromFile( projectPath ); XmlTextWriter writer = new XmlTextWriter( projectPath, System.Text.Encoding.UTF8 ); writer.Formatting = Formatting.Indented; writer.WriteStartElement( "NUnitProject" ); if ( configs.Count > 0 || this.BasePath != this.DefaultBasePath ) { writer.WriteStartElement( "Settings" ); if ( configs.Count > 0 ) writer.WriteAttributeString( "activeconfig", ActiveConfigName ); if ( this.BasePath != this.DefaultBasePath ) writer.WriteAttributeString( "appbase", this.BasePath ); writer.WriteEndElement(); } foreach( ProjectConfig config in Configs ) { writer.WriteStartElement( "Config" ); writer.WriteAttributeString( "name", config.Name ); string appbase = config.BasePath; if ( !PathUtils.SamePathOrUnder( this.BasePath, appbase ) ) writer.WriteAttributeString( "appbase", appbase ); else if ( config.RelativeBasePath != null ) writer.WriteAttributeString( "appbase", config.RelativeBasePath ); string configFile = config.ConfigurationFile; if ( configFile != null && configFile != this.ConfigurationFile ) writer.WriteAttributeString( "configfile", config.ConfigurationFile ); if ( config.BinPathType == BinPathType.Manual ) writer.WriteAttributeString( "binpath", config.PrivateBinPath ); else writer.WriteAttributeString( "binpathtype", config.BinPathType.ToString() ); foreach( string assembly in config.Assemblies ) { writer.WriteStartElement( "assembly" ); writer.WriteAttributeString( "path", PathUtils.RelativePath( config.BasePath, assembly ) ); writer.WriteEndElement(); } writer.WriteEndElement(); } writer.WriteEndElement(); writer.Close(); this.IsDirty = false; // Once we save a project, it's no longer // loaded as an assembly wrapper on reload. this.isAssemblyWrapper = false; } public void Save( string projectPath ) { this.ProjectPath = projectPath; Save(); } #endregion } }