using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Questionnaire;
using Unity.Multiplayer.Center.Recommendations;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Unity.MultiplayerCenterTests
{
///
/// This is an integration test based on known data. It ensures that the recommendation matches the expected results.
///
[TestFixture]
partial class RecommendationTests
{
[SetUp]
[TearDown]
public void Setup()
{
Object.DestroyImmediate(RecommenderSystemDataObject.instance); // force reload from disk if accessed
}
///
/// Ensures that the packages recommended for a given preset match the expected packages (4 players)
/// Note that this does not handle hidden dependencies
///
[TestCase(Preset.None)]
[TestCase(Preset.Adventure,
"com.unity.netcode.gameobjects",
"com.unity.multiplayer.playmode",
"com.unity.multiplayer.widgets",
"com.unity.multiplayer.tools",
"com.unity.dedicated-server",
"com.unity.services.multiplayer",
"com.unity.services.vivox")]
[TestCase(Preset.Shooter,
"com.unity.netcode",
"com.unity.entities.graphics",
"com.unity.multiplayer.widgets",
"com.unity.multiplayer.playmode",
"com.unity.services.multiplayer",
"com.unity.services.vivox")]
[TestCase(Preset.Racing,
"com.unity.netcode",
"com.unity.entities.graphics",
"com.unity.multiplayer.widgets",
"com.unity.multiplayer.playmode",
"com.unity.services.multiplayer",
"com.unity.services.vivox")]
[TestCase(Preset.TurnBased,
"com.unity.services.cloudcode",
"com.unity.services.deployment",
"com.unity.multiplayer.playmode",
"com.unity.services.multiplayer",
"com.unity.services.vivox")]
[TestCase(Preset.Simulation,
"com.unity.netcode.gameobjects",
"com.unity.multiplayer.playmode",
"com.unity.multiplayer.widgets",
"com.unity.multiplayer.tools",
"com.unity.services.multiplayer",
"com.unity.services.vivox")]
[TestCase(Preset.Strategy,
"com.unity.multiplayer.playmode",
"com.unity.services.multiplayer",
"com.unity.transport",
"com.unity.services.vivox")]
[TestCase(Preset.Sports,
"com.unity.netcode",
"com.unity.entities.graphics",
"com.unity.multiplayer.widgets",
"com.unity.multiplayer.playmode",
"com.unity.services.multiplayer",
"com.unity.services.vivox")]
[TestCase(Preset.RolePlaying,
"com.unity.multiplayer.playmode",
"com.unity.services.multiplayer",
"com.unity.transport",
"com.unity.services.vivox")]
[TestCase(Preset.Async,
"com.unity.services.cloudcode",
"com.unity.services.deployment",
"com.unity.services.vivox",
"com.unity.services.multiplayer",
"com.unity.multiplayer.playmode")]
[TestCase(Preset.Fighting,
"com.unity.multiplayer.playmode",
"com.unity.services.multiplayer",
"com.unity.transport",
"com.unity.services.vivox")]
[TestCase(Preset.Sandbox,
"com.unity.netcode.gameobjects",
"com.unity.multiplayer.playmode",
"com.unity.multiplayer.widgets",
"com.unity.multiplayer.tools",
"com.unity.services.multiplayer",
"com.unity.services.vivox")]
public void TestPreset_RecommendedPackagesMatchesExpected(Preset preset, params string[] expected)
{
var recommendation = UtilsForRecommendationTests.ComputeRecommendationForPreset(preset);
var allPackages = RecommenderSystem.GetSolutionsToRecommendedPackageViewData();
if (expected == null || expected.Length == 0)
{
Assert.IsNull(recommendation);
return;
}
var actualRecommendedPackages = RecommendationUtils.PackagesToInstall(recommendation, allPackages)
.Select(e => e.PackageId).ToArray();
// Use AreEqual instead of AreEquivalent to get a better error message
Array.Sort(expected);
Array.Sort(actualRecommendedPackages);
CollectionAssert.AreEqual(expected, actualRecommendedPackages);
}
[TestCaseSource(nameof(AdventurePresetCases))]
[TestCaseSource(nameof(SandboxPresetCases))]
[TestCaseSource(nameof(AsyncPresetCases))]
[TestCaseSource(nameof(TurnBasedPresetCases))]
[TestCaseSource(nameof(FightingPresetCases))]
[TestCaseSource(nameof(RacingPresetCases))]
[TestCaseSource(nameof(RolePlayingPresetCases))]
[TestCaseSource(nameof(ShooterPresetCases))]
[TestCaseSource(nameof(SimulationPresetCases))]
[TestCaseSource(nameof(StrategyPresetCases))]
[TestCaseSource(nameof(SportPresetCases))]
public void TestPreset_RecommendedSolutionsAreValid(string playerCount, PossibleSolution netcode, PossibleSolution hosting, Preset preset)
{
var recommendation = UtilsForRecommendationTests.ComputeRecommendationForPreset(preset, playerCount: playerCount);
Assert.NotNull(recommendation);
AssertTheRightSolutionsAreRecommended(netcode, hosting, recommendation);
AssertAllDynamicReasonsAreProperlyFormed(recommendation);
}
// First line of table for case "cheating prevention not so important"
[TestCase(PossibleSolution.DA, PossibleSolution.NGO, "NoCost", "Slow", "2", "4", "8" )]
[TestCase(PossibleSolution.DA, PossibleSolution.NGO,"NoCost", "Fast", "2", "4", "8" )]
[TestCase(PossibleSolution.DA, PossibleSolution.NGO,"BestMargin", "Slow", "2", "4", "8" )]
[TestCase(PossibleSolution.DA, PossibleSolution.NGO,"BestMargin", "Fast", "2", "4", "8" )]
[TestCase(PossibleSolution.DA, PossibleSolution.NGO,"BestExperience", "Slow", "2", "4", "8")]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestExperience", "Fast", "2","4", "8")]
// Second line
[TestCase(PossibleSolution.DA, PossibleSolution.NGO,"NoCost", "Slow", "16", "64+" )]
[TestCase(PossibleSolution.DA, PossibleSolution.N4E,"NoCost", "Fast", "16", "64+" )]
[TestCase(PossibleSolution.DA, PossibleSolution.NGO,"BestMargin", "Slow", "16", "64+" )]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestMargin", "Fast", "16", "64+" )]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestExperience", "Slow", "16", "64+")]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestExperience", "Fast", "16", "64+")]
// Third line
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"NoCost", "Slow", "128", "256", "512" )]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"NoCost", "Fast", "128", "256", "512" )]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestMargin", "Slow", "128", "256", "512" )]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestMargin", "Fast", "128", "256", "512" )]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestExperience", "Slow", "128", "256", "512")]
[TestCase(PossibleSolution.DS, PossibleSolution.N4E,"BestExperience", "Fast", "128", "256", "512")]
public void TestGameSpecsForClientServerWithoutPreset_CheatingNotImportant_MatchesMiroTable(PossibleSolution expectedHosting,
PossibleSolution expectedNetcode, string costSensitivity, string pace, params string[] playerCounts)
{
foreach (var playerCount in playerCounts)
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerData = GenerateAnswerDataForClientServer(false, pace, playerCount, costSensitivity);
var recommendation = RecommenderSystem.GetRecommendation(questionnaireData, answerData);
Assert.NotNull(recommendation);
var msg = $"{pace}, {costSensitivity}, {playerCount} players: ";
AssertTheRightSolutionsAreRecommended(expectedNetcode, expectedHosting, recommendation, msg);
}
}
// First line of table for case "cheating prevention important"
[TestCase( PossibleSolution.NGO, "NoCost", "Slow", "2", "4", "8" )]
[TestCase(PossibleSolution.N4E,"NoCost", "Fast", "2", "4", "8" )]
[TestCase(PossibleSolution.NGO,"BestMargin", "Slow", "2", "4", "8" )]
[TestCase(PossibleSolution.N4E,"BestMargin", "Fast", "2", "4", "8" )]
[TestCase(PossibleSolution.NGO,"BestExperience", "Slow", "2", "4", "8")]
[TestCase(PossibleSolution.N4E,"BestExperience", "Fast", "2", "4", "8")]
// Second line
[TestCase(PossibleSolution.N4E,"NoCost", "Slow", "16", "64+" )]
[TestCase(PossibleSolution.N4E,"NoCost", "Fast", "16", "64+" )]
[TestCase(PossibleSolution.NGO,"BestMargin", "Slow", "16", "64+" )]
[TestCase(PossibleSolution.N4E,"BestMargin", "Fast", "16", "64+" )]
[TestCase(PossibleSolution.N4E,"BestExperience", "Slow", "16", "64+")]
[TestCase(PossibleSolution.N4E,"BestExperience", "Fast", "16", "64+")]
// Third line
[TestCase(PossibleSolution.N4E,"NoCost", "Slow", "128", "256", "512" )]
[TestCase(PossibleSolution.N4E,"NoCost", "Fast", "128", "256", "512" )]
[TestCase(PossibleSolution.N4E,"BestMargin", "Slow", "128", "256", "512" )]
[TestCase(PossibleSolution.N4E,"BestMargin", "Fast", "128", "256", "512" )]
[TestCase(PossibleSolution.N4E,"BestExperience", "Slow", "128", "256", "512")]
[TestCase(PossibleSolution.N4E,"BestExperience", "Fast", "128", "256", "512")]
public void TestGameSpecsForClientServerWithoutPreset_CheatingImportant_MatchesMiroTable(
PossibleSolution expectedNetcode, string costSensitivity, string pace, params string[] playerCounts)
{
const PossibleSolution expectedHosting = PossibleSolution.DS; // for all cases
foreach (var playerCount in playerCounts)
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerData = GenerateAnswerDataForClientServer(true, pace, playerCount, costSensitivity);
var recommendation = RecommenderSystem.GetRecommendation(questionnaireData, answerData);
Assert.NotNull(recommendation);
var msg = $"{pace}, {costSensitivity}, {playerCount} players: ";
AssertTheRightSolutionsAreRecommended(expectedNetcode, expectedHosting, recommendation, msg);
}
}
static AnswerData GenerateAnswerDataForClientServer(bool cheatingImportant, string pace, string playerCount, string costSensitivity)
{
return new AnswerData(){ Answers = new List
{
new () { QuestionId = "CostSensitivity", Answers = new() {costSensitivity}},
new () { QuestionId = "Pace", Answers = new() {pace}},
new () { QuestionId = "PlayerCount", Answers = new() {playerCount}},
new () { QuestionId = "NetcodeArchitecture", Answers = new() {"ClientServer"}},
new () { QuestionId = "Cheating", Answers = new() {cheatingImportant ? "CheatingImportant" : "CheatingNotImportant"}}
}};
}
[Test]
public void PackageLists_PackagesHaveNames()
{
var allpackages = RecommenderSystemDataObject.instance.RecommenderSystemData.PackageDetailsById;
foreach (var (id, details) in allpackages)
{
Assert.False(string.IsNullOrEmpty(details.Name), $"Package {id} has no name");
}
}
[Test]
public void PackageLists_DependenciesAreAllValid()
{
var allpackages = RecommenderSystemDataObject.instance.RecommenderSystemData.PackageDetailsById;
foreach (var (id, details) in allpackages)
{
Assert.NotNull(details, $"Package {id} has no details in RecommenderSystemData.PackageDetails");
if(details.AdditionalPackages == null)
continue;
foreach (var additionalPackageId in details.AdditionalPackages)
{
var additionalPackage = RecommendationUtils.GetPackageDetailForPackageId(additionalPackageId);
Assert.NotNull(additionalPackage, $"Package {id} has an invalid dependency: {additionalPackageId}. It should be added to RecommenderSystemData.PackageDetails");
}
}
}
[TestCase(PossibleSolution.NGO, PossibleSolution.LS, true)]
[TestCase(PossibleSolution.NGO, PossibleSolution.DA, true)]
[TestCase(PossibleSolution.NGO, PossibleSolution.DS, true)]
[TestCase(PossibleSolution.NGO, PossibleSolution.CloudCode, true)] // ?
[TestCase(PossibleSolution.N4E, PossibleSolution.LS, true)]
[TestCase(PossibleSolution.N4E, PossibleSolution.DA, false)]
[TestCase(PossibleSolution.N4E, PossibleSolution.DS, true)]
[TestCase(PossibleSolution.N4E, PossibleSolution.CloudCode, true)] // ?
[TestCase(PossibleSolution.CustomNetcode, PossibleSolution.LS, true)]
[TestCase(PossibleSolution.CustomNetcode, PossibleSolution.DA, false)]
[TestCase(PossibleSolution.CustomNetcode, PossibleSolution.DS, true)]
[TestCase(PossibleSolution.CustomNetcode, PossibleSolution.CloudCode, true)] // ?
[TestCase(PossibleSolution.NoNetcode, PossibleSolution.LS, true)]
[TestCase(PossibleSolution.NoNetcode, PossibleSolution.DA, false)] // ?
[TestCase(PossibleSolution.NoNetcode, PossibleSolution.DS, true)]
[TestCase(PossibleSolution.NoNetcode, PossibleSolution.CloudCode, true)]
public void TestIncompatibilityWithSolution_MatchesExpected(PossibleSolution netcode, PossibleSolution hostingModel, bool expected)
{
var recommendationData = RecommenderSystemDataObject.instance.RecommenderSystemData;
var actual = recommendationData.IsHostingModelCompatibleWithNetcode(netcode, hostingModel, out string reason);
Assert.AreEqual(expected, actual, $"Hosting model {hostingModel} should be {(expected ? "compatible" : "incompatible")} with netcode {netcode}");
Assert.AreEqual(expected, string.IsNullOrEmpty(reason), "reason is " + reason);
}
// Additional packages are not used in version 1.0.0. We suspect however that killing the feature is not wise.
// Therefore, we check that the logic is intact
[Test]
public void TestAdditionalPackagesStillWork()
{
var packageDetails = RecommenderSystemDataObject.instance.RecommenderSystemData.PackageDetailsById;
// nonsensical change, but with existing package.
packageDetails["com.unity.netcode"].AdditionalPackages = new[] {"com.unity.netcode.gameobjects"};
var mainPackages = new List
{
new (packageDetails["com.unity.netcode"], RecommendationType.MainArchitectureChoice, null),
new (packageDetails["com.unity.services.multiplayer"], RecommendationType.HostingFeatured, null)
};
var expectedPackages = new List {"com.unity.netcode", "com.unity.services.multiplayer",
"com.unity.netcode.gameobjects", // added as additional package
"com.unity.multiplayer.center.quickstart"}; // always added
RecommendationUtils.GetPackagesWithAdditionalPackages(mainPackages, out var allPackages, out var allNames, out var tooltip);
expectedPackages.Sort();
allPackages.Sort();
CollectionAssert.AreEqual(expectedPackages, allPackages);
CollectionAssert.Contains(allNames, "Netcode for GameObjects");
Assert.True(tooltip.Contains("Netcode for GameObjects"));
Assert.AreEqual(allPackages.Count -1, allNames.Count, "Quickstart should not be included in the names");
}
static void AssertTheRightSolutionsAreRecommended(PossibleSolution expectedNetcode, PossibleSolution expectedHosting, RecommendationViewData recommendation, string msg=null)
{
AssertHighestScoreSolution(expectedNetcode, recommendation.NetcodeOptions, msg);
AssertRightSolution(expectedNetcode, recommendation.NetcodeOptions, msg);
if(expectedNetcode != PossibleSolution.NGO && expectedHosting == PossibleSolution.DA)
AssertHighestScoreSolution(expectedHosting, recommendation.ServerArchitectureOptions, msg);
else
AssertRightSolution(expectedHosting, recommendation.ServerArchitectureOptions, msg);
}
static void AssertAllDynamicReasonsAreProperlyFormed(RecommendationViewData recommendation)
{
foreach (var hostingModelRecommendation in recommendation.ServerArchitectureOptions)
{
AssertHasProperDynamicReason(hostingModelRecommendation);
}
foreach (var netcodeRecommendation in recommendation.NetcodeOptions)
{
AssertHasProperDynamicReason(netcodeRecommendation);
}
}
static void AssertHasProperDynamicReason(RecommendedSolutionViewData solution)
{
Assert.False(solution.Reason.Contains(Scoring.DynamicKeyword), $"Reason for {solution.Solution} contain dynamic keyword");
Assert.True(solution.Reason.Length > 10, $"Reason for {solution.Solution} is too short ({solution.Reason})");
Assert.False(solution.Reason.EndsWith(".."), $"Reason for {solution.Solution} ends with two dots ({solution.Reason})");
Assert.True(solution.Reason.EndsWith("."), $"Reason for {solution.Solution} does not end with a dot ({solution.Reason})");
}
static void AssertRightSolution(PossibleSolution expectedNetcode, RecommendedSolutionViewData[] data, string msg="")
{
var selectedView = data.FirstOrDefault(e => e.Selected);
Assert.NotNull(selectedView, $"{msg}No solution selected");
Assert.AreEqual(expectedNetcode, selectedView.Solution,
$"{msg}Expected {expectedNetcode} but got {selectedView.Solution} selected.{string.Join(", ", data.Select(e => $"{e.Solution}: {e.Score}"))}");
}
static void AssertHighestScoreSolution(PossibleSolution expectedNetcode, RecommendedSolutionViewData[] data, string msg = "")
{
var maxScore = data.Max(e => e.Score);
var solutionsWithMax = data.Where(e => Mathf.Approximately(e.Score, maxScore));
Assert.AreEqual(1, solutionsWithMax.Count(), $"{msg}Multiple solutions with max score");
var solutionWithMax = solutionsWithMax.First();
Assert.True(solutionWithMax.RecommendationType is RecommendationType.MainArchitectureChoice or RecommendationType.Incompatible, $"The solution with the max score does not have the right recommendation type ({solutionWithMax.RecommendationType}");
Assert.AreEqual(expectedNetcode, solutionWithMax.Solution, $"{msg}Expected {expectedNetcode} but highest score was {solutionWithMax.Solution}.{string.Join(", ", data.Select(e => $"{e.Solution}: {e.Score}"))}");
}
}
}