Day: May 27, 2011

Using log4net and Ninject auto-binding in FitNesse

FitNesse executes dotnet assembly through (Syterra) runner (for fitSharp), this brought up some path related problems. One problem we faced is log4net configuration, putting log4net.config in assembly path doesn’t work.

From fit content page we can see:


!define COMMAND_PATTERN {%m -a C:\Source\project\TestHelper\App.config -r fitnesse.fitserver.FitServer,dotnet4\fit.dll %p}
!define TEST_RUNNER {dotnet4\Runner.exe}
!path C:\Source\project\TestSuite\bin\Release\TestSuite.dll

!contents
'''Prerequisites:'''

'''Test Case:'''

The only config file runner can take is the one from test helper, putting log4net in runner’s path might work, but it will introduce maintenance headache. The cleaner way to configure log4net might be to do it through code, idea from this post.

 public static class SimpleLogger
    {
        static SimpleLogger()
        {
            ConfigureLog();
        }
        public static ILog For(object objectToLog)
        {
            return For(objectToLog.GetType());
        }
        public static ILog For(Type typeToLog)
        {
            return LogManager.GetLogger(typeToLog);
        }

        private static void ConfigureLog()
        {
            Logger root;
            root = ((Hierarchy)LogManager.GetRepository()).Root;

            root.AddAppender(GetFileAppender());
            root.Repository.Configured = true;
        }

        private static FileAppender GetFileAppender()
        {
            FileAppender lAppender = new FileAppender();
            lAppender.Name = "File";
            lAppender.AppendToFile = true;
            lAppender.File = @"c:\tmp\log.txt";
            lAppender.Layout = new
            log4net.Layout.PatternLayout("%date{dd-MM-yyyy HH:mm:ss,fff} %5level [%2thread] %message (%logger{1}:%line)%n");
            lAppender.Threshold = log4net.Core.Level.All;
            lAppender.ActivateOptions();

            return lAppender;
        }
    }

Another problem we got is Ninject convention extension, aka auto-binding. This code works in application but not in fitnesse runner:

 Kernel.Scan(x =>
              {
//                x.FromAssemblyContaining<SampleClass>();  // This works in fitnesse runner, but will be messy if sample classes are too many.
//                string path = Path.GetDirectoryName((new System.Uri(Assembly.GetExecutingAssembly().CodeBase)).AbsolutePath);
//               x.FromAssembliesInPath(path);
//               x.FromAssembliesInPath(".");
                  x.FromAssembliesMatching("*");  // Doesn't work in fitnesse runner
//                  x.AutoLoadModules();  // Don't do it if you have multiple modules defined.
                x.BindWithDefaultConventions();
                x.InTransientScope();
            });

The way Ninject doing auto-binding is to create a temporary appDomain, then try to load it into this temp domain.

 // private static IEnumerable FindAssemblies(IEnumerable filenames,
 // Predicate filter)
 {
 AppDomain temporaryDomain = CreateTemporaryAppDomain();

foreach (string file in filenames)
 {
 Assembly assembly = null;

if (File.Exists(file))
 {
 try
 {
 var name = new AssemblyName { CodeBase = file };
 assembly = temporaryDomain.Load(name);
 // assembly = Assembly.ReflectionOnlyLoadFrom(file); // this works in fitnesse
 }
 catch (.....
 

The construct of AssemblyName by setting file string to CodeBase actually failed in fitnesse environment, this can be proven by checking the AssemblyName.FullName. In this case, it’s empty.
Google search found a different way to construct Assembly simply by calling Assembly.ReflectionOnlyLoadFrom(file);
Our new IoC setup code looks then like this:

        static void Scan()
        {
              Kernel.Scan(x =>
              {
                x.From(get_all_assembly_instances());
                x.BindWithDefaultConventions();
                x.InTransientScope();
            });
        }

        static IEnumerable<Assembly> get_all_assembly_instances()
        {
            string path = Path.GetDirectoryName(Path.GetFullPath(new System.Uri(Assembly.GetExecutingAssembly().CodeBase)).AbsolutePath));
            IEnumerable<string> assemblies = Directory.GetFiles(path).Where(IsAssemblyFile);

            IEnumerable<AssemblyName> assemblyNames = FindAssemblies(assemblies, y=>true);
            return assemblyNames.Select(name => Assembly.Load(name));
        }
// IsAssemblyFile, FindAssemblies can be copied from https://github.com/ninject/ninject.extensions.conventions/blob/master/src/Ninject.Extensions.Conventions/AssemblyScanner.cs
// While FindAssemblies needs to tweet to use  Assembly.ReflectionOnlyLoadFrom(file); to get Assembly instance.
        private static IEnumerable<AssemblyName> FindAssemblies(IEnumerable<string> filenames,
                                                                 Predicate<Assembly> filter)
        {
            return
                filenames.Select(Assembly.ReflectionOnlyLoadFrom).Where(assembly => filter(assembly)).
                    Select(assembly => assembly.GetName());
        }