Пример #1
0
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var xdbEnabled = data.SitecoreInfo.IsAnalyticsEnabled;
            var name       = "reporting";
            var reporting  = data.Databases.Sql[name];

            if (reporting == null)
            {
                var message = $"The {name} connection string is not presented in the ConnectionStrings.config file";
                if (xdbEnabled)
                {
                    output.Error(message);
                }
                else
                {
                    output.Debug(message + ", but that's okay since xdb is disabled");
                }

                return;
            }

            var schema = reporting.Schema;

            var sb = new List <string>();

            // check tables
            foreach (var tableName in TableNames)
            {
                if (!schema.Tables.ContainsKey(tableName))
                {
                    sb.Add($"{name}.Tables.dbo.{tableName}");
                }
            }

            foreach (var procedureName in ProcedureNames)
            {
                if (!schema.StoredProcedures.ContainsKey(procedureName))
                {
                    sb.Add($"{name}.Programmability.Stored Procedures.dbo.{procedureName}");
                }
            }

            if (sb.Count > 0)
            {
                var message = "One or several objects are missing in the reporting database. This may happen if EXM SQL script was not run or ended with error. Please refer to EXM installation guide for more details.";
                if (xdbEnabled)
                {
                    output.Error(message, detailed: new DetailedMessage(new BulletedList(sb)));
                }
                else
                {
                    output.Debug(message);
                }
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var ecmVersion = EcmHelper.GetEcmVersion(data);

            if (ecmVersion == null)
            {
                return;
            }

            if (ecmVersion.Major == 3 && ecmVersion.Minor >= 3)
            {
                Error(data, output, "exm.web");
                Error(data, output, "EXM.CryptographicKey");
                Error(data, output, "EXM.AuthenticationKey");

                if (data.Databases.ConnectionStrings.ContainsKey("master"))
                {
                    Error(data, output, "exm.master");
                }

                if (data.ServerRoles.Contains(ServerRole.ContentDelivery) &&
                    !data.ServerRoles.Contains(ServerRole.ContentManagement))
                {
                    Error(data, output, "EXM.InternalApiKey");
                    Error(data, output, "EmailCampaignClientService");
                }

                return;
            }

            if (!data.ServerRoles.Contains(ServerRole.ContentManagement))
            {
                return;
            }

            if (ecmVersion.Major == 3 && ecmVersion.Minor >= 1)
            {
                if (!data.Databases.Sql.DatabaseNames.Contains("exm.dispatch"))
                {
                    output.Error(GetErrorMessage("exm.dispatch"));
                }
            }

            if (ecmVersion.Major == 2 && ecmVersion.Minor == 2 || ecmVersion.Major == 3 && ecmVersion.Minor == 0)
            {
                if (!data.Databases.Mongo.DatabaseNames.Contains("ecm.dispatch"))
                {
                    output.Error(GetErrorMessage("ecm.dispatch"));
                }
            }
        }
Пример #3
0
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var core = data.Databases.Sql["core"];

            if (core == null)
            {
                output.Error("The core database is not found");

                return;
            }

            var eventQueueSize = core.CountRows("EventQueue");

            if (eventQueueSize == 0)
            {
                return;
            }

            var propertyChangedEventsCount = core.CountRows("EventQueue", Condition);

            Assert.IsTrue(propertyChangedEventsCount >= 0, "propertyChangedEventsCount >= 0");
            Assert.IsTrue(eventQueueSize > 0, "eventQueueSize > 0");

            var ratio = propertyChangedEventsCount * 100 / eventQueueSize;

            if (ratio < RateLow)
            {
                return;
            }

            var message = $"The EventQueue table is more than {RateMedium}% filled with the PropertyChangedRemoteEvent records.";

            if (ratio < RateMedium)
            {
                output.Debug(message);
                return;
            }

            if (ratio < RateHigh)
            {
                output.Warning(message, Link);
                return;
            }

            output.Error(message, Link);
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            foreach (var database in data.Databases.Sql.All)
            {
                var arr = database.Metrics.Tables
                          .SelectMany(t => t.Value.IndexesHealth
                                      .Select(i => new
                {
                    Name  = "Tables." + t.Key + ".Indexes." + i.Value.Name,
                    Value = i.Value.AverageFragmentationInPercent
                }))
                          .Where(x => x.Value > 10)
                          .ToArray();

                if (arr.Length <= 0)
                {
                    continue;
                }

                var indexes = arr.ToArray(x => $"{x.Name} ({(int)x.Value}%)");
                var message = new ShortMessage(
                    new BoldText(database.Name),
                    new Text($" database indexes require defragmentation."),
                    new BulletedList(indexes),
                    new Text("Refer to the 2.1 section in CMS Performance Tuning Guide for details."));

                output.Error(message);
            }
        }
Пример #5
0
        public override void Process(ISolutionResourceContext data, ITestOutputContext output)
        {
            var map = data

                      // exclude all CD instances - they do not participate in publishing
                      .Where(x => x.Value.ServerRoles.All(z => z != ServerRole.ContentDelivery))

                      // group all publishing instance setting values to set { 'Pub1': ['Cm1', 'Cm2'], 'Pub2': ['BadCm3'], '': ['BadCm4'] }
                      .GroupBy(x => x.Value.SitecoreInfo.GetSetting(PublishingInstanceSetting))
                      .ToMap(x => x.Key, x => x.ToArray(z => z.Key));

            if (map.Count == 0)
            {
                output.Debug("No publishing instance specified");

                return;
            }

            if (map.Count == 1)
            {
                output.Debug($"Publishing instance is consistent: {map.Keys.FirstOrDefault().EmptyToNull() ?? "N/A"}");

                return;
            }

            var message  = GetMessage(map);
            var detailed = GetDetailed(map);

            output.Error(message, detailed: detailed);
        }
Пример #6
0
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            if (data.WebServer.Server.IisVersion.ProductMajorPart < 7)
            {
                output.Error("IIS version is earlier one than officially supported. Sitecore XP supports IIS versions: 7.0, 7.5, 8.0, 8.5");
            }
        }
        private void Error(IInstanceResourceContext data, ITestOutputContext output, string name)
        {
            var value = data.Databases.ConnectionStrings.TryGetValue(name.ToLower());

            if (value == null)
            {
                output.Error(GetErrorMessage(name));
            }
        }
Пример #8
0
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            if (data.WebServer.Server.FrameworkVersions.All(p => !FrameworkVersion.v45x.HasFlag(p)))
            {
                output.Error("Sitecore XP requires .NET Framework 4.5.");
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            var value = data.SitecoreInfo.GetSetting("Analytics.ClusterName", "default-cd-cluster");

            if (string.IsNullOrWhiteSpace(value) || value == "default-cd-cluster")
            {
                output.Error(new ShortMessage("The Analytics.ClusterName setting is not configured"), new Uri("https://sitecore.stackexchange.com/questions/4970/analytics-clustername-in-a-multi-site-scaled-environment"));
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var logs = data.Logs.GetSitecoreLogEntries(LogLevel.Error);

            if (logs.Any(x => x != null && x.Message.Contains(Pattern)))
            {
                output.Error(Message, Link);
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var pool = data.WebServer.Site.ApplicationPool;

            if (pool.MaxWorkerProcesses > 1)
            {
                output.Error(GetErrorMessage(pool));
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            if (
                data.SitecoreInfo.Configuration.SelectSingleNode("/configuration/sitecore/scheduling/agent[@hint='ECM']") != null ||
                data.SitecoreInfo.Configuration.SelectSingleNode("/configuration/sitecore/scheduling/agent[@hint='EXM tasks']") != null ||
                data.SitecoreInfo.Configuration.SelectSingleNode("/configuration/sitecore/scheduling/agent[@hint='EXM instance task']") != null)
            {
                output.Error(ErrorMessage);
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var config            = data.SitecoreInfo.Configuration;
            var traceToLogElement = config.SelectSingleNode("/configuration/sitecore/pipelines/publishItem/processor[@type='Sitecore.Publishing.Pipelines.PublishItem.UpdateStatistics, Sitecore.Kernel']/traceToLog");

            if (traceToLogElement != null && traceToLogElement.InnerText.Equals("true", StringComparison.OrdinalIgnoreCase))
            {
                output.Error(MessageTraceToLogEnabled);
            }
        }
Пример #14
0
        public override void Process(ISolutionResourceContext solution, ITestOutputContext output)
        {
            var instancesWithWrongGlobalAsax = new List <string>();
            var instancesWithUnclear         = new Map();

            foreach (var data in solution.Values)
            {
                var originalGlobalAsaxFile = data.SitecoreInfo.GlobalAsaxFile;
                var globalAsaxFile         = originalGlobalAsaxFile.Replace(" ", string.Empty).ToLower().Replace("'", "\"");

                // If inherits points to System.Web.HttpApplication, output an error.
                if (globalAsaxFile.Contains("Inherits=\"System.Web.HttpApplication\"".ToLower()))
                {
                    instancesWithWrongGlobalAsax.Add(data.InstanceName);

                    continue;
                }

                // If inherits is missing, output an error.
                if (!globalAsaxFile.Contains("Inherits".ToLower()))
                {
                    instancesWithWrongGlobalAsax.Add(data.InstanceName);

                    continue;
                }

                // If inherits points to something else, output a warning that user needs to check manually if class inherits from Sitecore.Web.Application.
                if (!(globalAsaxFile.Contains("Inherits=\"Sitecore.Web.Application\"".ToLower()) ||
                      globalAsaxFile.Contains("Inherits=\"Sitecore.ContentSearch.SolrProvider.CastleWindsorIntegration.WindsorApplication\"".ToLower()) ||
                      globalAsaxFile.Contains("Inherits=\"Sitecore.ContentSearch.SolrProvider.AutoFacIntegration.AutoFacApplication\"".ToLower()) ||
                      globalAsaxFile.Contains("Inherits=\"Sitecore.ContentSearch.SolrProvider.NinjectIntegration.NinjectApplication\"".ToLower()) ||
                      globalAsaxFile.Contains("Inherits=\"Sitecore.ContentSearch.SolrProvider.StructureMapIntegration.StructureMapApplication\"".ToLower()) ||
                      globalAsaxFile.Contains("Inherits=\"Sitecore.ContentSearch.SolrProvider.UnityIntegration.UnityApplication\"".ToLower())))
                {
                    instancesWithUnclear.Add(data.InstanceName, originalGlobalAsaxFile);
                }
            }

            if (instancesWithWrongGlobalAsax.Count > 0)
            {
                output.Error(SystemWebMessage, detailed: new DetailedMessage(new BulletedList(instancesWithWrongGlobalAsax)));
            }

            if (instancesWithUnclear.Count > 0)
            {
                output.Warning(
                    new ShortMessage(
                        new Text(SystemWebMessage),
                        new Text(Comment)),
                    detailed: new DetailedMessage(new Table(instancesWithUnclear.ToArray(x => new TableRow(new Pair("Instance", x.Key), new Pair("Value", x.Value))))));
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var driveName = data.WebServer.Site.WebRoot.Root.Name;
            var size      = data.FileSystem.Drives.GetAvailableFreeSpace(driveName);

            if (size.GB < Minimum)
            {
                output.Error(GetErrorMessage(size, driveName));
            }
            else if (size.GB < Recommended)
            {
                output.Warning(GetWarningMessage(size, driveName));
            }
        }
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var logs = data.Logs.GetSitecoreLogEntries(LogLevel.Error);

            foreach (var log in logs)
            {
                var ex = log.RawText;
                if (ex.Contains(Exception))
                {
                    output.Error(Message, Url);
                    return;
                }
            }
        }
Пример #17
0
    public override void Process(IInstanceResourceContext data, ITestOutputContext output)
    {
      Assert.ArgumentNotNull(data, nameof(data));

      var info = data.SitecoreInfo;
      var defaults = info.SitecoreDefaults;
      var defaultCachesPerDatabase = new Map<Map<CacheSizeDetails>>();
      var belowDefaultCachesPerDatabase = new Map<Map<CacheSizeDetails>>();
      foreach (var database in info.GetDatabases().Values)
      {
        if (database.Name == "filesystem")
        {
          continue;
        }

        var defaultDatabase = defaults.GetDatabases().TryGetValue(database.Name);
        if (defaultDatabase == null)
        {
          continue;
        }

        Process(info, output, database, defaultDatabase, defaultCachesPerDatabase, belowDefaultCachesPerDatabase);
      }

      if (defaultCachesPerDatabase.Any())
      {
        var message = "One or several Sitecore caches are not tuned up and use default settings which may lead to performance degradation";

        output.Warning(message, detailed: GetMessage(defaultCachesPerDatabase));
      }

      if (belowDefaultCachesPerDatabase.Any())
      {
        var message = "One or several Sitecore caches are use custom configuration which is below the minimum recommended values (set up by default) which may lead to performance degradation.";

        output.Error(message, detailed: GetMessage(belowDefaultCachesPerDatabase));
      }
    }
Пример #18
0
        private static void ProcessProperties([NotNull] IInstanceResourceContext data, ITestOutputContext output, [NotNull] IEnumerable <Property> properties, [NotNull] string pathFormat, params object[] arguments)
        {
            Assert.ArgumentNotNull(data, nameof(data));
            Assert.ArgumentNotNull(properties, nameof(properties));
            Assert.ArgumentNotNull(pathFormat, nameof(pathFormat));

            // defaults
            arguments = arguments ?? new object[0];

            var path = arguments.Length > 0 ? string.Format(pathFormat, arguments) : pathFormat;

            foreach (var property in properties)
            {
                Assert.IsNotNull(property, "property");

                var actual   = property.Actual;
                var expected = property.Default;
                if (actual == null && expected == null)
                {
                    continue;
                }

                if (actual != null && actual.ToString().Equals((expected ?? string.Empty).ToString(), StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                var message = $"Database schema mismatch: {path}.{property.Title}. Expected: {expected ?? "<null>"}. Actual: {actual ?? "<null>"}.";
                if (property.IsError)
                {
                    output.Error(message);
                }
                else
                {
                    output.Warning(message);
                }
            }
        }
Пример #19
0
        public override void Process(ISolutionResourceContext data, ITestOutputContext output)
        {
            var inprocMap        = new Map <string[]>();
            var inconsistencyMap = new Map <Map>();

            var clusters = data.Values
                           .Where(x => x.ServerRoles.Any(r => r == ServerRole.ContentDelivery))
                           .Where(x => x.SitecoreInfo.IsAnalyticsEnabled)
                           .GroupBy(i => i.SitecoreInfo.GetSetting("Analytics.ClusterName").EmptyToNull() ?? "[empty]")
                           .ToDictionary(x => x.Key, x => x.ToArray());

            foreach (var cluster in clusters)
            {
                var clusterName = cluster.Key;
                var instances   = cluster.Value;

                if (instances.Length == 0)
                {
                    throw new NotImplementedException("impossible");
                }

                if (instances.Length == 1)
                {
                    // single CD instance in cluster means we don't need to check anything
                    output.Debug($"Cluster {clusterName.EmptyToNull() ?? "[empty]"} has single instance");

                    continue;
                }

                var defaultProviders = instances
                                       .Select(x => new
                {
                    x.SitecoreInfo,
                    DefaultProviderName = x.SitecoreInfo.Configuration.SelectSingleElement("/configuration/sitecore/tracking/sharedSessionState").GetAttribute("defaultProvider")
                })
                                       .Select(x => new
                {
                    x.SitecoreInfo,
                    DefaultProvider = x.SitecoreInfo.Configuration.SelectSingleElement($"/configuration/sitecore/tracking/sharedSessionState/providers/add[@name='{x.DefaultProviderName}']")
                })
                                       .Select(x => new
                {
                    x.SitecoreInfo,
                    x.DefaultProvider,
                    DefaultProviderType = TypeRef.Parse(x.DefaultProvider.GetAttribute("type"))
                })
                                       .ToArray();

                // check that all instances don't use InProc

                var gr = defaultProviders
                         .GroupBy(x => x.DefaultProviderType)
                         .ToArray();

                var instancesInProc = gr
                                      .FirstOrDefault(x => x.Key.Equals(InProcType))?
                                      .Select(x => x.SitecoreInfo.InstanceName)
                                      .ToArray();

                if (instancesInProc != null)
                {
                    inprocMap.Add(clusterName, instancesInProc);

                    continue;
                }

                // check that all instances use same shared session

                var data1 = defaultProviders
                            .Select(x => new
                {
                    x.SitecoreInfo,
                    ConnectionStringName = x.DefaultProvider.GetAttribute("connectionString")
                })
                            .ToMap(
                    x => x.SitecoreInfo.InstanceName,
                    x => x.SitecoreInfo.GetConnectionString(x.ConnectionStringName));

                var map = data1
                          .GroupBy(x => x.Value)
                          .ToArray();

                if (map.Length <= 1)
                {
                    output.Debug($"Cluster {clusterName} has consistent shared session configuration, connection string: {map.FirstOrDefault()?.Key}");

                    continue;
                }

                inconsistencyMap.Add(clusterName, data1);
            }

            if (inprocMap.Any())
            {
                var message = new ShortMessage(
                    new Text($"InProc shared session mode is used among Sitecore instances which is not supported."),
                    new BulletedList(inprocMap.Keys.ToArray(clusterName => new Container(
                                                                new Text($"Cluster: {clusterName}"),
                                                                BulletedList.Create(clusters[clusterName], instance => $"{instance.SitecoreInfo.InstanceName} - {IsAffected(inprocMap, clusterName)}")))));

                output.Error(message);
            }

            if (inconsistencyMap.Any())
            {
                var message = GetMessage(inconsistencyMap);

                output.Error(message);
            }
        }
Пример #20
0
        public override void Process(IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(data, nameof(data));

            var config   = data.SitecoreInfo.Configuration;
            var sitecore = config.SelectSingleNode("/configuration/sitecore");

            if (sitecore == null)
            {
                return;
            }

            var indexes = config.SelectElements(@"/configuration/sitecore/contentSearch/configuration/indexes/index");

            foreach (var index in indexes)
            {
                if (index == null)
                {
                    continue;
                }

                var indexDatabases = index.SelectElements("locations/*/Database").Select(x => x.InnerText).Where(x => !string.IsNullOrEmpty(x)).Distinct().ToArray();
                if (!indexDatabases.Any())
                {
                    continue;
                }

                var indexName             = index.GetAttribute("id");
                var indexStrategiesXPaths = index.SelectElements("strategies/strategy").Select(x => x.GetAttribute("ref")).Distinct().ToArray();
                foreach (var strategyXPath in indexStrategiesXPaths)
                {
                    if (string.IsNullOrEmpty(strategyXPath))
                    {
                        output.Debug(GetIndexStrategiesCorruptedMessage(indexName));
                        break;
                    }

                    var strategy = sitecore.SelectSingleNode(strategyXPath);
                    if (strategy == null)
                    {
                        output.Debug(GetIndexStrategiesCorruptedMessage(indexName));
                        continue;
                    }

                    var strategyDatabase = strategy.SelectSingleNode("param[@desc='database']").With(x => x.InnerText);

                    if (strategy.Name.Equals("manual", StringComparison.InvariantCultureIgnoreCase))
                    {
                        continue;
                    }

                    if (string.IsNullOrEmpty(strategyDatabase))
                    {
                        output.Debug(GetStrategyCorruptedMessage(strategy.Name));
                        continue;
                    }

                    if (!indexDatabases.Any(x => x.Equals(strategyDatabase)))
                    {
                        output.Error(GetErrorMessage(indexName, string.Join(", ", indexDatabases), strategy.Name, strategyDatabase));
                    }
                }
            }
        }
Пример #21
0
        private static void ProcessDatabase([NotNull] ISqlDatabase database, [NotNull] IReadOnlyDictionary <string, IReleaseDefaultSqlDatabase> defaultDatabases, [NotNull] IInstanceResourceContext data, ITestOutputContext output)
        {
            Assert.ArgumentNotNull(database, nameof(database));
            Assert.ArgumentNotNull(defaultDatabases, nameof(defaultDatabases));
            Assert.ArgumentNotNull(data, nameof(data));

            var databaseName = database.Name;

            if (string.IsNullOrEmpty(databaseName) || !defaultDatabases.ContainsKey(databaseName))
            {
                return;
            }

            var connectionString = database.ConnectionString;

            if (string.IsNullOrEmpty(connectionString))
            {
                return;
            }

            var actualSchema    = database.Schema;
            var defaultDatabase = defaultDatabases[databaseName];

            Assert.IsNotNull(defaultDatabase, "defaultDatabase");

            var defaultSchema = defaultDatabase.Schema;

            foreach (var tablePair in defaultSchema.Tables)
            {
                var tableName = tablePair.Key;
                Assert.IsNotNull(tableName, "tableName");

                if (!actualSchema.Tables.ContainsKey(tableName))
                {
                    output.Error($"Database schema mismatch: {databaseName}.Tables.dbo.{tableName} - missing");

                    continue;
                }

                var actualTable = actualSchema.Tables[tableName];
                Assert.IsNotNull(actualTable, "actualTable");

                var defaultTable = tablePair.Value;
                Assert.IsNotNull(defaultTable, "defaultTable");

                var actualColumns = actualTable.Columns;
                foreach (var defaultColumnPair in defaultTable.Columns)
                {
                    var columnName = defaultColumnPair.Key;
                    Assert.IsNotNull(columnName, "columnName");

                    if (!actualColumns.ContainsKey(columnName))
                    {
                        output.Error($"Database schema mismatch: {databaseName}.Tables.dbo.{tableName}.Columns.{columnName} - missing");

                        continue;
                    }

                    var defaultColumn = defaultColumnPair.Value;
                    Assert.IsNotNull(defaultColumn, "defaultColumn");

                    var actualColumn = actualColumns[columnName];
                    Assert.IsNotNull(actualColumn, "actualColumn");

                    var columnProperties = new[]
                    {
                        new Property
                        {
                            Title   = "Type",
                            Default = defaultColumn.Type,
                            Actual  = actualColumn.Type,
                            IsError = true
                        },

                        // TODO: enable that when SSPG schema matches SDT/SIS
                        //new Property
                        //{
                        //  Title = "Length",
                        //  Default = defaultColumn.Length,
                        //  Actual = actualColumn.Length,
                        //  IsError = false
                        //},
                        new Property
                        {
                            Title   = "Nullable",
                            Default = defaultColumn.Nullable,
                            Actual  = actualColumn.Nullable,
                            IsError = true
                        },
                        new Property
                        {
                            Title   = "PrimaryKey",
                            Default = defaultColumn.PrimaryKey,
                            Actual  = actualColumn.PrimaryKey,
                            IsError = true
                        },
                        new Property
                        {
                            Title   = "ForeignKey",
                            Default = defaultColumn.ForeignKey,
                            Actual  = actualColumn.ForeignKey,
                            IsError = false
                        }

                        // TODO: enable that when SSPG schema matches SDT/SIS
                        //new Property
                        //{
                        //  Title = "Constraint",
                        //  Default = defaultColumn.Constraint,
                        //  Actual = actualColumn.Constraint,
                        //  IsError = false
                        //},
                    };

                    ProcessProperties(data, output, columnProperties, "Database schema mismatch: {0}.Tables.dbo.{1}.Columns.{2}", databaseName, tableName, columnName);
                }

                // TODO: remove when SDT can generate accurate indexes schema for local instance
                return;

                var actualIndexes = actualTable.Indexes;
                foreach (var defaultIndexPair in defaultTable.Indexes)
                {
                    var indexName = defaultIndexPair.Key;
                    Assert.IsNotNull(indexName, "indexName");

                    if (!actualIndexes.ContainsKey(indexName))
                    {
                        var message = $"Database schema mismatch: {databaseName}.Tables.dbo.{tableName}.Indexes.{indexName} - missing";
                        output.Error(message);

                        continue;
                    }

                    var actualIndex = actualIndexes[indexName];
                    Assert.IsNotNull(actualIndex, "actualIndex");

                    var defaultIndex = defaultIndexPair.Value;
                    Assert.IsNotNull(defaultIndex, "defaultValue");

                    var indexProperties = new[]
                    {
                        new Property
                        {
                            Title   = "Type",
                            Default = defaultIndex.Type,
                            Actual  = actualIndex.Type,
                            IsError = true
                        },
                        new Property
                        {
                            Title   = "IgnoreDuplicateKeys",
                            Default = defaultIndex.IgnoreDuplicateKeys,
                            Actual  = actualIndex.IgnoreDuplicateKeys,
                            IsError = false
                        },
                        new Property
                        {
                            Title   = "DisallowPageLocks",
                            Default = defaultIndex.DisallowPageLocks,
                            Actual  = actualIndex.DisallowPageLocks,
                            IsError = false
                        },
                        new Property
                        {
                            Title   = "DisallowRowLocks",
                            Default = defaultIndex.DisallowRowLocks,
                            Actual  = actualIndex.DisallowRowLocks,
                            IsError = false
                        }
                    };

                    ProcessProperties(data, output, indexProperties, "{0}.Tables.dbo.{1}.Indexes.{2}", databaseName, tableName, indexName);

                    var actualIndexColumns  = actualIndex.Columns;
                    var defaultIndexColumns = defaultIndex.Columns;
                    foreach (var defaultIndexColumnPair in defaultIndexColumns)
                    {
                        var columnName = defaultIndexColumnPair.Key;
                        Assert.IsNotNull(columnName, "columnName");

                        if (!actualIndexColumns.ContainsKey(columnName))
                        {
                            output.Error($"Database schema mismatch: {databaseName}.Tables.dbo.{tableName}.Columns.{columnName} - missing");

                            continue;
                        }

                        var actualIndexColumn = actualIndexColumns[columnName];
                        Assert.IsNotNull(actualIndexColumn, "actualIndexColumn");

                        var defaultIndexColumn = defaultIndexColumnPair.Value;
                        Assert.IsNotNull(defaultIndexColumn, "defaultIndexColumn");

                        var indexColumnProperties = new[]
                        {
                            new Property
                            {
                                Title   = "Descending",
                                Default = defaultIndexColumn.Descending,
                                Actual  = actualIndexColumn.Descending,
                                IsError = false
                            },
                            new Property
                            {
                                Title   = "IsComputed",
                                Default = defaultIndexColumn.IsComputed,
                                Actual  = defaultIndexColumn.IsComputed,
                                IsError = false
                            },
                            new Property
                            {
                                Title   = "IsIncluded",
                                Default = defaultIndexColumn.IsIncluded,
                                Actual  = defaultIndexColumn.IsIncluded,
                                IsError = false
                            }
                        };

                        ProcessProperties(data, output, indexColumnProperties, "{0}.Tables.dbo.{1}.Indexes.{2}.Columns.{3}", databaseName, tableName, indexName, columnName);
                    }

                    foreach (var actualIndexColumnPair in actualIndexColumns)
                    {
                        var columnName = actualIndexColumnPair.Key;
                        Assert.IsNotNull(columnName, "columnName");

                        if (!actualIndexColumns.ContainsKey(columnName))
                        {
                            output.Warning($"Database schema mismatch: {databaseName}.Tables.dbo.{tableName}.Columns.{columnName} - unexpected column");
                        }
                    }
                }
            }

            foreach (var procedure in defaultSchema.StoredProcedures)
            {
                var procedureName = procedure.Key;
                Assert.IsNotNull(procedureName, "procedureName");

                var actualSet = actualSchema.StoredProcedures;
                if (!actualSet.ContainsKey(procedureName))
                {
                    var message = $"Database schema mismatch: {databaseName}.Programmability.Stored Procedures.{procedureName} - missing";
                    output.Error(message);

                    continue;
                }

                var defaultProcedure = procedure.Value;
                Assert.IsNotNull(defaultProcedure, "defaultProcedure");

                var actualProcedure = actualSet[procedureName];
                Assert.IsNotNull(actualProcedure, "actualProcedure");

                var procedureProperties = new[]
                {
                    new Property
                    {
                        Title   = "Header",
                        Default = defaultProcedure.Header,
                        Actual  = actualProcedure.Header,
                        IsError = true
                    },
                    new Property
                    {
                        Title   = "Body",
                        Default = defaultProcedure.Body,
                        Actual  = actualProcedure.Body,
                        IsError = false
                    }
                };

                ProcessProperties(data, output, procedureProperties, "{0}.Programmability.Stored Procedures.{1}", databaseName, procedureName);
            }
        }
        public override void Process(ISolutionResourceContext data, ITestOutputContext output)
        {
            var errors = new List <string>();

            // get a dictionary where key is instance's SitecoreInfo, and values are shared session state providers' XML configuration
            var sharedProviders = data.Values // identifier is a combination of connection string and sessionType value e.g. shared or private or any custom string
                                  .Select(x => new
            {
                x.SitecoreInfo,
                SharedSessionState = x.SitecoreInfo.Configuration.SelectSingleElement("/configuration/sitecore/tracking/sharedSessionState")
            })
                                  .Where(x => x.SharedSessionState != null || // skip those that don't have shared session at all - like publishing instance
                                         ReturnFalse(_ => output.Debug($"Shared session state configuration is missing in {x.SitecoreInfo.InstanceName}")))
                                  .Select(x => new
            {
                x.SitecoreInfo,
                DefaultProviderName = x.SharedSessionState.GetAttribute("defaultProvider")
            })
                                  .Where(x => !string.IsNullOrWhiteSpace(x.DefaultProviderName) ||
                                         ReturnFalse(_ => errors.Add($"Shared session state default provider is not set - in {x.SitecoreInfo.InstanceName}")))
                                  .Select(x => new
            {
                x.SitecoreInfo,
                x.DefaultProviderName,
                Provider = x.SitecoreInfo.Configuration.SelectSingleElement($"/configuration/sitecore/tracking/sharedSessionState/providers/add[@name='{x.DefaultProviderName}']")
            })
                                  .Where(x => x.Provider != null ||
                                         ReturnFalse(_ => errors.Add($"Shared session state default provider {x.DefaultProviderName} cannot be found - in {x.SitecoreInfo.InstanceName}")))
                                  .ToDictionary(
                x => x.SitecoreInfo,
                x => x.Provider);

            // get a dictionary where key is instance's SitecoreInfo, and values are private session state providers' XML configuration
            var privateProviders = data.Values // identifier is a combination of connection string and sessionType value e.g. shared or private or any custom string
                                   .Select(x => new
            {
                x.SitecoreInfo,
                SessionState = x.SitecoreInfo.Configuration.SelectSingleElement("/configuration/sessionState")
            })
                                   .Where(x => x.SessionState != null ||
                                          ReturnFalse(_ => output.Debug($"Private session state configuration is missing in {x.SitecoreInfo.InstanceName}")))
                                   .Select(x => new
            {
                x.SitecoreInfo,
                Mode = x.SessionState.GetAttribute("mode").EmptyToNull() ?? "InProc" // if not specified, InProc is used by default
            })
                                   .Where(x => !x.Mode.Equals("InProc", StringComparison.OrdinalIgnoreCase) ||
                                          ReturnFalse(_ => output.Debug($"Private session state configuration is InProc in {x.SitecoreInfo.InstanceName}")))
                                   .Select(x => new
            {
                x.SitecoreInfo,
                CustomProviderName = x.SitecoreInfo.Configuration.SelectSingleElement("/configuration/sessionState").GetAttribute("customProvider")
            })
                                   .Where(x => !string.IsNullOrWhiteSpace(x.CustomProviderName) ||
                                          ReturnFalse(_ => errors.Add($"Private session state custom provider is not set - in {x.SitecoreInfo.InstanceName}")))
                                   .Select(x => new
            {
                x.SitecoreInfo,
                x.CustomProviderName,
                Provider = x.SitecoreInfo.Configuration.SelectSingleElement($"/configuration/sessionState/providers/add[@name='{x.CustomProviderName}']")
            })
                                   .Where(x => x.Provider != null ||
                                          ReturnFalse(_ => errors.Add($"Private session state custom provider {x.CustomProviderName} cannot be found - in {x.SitecoreInfo.InstanceName}")))
                                   .ToDictionary(
                x => x.SitecoreInfo,
                x => x.Provider);

            if (errors.Any())
            {
                output.CannotRun("There are errors in configuration files that prevent test run. " + new BulletedList(errors));

                return;
            }

            var sharedSessionIdentifiers  = GetIdentifiers(sharedProviders);
            var privateSessionIdentifiers = GetIdentifiers(privateProviders);

            // shared and private identifiers must not intersect
            var done = new List <string>();

            foreach (var privateSessionIdentifier in privateSessionIdentifiers)
            {
                var privateId = privateSessionIdentifier.Key;
                if (done.Contains(privateId))
                {
                    continue;
                }

                foreach (var sharedSessionIdentifier in sharedSessionIdentifiers)
                {
                    var sharedId = sharedSessionIdentifier.Key;
                    if (done.Contains(sharedId))
                    {
                        continue;
                    }

                    var sharedInstancesNames  = sharedSessionIdentifier.Value.Select(x => x.InstanceName);
                    var privateInstancesNames = privateSessionIdentifier.Value.Select(x => x.InstanceName);
                    if (string.Equals(privateId, sharedId, StringComparison.OrdinalIgnoreCase))
                    {
                        output.Error(GetMessage(privateId, privateInstancesNames, sharedInstancesNames));

                        done.Add(privateId);
                    }
                }
            }
        }