This commit was manufactured by cvs2svn to create branch 'mono-1-0'.
[mono.git] / mcs / nunit20 / util / NUnitProject.cs
1 #region Copyright (c) 2002-2003, James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole, Philip A. Craig
2 /************************************************************************************
3 '
4 ' Copyright  2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole
5 ' Copyright  2000-2002 Philip A. Craig
6 '
7 ' This software is provided 'as-is', without any express or implied warranty. In no 
8 ' event will the authors be held liable for any damages arising from the use of this 
9 ' software.
10
11 ' Permission is granted to anyone to use this software for any purpose, including 
12 ' commercial applications, and to alter it and redistribute it freely, subject to the 
13 ' following restrictions:
14 '
15 ' 1. The origin of this software must not be misrepresented; you must not claim that 
16 ' you wrote the original software. If you use this software in a product, an 
17 ' acknowledgment (see the following) in the product documentation is required.
18 '
19 ' Portions Copyright  2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole
20 ' or Copyright  2000-2002 Philip A. Craig
21 '
22 ' 2. Altered source versions must be plainly marked as such, and must not be 
23 ' misrepresented as being the original software.
24 '
25 ' 3. This notice may not be removed or altered from any source distribution.
26 '
27 '***********************************************************************************/
28 #endregion
29
30 using System;
31 using System.Collections;
32 using System.Xml;
33 using System.Xml.Schema;
34 using System.IO;
35 using System.Threading;
36 using NUnit.Core;
37
38 namespace NUnit.Util
39 {
40         /// <summary>
41         /// Types of changes that may occur to a config
42         /// </summary>
43         public enum ProjectChangeType
44         {
45                 ActiveConfig,
46                 AddConfig,
47                 RemoveConfig,
48                 UpdateConfig,
49                 Other
50         }
51
52         /// <summary>
53         ///  Arguments for a project event
54         /// </summary>
55         public class ProjectEventArgs : EventArgs
56         {
57                 public ProjectChangeType type;
58                 public string configName;
59
60                 public ProjectEventArgs( ProjectChangeType type, string configName )
61                 {
62                         this.type = type;
63                         this.configName = configName;
64                 }
65         }
66
67         /// <summary>
68         /// Delegate to be used to handle project events
69         /// </summary>
70         public delegate void ProjectEventHandler( object sender, ProjectEventArgs e );
71
72         /// <summary>
73         /// Class that represents an NUnit test project
74         /// </summary>
75         public class NUnitProject
76         {
77                 #region Static and instance variables
78
79                 /// <summary>
80                 /// Used to generate default names for projects
81                 /// </summary>
82                 private static int projectSeed = 0;
83
84                 /// <summary>
85                 /// The extension used for test projects
86                 /// </summary>
87                 private static readonly string nunitExtension = ".nunit";
88
89                 /// <summary>
90                 /// Path to the file storing this project
91                 /// </summary>
92                 protected string projectPath;
93
94                 /// <summary>
95                 ///  Whether the project is dirty
96                 /// </summary>
97                 protected bool isDirty = false;
98                 
99                 /// <summary>
100                 /// Collection of configs for the project
101                 /// </summary>
102                 protected ProjectConfigCollection configs;
103
104                 /// <summary>
105                 /// The currently active configuration
106                 /// </summary>
107                 private ProjectConfig activeConfig;
108
109                 /// <summary>
110                 /// Flag indicating that this project is a
111                 /// temporary wrapper for an assembly.
112                 /// </summary>
113                 private bool isAssemblyWrapper = false;
114
115                 #endregion
116
117                 #region Constructor
118
119                 public NUnitProject( string projectPath )
120                 {
121                         this.projectPath = Path.GetFullPath( projectPath );
122                         configs = new ProjectConfigCollection( this );          
123                 }
124
125                 #endregion
126
127                 #region Static Methods
128
129                 // True if it's one of our project types
130                 public static bool IsProjectFile( string path )
131                 {
132                         return Path.GetExtension( path ) == nunitExtension;
133                 }
134
135                 // True if it's ours or one we can load
136                 public static bool CanLoadAsProject( string path )
137                 {
138                         return  IsProjectFile( path ) ||
139                                         VSProject.IsProjectFile( path ) ||
140                                         VSProject.IsSolutionFile( path );
141                 }
142
143                 public static string GenerateProjectName()
144                 {
145                         return string.Format( "Project{0}", ++projectSeed );
146                 }
147
148                 public static NUnitProject EmptyProject()
149                 {
150                         return new NUnitProject( GenerateProjectName() );
151                 }
152
153                 public static NUnitProject NewProject()
154                 {
155                         NUnitProject project = EmptyProject();
156
157                         project.Configs.Add( "Debug" );
158                         project.Configs.Add( "Release" );
159                         project.IsDirty = false;
160
161                         return project;
162                 }
163
164                 /// <summary>
165                 /// Return a test project by either loading it from
166                 /// the supplied path, creating one from a VS file
167                 /// or wrapping an assembly.
168                 /// </summary>
169                 public static NUnitProject LoadProject( string path )
170                 {
171                         if ( NUnitProject.IsProjectFile( path ) )
172                         {
173                                 NUnitProject project = new NUnitProject( path );
174                                 project.Load();
175                                 return project;
176                         }
177                         else if ( VSProject.IsProjectFile( path ) )
178                                 return NUnitProject.FromVSProject( path );
179                         else if ( VSProject.IsSolutionFile( path ) )
180                                 return NUnitProject.FromVSSolution( path );
181                         else
182                                 return NUnitProject.FromAssembly( path );
183                         
184                 }
185
186                 /// <summary>
187                 /// Creates a project to wrap a list of assemblies
188                 /// </summary>
189                 public static NUnitProject FromAssemblies( string[] assemblies )
190                 {
191                         // if only one assembly is passed in then the configuration file
192                         // should follow the name of the assembly. This will only happen
193                         // if the LoadAssembly method is called. Currently the console ui
194                         // does not differentiate between having one or multiple assemblies
195                         // passed in.
196                         if ( assemblies.Length == 1)
197                                 return NUnitProject.FromAssembly(assemblies[0]);
198
199
200                         NUnitProject project = NUnitProject.EmptyProject();
201                         ProjectConfig config = new ProjectConfig( "Default" );
202                         foreach( string assembly in assemblies )
203                         {
204                                 string fullPath = Path.GetFullPath( assembly );
205
206                                 if ( !File.Exists( fullPath ) )
207                                         throw new FileNotFoundException( string.Format( "Assembly not found: {0}", fullPath ) );
208                                 
209                                 config.Assemblies.Add( fullPath );
210                         }
211
212                         project.Configs.Add( config );
213
214                         // TODO: Deduce application base, and provide a
215                         // better value for loadpath and project path
216                         // analagous to how new projects are handled
217                         string basePath = Path.GetDirectoryName( Path.GetFullPath( assemblies[0] ) );
218                         project.projectPath = Path.Combine( basePath, project.Name + ".nunit" );
219
220                         project.IsDirty = true;
221
222                         return project;
223                 }
224
225                 /// <summary>
226                 /// Creates a project to wrap an assembly
227                 /// </summary>
228                 public static NUnitProject FromAssembly( string assemblyPath )
229                 {
230                         if ( !File.Exists( assemblyPath ) )
231                                 throw new FileNotFoundException( string.Format( "Assembly not found: {0}", assemblyPath ) );
232
233                         string fullPath = Path.GetFullPath( assemblyPath );
234
235                         NUnitProject project = new NUnitProject( fullPath );
236                         
237                         ProjectConfig config = new ProjectConfig( "Default" );
238                         config.Assemblies.Add( fullPath );
239                         project.Configs.Add( config );
240
241                         project.isAssemblyWrapper = true;
242                         project.IsDirty = false;
243
244                         return project;
245                 }
246
247                 public static NUnitProject FromVSProject( string vsProjectPath )
248                 {
249                         NUnitProject project = new NUnitProject( Path.GetFullPath( vsProjectPath ) );
250
251                         VSProject vsProject = new VSProject( vsProjectPath );
252                         project.Add( vsProject );
253
254                         project.isDirty = false;
255
256                         return project;
257                 }
258
259                 public static NUnitProject FromVSSolution( string solutionPath )
260                 {
261                         NUnitProject project = new NUnitProject( Path.GetFullPath( solutionPath ) );
262
263                         string solutionDirectory = Path.GetDirectoryName( solutionPath );
264                         StreamReader reader = new StreamReader( solutionPath );
265
266                         char[] delims = { '=', ',' };
267                         char[] trimchars = { ' ', '"' };
268
269                         string line = reader.ReadLine();
270                         while ( line != null )
271                         {
272                                 if ( line.StartsWith( "Project" ) )
273                                 {
274                                         string[] parts = line.Split( delims );
275                                         string vsProjectPath = Path.Combine( solutionDirectory, parts[2].Trim(trimchars) );
276                                         
277                                         if ( VSProject.IsProjectFile( vsProjectPath ) )
278                                                 project.Add( new VSProject( vsProjectPath ) );
279                                 }
280
281                                 line = reader.ReadLine();
282                         }
283
284                         project.isDirty = false;
285
286                         return project;
287                 }
288
289                 /// <summary>
290                 /// Figure out the proper name to be used when saving a file.
291                 /// </summary>
292                 public static string ProjectPathFromFile( string path )
293                 {
294                         string fileName = Path.GetFileNameWithoutExtension( path ) + nunitExtension;
295                         return Path.Combine( Path.GetDirectoryName( path ), fileName );
296                 }
297
298                 #endregion
299
300                 #region Properties and Events
301
302                 public static int ProjectSeed
303                 {
304                         get { return projectSeed; }
305                         set { projectSeed = value; }
306                 }
307
308                 /// <summary>
309                 /// The path to which a project will be saved.
310                 /// </summary>
311                 public string ProjectPath
312                 {
313                         get { return projectPath; }
314                         set 
315                         {
316                                 projectPath = Path.GetFullPath( value );
317                                 isDirty = true;
318                         }
319                 }
320
321                 /// <summary>
322                 /// The base path for the project is the
323                 /// directory part of the project path.
324                 /// </summary>
325                 public string BasePath
326                 {
327                         get { return Path.GetDirectoryName( projectPath ); }
328                 }
329
330                 /// <summary>
331                 /// The name of the project.
332                 /// </summary>
333                 public string Name
334                 {
335                         get { return Path.GetFileNameWithoutExtension( projectPath ); }
336                 }
337
338                 public ProjectConfig ActiveConfig
339                 {
340                         get 
341                         { 
342                                 // In case the previous active config was removed
343                                 if ( activeConfig != null && !configs.Contains( activeConfig ) )
344                                         activeConfig = null;
345                                 
346                                 // In case no active config is set or it was removed
347                                 if ( activeConfig == null && configs.Count > 0 )
348                                         activeConfig = configs[0];
349                                 
350                                 return activeConfig; 
351                         }
352                 }
353
354                 // Safe access to name of the active config
355                 public string ActiveConfigName
356                 {
357                         get
358                         {
359                                 ProjectConfig config = ActiveConfig;
360                                 return config == null ? null : config.Name;
361                         }
362                 }
363
364                 public bool IsLoadable
365                 {
366                         get
367                         {
368                                 return  ActiveConfig != null &&
369                                         ActiveConfig.Assemblies.Count > 0;
370                         }
371                 }
372
373                 // A project made from a single assembly is treated
374                 // as a transparent wrapper for some purposes until
375                 // a change is made to it.
376                 public bool IsAssemblyWrapper
377                 {
378                         get { return isAssemblyWrapper; }
379                 }
380
381                 public string ConfigurationFile
382                 {
383                         get 
384                         { 
385                                 // TODO: Check this
386                                 return isAssemblyWrapper
387                                           ? Path.GetFileName( projectPath ) + ".config"
388                                           : Path.GetFileNameWithoutExtension( projectPath ) + ".config";
389                         }
390                 }
391
392                 public bool IsDirty
393                 {
394                         get { return isDirty; }
395                         set { isDirty = value; }
396                 }
397
398                 public ProjectConfigCollection Configs
399                 {
400                         get { return configs; }
401                 }
402
403                 public event ProjectEventHandler Changed;
404
405                 #endregion
406
407                 #region Instance Methods
408
409                 public void SetActiveConfig( int index )
410                 {
411                         activeConfig = configs[index];
412                         OnProjectChange( ProjectChangeType.ActiveConfig, activeConfig.Name );
413                 }
414
415                 public void SetActiveConfig( string name )
416                 {
417                         foreach( ProjectConfig config in configs )
418                         {
419                                 if ( config.Name == name )
420                                 {
421                                         activeConfig = config;
422                                         OnProjectChange( ProjectChangeType.ActiveConfig, activeConfig.Name );
423                                         break;
424                                 }
425                         }
426                 }
427
428                 public void OnProjectChange( ProjectChangeType type, string configName )
429                 {
430                         isDirty = true;
431
432                         if ( isAssemblyWrapper )
433                         {
434                                 projectPath = Path.ChangeExtension( projectPath, ".nunit" );
435                                 isAssemblyWrapper = false;
436                         }
437
438                         if ( Changed != null )
439                                 Changed( this, new ProjectEventArgs( type, configName ) );
440
441                         if ( type == ProjectChangeType.RemoveConfig && activeConfig.Name == configName )
442                         {
443                                 if ( configs.Count > 0 )
444                                         SetActiveConfig( 0 );
445                         }
446                 }
447
448                 public void Add( VSProject vsProject )
449                 {
450                         foreach( VSProjectConfig vsConfig in vsProject.Configs )
451                         {
452                                 string name = vsConfig.Name;
453
454                                 if ( !this.Configs.Contains( name ) )
455                                         this.Configs.Add( name );
456
457                                 ProjectConfig config = this.Configs[name];
458
459                                 foreach ( string assembly in vsConfig.Assemblies )
460                                         config.Assemblies.Add( assembly );
461                         }
462                 }
463
464                 public void Load()
465                 {
466                         XmlTextReader reader = new XmlTextReader( projectPath );
467
468                         string activeConfigName = null;
469                         ProjectConfig currentConfig = null;
470                         
471                         try
472                         {
473                                 reader.MoveToContent();
474                                 if ( reader.NodeType != XmlNodeType.Element || reader.Name != "NUnitProject" )
475                                         throw new ProjectFormatException( 
476                                                 "Invalid project format: <NUnitProject> expected.", 
477                                                 reader.LineNumber, reader.LinePosition );
478
479                                 while( reader.Read() )
480                                         if ( reader.NodeType == XmlNodeType.Element )
481                                                 switch( reader.Name )
482                                                 {
483                                                         case "Settings":
484                                                                 if ( reader.NodeType == XmlNodeType.Element )
485                                                                         activeConfigName = reader.GetAttribute( "activeconfig" );
486                                                                 break;
487
488                                                         case "Config":
489                                                                 if ( reader.NodeType == XmlNodeType.Element )
490                                                                 {
491                                                                         string configName = reader.GetAttribute( "name" );
492                                                                         currentConfig = new ProjectConfig( configName );
493                                                                         currentConfig.BasePath = reader.GetAttribute( "appbase" );
494                                                                         currentConfig.ConfigurationFile = reader.GetAttribute( "configfile" );
495
496                                                                         string binpath = reader.GetAttribute( "binpath" );
497                                                                         string type = reader.GetAttribute( "binpathtype" );
498                                                                         if ( type == null )
499                                                                                 if ( binpath == null )
500                                                                                         currentConfig.BinPathType = BinPathType.Auto;
501                                                                                 else
502                                                                                         currentConfig.BinPathType = BinPathType.Manual;
503                                                                         else
504                                                                                 currentConfig.BinPathType = (BinPathType)Enum.Parse( typeof( BinPathType ), type, true );
505                                                                         Configs.Add( currentConfig );
506                                                                         if ( configName == activeConfigName )
507                                                                                 activeConfig = currentConfig;
508                                                                 }
509                                                                 else if ( reader.NodeType == XmlNodeType.EndElement )
510                                                                         currentConfig = null;
511                                                                 break;
512
513                                                         case "assembly":
514                                                                 if ( reader.NodeType == XmlNodeType.Element && currentConfig != null )
515                                                                 {
516                                                                         string path = reader.GetAttribute( "path" );
517                                                                         string test = reader.GetAttribute( "test" );
518                                                                         bool hasTests = test == null ? true : bool.Parse( test );
519                                                                         currentConfig.Assemblies.Add( 
520                                                                                 Path.Combine( currentConfig.BasePath, path ),
521                                                                                 hasTests );
522                                                                 }
523                                                                 break;
524
525                                                         default:
526                                                                 break;
527                                                 }
528
529                                 this.IsDirty = false;
530                         }
531                         catch( XmlException e )
532                         {
533                                 throw new ProjectFormatException(
534                                         string.Format( "Invalid project format: {0}", e.Message ),
535                                         e.LineNumber, e.LinePosition );
536                         }
537                         catch( Exception e )
538                         {
539                                 throw new ProjectFormatException( 
540                                         string.Format( "Invalid project format: {0} Line {1}, Position {2}", 
541                                         e.Message, reader.LineNumber, reader.LinePosition ),
542                                         reader.LineNumber, reader.LinePosition );
543                         }
544                         finally
545                         {
546                                 reader.Close();
547                         }
548                 }
549
550                 public void Save()
551                 {
552                         projectPath = ProjectPathFromFile( projectPath );
553
554                         XmlTextWriter writer = new XmlTextWriter(  projectPath, System.Text.Encoding.UTF8 );
555                         writer.Formatting = Formatting.Indented;
556
557                         writer.WriteStartElement( "NUnitProject" );
558                         
559                         if ( configs.Count > 0 )
560                         {
561                                 writer.WriteStartElement( "Settings" );
562                                 writer.WriteAttributeString( "activeconfig", ActiveConfigName );
563                                 writer.WriteEndElement();
564                         }
565                         
566                         foreach( ProjectConfig config in Configs )
567                         {
568                                 writer.WriteStartElement( "Config" );
569                                 writer.WriteAttributeString( "name", config.Name );
570                                 if ( config.RelativeBasePath != null )
571                                         writer.WriteAttributeString( "appbase", config.RelativeBasePath );
572                                 
573                                 string configFile = config.ConfigurationFile;
574                                 if ( configFile != null && configFile != this.ConfigurationFile )
575                                         writer.WriteAttributeString( "configfile", config.ConfigurationFile );
576                                 
577                                 if ( config.BinPathType == BinPathType.Manual )
578                                         writer.WriteAttributeString( "binpath", config.PrivateBinPath );
579                                 else
580                                         writer.WriteAttributeString( "binpathtype", config.BinPathType.ToString() );
581
582                                 foreach( AssemblyListItem assembly in config.Assemblies )
583                                 {
584                                         writer.WriteStartElement( "assembly" );
585                                         writer.WriteAttributeString( "path", config.RelativePathTo( assembly.FullPath ) );
586                                         if ( !assembly.HasTests )
587                                                 writer.WriteAttributeString( "test", "false" );
588                                         writer.WriteEndElement();
589                                 }
590
591                                 writer.WriteEndElement();
592                         }
593
594                         writer.WriteEndElement();
595
596                         writer.Close();
597                         this.IsDirty = false;
598
599                         // Once we save a project, it's no longer
600                         // loaded as an assembly wrapper on reload.
601                         this.isAssemblyWrapper = false;
602                 }
603
604                 public void Save( string projectPath )
605                 {
606                         this.ProjectPath = projectPath;
607                         Save();
608                 }
609
610                 #endregion
611         }
612 }