#region Copyright & License
//
// Copyright 2001-2005 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

using System;
using System.Collections;
using System.IO;
using System.Text.RegularExpressions;

using log4net.Layout;
using log4net.Core;

using NUnit.Framework;
using log4net.Repository.Hierarchy;
using log4net.Appender;

namespace log4net.Tests.Appender
{
	/// <summary>
	/// Used for internal unit testing the <see cref="RollingFileAppender"/> class.
	/// </summary>
	[TestFixture] public class RollingFileAppenderTest
	{
		const string _fileName = "test_41d3d834_4320f4da.log";
		const string _testMessage = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567";
		const int _iMaximumFileSize = 450;	// in bytes
		int _iMessagesLoggedThisFile = 0;
		int _iMessagesLogged = 0;
		int _iCountDirection = 0;
		int _MaxSizeRollBackups = 3;
		CountingAppender _caRoot;
		Logger _root;

		private class SilentErrorHandler : IErrorHandler
		{
			System.Text.StringBuilder m_buffer=new System.Text.StringBuilder();

			public string Message
			{
				get {return m_buffer.ToString();}
			}

			public void Error(string message)									{m_buffer.Append(message+"\n");}
			public void Error(string message, Exception e)						{m_buffer.Append(message+"\n"+e.Message+"\n");}
			public void Error(string message, Exception e, ErrorCode errorCode)	{m_buffer.Append(message+"\n"+e.Message+"\n");}
		}
		/// <summary>
		/// Sets up variables used for the tests
		/// </summary>
		private void InitializeVariables()
		{
			_iMessagesLoggedThisFile = 0;
			_iMessagesLogged = 0;
			_iCountDirection = +1;  // Up
			_MaxSizeRollBackups = 3;
		}

		/// <summary>
		/// Shuts down any loggers in the hierarchy, along
		/// with all appenders, and deletes any test files used
		/// for logging.
		/// </summary>
		private void ResetAndDeleteTestFiles()
		{
			// Regular users should not use the clear method lightly!
			LogManager.GetRepository().ResetConfiguration();
			LogManager.GetRepository().Shutdown();
			((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Clear();

			DeleteTestFiles();
		}

		/// <summary>
		/// Any initialization that happens before each test can
		/// go here
		/// </summary>
		[SetUp] public void SetUp() 
		{
			ResetAndDeleteTestFiles();
			InitializeVariables();
		}

		/// <summary>
		/// Any steps that happen after each test go here
		/// </summary>
		[TearDown] public void TearDown() 
		{
			ResetAndDeleteTestFiles();
		}

		/// <summary>
		/// Finds the number of files that match the base file name,
		/// and matches the result against an expected count
		/// </summary>
		/// <param name="iExpectedCount"></param>
		private void VerifyFileCount( int iExpectedCount )
		{
			ArrayList alFiles = GetExistingFiles(_fileName);
			Assert.IsNotNull(alFiles);
			Assert.AreEqual(iExpectedCount, alFiles.Count);
		}

		/// <summary>
		/// Creates a file with the given number, and the shared base file name
		/// </summary>
		/// <param name="iFileNumber"></param>
		private void CreateFile( int iFileNumber )
		{
			FileInfo fileInfo = new FileInfo( MakeFileName(_fileName, iFileNumber) );

			FileStream fileStream = null;
			try 
			{
				fileStream = fileInfo.Create();
			}
			finally
			{
				if (null != fileStream)
				{
					try
					{
						fileStream.Close();
					} 
					catch {}
				}
				fileStream = null;
			}
		}

		/// <summary>
		/// Verifies that the code correctly loads all filenames
		/// </summary>
		[Test] public void TestGetExistingFiles()
		{
			VerifyFileCount(0);
			CreateFile(0);
			VerifyFileCount(1);
			CreateFile(1);
			VerifyFileCount(2);
		}

		/// <summary>
		/// Removes all test files that exist
		/// </summary>
		private void DeleteTestFiles()
		{
			ArrayList alFiles = GetExistingFiles(_fileName);
			foreach(string sFile in alFiles)
			{
				try
				{
					System.Diagnostics.Debug.WriteLine("Deleting test file " + sFile);
					System.IO.File.Delete(sFile);
				} 
				catch (Exception ex)
				{
					System.Diagnostics.Debug.WriteLine("Exception while deleting test file " + ex.ToString());
				}
			}
		}

		/// <summary>
		/// Generates a file name associated with the count.
		/// </summary>
		/// <param name="iFileCount"></param>
		/// <returns></returns>
		private string MakeFileName(int iFileCount)
		{
			return MakeFileName(_fileName, iFileCount);
		}

		/// <summary>
		/// Generates a file name associated with the count, using
		/// the base file name.
		/// </summary>
		/// <param name="sBaseFile"></param>
		/// <param name="iFileCount"></param>
		/// <returns></returns>
		private string MakeFileName(string sBaseFile, int iFileCount)
		{
			if (0 == iFileCount) 
			{
				return sBaseFile;
			} 
			return sBaseFile + "." + iFileCount;
		}

		/// <summary>
		/// Returns a RollingFileAppender using all the internal settings for maximum
		/// file size and number of backups
		/// </summary>
		/// <returns></returns>
		private RollingFileAppender CreateAppender()
		{
			return CreateAppender(new FileAppender.ExclusiveLock());
		}

		/// <summary>
		/// Returns a RollingFileAppender using all the internal settings for maximum
		/// file size and number of backups
		/// </summary>
		/// <param name="lockModel">The locking model to test</param>
		/// <returns></returns>
		private RollingFileAppender CreateAppender(FileAppender.LockingModelBase lockModel)
		{
			//
			// Use a basic pattern that
			// includes just the message and a CR/LF.
			//
			PatternLayout layout = new PatternLayout("%m%n");

			//
			// Create the new appender
			//
			RollingFileAppender appender = new RollingFileAppender();
			appender.Layout			  = layout;
			appender.File				= _fileName;
			appender.MaximumFileSize	 = _iMaximumFileSize.ToString();
			appender.MaxSizeRollBackups  = _MaxSizeRollBackups;
			appender.CountDirection	  = _iCountDirection;
			appender.RollingStyle		= RollingFileAppender.RollingMode.Size;
			appender.LockingModel		=lockModel;
			
			appender.ActivateOptions();

			return appender;
		}

		/// <summary>
		/// Used for test purposes, a table of these objects can be used to identify
		/// any existing files and their expected length.
		/// </summary>
		public class RollFileEntry
		{
			/// <summary>
			/// Stores the name of the file
			/// </summary>
			private string m_fileName;

			/// <summary>
			/// The expected length of the file
			/// </summary>
			private long m_fileLength;

			/// <summary>
			/// Default constructor
			/// </summary>
			public RollFileEntry() {}

			/// <summary>
			/// Constructor used when the fileInfo and expected length are known
			/// </summary>
			/// <param name="fileName"></param>
			/// <param name="fileLength"></param>
			public RollFileEntry(string fileName, long fileLength)
			{
				m_fileName   = fileName;
				m_fileLength = fileLength;
			}

			/// <summary>
			/// Stores the name of the file
			/// </summary>
			public string FileName
			{
				get { return m_fileName; }
			}
						   
			/// <summary>
			/// The expected length of the file
			/// </summary>
			public long FileLength
			{
				get { return m_fileLength; }
			}
		}

		/// <summary>
		/// Used for table-driven testing.  This class holds information that can be used
		/// for testing of file rolling.
		/// </summary>
		public class RollConditions
		{
			/// <summary>
			/// A table of entries showing files that should exist and their expected sizes
			/// before logging is called
			/// </summary>
			private RollFileEntry[] m_preLogFileEntries;

			/// <summary>
			/// A table of entries showing files that should exist and their expected sizes
			/// after a message is logged
			/// </summary>
			private RollFileEntry[] m_postLogFileEntries;

			/// <summary>
			/// Constructor, taking all required parameters
			/// </summary>
			/// <param name="preLogFileEntries"></param>
			/// <param name="postLogFileEntries"></param>
			public RollConditions( RollFileEntry[] preLogFileEntries, RollFileEntry[] postLogFileEntries )
			{
				m_preLogFileEntries  = preLogFileEntries;
				m_postLogFileEntries = postLogFileEntries;
			}

			/// <summary>
			/// A table of entries showing files that should exist and their expected sizes
			/// before logging is called
			/// </summary>
			public RollFileEntry[] GetPreLogFileEntries()
			{
				return m_preLogFileEntries;
			}

			/// <summary>
			/// A table of entries showing files that should exist and their expected sizes
			/// after a message is logged
			/// </summary>
			public RollFileEntry[] GetPostLogFileEntries()
			{
				return m_postLogFileEntries;
			}
		}

		private void VerifyExistenceAndRemoveFromList( ArrayList alExisting, string sFileName, FileInfo file, RollFileEntry entry )
		{
			Assert.IsTrue(alExisting.Contains( sFileName ), "filename {0} not found in test directory", sFileName);
			Assert.AreEqual(entry.FileLength, file.Length, "file length mismatch");
			// Remove this file from the list
			alExisting.Remove( sFileName );
		}

		/// <summary>
		/// Checks that all the expected files exist, and only the expected files.  Also
		/// verifies the length of all files against the expected length
		/// </summary>
		/// <param name="sBaseFileName"></param>
		/// <param name="fileEntries"></param>
		private void VerifyFileConditions( string sBaseFileName, RollFileEntry[] fileEntries )
		{
			ArrayList alExisting = GetExistingFiles( sBaseFileName );
			if (null != fileEntries )
			{
				//					AssertEquals( "File count mismatch", alExisting.Count, fileEntries.Length );
				foreach( RollFileEntry rollFile in fileEntries )
				{
					string sFileName = rollFile.FileName;
					FileInfo file = new FileInfo(sFileName);

					if (rollFile.FileLength > 0 )
					{
						Assert.IsTrue(file.Exists, "filename {0} does not exist", sFileName );
						VerifyExistenceAndRemoveFromList( alExisting, sFileName, file, rollFile );
					}
					else
					{
						// If length is 0, file may not exist yet.  If file exists, make sure length
						// is zero.  If file doesn't exist, this is OK

						if (file.Exists )
						{
							VerifyExistenceAndRemoveFromList( alExisting, sFileName, file, rollFile );
						}
					}
				}
			}
			else
			{
				Assert.AreEqual(0, alExisting.Count);
			}

			// This check ensures no extra files matching the wildcard pattern exist.
			// We only want the files we expect, and no others
			Assert.AreEqual(0, alExisting.Count);
		}

		/// <summary>
		/// Called before logging a message to check that all the expected files exist, 
		/// and only the expected files.  Also verifies the length of all files against 
		/// the expected length
		/// </summary>
		/// <param name="sBaseFileName"></param>
		/// <param name="entry"></param>
		private void VerifyPreConditions( string sBaseFileName, RollConditions entry )
		{
			VerifyFileConditions( sBaseFileName, entry.GetPreLogFileEntries() );
		}

		/// <summary>
		/// Called after logging a message to check that all the expected files exist, 
		/// and only the expected files.  Also verifies the length of all files against 
		/// the expected length
		/// </summary>
		/// <param name="sBaseFileName"></param>
		/// <param name="entry"></param>
		private void VerifyPostConditions( string sBaseFileName, RollConditions entry )
		{
			VerifyFileConditions( sBaseFileName, entry.GetPostLogFileEntries() );
		}

		/// <summary>
		/// Logs a message, verifying the expected message counts against the 
		/// current running totals.
		/// </summary>
		/// <param name="entry"></param>
		/// <param name="sMessageToLog"></param>
		private void LogMessage( RollConditions entry, string sMessageToLog )
		{
			Assert.AreEqual(_caRoot.Counter, _iMessagesLogged++);
			_root.Log(Level.Debug, sMessageToLog, null);
			Assert.AreEqual(_caRoot.Counter, _iMessagesLogged); 
			_iMessagesLoggedThisFile++;
		}

		private void DumpFileEntry( RollFileEntry entry )
		{
			System.Diagnostics.Debug.WriteLine( "\tfile   name: " + entry.FileName );
			System.Diagnostics.Debug.WriteLine( "\tfile length: " + entry.FileLength );
		}

		private void DumpTableEntry( RollConditions entry )
		{
			System.Diagnostics.Debug.WriteLine( "Pre-Conditions" );
			foreach( RollFileEntry file in entry.GetPreLogFileEntries() )
			{
				DumpFileEntry( file );
			}
			System.Diagnostics.Debug.WriteLine( "Post-Conditions" );
			foreach( RollFileEntry file in entry.GetPostLogFileEntries() )
			{
				DumpFileEntry( file );
			}
			//				System.Diagnostics.Debug.WriteLine("");
		}

		/// <summary>
		/// Runs through all table entries, logging messages.  Before each message is logged,
		/// pre-conditions are checked to ensure the expected files exist and they are the
		/// expected size.  After logging, verifies the same.
		/// </summary>
		/// <param name="sBaseFileName"></param>
		/// <param name="entries"></param>
		/// <param name="sMessageToLog"></param>
		private void RollFromTableEntries( string sBaseFileName, RollConditions[] entries, string sMessageToLog )
		{
			for( int i=0; i<entries.Length; i++ )
			{
				RollConditions entry = entries[i];

				//					System.Diagnostics.Debug.WriteLine( i + ": Entry " + i + " pre/post conditions");
				//					DumpTableEntry( entry );
				//					System.Diagnostics.Debug.WriteLine( i + ": Testing entry pre-conditions");
				VerifyPreConditions( sBaseFileName, entry );
				//					System.Diagnostics.Debug.WriteLine( i + ": Logging message");
				LogMessage( entry, sMessageToLog );
				//					System.Diagnostics.Debug.WriteLine( i + ": Testing entry post-conditions");
				VerifyPostConditions( sBaseFileName, entry );
				//					System.Diagnostics.Debug.WriteLine( i + ": Finished validating entry\n");
			}
		}

		/// <summary>
		/// Returns the number of bytes logged per message, including
		/// any CR/LF characters in addition to the message length.
		/// </summary>
		/// <param name="sMessage"></param>
		/// <returns></returns>
		private int TotalMessageLength( string sMessage )
		{
			const int iLengthCRLF = 2;
			return sMessage.Length + iLengthCRLF;
		}
 
		/// <summary>
		/// Determines how many messages of a fixed length can be logged
		/// to a single file before the file rolls.
		/// </summary>
		/// <param name="iMessageLength"></param>
		/// <returns></returns>
		private int MessagesPerFile( int iMessageLength )
		{
			int iMessagesPerFile = _iMaximumFileSize / iMessageLength;
				
			//
			// RollingFileAppender checks for wrap BEFORE logging,
			// so we will actually get one more message per file than
			// we would otherwise.
			//
			if (iMessagesPerFile * iMessageLength < _iMaximumFileSize ) 
			{
				iMessagesPerFile++;
			}

			return iMessagesPerFile;
		}

		/// <summary>
		/// Determines the name of the current file
		/// </summary>
		/// <returns></returns>
		private string GetCurrentFile()
		{
			// Current file name is always the base file name when
			// counting.  Dates will need a different approach
			return _fileName;
		}

		/// <summary>
		/// Turns a group of file names into an array of file entries that include the name
		/// and a size.  This is useful for assigning the properties of backup files, when
		/// the length of the files are all the same size due to a fixed message length.
		/// </summary>
		/// <param name="sBackupGroup"></param>
		/// <param name="iBackupFileLength"></param>
		/// <returns></returns>
		private RollFileEntry[] MakeBackupFileEntriesFromBackupGroup( string sBackupGroup, int iBackupFileLength )
		{
			string[] sFiles = sBackupGroup.Split( ' ' );

			ArrayList alEntries = new ArrayList();

			for( int i=0; i<sFiles.Length; i++ )
			{
				// Weed out any whitespace entries from the array
				if (sFiles[i].Trim().Length > 0 )
				{
					alEntries.Add( new RollFileEntry( sFiles[i], iBackupFileLength ) );
				}
			}

			return (RollFileEntry[])alEntries.ToArray(typeof(RollFileEntry));
		}

		/// <summary>
		/// Finds the iGroup group in the string (comma separated groups)
		/// </summary>
		/// <param name="sBackupGroups"></param>
		/// <param name="iGroup"></param>
		/// <returns></returns>
		private string GetBackupGroup( string sBackupGroups, int iGroup )
		{
			string[] sGroups = sBackupGroups.Split( ',' );
			return sGroups[iGroup];
		}

		/// <summary>
		/// Builds a collection of file entries based on the file names
		/// specified in a groups string and the max file size from the
		/// stats object
		/// </summary>
		/// <param name="sBackupGroups"></param>
		/// <param name="stats"></param>
		/// <returns></returns>
		private RollFileEntry[] MakeBackupFileEntriesForPreCondition( string sBackupGroups, RollingStats stats )
		{
			if (0 == stats.NumberOfFileRolls )
			{
				return null;	// first round has no previous backups
			}
			string sGroup;
			if (0 == stats.MessagesThisFile )
			{
				// first file has special pattern...since rolling doesn't occur when message
				// is logged, rather before next message is logged.
				if (stats.NumberOfFileRolls <= 1 )
				{
					return null;   
				}
				// Use backup files from previous round.  The minus 2 is because we have already
				// rolled, and the first round uses null instead of the string
				sGroup = GetBackupGroup( sBackupGroups, stats.NumberOfFileRolls-2 );
			}
			else
			{
				sGroup = GetBackupGroup( sBackupGroups, stats.NumberOfFileRolls-1 );
			}
			return MakeBackupFileEntriesFromBackupGroup( sGroup, stats.MaximumFileSize );
		}

		/// <summary>
		/// Builds a collection of file entries based on the file names
		/// specified in a groups string and the max file size from the
		/// stats object
		/// </summary>
		/// <param name="sBackupGroups"></param>
		/// <param name="stats"></param>
		/// <returns></returns>
		private RollFileEntry[] MakeBackupFileEntriesForPostCondition( string sBackupGroups, RollingStats stats )
		{
			if (0 == stats.NumberOfFileRolls)
			{
				return null;	// first round has no previous backups
			}
			string sGroup = GetBackupGroup( sBackupGroups, stats.NumberOfFileRolls-1 );
			return MakeBackupFileEntriesFromBackupGroup( sGroup, stats.MaximumFileSize );
		}


		/// <summary>
		/// This class holds information that is used while we are generating
		/// test data sets
		/// </summary>
		public class RollingStats
		{
			private int iTotalMessageLength;
			private int iMessagesPerFile;
			private int iMessagesThisFile;
			private int iNumberOfFileRolls;

			/// <summary>
			/// Default constructor
			/// </summary>
			public RollingStats() {}

			/// <summary>
			/// Number of total bytes a log file can reach.
			/// </summary>
			public int MaximumFileSize
			{
				get { return TotalMessageLength * MessagesPerFile; }
			}

			/// <summary>
			/// The length of a message, including any CR/LF characters.
			/// This length assumes all messages are a fixed length for
			/// test purposes.
			/// </summary>
			public int TotalMessageLength
			{
				get { return iTotalMessageLength; }
				set { iTotalMessageLength = value;}
			}

			/// <summary>
			/// A count of the number of messages that are logged to each
			/// file.
			/// </summary>
			public int MessagesPerFile
			{
				get { return iMessagesPerFile; }
				set { iMessagesPerFile = value; }
			}

			/// <summary>
			/// Counts how many messages have been logged to the current file
			/// </summary>
			public int MessagesThisFile
			{
				get { return iMessagesThisFile; }
				set { iMessagesThisFile = value; }
			}

			/// <summary>
			/// Counts how many times a file roll has occurred
			/// </summary>
			public int NumberOfFileRolls
			{
				get { return iNumberOfFileRolls; }
				set { iNumberOfFileRolls = value; }
			}
		}

		/// <summary>
		/// The stats are used to keep track of progress while we are algorithmically
		/// generating a table of pre/post condition tests for file rolling.
		/// </summary>
		/// <param name="sTestMessage"></param>
		/// <returns></returns>
		private RollingStats InitializeStats( string sTestMessage )
		{
			RollingStats rollingStats = new RollingStats();

			rollingStats.TotalMessageLength = TotalMessageLength( sTestMessage );
			rollingStats.MessagesPerFile	= MessagesPerFile( rollingStats.TotalMessageLength );
			rollingStats.MessagesThisFile   = 0;
			rollingStats.NumberOfFileRolls  = 0;

			return rollingStats;
		}

		/// <summary>
		/// Takes an existing array of RollFileEntry objects, creates a new array one element
		/// bigger, and appends the final element to the end.  If the existing entries are
		/// null (no entries), then a one-element array is returned with the final element
		/// as the only entry.
		/// </summary>
		/// <param name="existing"></param>
		/// <param name="final"></param>
		/// <returns></returns>
		private RollFileEntry[] AddFinalElement( RollFileEntry[] existing, RollFileEntry final )
		{
			int iLength = 1;
			if (null != existing )
			{
				iLength += existing.Length;
			}
			RollFileEntry[] combined = new RollFileEntry[iLength];
			if (null != existing )
			{
				System.Array.Copy( existing, 0, combined, 0, existing.Length );
			}
			combined[iLength-1] = final;
			return combined;
		}

		/// <summary>
		/// Generates the pre and post condition arrays from an array of backup files and the
		/// current file / next file.
		/// </summary>
		/// <param name="sBackupFiles"></param>
		/// <param name="preCondition"></param>
		/// <param name="current"></param>
		/// <param name="currentNext"></param>
		/// <param name="rollingStats"></param>
		/// <returns></returns>
		private RollConditions BuildTableEntry( string sBackupFiles, RollConditions preCondition, RollFileEntry current, RollFileEntry currentNext, RollingStats rollingStats )
		{
			RollFileEntry[] backupsPost = MakeBackupFileEntriesForPostCondition( sBackupFiles, rollingStats );
			RollFileEntry[] post		= AddFinalElement( backupsPost, currentNext	);
			if (null == preCondition )
			{
				return new RollConditions( AddFinalElement(null, current), post );
			}
			return new RollConditions( preCondition.GetPostLogFileEntries(), post );
		}

		/// <summary>
		/// Returns a RollFileEntry that represents the next state of the current file,
		/// based on the current state.  When the current state would roll, the next
		/// entry is the current file wrapped to 0 bytes.  Otherwise, the next state
		/// is the post-condition passed in as the currentNext parameter
		/// </summary>
		/// <param name="rollingStats"></param>
		/// <param name="currentNext"></param>
		/// <returns></returns>
		private RollFileEntry MoveNextEntry( RollingStats rollingStats, RollFileEntry currentNext )
		{
			rollingStats.MessagesThisFile = rollingStats.MessagesThisFile + 1;
			if (rollingStats.MessagesThisFile >= rollingStats.MessagesPerFile )
			{
				rollingStats.MessagesThisFile = 0;
				rollingStats.NumberOfFileRolls = rollingStats.NumberOfFileRolls + 1;

				return new RollFileEntry( GetCurrentFile(), 0 );
			}
			else
			{
				return currentNext;
			}
		}

		/// <summary>
		/// Callback point for the regular expression parser.  Turns
		/// the number into a file name.
		/// </summary>
		/// <param name="match"></param>
		/// <returns></returns>
		private string NumberedNameMaker( Match match )
		{
			Int32 iValue = Int32.Parse(match.Value);
			return MakeFileName( _fileName, iValue );
		}

		/// <summary>
		/// Parses a numeric list of files, turning them into file names.
		/// Calls back to a method that does the actual replacement, turning
		/// the numeric value into a filename.
		/// </summary>
		/// <param name="sBackupInfo"></param>
		/// <param name="evaluator"></param>
		/// <returns></returns>
		string ConvertToFiles( string sBackupInfo, MatchEvaluator evaluator )
		{
			Regex regex = new Regex(@"\d+");
			return regex.Replace( sBackupInfo, evaluator );
		}

		/// <summary>
		/// Makes test entries used for verifying counted file names
		/// </summary>
		/// <param name="sTestMessage">A message to log repeatedly</param>
		/// <param name="sBackupInfo">Filename groups used to indicate backup file name progression
		/// that results after each message is logged</param>
		/// <param name="iMessagesToLog">How many times the test message will be repeatedly logged</param>
		/// <returns></returns>
		private RollConditions[] MakeNumericTestEntries(  string sTestMessage, string sBackupInfo, int iMessagesToLog )
		{
			return MakeTestEntries(  
				sTestMessage, 
				sBackupInfo, 
				iMessagesToLog, 
				new MatchEvaluator(NumberedNameMaker ) );
		}

		/// <summary>
		/// This routine takes a list of backup file names and a message that will be logged
		/// repeatedly, and generates a collection of objects containing pre-condition and 
		/// post-condition information.  This pre/post information shows the names and expected 
		/// file sizes for all files just before and just after a message is logged.
		/// </summary>
		/// <param name="sTestMessage">A message to log repeatedly</param>
		/// <param name="sBackupInfo">Filename groups used to indicate backup file name progression
		/// that results after each message is logged</param>
		/// <param name="iMessagesToLog">How many times the test message will be repeatedly logged</param>
		/// <param name="evaluator">Function that can turn a number into a filename</param>
		/// <returns></returns>
		private RollConditions[] MakeTestEntries( string sTestMessage, string sBackupInfo, int iMessagesToLog, MatchEvaluator evaluator )
		{
			string sBackupFiles = ConvertToFiles( sBackupInfo, evaluator );

			RollConditions[] table = new RollConditions[iMessagesToLog];

			RollingStats rollingStats = InitializeStats( sTestMessage );

			RollConditions preCondition = null;
			rollingStats.MessagesThisFile = 0;

			RollFileEntry currentFile = new RollFileEntry( GetCurrentFile(), 0 );
			for( int i=0; i<iMessagesToLog; i++ )
			{
				RollFileEntry currentNext = new RollFileEntry( 
					GetCurrentFile(), 
					(1 + rollingStats.MessagesThisFile) * rollingStats.TotalMessageLength );

				table[i] = BuildTableEntry( sBackupFiles, preCondition, currentFile, currentNext, rollingStats );
				preCondition = table[i];

				//System.Diagnostics.Debug.WriteLine( "Message " + i );
				//DumpTableEntry( table[i] );

				currentFile = MoveNextEntry( rollingStats, currentNext );
			}

			return table;
		}

		/// <summary>
		/// Uses the externally defined rolling table to verify rolling names/sizes
		/// 
		/// Pattern is:  check pre-conditions.  Log messages, checking size of current file.
		/// when size exceeds limit, check post conditions.  Can determine from message the
		/// number of messages N that will cause a roll to occur.  Challenge is to verify the
		/// expected files, their sizes, and the names.  For a message of length L, the backups
		/// will be of size (N * L), and the current file will be of size (K * L), where K is
		/// the number of messages that have been logged to this file.
		///
		/// File sizes can be checked algorithmically.  
		/// 
		/// File names are generated using a table driven algorithm, where a number is turned into
		/// the actual filename.
		/// 
		/// The entries are comma-separated, with spaces between the names.  Each comma indicates
		/// a 'roll', and the group between commas indicates the numbers for all backup files that
		/// occur as a result of the roll.  It is assumed that no backup files exist before a roll 
		/// occurs
		/// </summary>
		/// <param name="table"></param>
		private void VerifyRolling( RollConditions[] table )
		{
			ConfigureRootAppender();
			RollFromTableEntries( _fileName, table, _testMessage );
		}

		/// <summary>
		/// Validates rolling using a fixed number of backup files, with
		/// count direction set to up, so that newer files have higher counts.
		/// Newest = N, Oldest = N-K, where K is the number of backups to allow
		/// and N is the number of times rolling has occurred.
		/// </summary>
		[Test] public void TestRollingCountUpFixedBackups()
		{
			//
			// Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
			// oldest, and 3 is the newest
			//
			string sBackupInfo = "1, 1 2, 1 2 3, 2 3 4, 3 4 5";

			//
			// Count Up
			//
			_iCountDirection = +1;

			//
			// Log 30 messages.  This is 5 groups, 6 checks per group ( 0, 100, 200, 300, 400, 500 
			// bytes for current file as messages are logged.
			//
			int iMessagesToLog = 30;	

			VerifyRolling( MakeNumericTestEntries( _testMessage, sBackupInfo, iMessagesToLog ) );
		}

		/// <summary>
		/// Validates rolling using an infinite number of backup files, with
		/// count direction set to up, so that newer files have higher counts.
		/// Newest = N, Oldest = 1, where N is the number of times rolling has 
		/// occurred.
		/// </summary>
		[Test] public void TestRollingCountUpInfiniteBackups()
		{
			//
			// Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
			// oldest, and 3 is the newest
			//
			string sBackupInfo = "1, 1 2, 1 2 3, 1 2 3 4, 1 2 3 4 5";

			//
			// Count Up
			//
			_iCountDirection = +1;

			//
			// Infinite backups
			//
			_MaxSizeRollBackups = -1;

			//
			// Log 30 messages.  This is 5 groups, 6 checks per group ( 0, 100, 200, 300, 400, 500 
			// bytes for current file as messages are logged.
			//
			int iMessagesToLog = 30;	

			VerifyRolling( MakeNumericTestEntries( _testMessage, sBackupInfo, iMessagesToLog ) );
		}

		/// <summary>
		/// Validates rolling with no backup files, with count direction set to up.
		/// Only the current file should be present, wrapping to 0 bytes once the
		/// previous file fills up.
		/// </summary>
		[Test] public void TestRollingCountUpZeroBackups()
		{
			//
			// Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
			// oldest, and 3 is the newest
			//
			string sBackupInfo = ", , , , ";

			//
			// Count Up
			//
			_iCountDirection = +1;

			//
			// No backups
			//
			_MaxSizeRollBackups = 0;

			//
			// Log 30 messages.  This is 5 groups, 6 checks per group ( 0, 100, 200, 300, 400, 500 
			// bytes for current file as messages are logged.
			//
			int iMessagesToLog = 30;	

			VerifyRolling( MakeNumericTestEntries( _testMessage, sBackupInfo, iMessagesToLog ) );
		}


		/// <summary>
		/// Validates rolling using a fixed number of backup files, with
		/// count direction set to down, so that older files have higher counts.
		/// Newest = 1, Oldest = N, where N is the number of backups to allow
		/// </summary>
		[Test] public void TestRollingCountDownFixedBackups()
		{
			//
			// Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
			// oldest, and 3 is the newest
			//
			string sBackupInfo = "1, 1 2, 1 2 3, 1 2 3, 1 2 3";

			//
			// Count Up
			//
			_iCountDirection = -1;

			//
			// Log 30 messages.  This is 5 groups, 6 checks per group ( 0, 100, 200, 300, 400, 500 
			// bytes for current file as messages are logged.
			//
			int iMessagesToLog = 30;	

			VerifyRolling( MakeNumericTestEntries( _testMessage, sBackupInfo, iMessagesToLog ) );
		}

		/// <summary>
		/// Validates rolling using an infinite number of backup files, with
		/// count direction set to down, so that older files have higher counts.
		/// Newest = 1, Oldest = N, where N is the number of times rolling has
		/// occurred
		/// </summary>
		[Test] public void TestRollingCountDownInfiniteBackups()
		{
			//
			// Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
			// oldest, and 3 is the newest
			//
			string sBackupInfo = "1, 1 2, 1 2 3, 1 2 3 4, 1 2 3 4 5";

			//
			// Count Down
			//
			_iCountDirection = -1;

			//
			// Infinite backups
			//
			_MaxSizeRollBackups = -1;

			//
			// Log 30 messages.  This is 5 groups, 6 checks per group ( 0, 100, 200, 300, 400, 500 
			// bytes for current file as messages are logged.
			//
			int iMessagesToLog = 30;	

			VerifyRolling( MakeNumericTestEntries( _testMessage, sBackupInfo, iMessagesToLog ) );
		}

		/// <summary>
		/// Validates rolling with no backup files, with count direction set to down.
		/// Only the current file should be present, wrapping to 0 bytes once the
		/// previous file fills up.
		/// </summary>
		[Test] public void TestRollingCountDownZeroBackups()
		{
			//
			// Oldest to newest when reading in a group left-to-right, so 1 2 3 means 1 is the
			// oldest, and 3 is the newest
			//
			string sBackupInfo = ", , , , ";

			//
			// Count Up
			//
			_iCountDirection = -1;

			//
			// No backups
			//
			_MaxSizeRollBackups = 0;

			//
			// Log 30 messages.  This is 5 groups, 6 checks per group ( 0, 100, 200, 300, 400, 500 
			// bytes for current file as messages are logged.
			//
			int iMessagesToLog = 30;	

			VerifyRolling( MakeNumericTestEntries( _testMessage, sBackupInfo, iMessagesToLog ) );
		}

		/// <summary>
		/// Configures the root appender for counting and rolling
		/// </summary>
		private void ConfigureRootAppender()
		{
			_root = ((log4net.Repository.Hierarchy.Hierarchy)LogManager.GetRepository()).Root;	
			_root.Level = Level.Debug;
			_caRoot = new CountingAppender();
			_root.AddAppender(_caRoot);
			Assert.AreEqual(_caRoot.Counter, 0); 

			//
			// Set the root appender with a RollingFileAppender
			//
			_root.AddAppender( CreateAppender() );

			_root.Repository.Configured = true;
		}

		/// <summary>
		/// Verifies that the current backup index is detected correctly when initializing
		/// </summary>
		/// <param name="sBaseFile"></param>
		/// <param name="alFiles"></param>
		/// <param name="iExpectedCurSizeRollBackups"></param>
		private void VerifyInitializeRollBackupsFromBaseFile( string sBaseFile, ArrayList alFiles, int iExpectedCurSizeRollBackups )
		{
			InitializeAndVerifyExpectedValue( alFiles, sBaseFile, CreateRollingFileAppender( "5,0,1" ), iExpectedCurSizeRollBackups );
		}

		/// <summary>
		/// Tests that the current backup index is 0 when no
		/// existing files are seen
		/// </summary>
		[Test] public void TestInitializeRollBackups1()
		{
			string sBaseFile = "LogFile.log";
			ArrayList arrFiles = new ArrayList();
			arrFiles.Add( "junk1" );
			arrFiles.Add( "junk1.log" );
			arrFiles.Add( "junk2.log" );
			arrFiles.Add( "junk.log.1" );
			arrFiles.Add( "junk.log.2" );

			int iExpectedCurSizeRollBackups = 0;
			VerifyInitializeRollBackupsFromBaseFile( sBaseFile, arrFiles, iExpectedCurSizeRollBackups );
		}

		/// <summary>
		/// Verifies that files are detected when the base file is specified
		/// </summary>
		/// <param name="sBaseFile"></param>
		private void VerifyInitializeRollBackupsFromBaseFile( string sBaseFile )
		{
			ArrayList alFiles = MakeTestDataFromString( sBaseFile, "0,1,2" );

			int iExpectedCurSizeRollBackups = 2;
			VerifyInitializeRollBackupsFromBaseFile( sBaseFile, alFiles, iExpectedCurSizeRollBackups );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountUpFixed()
		{
			ArrayList alFiles = MakeTestDataFromString( "3,4,5" );
			int iExpectedValue = 5;
			InitializeAndVerifyExpectedValue( alFiles, _fileName, CreateRollingFileAppender( "3,0,1" ), iExpectedValue );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountUpFixed2()
		{
			ArrayList alFiles = MakeTestDataFromString( "0,3" );
			int iExpectedValue = 3;
			InitializeAndVerifyExpectedValue( alFiles, _fileName, CreateRollingFileAppender( "3,0,1" ), iExpectedValue );
		}

		/// <summary>
		/// Verifies that count stays at 0 for the zero backups case
		/// when counting up
		/// </summary>
		[Test] public void TestInitializeCountUpZeroBackups()
		{
			ArrayList alFiles = MakeTestDataFromString( "0,3" );
			int iExpectedValue = 0;
			InitializeAndVerifyExpectedValue( alFiles, _fileName, CreateRollingFileAppender( "0,0,1" ), iExpectedValue );
		}

		/// <summary>
		/// Verifies that count stays at 0 for the zero backups case
		/// when counting down
		/// </summary>
		[Test] public void TestInitializeCountDownZeroBackups()
		{
			ArrayList alFiles = MakeTestDataFromString( "0,3" );
			int iExpectedValue = 0;
			InitializeAndVerifyExpectedValue( alFiles, _fileName, CreateRollingFileAppender( "0,0,-1" ), iExpectedValue );
		}


		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountDownFixed()
		{
			ArrayList alFiles = MakeTestDataFromString( "4,5,6" );
			VerifyInitializeDownFixedExpectedValue( alFiles, _fileName, 0 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountDownFixed2()
		{
			ArrayList alFiles = MakeTestDataFromString( "1,5,6" );
			VerifyInitializeDownFixedExpectedValue( alFiles, _fileName, 1 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountDownFixed3()
		{
			ArrayList alFiles = MakeTestDataFromString( "2,5,6" );
			VerifyInitializeDownFixedExpectedValue( alFiles, _fileName, 2 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountDownFixed4()
		{
			ArrayList alFiles = MakeTestDataFromString( "3,5,6" );
			VerifyInitializeDownFixedExpectedValue( alFiles, _fileName, 3 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountDownFixed5()
		{
			ArrayList alFiles = MakeTestDataFromString( "1,2,3" );
			VerifyInitializeDownFixedExpectedValue( alFiles, _fileName, 3 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountDownFixed6()
		{
			ArrayList alFiles = MakeTestDataFromString( "1,2" );
			VerifyInitializeDownFixedExpectedValue( alFiles, _fileName, 2 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// </summary>
		[Test] public void TestInitializeCountDownFixed7()
		{
			ArrayList alFiles = MakeTestDataFromString( "2,3" );
			VerifyInitializeDownFixedExpectedValue( alFiles, _fileName, 3 );
		}

		private void InitializeAndVerifyExpectedValue( ArrayList alFiles, string sBaseFile, RollingFileAppender rfa, int iExpectedValue )
		{
			InitializeRollBackups( rfa, sBaseFile, alFiles );
			Assert.AreEqual( iExpectedValue, GetFieldCurSizeRollBackups(rfa) );
		}

		/// <summary>
		/// Tests the count down case, with infinite max backups, to see that
		/// initialization of the rolling file appender results in the expected value
		/// </summary>
		/// <param name="alFiles"></param>
		/// <param name="sBaseFile"></param>
		/// <param name="iExpectedValue"></param>
		private void VerifyInitializeDownInfiniteExpectedValue( ArrayList alFiles, string sBaseFile, int iExpectedValue )
		{
			InitializeAndVerifyExpectedValue( alFiles, sBaseFile, CreateRollingFileAppender( "-1,0,-1" ), iExpectedValue );
		}

		/// <summary>
		/// Creates a RollingFileAppender with the desired values, where the
		/// values are passed as a comma separated string, with 3 parameters,
		/// m_maxSizeRollBackups, m_curSizeRollBackups, CountDirection
		/// </summary>
		/// <param name="sParams"></param>
		/// <returns></returns>
		private RollingFileAppender CreateRollingFileAppender( string sParams )
		{
			string[] asParams = sParams.Split(',');
			if (null == asParams || asParams.Length != 3 )
			{
				throw new ArgumentOutOfRangeException(sParams, sParams, "Must have 3 comma separated params: MaxSizeRollBackups, CurSizeRollBackups, CountDirection" );
			}

			RollingFileAppender rfa = new RollingFileAppender();
			rfa.RollingStyle = RollingFileAppender.RollingMode.Size;
			SetFieldMaxSizeRollBackups(rfa, Int32.Parse(asParams[0].Trim()));
			SetFieldCurSizeRollBackups(rfa, Int32.Parse(asParams[1].Trim()));
			rfa.CountDirection	   = Int32.Parse(asParams[2].Trim());

			return rfa;
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting down
		/// and infinite backups are selected
		/// </summary>
		[Test] public void TestInitializeCountDownInfinite()
		{
			ArrayList alFiles = MakeTestDataFromString( "2,3" );
			VerifyInitializeDownInfiniteExpectedValue( alFiles, _fileName, 3 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting down
		/// and infinite backups are selected
		/// </summary>
		[Test] public void TestInitializeCountDownInfinite2()
		{
			ArrayList alFiles = MakeTestDataFromString( "2,3,4,5,6,7,8,9,10" );
			VerifyInitializeDownInfiniteExpectedValue( alFiles, _fileName, 10 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting down
		/// and infinite backups are selected
		/// </summary>
		[Test] public void TestInitializeCountDownInfinite3()
		{
			ArrayList alFiles = MakeTestDataFromString( "9,10,3,4,5,7,9,6,1,2,8" );
			VerifyInitializeDownInfiniteExpectedValue( alFiles, _fileName, 10 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// and infinite backups are selected
		/// </summary>
		[Test] public void TestInitializeCountUpInfinite()
		{
			ArrayList alFiles = MakeTestDataFromString( "2,3" );
			VerifyInitializeUpInfiniteExpectedValue( alFiles, _fileName, 3 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// and infinite backups are selected
		/// </summary>
		[Test] public void TestInitializeCountUpInfinite2()
		{
			ArrayList alFiles = MakeTestDataFromString( "2,3,4,5,6,7,8,9,10" );
			VerifyInitializeUpInfiniteExpectedValue( alFiles, _fileName, 10 );
		}

		/// <summary>
		/// Verifies that count goes to the highest when counting up
		/// and infinite backups are selected
		/// </summary>
		[Test] public void TestInitializeCountUpInfinite3()
		{
			ArrayList alFiles = MakeTestDataFromString( "9,10,3,4,5,7,9,6,1,2,8" );
			VerifyInitializeUpInfiniteExpectedValue( alFiles, _fileName, 10 );
		}

		/// <summary>
		/// Creates a logger hierarchy, configures a rolling file appender and returns an ILogger
		/// </summary>
		/// <param name="filename">The filename to log to</param>
		/// <param name="lockModel">The locking model to use.</param>
		/// <param name="handler">The error handler to use.</param>
		/// <returns>A configured ILogger</returns>
		private ILogger CreateLogger(string filename,FileAppender.LockingModelBase lockModel, IErrorHandler handler)
		{
			log4net.Repository.Hierarchy.Hierarchy h = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.CreateRepository("TestRepository");

			log4net.Appender.RollingFileAppender appender = new log4net.Appender.RollingFileAppender();
			appender.File = filename;
			appender.AppendToFile = false;
			appender.CountDirection=0;
			appender.RollingStyle=RollingFileAppender.RollingMode.Size;
			appender.MaxFileSize=100000;
			appender.Encoding=System.Text.Encoding.ASCII;
			appender.ErrorHandler=handler;
			if (lockModel!=null) {appender.LockingModel=lockModel;}
			
			log4net.Layout.PatternLayout layout = new log4net.Layout.PatternLayout();
			layout.ConversionPattern = "%m%n";
			layout.ActivateOptions();

			appender.Layout = layout;
			appender.ActivateOptions();

			h.Root.AddAppender(appender);
			h.Configured = true;

			ILogger log=h.GetLogger("Logger");
			return log;
		}

		/// <summary>
		/// Destroys the logger hierarchy created by <see cref="RollingFileAppenderTests.CreateLogger"/>
		/// </summary>
		private void DestroyLogger()
		{
			log4net.Repository.Hierarchy.Hierarchy h = (log4net.Repository.Hierarchy.Hierarchy)log4net.LogManager.GetRepository("TestRepository");
			h.ResetConfiguration();
			//Replace the repository selector so that we can recreate the hierarchy with the same name if necessary
			LoggerManager.RepositorySelector=new DefaultRepositorySelector(log4net.Util.SystemInfo.GetTypeFromString("log4net.Repository.Hierarchy.Hierarchy",true,true));
		}

		private void AssertFileEquals(string filename, string contents)
		{
			StreamReader sr=new StreamReader(filename);
			string logcont=sr.ReadToEnd();
			sr.Close();

			Assert.AreEqual(contents,logcont, "Log contents is not what is expected");

			System.IO.File.Delete(filename);
		}

		/// <summary>
		/// Verifies that logging a message actually produces output
		/// </summary>
		[Test] public void TestLogOutput()
		{
			String filename="test.log";
			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,new FileAppender.ExclusiveLock(), sh);
			log.Log(this.GetType(),Level.Info,"This is a message",null);
			log.Log(this.GetType(),Level.Info,"This is a message 2",null);
			DestroyLogger();

			AssertFileEquals(filename,"This is a message"+Environment.NewLine+"This is a message 2"+Environment.NewLine);
			Assert.AreEqual("",sh.Message, "Unexpected error message");
		}

		/// <summary>
		/// Verifies that attempting to log to a locked file fails gracefully
		/// </summary>
		[Test] public void TestExclusiveLockFails()
		{
			String filename="test.log";

			FileStream fs=new FileStream(filename,FileMode.Create,FileAccess.Write,FileShare.None);
			fs.Write(System.Text.Encoding.ASCII.GetBytes("Test"),0,4);

			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,new FileAppender.ExclusiveLock(),sh);
			log.Log(this.GetType(),Level.Info,"This is a message",null);
			log.Log(this.GetType(),Level.Info,"This is a message 2",null);
			DestroyLogger();
			fs.Close();

			AssertFileEquals(filename,"Test");
			Assert.AreEqual(sh.Message.Substring(0,30), "Unable to acquire lock on file", "Expecting an error message");
		}

		/// <summary>
		/// Verifies that attempting to log to a locked file recovers if the lock is released
		/// </summary>
		[Test] public void TestExclusiveLockRecovers()
		{
			String filename="test.log";

			FileStream fs=new FileStream(filename,FileMode.Create,FileAccess.Write,FileShare.None);
			fs.Write(System.Text.Encoding.ASCII.GetBytes("Test"),0,4);

			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,new FileAppender.ExclusiveLock(),sh);
			log.Log(this.GetType(),Level.Info,"This is a message",null);
			fs.Close();
			log.Log(this.GetType(),Level.Info,"This is a message 2",null);
			DestroyLogger();

			AssertFileEquals(filename,"This is a message 2"+Environment.NewLine);
			Assert.AreEqual("Unable to acquire lock on file",sh.Message.Substring(0,30), "Expecting an error message");
		}

		/// <summary>
		/// Verifies that attempting to log to a file with ExclusiveLock really locks the file
		/// </summary>
		[Test] public void TestExclusiveLockLocks()
		{
			String filename="test.log";
			bool locked=false;

			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,new FileAppender.ExclusiveLock(),sh);
			log.Log(this.GetType(),Level.Info,"This is a message",null);

			try
			{
				FileStream fs=new FileStream(filename,FileMode.Create,FileAccess.Write,FileShare.None);
				fs.Write(System.Text.Encoding.ASCII.GetBytes("Test"),0,4);
				fs.Close();
			}
			catch (System.IO.IOException e1)
			{
				Assert.AreEqual("The process cannot access the file ",e1.Message.Substring(0,35), "Unexpected exception");
				locked=true;
			}

			log.Log(this.GetType(),Level.Info,"This is a message 2",null);
			DestroyLogger();

			Assert.IsTrue(locked, "File was not locked");
			AssertFileEquals(filename,"This is a message"+Environment.NewLine+"This is a message 2"+Environment.NewLine);
			Assert.AreEqual("",sh.Message, "Unexpected error message");
		}


		/// <summary>
		/// Verifies that attempting to log to a locked file fails gracefully
		/// </summary>
		[Test] public void TestMinimalLockFails()
		{
			String filename="test.log";

			FileStream fs=new FileStream(filename,FileMode.Create,FileAccess.Write,FileShare.None);
			fs.Write(System.Text.Encoding.ASCII.GetBytes("Test"),0,4);

			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,new FileAppender.MinimalLock(),sh);
			log.Log(this.GetType(),Level.Info,"This is a message",null);
			log.Log(this.GetType(),Level.Info,"This is a message 2",null);
			DestroyLogger();
			fs.Close();

			AssertFileEquals(filename,"Test");
			Assert.AreEqual("Unable to acquire lock on file",sh.Message.Substring(0,30), "Expecting an error message");
		}

		/// <summary>
		/// Verifies that attempting to log to a locked file recovers if the lock is released
		/// </summary>
		[Test] public void TestMinimalLockRecovers()
		{
			String filename="test.log";

			FileStream fs=new FileStream(filename,FileMode.Create,FileAccess.Write,FileShare.None);
			fs.Write(System.Text.Encoding.ASCII.GetBytes("Test"),0,4);

			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,new FileAppender.MinimalLock(),sh);
			log.Log(this.GetType(),Level.Info,"This is a message",null);
			fs.Close();
			log.Log(this.GetType(),Level.Info,"This is a message 2",null);
			DestroyLogger();

			AssertFileEquals(filename,"This is a message 2"+Environment.NewLine);
			Assert.AreEqual("Unable to acquire lock on file",sh.Message.Substring(0,30), "Expecting an error message");
		}

		/// <summary>
		/// Verifies that attempting to log to a file with ExclusiveLock really locks the file
		/// </summary>
		[Test] public void TestMinimalLockUnlocks()
		{
			String filename="test.log";
			bool locked=false;

			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,new FileAppender.MinimalLock(),sh);
			log.Log(this.GetType(),Level.Info,"This is a message",null);

			locked=true;
			FileStream fs=new FileStream(filename,FileMode.Append,FileAccess.Write,FileShare.None);
			fs.Write(System.Text.Encoding.ASCII.GetBytes("Test"+Environment.NewLine),0,4+Environment.NewLine.Length);
			fs.Close();

			log.Log(this.GetType(),Level.Info,"This is a message 2",null);
			DestroyLogger();

			Assert.IsTrue(locked, "File was not locked");
			AssertFileEquals(filename,"This is a message"+Environment.NewLine+"Test"+Environment.NewLine+"This is a message 2"+Environment.NewLine);
			Assert.AreEqual("",sh.Message, "Unexpected error message");
		}

		/// <summary>
		/// Verify that the default LockModel is ExclusiveLock, to maintain backwards compatibility with previous behaviour
		/// </summary>
		[Test] public void TestDefaultLockingModel()
		{
			String filename="test.log";
			SilentErrorHandler sh=new SilentErrorHandler();
			ILogger log=CreateLogger(filename,null,sh);

			IAppender[] appenders=log.Repository.GetAppenders();
			Assert.AreEqual(1,appenders.Length, "The wrong number of appenders are configured");

			RollingFileAppender rfa=(RollingFileAppender)(appenders[0]);
			Assert.AreEqual(log4net.Util.SystemInfo.GetTypeFromString("log4net.Appender.FileAppender+ExclusiveLock",true,true),rfa.LockingModel.GetType(), "The LockingModel is of an unexpected type");

			DestroyLogger();
		}
		
		/// <summary>
		/// Tests the count up case, with infinite max backups , to see that
		/// initialization of the rolling file appender results in the expected value
		/// </summary>
		/// <param name="alFiles"></param>
		/// <param name="sBaseFile"></param>
		/// <param name="iExpectedValue"></param>
		private void VerifyInitializeUpInfiniteExpectedValue( ArrayList alFiles, string sBaseFile, int iExpectedValue )
		{
			InitializeAndVerifyExpectedValue( alFiles, sBaseFile, CreateRollingFileAppender( "-1,0,1" ), iExpectedValue );
		}



		/// <summary>
		/// Tests the count down case, with max backups limited to 3, to see that
		/// initialization of the rolling file appender results in the expected value
		/// </summary>
		/// <param name="alFiles"></param>
		/// <param name="sBaseFile"></param>
		/// <param name="iExpectedValue"></param>
		private void VerifyInitializeDownFixedExpectedValue( ArrayList alFiles, string sBaseFile, int iExpectedValue )
		{
			InitializeAndVerifyExpectedValue( alFiles, sBaseFile, CreateRollingFileAppender( "3,0,-1" ), iExpectedValue );
		}

		/// <summary>
		/// Turns a string of comma separated numbers into a collection of filenames
		/// generated from the numbers.  
		/// 
		/// Defaults to filename in _fileName variable.
		/// 
		/// </summary>
		/// <param name="sFileNumbers">Comma separated list of numbers for counted file names</param>
		/// <returns></returns>
		private ArrayList MakeTestDataFromString( string sFileNumbers )
		{
			return MakeTestDataFromString( _fileName, sFileNumbers );
		}

		/// <summary>
		/// Turns a string of comma separated numbers into a collection of filenames
		/// generated from the numbers
		/// 
		/// Uses the input filename.
		/// </summary>
		/// <param name="sFileName">Name of file to combine with numbers when generating counted file names</param>
		/// <param name="sFileNumbers">Comma separated list of numbers for counted file names</param>
		/// <returns></returns>
		private ArrayList MakeTestDataFromString( string sFileName, string sFileNumbers )
		{
			ArrayList alFiles = new ArrayList();

			string[] sNumbers = sFileNumbers.Split( ',' );
			foreach( string sNumber in sNumbers )
			{
				Int32 iValue = Int32.Parse( sNumber.Trim() );
				alFiles.Add( MakeFileName(sFileName, iValue) );
			}

			return alFiles;
		}

		/// <summary>
		/// Tests that the current backup index is correctly detected
		/// for a file with no extension
		/// </summary>
		[Test] public void TestInitializeRollBackups2()
		{
			VerifyInitializeRollBackupsFromBaseFile( "LogFile" );
		}

		/// <summary>
		/// Tests that the current backup index is correctly detected
		/// for a file with a .log extension
		/// </summary>
		[Test] public void TestInitializeRollBackups3()
		{
			VerifyInitializeRollBackupsFromBaseFile( "LogFile.log" );
		}

		/// <summary>
		/// Makes sure that the initialization can detect the backup
		/// number correctly.
		/// </summary>
		/// <param name="iBackups"></param>
		/// <param name="iMaxSizeRollBackups"></param>
		public void VerifyInitializeRollBackups(int iBackups, int iMaxSizeRollBackups)
		{
			string sBaseFile = "LogFile.log";
			ArrayList arrFiles = new ArrayList();
			arrFiles.Add( "junk1" );
			for( int i=0; i<iBackups; i++ ) 
			{
				arrFiles.Add( MakeFileName(sBaseFile, i) );
			}
			RollingFileAppender rfa = new RollingFileAppender();
			rfa.RollingStyle = RollingFileAppender.RollingMode.Size;
			SetFieldMaxSizeRollBackups(rfa, iMaxSizeRollBackups);
			SetFieldCurSizeRollBackups(rfa, 0);
			InitializeRollBackups( rfa, sBaseFile, arrFiles );

			// iBackups	/ Meaning
			// 0 = none
			// 1 = file.log
			// 2 = file.log.1
			// 3 = file.log.2
			if (0 == iBackups || 
				1 == iBackups ) 
			{
				Assert.AreEqual( 0, GetFieldCurSizeRollBackups(rfa) );
			} 
			else 
			{
				Assert.AreEqual( Math.Min( iBackups-1, iMaxSizeRollBackups), GetFieldCurSizeRollBackups(rfa) );
			}
		}

		/// <summary>
		/// Tests that the current backup index is correctly detected,
		/// and gets no bigger than the max backups setting
		/// </summary>
		[Test] public void TestInitializeRollBackups4()
		{
			const int iMaxRollBackups = 5;
			VerifyInitializeRollBackups( 0, iMaxRollBackups );
			VerifyInitializeRollBackups( 1, iMaxRollBackups );
			VerifyInitializeRollBackups( 2, iMaxRollBackups );
			VerifyInitializeRollBackups( 3, iMaxRollBackups );
			VerifyInitializeRollBackups( 4, iMaxRollBackups );
			VerifyInitializeRollBackups( 5, iMaxRollBackups );
			VerifyInitializeRollBackups( 6, iMaxRollBackups );
			// Final we cap out at the max value
			VerifyInitializeRollBackups( 7, iMaxRollBackups );
			VerifyInitializeRollBackups( 8, iMaxRollBackups );
		}

		/// <summary>
		/// 
		/// </summary>
		[Test,Ignore("Not Implemented: Want to test counted files limited up, to see that others are ?? ignored? deleted?")]
		public void TestInitialization3()
		{
		}

		/// <summary>
		/// 
		/// </summary>
		[Test,Ignore("Not Implemented: Want to test counted files limited down, to see that others are ?? ignored? deleted?")]
		public void TestInitialization4()
		{
		}

		/// <summary>
		/// 
		/// </summary>
		[Test,Ignore("Not Implemented: Want to test dated files with a limit, to see that others are ?? ignored? deleted?")]
		public void TestInitialization5()
		{
		}

		/// <summary>
		/// 
		/// </summary>
		[Test,Ignore("Not Implemented: Want to test dated files with no limit, to see that others are ?? ignored? deleted?")]
		public void TestInitialization6()
		{
		}

		/// <summary>
		/// 
		/// </summary>
		[Test,Ignore("Not Implemented: Want to test dated files with mixed dates existing, to see that other dates do not matter")]
		public void TestInitialization7()
		{
		}


		//
		// Helper functions to dig into the appender
		//

		private static ArrayList GetExistingFiles(string baseFilePath)
		{
			RollingFileAppender appender = new RollingFileAppender();
			appender.SecurityContext = log4net.Util.NullSecurityContext.Instance;

			return (ArrayList)Utils.InvokeMethod(appender, "GetExistingFiles", baseFilePath);
		}

		private void InitializeRollBackups(RollingFileAppender appender, string baseFile, ArrayList arrayFiles)
		{
			Utils.InvokeMethod(appender, "InitializeRollBackups", baseFile, arrayFiles);
		}

		private int GetFieldCurSizeRollBackups(RollingFileAppender appender)
		{
			return (int)Utils.GetField(appender, "m_curSizeRollBackups");
		}

		private void SetFieldCurSizeRollBackups(RollingFileAppender appender, int val)
		{
			Utils.SetField(appender, "m_curSizeRollBackups", val);
		}
		private void SetFieldMaxSizeRollBackups(RollingFileAppender appender, int val)
		{
			Utils.SetField(appender, "m_maxSizeRollBackups", val);
		}
	}

	[TestFixture] public class RollingFileAppenderSubClassTest : RollingFileAppender
	{
		[Test] public void TestComputeCheckPeriod()
		{
			RollingFileAppender rfa = new RollingFileAppender();

			Assert.AreEqual(RollPoint.TopOfMinute, InvokeComputeCheckPeriod(rfa, ".yyyy-MM-dd HH:mm"), "TopOfMinute pattern");
			Assert.AreEqual(RollPoint.TopOfHour, InvokeComputeCheckPeriod(rfa, ".yyyy-MM-dd HH"), "TopOfHour pattern");
			Assert.AreEqual(RollPoint.HalfDay, InvokeComputeCheckPeriod(rfa, ".yyyy-MM-dd tt"), "HalfDay pattern");
			Assert.AreEqual(RollPoint.TopOfDay, InvokeComputeCheckPeriod(rfa, ".yyyy-MM-dd"), "TopOfDay pattern");
			Assert.AreEqual(RollPoint.TopOfMonth, InvokeComputeCheckPeriod(rfa, ".yyyy-MM"), "TopOfMonth pattern");

			// Test invalid roll point
			Assert.AreEqual(RollPoint.InvalidRollPoint, InvokeComputeCheckPeriod(rfa, "..."), "TopOfMonth pattern");
		}

		private static RollPoint InvokeComputeCheckPeriod(RollingFileAppender rollingFileAppender, string datePattern) 
		{
			return (RollPoint)Utils.InvokeMethod(rollingFileAppender, "ComputeCheckPeriod", datePattern);
		}
	}

}
