Badgerati/Edison

View on GitHub
Edison.Engine/Contexts/EdisonContext.cs

Summary

Maintainability
C
1 day
Test Coverage
/*
Edison is designed to be simpler and more performant unit/integration testing framework.

Copyright (c) 2015, Matthew Kelly (Badgerati)
Company: Cadaeic Studios
License: MIT (see LICENSE for details)
 */

using Edison.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Edison.Engine.Threading;
using Edison.Engine.Core.Enums;
using Edison.Engine.Utilities.Structures;
using System.Diagnostics;
using Edison.Engine.Events;
using Edison.Engine.Repositories.Interfaces;
using Edison.Injector;
using System.Threading.Tasks;
using Edison.Engine.Utilities.Helpers;
using Edison.Engine.Repositories.Outputs;

namespace Edison.Engine.Contexts
{
    [Serializable]
    public class EdisonContext
    {

        #region Repositories

        private IAssemblyRepository AssemblyRepository
        {
            get { return DIContainer.Instance.Get<IAssemblyRepository>(); }
        }

        private IPathRepository PathRepository
        {
            get { return DIContainer.Instance.Get<IPathRepository>(); }
        }

        private IReflectionRepository ReflectionRepository
        {
            get { return DIContainer.Instance.Get<IReflectionRepository>(); }
        }

        private IFileRepository FileRepository
        {
            get { return DIContainer.Instance.Get<IFileRepository>(); }
        }

        private IDirectoryRepository DirectoryRepository
        {
            get { return DIContainer.Instance.Get<IDirectoryRepository>(); }
        }

        private IAppDomainRepository AppDomainRepository
        {
            get { return DIContainer.Instance.Get<IAppDomainRepository>(); }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the assembly paths.
        /// </summary>
        /// <value>
        /// The assembly paths.
        /// </value>
        public List<string> Assemblies { get; set; }

        [Obsolete("This will be deprecated in future releases, use the Assemblies property instead.")]
        public List<string> AssemblyPaths
        {
            get { return Assemblies; }
            set { Assemblies = value; }
        }

        /// <summary>
        /// Gets or sets the included categories.
        /// </summary>
        /// <value>
        /// The included categories.
        /// </value>
        public List<string> IncludedCategories { get; set; }

        /// <summary>
        /// Gets or sets the excluded categories.
        /// </summary>
        /// <value>
        /// The excluded categories.
        /// </value>
        public List<string> ExcludedCategories { get; set; }

        /// <summary>
        /// Gets or sets the fixtures.
        /// </summary>
        /// <value>
        /// The fixtures.
        /// </value>
        public List<string> Fixtures { get; set; }

        /// <summary>
        /// Gets the tests.
        /// </summary>
        /// <value>
        /// The tests.
        /// </value>
        public List<string> Tests { get; set; }

        /// <summary>
        /// Gets or sets the number of fixture threads.
        /// </summary>
        /// <value>
        /// The number of fixture threads.
        /// </value>
        public int NumberOfFixtureThreads { get; set; }

        /// <summary>
        /// Gets or sets the number of test threads.
        /// </summary>
        /// <value>
        /// The number of test threads.
        /// </value>
        public int NumberOfTestThreads { get; set; }

        /// <summary>
        /// Gets or sets the output file name.
        /// </summary>
        /// <value>
        /// The output file.
        /// </value>
        public string OutputFile { get; set; }

        /// <summary>
        /// Gets or sets the output directory path.
        /// </summary>
        /// <value>
        /// The output folder.
        /// </value>
        public string OutputDirectory { get; set; }

        /// <summary>
        /// Gets or sets the type of the output.
        /// </summary>
        /// <value>
        /// The type of the output.
        /// </value>
        public OutputType OutputType { get; set; }

        /// <summary>
        /// Gets or sets the type of the console output.
        /// </summary>
        /// <value>
        /// The type of the console output.
        /// </value>
        public OutputType ConsoleOutputType { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether file output should be disabled.
        /// </summary>
        /// <value>
        ///   <c>true</c> if file output is disabled; otherwise, <c>false</c>.
        /// </value>
        public bool DisableFileOutput { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether console output should be disabled.
        /// </summary>
        /// <value>
        /// <c>true</c> if console output is disabled; otherwise, <c>false</c>.
        /// </value>
        public bool DisableConsoleOutput { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether test output should be disabled.
        /// </summary>
        /// <value>
        ///   <c>true</c> if test output is disabled; otherwise, <c>false</c>.
        /// </value>
        public bool DisableTestOutput { get; set; }

        /// <summary>
        /// Gets or sets the test result URL.
        /// </summary>
        /// <value>
        /// The test result URL.
        /// </value>
        public string TestResultURL { get; set; }

        /// <summary>
        /// Gets or sets the test run identifier.
        /// </summary>
        /// <value>
        /// The test run identifier.
        /// </value>
        public string TestRunId { get; set; }

        /// <summary>
        /// Gets or sets the test run's informative name.
        /// </summary>
        /// <value>
        /// The test run's informative name.
        /// </value>
        public string TestRunName { get; set; }

        /// <summary>
        /// Gets or sets the test run's project name.
        /// </summary>
        /// <value>
        /// The test run's project name.
        /// </value>
        public string TestRunProject { get; set; }

        /// <summary>
        /// Gets or sets the name of the environment that the test run occurred.
        /// </summary>
        /// <value>
        /// The name of the environment the rest run occurred.
        /// </value>
        public string TestRunEnvironment { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the context should re-run failed tests.
        /// </summary>
        /// <value>
        ///   <c>true</c> if re-run failed tests; otherwise, <c>false</c>.
        /// </value>
        public bool RerunFailedTests { get; set; }

        /// <summary>
        /// Gets or sets the re-run threshold value.
        /// </summary>
        /// <value>
        /// The rerun threshold.
        /// </value>
        public int RerunThreshold { get; set; }

        /// <summary>
        /// Gets or sets the test suite.
        /// </summary>
        /// <value>
        /// The suite.
        /// </value>
        public string Suite { get; set; }

        /// <summary>
        /// Gets or sets the solution.
        /// </summary>
        /// <value>
        /// The solution.
        /// </value>
        public string Solution { get; set; }

        /// <summary>
        /// Gets or sets the solution configuration.
        /// </summary>
        /// <value>
        /// The solution configuration.
        /// </value>
        public string SolutionConfiguration { get; set; }

        /// <summary>
        /// Gets or sets the slack token.
        /// </summary>
        /// <value>
        /// The slack token.
        /// </value>
        public string SlackToken { get; set; }

        #endregion

        #region Public Readonly Properties

        /// <summary>
        /// Gets a value indicating whether this instance is running.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance is running; otherwise, <c>false</c>.
        /// </value>
        public bool IsRunning { get; private set; }

        /// <summary>
        /// Gets the current assembly that is currently having its tests executed.
        /// </summary>
        /// <value>
        /// The current assembly.
        /// </value>
        public string CurrentAssembly { get; private set; }

        #endregion

        #region Private Fields

        private TestResultDictionary ResultQueue;
        private IList<TestFixtureThread> ParallelThreads = default(IList<TestFixtureThread>);
        private Task ParallelTask = default(Task);
        private TestFixtureThread SingularThread = default(TestFixtureThread);
        private Task SingularTask = default(Task);
        private IList<ResultCalloutThread> ResultCalloutThreads = default(IList<ResultCalloutThread>);
        private Task ResultCalloutTask = default(Task);

        #endregion

        #region Events

        public event TestResultEventHandler OnTestResult;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="EdisonContext"/> class.
        /// </summary>
        private EdisonContext()
        {
            Assemblies = new List<string>(1);
            IncludedCategories = new List<string>();
            ExcludedCategories = new List<string>();
            Fixtures = new List<string>();
            Tests = new List<string>();
            NumberOfFixtureThreads = 1;
            NumberOfTestThreads = 1;
            ConsoleOutputType = OutputType.Txt;
            OutputType = OutputType.Json;
            OutputDirectory = Environment.CurrentDirectory;
            OutputFile = "ResultFile";
            TestResultURL = string.Empty;
            IsRunning = true;
            RerunThreshold = 100;
        }

        /// <summary>
        /// Creates an instance of an EdisonContext.
        /// </summary>
        /// <returns></returns>
        public static EdisonContext Create()
        {
            return new EdisonContext();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Validates this instance.
        /// </summary>
        public void Validate()
        {
            ContextValidator.Validate(this);
        }

        /// <summary>
        /// Runs this instance after passing validation, executing tests in the passed assemblies/solution.
        /// </summary>
        /// <returns></returns>
        public TestResultDictionary Run()
        {
            //run validation first
            Validate();

            // if we have a test result URL, send start event
            if (!string.IsNullOrWhiteSpace(TestResultURL))
            {
                TestResultUrlHelper.SendStart(this);
            }

            // create initial timer
            var timer = new Stopwatch();

            // setup a try-finally, so that if at any we stop we notify a possible endpoint
            try
            {
                //start timer
                timer.Start();

                //set logging output
                SetupLogging();

                //set output logging type
                Logger.Instance.ConsoleOutputType = ConsoleOutputType;

                //create results queue/list
                ResultQueue = new TestResultDictionary(this);

                //bind test result events
                if (OnTestResult != default(TestResultEventHandler))
                {
                    ResultQueue.OnTestResult += OnTestResult;
                }

                //loop through all assemblies, running their tests
                RunAssemblies();

                //stop the timer
                timer.Stop();

                //if we have single/none line logging, post the failed test messages
                if (Logger.Instance.IsSingleOrNoLined && ResultQueue.FailedTestResults.Any())
                {
                    WriteFailedResultsToConsole();
                }
            }
            finally
            {
                // wait for the callout task to finish (in case a fatal error was thrown)
                StopResultCalloutTask();

                // if we have a test result URL, send end event
                if (!string.IsNullOrWhiteSpace(TestResultURL))
                {
                    TestResultUrlHelper.SendEnd(this);
                }
            }

            //create result file and write
            WriteResultsToFile();

            //write results and timer
            Logger.Instance.WriteDoubleLine(Environment.NewLine);
            Logger.Instance.WriteMessage(ResultQueue.ToTotalString());
            Logger.Instance.WriteMessage(string.Format("Total time: {0}", timer.Elapsed));
            Logger.Instance.WriteDoubleLine(postcede: Environment.NewLine);

            // end the run, and return results
            IsRunning = false;
            return ResultQueue;
        }

        /// <summary>
        /// Interrupts this instance.
        /// </summary>
        public void Interrupt()
        {
            // first attempt to stop the parallel threads
            if (ParallelThreads != default(IList<TestFixtureThread>))
            {
                foreach (var thread in ParallelThreads)
                {
                    thread.Interrupt();
                }

                Task.WaitAll(ParallelTask);
            }

            // then attempt to stop the singular threads
            if (SingularThread != default(TestFixtureThread))
            {
                SingularThread.Interrupt();
                Task.WaitAll(SingularTask);
            }

            // finally, stop the callouts
            StopResultCalloutTask();

            // log
            Logger.Instance.WriteMessage(string.Format("{1}{1}{0}", "EDISON STOPPED", Environment.NewLine));
        }

        /// <summary>
        /// Sends a test result to a callout endpoint asynchronously.
        /// </summary>
        /// <param name="result">The test result to send.</param>
        /// <param name="type">The type of callout to send to.</param>
        public void SendTestResultCallout(TestResult result, ResultCalloutType type)
        {
            // if no result, return
            if (result == default(TestResult))
            {
                return;
            }

            // apply naive load balancing, grab one with lowest results
            var thread = ResultCalloutThreads[0].Count <= ResultCalloutThreads[1].Count
                ? ResultCalloutThreads[0]
                : ResultCalloutThreads[1];

            // add to thread
            thread.Add(result, type);
        }

        #endregion

        #region Private Methods

        private Assembly ResolveAssembly(object sender, ResolveEventArgs args)
        {
            var name = new AssemblyName(args.Name).Name;
            var path = PathRepository.GetDirectoryName(args.RequestingAssembly == null ? "." : args.RequestingAssembly.Location);
            return AssemblyRepository.LoadFrom(path + "\\" + name + ".dll");
        }

        private void RunAssemblies()
        {
            //remove any duplicate assembly paths
            var assemblies = Assemblies.Distinct();

            //bind assembly event resolver
            AppDomainRepository.CurrentDomain.AssemblyResolve += ResolveAssembly;

            //loop through all assemblies, running their tests
            foreach (var assemblyPath in assemblies)
            {
                CurrentAssembly = PathRepository.GetFileName(assemblyPath);

                var assembly = AssemblyRepository.LoadFile(assemblyPath);
                AppDomainRepository.SetAppConfig(PathRepository.GetFullPath(assemblyPath) + ".config");

                //global setup
                var globalSetupFixture = AssemblyRepository.GetTypes<SetupFixtureAttribute>(assembly, default(IList<string>), default(IList<string>), null).SingleOrDefault();
                var globalActivator = default(object);
                var globalSetupEx = default(Exception);

                if (globalSetupFixture != default(Type))
                {
                    globalActivator = Activator.CreateInstance(globalSetupFixture, null);
                    globalSetupEx = RunGlobalSetup(globalSetupFixture, globalActivator);
                }

                //test fixtures an threads
                RunThreads(assembly, globalSetupEx);

                //global teardown
                if (globalSetupFixture != default(Type))
                {
                    RunGlobalTeardown(globalSetupFixture, globalActivator);
                }
            }
        }

        private void SetupLogging()
        {
            if (DisableConsoleOutput)
            {
                Logger.Instance.Disable();
                Logger.Instance.DisableConsole();
            }

            if (DisableTestOutput)
            {
                Logger.Instance.DisableConsole();
            }

            Logger.Instance.ConsoleOutputType = ConsoleOutputType;
        }

        private Exception RunGlobalSetup(Type fixture, object activator)
        {
            if (fixture == default(Type))
            {
                return default(Exception);
            }

            try
            {
                var setup = ReflectionRepository.GetMethods<SetupAttribute>(fixture);
                ReflectionRepository.Invoke(setup, activator);
            }
            catch (Exception ex)
            {
                return ex;
            }

            return default(Exception);
        }

        private void RunGlobalTeardown(Type fixture, object activator)
        {
            if (fixture == default(Type) || activator == default(object))
            {
                return;
            }

            try
            {
                var teardown = ReflectionRepository.GetMethods<TeardownAttribute>(fixture);
                ReflectionRepository.Invoke(teardown, activator);
            }
            catch (Exception ex)
            {
                Logger.Instance.WriteInnerException(ex, true);
            }
        }

        private void WriteResultsToFile()
        {
            if (DisableFileOutput)
            {
                Logger.Instance.WriteMessage("Output file creation disabled");
                return;
            }

            Logger.Instance.WriteDoubleLine(Environment.NewLine);

            if (Logger.Instance.IsSingleOrNoLined)
            {
                Logger.Instance.WriteMessage(Environment.NewLine);
            }

            Logger.Instance.WriteMessage("Creating output file...");
            var file = Logger.Instance.CreateFile(OutputDirectory, OutputFile, OutputType);

            if (!string.IsNullOrWhiteSpace(file))
            {
                var results = ResultQueue.TestResults.ToList();
                var output = OutputRepositoryFactory.Get(OutputType);

                if (!string.IsNullOrWhiteSpace(output.OpenTag))
                {
                    Logger.Instance.WriteToFile(file, output.OpenTag + Environment.NewLine);
                }

                for (var i = 0; i < results.Count; i++)
                {
                    Logger.Instance.WriteResultToFile(file, i == (results.Count - 1), results[i], output);
                }

                if (!string.IsNullOrWhiteSpace(output.CloseTag))
                {
                    Logger.Instance.WriteToFile(file, Environment.NewLine + output.CloseTag);
                }

                Logger.Instance.WriteMessage(string.Format("Output file created: {0}{1}", file, Environment.NewLine));
            }
        }

        private void WriteFailedResultsToConsole()
        {
            // get all of the failed tests
            var failedResults = ResultQueue.FailedTestResults;

            try
            {
                // set the logger to be output type of TXT
                Logger.Instance.ConsoleOutputType = OutputType.Txt;
                Logger.Instance.WriteDoubleLine(Environment.NewLine, Environment.NewLine);

                // write each of the failed results the console
                foreach (var result in failedResults)
                {
                    Logger.Instance.WriteTestResult(result);
                }
            }
            finally
            {
                // reset the console output type of the logger
                Logger.Instance.WriteDoubleLine(string.Empty, Environment.NewLine);
                Logger.Instance.ConsoleOutputType = ConsoleOutputType;
            }
        }

        #endregion

        #region Threading

        private void RunThreads(Assembly assembly, Exception globalSetupEx)
        {

            #region Fetch tests to run

            // get all possible test fixtures
            var testFixtures = AssemblyRepository.GetTestFixtures(assembly, IncludedCategories, ExcludedCategories, Fixtures, Tests, Suite).ToList();
            var singularTestFixtures = default(List<Type>);

            if (!testFixtures.Any())
            {
                return;
            }

            #endregion

            #region Callout Threads

            ResultCalloutThreads = new List<ResultCalloutThread>()
            {
                new ResultCalloutThread(this),
                new ResultCalloutThread(this)
            };

            ResultCalloutTask = Task.Run(() => Parallel.ForEach(ResultCalloutThreads, thread => thread.Run()));

            #endregion

            #region Parallel Threads

            // if we're running in parallel, remove any singular test fixtures
            if (NumberOfFixtureThreads > 1 && testFixtures.Count() != 1)
            {
                singularTestFixtures = testFixtures.Where(t => ReflectionRepository.HasValidConcurrency(t, ConcurrencyType.Serial)).OrderBy(t => t.FullName).ToList();
                testFixtures = testFixtures.Where(t => ReflectionRepository.HasValidConcurrency(t, ConcurrencyType.Parallel)).OrderBy(t => t.FullName).ToList();
            }

            var fixturesCount = testFixtures.Count();
            var fixturesThreadCount = NumberOfFixtureThreads;
            if (fixturesCount < NumberOfFixtureThreads)
            {
                fixturesThreadCount = fixturesCount;
            }

            ParallelThreads = new List<TestFixtureThread>(fixturesThreadCount);
            var segment = fixturesCount == 0 ? 0 : (int)Math.Round((double)fixturesCount / (double)fixturesThreadCount, MidpointRounding.ToEven);

            // setup all the threads that are to be run in parallel
            var threadCount = 1;
            for (threadCount = 1; threadCount <= fixturesThreadCount; threadCount++)
            {
                var testFixturesSegment = threadCount == fixturesThreadCount
                    ? testFixtures.Skip((int)((threadCount - 1) * segment))
                    : testFixtures.Skip((int)((threadCount - 1) * segment)).Take((int)(segment));

                var thread = new TestFixtureThread(threadCount, this, ResultQueue, testFixturesSegment, globalSetupEx, ConcurrencyType.Parallel, NumberOfTestThreads);
                ParallelThreads.Add(thread);
            }

            // run the parallel threads
            ParallelTask = Task.Run(() => Parallel.ForEach(ParallelThreads, thread => thread.RunTestFixtures()));
            Task.WaitAll(ParallelTask);

            #endregion

            #region Singular Thread

            // setup - if needed - the singular thread
            if (NumberOfFixtureThreads > 1 && !EnumerableHelper.IsNullOrEmpty(singularTestFixtures))
            {
                SingularThread = new TestFixtureThread(threadCount, this, ResultQueue, singularTestFixtures, globalSetupEx, ConcurrencyType.Serial, NumberOfTestThreads);
                SingularTask = Task.Factory.StartNew(() => SingularThread.RunTestFixtures());
                Task.WaitAll(SingularTask);
            }

            // if an exception was thrown in global setup, return at this point
            if (globalSetupEx != default(Exception))
            {
                return;
            }

            #endregion

            #region Rerun Failed Tests

            // if enabled, and under threshold, re-run failed tests
            if (RerunFailedTests && ResultQueue.TotalFailedCount != 0 && ResultQueue.TotalCount != 0)
            {
                var percentageFailed = (int)ResultQueue.FailureRate;

                if (percentageFailed <= RerunThreshold)
                {
                    var failedTests = ResultQueue.FailedTestResults.Select(x => x.BasicName).ToList();
                    var rerunTestFixtures = AssemblyRepository.GetTestFixtures(assembly, default(IList<string>), default(IList<string>), default(IList<string>), failedTests, null).ToList();
                    SingularThread = new TestFixtureThread(threadCount + 1, this, ResultQueue, rerunTestFixtures, default(Exception), ConcurrencyType.Serial, 1);
                    SingularTask = Task.Factory.StartNew(() => SingularThread.RunTestFixtures());
                    Task.WaitAll(SingularTask);
                }
            }

            #endregion

            #region Wait for callouts to send

            StopResultCalloutTask();

            #endregion
        }

        private void StopResultCalloutTask()
        {
            if (ResultCalloutThreads != default(IList<ResultCalloutThread>))
            {
                foreach (var thread in ResultCalloutThreads)
                {
                    thread.Interrupt();
                }

                Task.WaitAll(ResultCalloutTask);
            }
        }

        #endregion

    }
}