This commit is contained in:
2025-01-17 13:10:42 +01:00
commit 4536213c91
15115 changed files with 1442174 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
{
"createSeparatePackage": false
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 96c71b811fa50403690b154e216fe217
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,211 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Center.Analytics;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Recommendations;
namespace Unity.MultiplayerCenterTests
{
[TestFixture]
internal class AnalyticsUtilsTests
{
[Test]
public void AnalyticsUtils_GetQuestionDisplayNames_RightCount()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var questionDisplayNames = AnalyticsUtils.GetQuestionDisplayNames(questionnaire);
Assert.AreEqual(questionDisplayNames.Count, questionnaire.Questions.Length);
}
/// <summary>
/// Note: this test checks exact values and has to be adapted in case those display names change.
/// Why do that? To force the developers to think about the impact on the analytics!
/// Contact the engine-data team and product.
/// </summary>
[Test]
public void AnalyticsUtils_GetQuestionDisplayNames_RightValuesForSelectedQuestions()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var questionDisplayNames = AnalyticsUtils.GetQuestionDisplayNames(questionnaire);
Assert.AreEqual("Number of Players per Session", questionDisplayNames["PlayerCount"]);
Assert.AreEqual("Gameplay Pace", questionDisplayNames["Pace"]);
Assert.AreEqual("Cheating / Modding Prevention", questionDisplayNames["Cheating"]);
Assert.AreEqual("Cost Sensitivity", questionDisplayNames["CostSensitivity"]);
Assert.AreEqual("Netcode Architecture", questionDisplayNames["NetcodeArchitecture"]);
}
[Test]
public void AnalyticsUtils_GetAnswerDisplayNames_RightCount()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerDisplayNames = AnalyticsUtils.GetAnswerDisplayNames(questionnaire);
// Note: 2 birds one stone: this test also nicely checks if we have duplicates with question ids
var expectedCount = questionnaire.Questions.Sum(question => question.Choices.Length);
Assert.AreEqual(answerDisplayNames.Count, expectedCount);
}
[Test]
public void AnalyticsUtils_GetPresetFullNames_RightCountAndIntValuesDidNotChange()
{
var presetFullNames = AnalyticsUtils.GetPresetFullNames();
Assert.AreEqual(12, presetFullNames.Length);
}
/// <summary>
/// Note: this test checks exact values and has to be adapted in case those display names change.
/// Why do that? To force the developers to think about the impact on the analytics!
/// Contact the engine-data team and product.
/// </summary>
[Test]
public void AnalyticsUtils_GetPresetFullNames_RightValues()
{
var presetFullNames = AnalyticsUtils.GetPresetFullNames();
Assert.AreEqual("-", presetFullNames[0]);
Assert.AreEqual("Adventure", presetFullNames[1]);
Assert.AreEqual("Shooter, Battle Royale, Battle Arena", presetFullNames[2]);
Assert.AreEqual("Racing", presetFullNames[3]);
Assert.AreEqual("Card Battle, Turn-based, Tabletop", presetFullNames[4]);
Assert.AreEqual("Simulation", presetFullNames[5]);
Assert.AreEqual("Strategy", presetFullNames[6]);
Assert.AreEqual("Sports", presetFullNames[7]);
Assert.AreEqual("Role-Playing, MMO", presetFullNames[8]);
Assert.AreEqual("Async, Idle, Hyper Casual, Puzzle", presetFullNames[9]);
Assert.AreEqual("Fighting", presetFullNames[10]);
Assert.AreEqual("Arcade, Platformer, Sandbox", presetFullNames[11]);
}
[Test]
public void AnalyticsUtils_ToGameSpecs_AllIdsAreInTheGameSpecs()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerDisplayNames = AnalyticsUtils.GetAnswerDisplayNames(questionnaire);
var answerData = UtilsForRecommendationTests.BuildAnswerMatching(questionnaire);
var questionIdToQuestionName = AnalyticsUtils.GetQuestionDisplayNames(questionnaire);
var gameSpecs = AnalyticsUtils.ToGameSpecs(answerData, answerDisplayNames, questionIdToQuestionName);
Assert.AreEqual(answerData.Answers.Count, gameSpecs.Length);
var questionIdsFromAnswerData = answerData.Answers.Select(answer => answer.QuestionId).ToArray();
var questionIdsFromGameSpecs = gameSpecs.Select(spec => spec.QuestionId).ToArray();
CollectionAssert.AreEquivalent(questionIdsFromAnswerData, questionIdsFromGameSpecs);
}
[Test]
public void AnalyticsUtils_ToGameSpecs_ValueCheck()
{
var answerDisplayNames = new Dictionary<string, string>(){
{"A1", "Display Name A1"},
{"A2", "Display Name A2"},
{"A3", "Display Name A3"},
{"A4", "Display Name A4"}
};
var questionIdToQuestionName = new Dictionary<string, string>()
{
{"Q1", "Display Name Q1"},
{"Q2", "Display Name Q2"}
};
var answerData = new AnswerData()
{
Answers = new List<AnsweredQuestion>()
{
new() {QuestionId = "Q2", Answers = new List<string>() {"A3"}},
new() {QuestionId = "Q1", Answers = new List<string>() {"A1"}}
}
};
var gameSpecs = AnalyticsUtils.ToGameSpecs(answerData, answerDisplayNames, questionIdToQuestionName);
var expectedGameSpecs = new GameSpec[]
{
new (){ QuestionId = "Q2", QuestionText = "Display Name Q2", AcceptsMultipleAnswers = false, AnswerId = "A3", AnswerText = "Display Name A3"},
new (){ QuestionId = "Q1", QuestionText = "Display Name Q1", AcceptsMultipleAnswers = false, AnswerId = "A1", AnswerText = "Display Name A1"},
};
CollectionAssert.AreEquivalent(expectedGameSpecs, gameSpecs);
}
[Test]
public void AnalyticsUtils_ToGameSpecs_NoEmptyString()
{
GetAnswersWithMatchingAnalyticsData(out var _, out var gameSpecs);
foreach (var spec in gameSpecs)
{
Assert.False(string.IsNullOrEmpty(spec.QuestionId), spec.QuestionId);
Assert.False(string.IsNullOrEmpty(spec.QuestionText), spec.QuestionText);
Assert.False(string.IsNullOrEmpty(spec.AnswerId), spec.AnswerId);
Assert.False(string.IsNullOrEmpty(spec.AnswerText), spec.AnswerText);
}
}
[Test]
public void AnalyticsUtils_AssumptionTest_HardCodedNumNetcodePackageMatchesRecommendations()
{
// note that the amount of possible netcode does not change with the answer data.
var someRecommendation = UtilsForRecommendationTests.GetSomeRecommendation();
var netcodePackageCount = someRecommendation.NetcodeOptions.Count(e
=> !string.IsNullOrEmpty(e.MainPackage?.PackageId));
Assert.AreEqual(netcodePackageCount, AnalyticsUtils.NumNetcodePackage);
}
[Test]
public void AnalyticsUtils_AssumptionTest_HardCodedNumHostingPackageMatchesRecommendations()
{
// note that the amount of possible netcode does not change with the answer data.
var someRecommendation = UtilsForRecommendationTests.GetSomeRecommendation();
var hostingPackageCount = someRecommendation.ServerArchitectureOptions.Count(e
=> !string.IsNullOrEmpty(e.MainPackage?.PackageId));
Assert.AreEqual(hostingPackageCount, AnalyticsUtils.NumHostingPackages);
}
[Test]
public void AnalyticsUtils_GetPackagesWithAnalyticsFormat_NetcodeValuesMakeSense()
{
const string ngo = "com.unity.netcode.gameobjects";
const string n4e = "com.unity.netcode";
const string tp = "com.unity.transport";
var someRecommendation = UtilsForRecommendationTests.GetSomeRecommendation();
var packageViews = RecommenderSystem.GetSolutionsToRecommendedPackageViewData();
var packagesAnalyticsFormat = AnalyticsUtils.GetPackagesWithAnalyticsFormat(someRecommendation, packageViews);
// all netcode packages are in the array
Assert.AreEqual(1,packagesAnalyticsFormat.Count(e => e.PackageId == ngo) );
Assert.AreEqual(1, packagesAnalyticsFormat.Count(e => e.PackageId == n4e));
Assert.AreEqual(1, packagesAnalyticsFormat.Count(e => e.PackageId == tp));
var netcodePackages = packagesAnalyticsFormat.Where(e => e.PackageId is ngo or n4e or tp).ToArray();
Assert.AreEqual(3, netcodePackages.Count()); // exactly 3 matches
Assert.AreEqual(1, netcodePackages.Count(e => e.SelectedForInstall)); // 1 selected
Assert.AreEqual(1, netcodePackages.Count(e => e.IsRecommended)); // 1 recommended
// Since we only look at the project direct dependencies, 1 netcode max can be installed in a normal setup
// edge case would be: some installed ngo/n4e and manually added Transport to the manifest
Assert.True(netcodePackages.Count(e => e.IsAlreadyInstalled) <= 1);
}
[Test]
public void AnalyticsUtils_GetPackagesWithAnalyticsFormat_AllPackagesAreIn()
{
var someRecommendation = UtilsForRecommendationTests.GetSomeRecommendation();
var packageViews = RecommenderSystem.GetSolutionsToRecommendedPackageViewData();
var packagesAnalyticsFormat = AnalyticsUtils.GetPackagesWithAnalyticsFormat(someRecommendation, packageViews);
var allPackages = RecommenderSystemDataObject.instance.RecommenderSystemData.PackageDetailsById.Keys;
foreach (var id in allPackages)
{
Assert.AreEqual(1, packagesAnalyticsFormat.Count(e => e.PackageId == id), id);
}
}
static void GetAnswersWithMatchingAnalyticsData(out AnswerData answerData, out GameSpec[] gameSpecs)
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerDisplayNames = AnalyticsUtils.GetAnswerDisplayNames(questionnaire);
answerData = UtilsForRecommendationTests.BuildAnswerMatching(questionnaire);
var questionIdToQuestionName = AnalyticsUtils.GetQuestionDisplayNames(questionnaire);
gameSpecs = AnalyticsUtils.ToGameSpecs(answerData, answerDisplayNames, questionIdToQuestionName);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c2c1cf24bea5410ea530b2c8a875b293
timeCreated: 1714641306

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly:InternalsVisibleTo("Unity.Multiplayer.Center.GettingStartedTab.Tests")]

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 89ffa359141b49fea9bfa6a1db3b18be
timeCreated: 1715084531

View File

@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Questionnaire;
namespace Unity.MultiplayerCenterTests
{
partial class RecommendationTests
{
public static IEnumerable<TestCaseData> AdventurePresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.NGO, PossibleSolution.DS, Preset.Adventure),
new("4", PossibleSolution.NGO, PossibleSolution.DS, Preset.Adventure),
new("8", PossibleSolution.NGO, PossibleSolution.DS, Preset.Adventure),
new("16", PossibleSolution.NGO, PossibleSolution.DS, Preset.Adventure),
new("64+", PossibleSolution.NGO, PossibleSolution.DS, Preset.Adventure),
new("128", PossibleSolution.N4E, PossibleSolution.DS, Preset.Adventure)
};
public static IEnumerable<TestCaseData> SandboxPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.NGO, PossibleSolution.DA, Preset.Sandbox),
new("4", PossibleSolution.NGO, PossibleSolution.DA, Preset.Sandbox),
new("8", PossibleSolution.NGO, PossibleSolution.DA, Preset.Sandbox),
new("16", PossibleSolution.NGO, PossibleSolution.DA, Preset.Sandbox),
new("64+", PossibleSolution.NGO, PossibleSolution.DA, Preset.Sandbox),
new("128", PossibleSolution.N4E, PossibleSolution.DS, Preset.Sandbox)
};
public static IEnumerable<TestCaseData> AsyncPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.Async),
new("4", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.Async),
new("8", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.Async),
new("16", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.Async),
new("64+", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.Async),
new("128", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.Async)
};
public static IEnumerable<TestCaseData> TurnBasedPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.TurnBased),
new("4", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.TurnBased),
new("8", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.TurnBased),
new("16", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.TurnBased),
new("64+", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.TurnBased),
new("128", PossibleSolution.NoNetcode, PossibleSolution.CloudCode, Preset.TurnBased)
};
public static IEnumerable<TestCaseData> FightingPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Fighting),
new("4", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Fighting),
new("8", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Fighting),
new("16", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Fighting),
new("64+", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Fighting),
new("128", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Fighting)
};
public static IEnumerable<TestCaseData> RacingPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.N4E, PossibleSolution.DS, Preset.Racing),
new("4", PossibleSolution.N4E, PossibleSolution.DS, Preset.Racing),
new("8", PossibleSolution.N4E, PossibleSolution.DS, Preset.Racing),
new("16", PossibleSolution.N4E, PossibleSolution.DS, Preset.Racing),
new("64+", PossibleSolution.N4E, PossibleSolution.DS, Preset.Racing),
new("128", PossibleSolution.N4E, PossibleSolution.DS, Preset.Racing)
};
public static IEnumerable<TestCaseData> RolePlayingPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.CustomNetcode, PossibleSolution.DS, Preset.RolePlaying),
new("4", PossibleSolution.CustomNetcode, PossibleSolution.DS, Preset.RolePlaying),
new("8", PossibleSolution.CustomNetcode, PossibleSolution.DS, Preset.RolePlaying),
new("16", PossibleSolution.CustomNetcode, PossibleSolution.DS, Preset.RolePlaying),
new("64+", PossibleSolution.CustomNetcode, PossibleSolution.DS, Preset.RolePlaying),
new("128", PossibleSolution.CustomNetcode, PossibleSolution.DS, Preset.RolePlaying)
};
public static IEnumerable<TestCaseData> ShooterPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.N4E, PossibleSolution.DS, Preset.Shooter),
new("4", PossibleSolution.N4E, PossibleSolution.DS, Preset.Shooter),
new("8", PossibleSolution.N4E, PossibleSolution.DS, Preset.Shooter),
new("16", PossibleSolution.N4E, PossibleSolution.DS, Preset.Shooter),
new("64+", PossibleSolution.N4E, PossibleSolution.DS, Preset.Shooter),
new("128", PossibleSolution.N4E, PossibleSolution.DS, Preset.Shooter)
};
public static IEnumerable<TestCaseData> SimulationPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.NGO, PossibleSolution.DA, Preset.Simulation),
new("4", PossibleSolution.NGO, PossibleSolution.DA, Preset.Simulation),
new("8", PossibleSolution.NGO, PossibleSolution.DA, Preset.Simulation),
new("16", PossibleSolution.NGO, PossibleSolution.DA, Preset.Simulation),
new("64+", PossibleSolution.NGO, PossibleSolution.DA, Preset.Simulation),
new("128", PossibleSolution.N4E, PossibleSolution.DS, Preset.Simulation)
};
public static IEnumerable<TestCaseData> StrategyPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Strategy),
new("4", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Strategy),
new("8", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Strategy),
new("16", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Strategy),
new("64+", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Strategy),
new("128", PossibleSolution.CustomNetcode, PossibleSolution.LS, Preset.Strategy)
};
public static IEnumerable<TestCaseData> SportPresetCases
=> new TestCaseData[]
{
new("2", PossibleSolution.N4E, PossibleSolution.DS, Preset.Sports),
new("4", PossibleSolution.N4E, PossibleSolution.DS, Preset.Sports),
new("8", PossibleSolution.N4E, PossibleSolution.DS, Preset.Sports),
new("16", PossibleSolution.N4E, PossibleSolution.DS, Preset.Sports),
new("64+", PossibleSolution.N4E, PossibleSolution.DS, Preset.Sports),
new("128", PossibleSolution.N4E, PossibleSolution.DS, Preset.Sports)
};
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8110dd6ddbc04e58b50978562ce0c00a
timeCreated: 1718365841

View File

@@ -0,0 +1,96 @@
using System;
using NUnit.Framework;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Questionnaire;
using UnityEngine;
namespace Unity.MultiplayerCenterTests
{
[TestFixture]
class LogicTests
{
[Test]
public void TryGetQuestionByQuestionId_IdExists_Found()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var success = Logic.TryGetQuestionByQuestionId(questionnaire, questionnaire.Questions[1].Id, out var question);
Assert.True(success);
Assert.NotNull(question);
}
[Test]
public void TryGetQuestionByQuestionId_StringIdExists_Found()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var success = Logic.TryGetQuestionByQuestionId(questionnaire, "Pace", out var question);
Assert.True(success);
Assert.NotNull(question);
}
[Test]
public void TryGetQuestionByQuestionId_IdDoesNotExist_NotFound()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var success = Logic.TryGetQuestionByQuestionId(questionnaire, "nonexistent", out var question);
Assert.False(success);
Assert.Null(question);
}
[Test]
public void TryGetAnswerByQuestionId_IdExists_Found()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var userAnswers = UtilsForRecommendationTests.BuildAnswerMatching(questionnaire);
var success = Logic.TryGetAnswerByQuestionId(userAnswers, "PlayerCount", out var answer);
Assert.True(success);
Assert.NotNull(answer);
Assert.AreEqual(1, answer.Answers.Count);
}
[Test]
public void TryGetAnswerByQuestionId_IdDoesNotExist_NotFound()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var userAnswers = UtilsForRecommendationTests.BuildAnswerMatching(questionnaire);
var success = Logic.TryGetAnswerByQuestionId(userAnswers, "nonexistent", out var answer);
Assert.False(success);
Assert.Null(answer);
}
[Test]
public void TestApplyPresetToAnswerData_WhenPlayerCountIsSet_PlayerCountStays()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var userAnswers = UtilsForRecommendationTests.BuildAnswerMatching(questionnaire);
Logic.TryGetAnswerByQuestionId(userAnswers, "PlayerCount", out var answer);
var chosenPlayerCount = Int32.Parse(answer.Answers[0]);
// No default answer, otherwise this test could succeed even if the logic is wrong
Assert.AreNotEqual(0, chosenPlayerCount);
Assert.AreNotEqual(2, chosenPlayerCount);
var (newAnswer, newReco) = Logic.ApplyPresetToAnswerData(userAnswers, Preset.Strategy, questionnaire);
Assert.NotNull(newAnswer);
Logic.TryGetAnswerByQuestionId(newAnswer, "PlayerCount", out var newAnsweredQuestion);
var newChosenPlayerCount = Int32.Parse(newAnsweredQuestion.Answers[0]);
Assert.AreEqual(chosenPlayerCount, newChosenPlayerCount);
Assert.NotNull(newReco);
}
[TestCase("1.2", "1.3", true)]
[TestCase("3.2", "1.3", false)]
[TestCase("1.21", "1.2", false)]
[TestCase("1.2", "1.2", false)]
[TestCase("0.2", "1.3", true)]
[TestCase("1.3.12", "1.3.13", true)]
[TestCase("1.3.11", "0.23", false)]
[TestCase("3.3.33", "3.3.33", false)]
[TestCase("3.3.3", "3.3.33", true)]
public void TestIsVersionLower_ReturnsCorrectResult(string versionToTest, string currentVersion, params bool[] expected)
{
var result = Logic.IsVersionLower(versionToTest, currentVersion);
Assert.AreEqual(expected[0], result);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ea491f91a8b84866ad13371715bf90c8
timeCreated: 1710847764

View File

@@ -0,0 +1,39 @@
using System;
using NUnit.Framework;
using Unity.Multiplayer.Center.Window;
using UnityEditor;
using UnityEngine.UIElements;
namespace Unity.MultiplayerCenterTests
{
class QuickstartTabTests
{
bool m_IsQuickstartPackageInstalled = false;
[OneTimeSetUp]
public void OneTimeSetup()
{
var packInfo = UnityEditor.PackageManager.PackageInfo.FindForPackageName("com.unity.multiplayer.center.quickstart");
m_IsQuickstartPackageInstalled = packInfo != null;
}
[SetUp]
public void Setup()
{
EditorWindow.GetWindow<MultiplayerCenterWindow>();
}
[Test]
public void QuickstartTab_QuickstartPackageMissingHelpboxExists()
{
if (m_IsQuickstartPackageInstalled)
{
Assert.Ignore("Skip test because it needs the quickstart package to be missing");
}
UtilsForGettingStartedTabTests.OpenGettingStartedTab();
var helpBox = EditorWindow.GetWindow<MultiplayerCenterWindow>().rootVisualElement.Q<HelpBox>();
Assert.NotNull(helpBox, "Helpbox for missing quickstart package not found");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 29cbd1c6a8c9419aac357b8d4edbdc84
timeCreated: 1721984533

View File

@@ -0,0 +1,125 @@
using NUnit.Framework;
using Unity.Multiplayer.Center.Analytics;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Common.Analytics;
using Unity.Multiplayer.Center.Window;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.MultiplayerCenterTests
{
[TestFixture]
internal class RecommendationAnalyticsTests
{
[OneTimeSetUp]
public void OneTimeSetup()
{
// Copy user choices to temp file to restore after tests.
UtilsForMultiplayerCenterTests.CopyUserChoicesToTempFile();
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
// restore user choices after tests.
UtilsForMultiplayerCenterTests.RestoreUserChoicesFromTempFile();
}
[SetUp]
public void SetUp()
{
UtilsForMultiplayerCenterTests.CloseMultiplayerCenterWindow();
}
[TearDown]
public void TearDown()
{
UtilsForMultiplayerCenterTests.CloseMultiplayerCenterWindow();
}
class MockThatCountsEvents : IMultiplayerCenterAnalytics
{
public int InstallationEventCount { get; private set; }
public int RecommendationEventCount { get; private set; }
public void SendInstallationEvent(AnswerData data, Preset preset, Package[] packages)
=> InstallationEventCount++;
public void SendRecommendationEvent(AnswerData data, Preset preset)
=> RecommendationEventCount++;
public void SendGettingStartedInteractionEvent(string targetPackageId, string sectionId,
InteractionDataType type, string displayName) { }
}
[Test]
public void RecommendationTabView_PresetSelectedViaUI_RecommendationEventSent()
{
UtilsForMultiplayerCenterTests.PopulateUserAnswersForPresetAndPlayerCount(Preset.Async, 4);
var view = CreateViewWithMockedAnalytics(out var mock);
Assert.NotNull(view.QuestionnaireView);
Assert.AreEqual(0, mock.RecommendationEventCount);
view.QuestionnaireView.RaisePresetSelected(Preset.Shooter);
Assert.AreEqual(1, mock.RecommendationEventCount);
}
[Test]
public void RecommendationTabView_NonePresetSelected_RecommendationEventNotSent()
{
UtilsForMultiplayerCenterTests.PopulateUserAnswersForPresetAndPlayerCount(Preset.Async, 4);
var view = CreateViewWithMockedAnalytics(out var mock);
Assert.NotNull(view.QuestionnaireView);
Assert.AreEqual(0, mock.RecommendationEventCount);
view.QuestionnaireView.RaisePresetSelected(Preset.None);
Assert.AreEqual(0, mock.RecommendationEventCount); // no recommendation => no event
}
[Test]
public void RecommendationTabView_PlayerCountChangedViaUI_RecommendationEventSent()
{
UtilsForMultiplayerCenterTests.PopulateUserAnswersForPresetAndPlayerCount(Preset.Adventure, 8);
var view = CreateViewWithMockedAnalytics(out var mock);
Assert.NotNull(view.QuestionnaireView);
Assert.AreEqual(0, mock.RecommendationEventCount);
view.QuestionnaireView.QuestionUpdated(UtilsForMultiplayerCenterTests.CreatePlayerCountAnswer(2));
Assert.AreEqual(1, mock.RecommendationEventCount);
}
[Test]
public void RecommendationTabView_NonMandatoryAnswerChangedViaUI_RecommendationEventSent()
{
UtilsForMultiplayerCenterTests.PopulateUserAnswersForPresetAndPlayerCount(Preset.Adventure, 8);
var view = CreateViewWithMockedAnalytics(out var mock);
Assert.NotNull(view.QuestionnaireView);
Assert.AreEqual(0, mock.RecommendationEventCount);
view.QuestionnaireView.QuestionUpdated(UtilsForMultiplayerCenterTests.GetAnsweredQuestionThatIsNotInAdventurePreset());
Assert.AreEqual(1, mock.RecommendationEventCount);
}
static RecommendationTabView CreateViewWithMockedAnalytics(out MockThatCountsEvents mock)
{
mock = new MockThatCountsEvents();
var view = new RecommendationTabView();
view.RootVisualElement = new VisualElement();
view.MultiplayerCenterAnalytics = mock;
view.Refresh();
return view;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 36b51591e13f48b5bed734724d149fb4
timeCreated: 1714672833

View File

@@ -0,0 +1,360 @@
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
{
/// <summary>
/// This is an integration test based on known data. It ensures that the recommendation matches the expected results.
/// </summary>
[TestFixture]
partial class RecommendationTests
{
[SetUp]
[TearDown]
public void Setup()
{
Object.DestroyImmediate(RecommenderSystemDataObject.instance); // force reload from disk if accessed
}
/// <summary>
/// Ensures that the packages recommended for a given preset match the expected packages (4 players)
/// Note that this does not handle hidden dependencies
/// </summary>
[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<AnsweredQuestion>
{
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<RecommendedPackageViewData>
{
new (packageDetails["com.unity.netcode"], RecommendationType.MainArchitectureChoice, null),
new (packageDetails["com.unity.services.multiplayer"], RecommendationType.HostingFeatured, null)
};
var expectedPackages = new List<string> {"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}"))}");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0efb92c924574ec59b71b9185a6ed8fb
timeCreated: 1700485818

View File

@@ -0,0 +1,237 @@
using System;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Questionnaire;
using Unity.Multiplayer.Center.Recommendations;
namespace Unity.MultiplayerCenterTests
{
/// <summary>
/// Basic test for the recommender system, without checking that the recommendation itself make sense.
/// </summary>
[TestFixture]
class RecommenderSystemUnitTests
{
const string k_NoNetcodeTitle = "No Netcode";
[SetUp]
[TearDown]
public void Cleanup()
{
UnityEngine.Object.DestroyImmediate(RecommenderSystemDataObject.instance); // force reload from disk if accessed
}
[Test]
public void TestEmptyQuestionnaireAndAnswer_ThrowsArgumentException()
{
var questionnaireData = new QuestionnaireData();
var answerData = new AnswerData();
// Having an empty questionnaire is an invalid state and should be caught.
Assert.Throws<ArgumentException>(() => RecommenderSystem.GetRecommendation(questionnaireData, answerData));
}
[Test]
public void TestEmptyQuestionnaire_ThrowsArgumentException()
{
var questionnaireData = new QuestionnaireData();
var answerData = UtilsForRecommendationTests.BuildAnswerMatching(UtilsForRecommendationTests.GetProjectQuestionnaire());
// Having an empty questionnaire is an invalid state and should be caught.
Assert.Throws<ArgumentException>(() => RecommenderSystem.GetRecommendation(questionnaireData, answerData));
}
[Test]
public void TestEmptyAnswer_ReturnsNull()
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerData = new AnswerData();
// Having empty answers is a normal state, where we don't have any recommendation to make.
var recommendation = RecommenderSystem.GetRecommendation(questionnaireData, answerData);
Assert.IsNull(recommendation);
}
[Test]
public void TestGetRecommendationForMatchingAnswers_NothingNull()
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerData = UtilsForRecommendationTests.BuildAnswerMatching(questionnaireData);
var recommendation = RecommenderSystem.GetRecommendation(questionnaireData, answerData);
Assert.NotNull(recommendation);
foreach (var solution in recommendation.NetcodeOptions)
{
if (solution.Solution is PossibleSolution.CustomNetcode or PossibleSolution.NoNetcode) continue;
UtilsForRecommendationTests.AssertRecommendedSolutionNotNull(solution);
}
foreach (var solution in recommendation.ServerArchitectureOptions)
{
UtilsForRecommendationTests.AssertRecommendedSolutionNotNull(solution, false);
}
}
[Test]
public void TestSolutionToPackageViewData_NothingNull()
{
var allPackages = RecommenderSystem.GetSolutionsToRecommendedPackageViewData();
UtilsForRecommendationTests.AssertAllRecommendedPackageNotNull(allPackages);
}
[Test]
public void TestGetRecommendationForMatchingAnswers_OnlyOneMainArchitecturePerCategory()
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerData = UtilsForRecommendationTests.BuildAnswerMatching(questionnaireData);
var recommendation = RecommenderSystem.GetRecommendation(questionnaireData, answerData);
AssertNoNetcodeIsTheLastNetcodeRecommendation(recommendation);
Assert.AreEqual(1, recommendation.NetcodeOptions.Count(x => x.RecommendationType == RecommendationType.MainArchitectureChoice));
Assert.AreEqual(1, recommendation.NetcodeOptions.Count(x => x.Selected));
Assert.AreEqual(1, recommendation.ServerArchitectureOptions.Count(x => x.RecommendationType == RecommendationType.MainArchitectureChoice));
Assert.AreEqual(1, recommendation.ServerArchitectureOptions.Count(x => x.Selected));
}
[Test]
public void TestGetSolutionsToRecommendedPackageViewData_AllSelectionsHaveSameCount()
{
var allPackages = RecommenderSystem.GetSolutionsToRecommendedPackageViewData();
var expectedCount = 16; // 4 netcode options * 4 hosting options
Assert.AreEqual(expectedCount, allPackages.Selections.Length);
PossibleSolution[] netcodeSolutions = {PossibleSolution.NGO, PossibleSolution.N4E, PossibleSolution.CustomNetcode, PossibleSolution.NoNetcode};
PossibleSolution[] hostingSolutions = {PossibleSolution.LS, PossibleSolution.DS, PossibleSolution.CloudCode, PossibleSolution.DA};
var referencePackageCount = allPackages.GetPackagesForSelection(PossibleSolution.NGO, PossibleSolution.LS).Length;
Assert.True(referencePackageCount > 0);
foreach (var netcode in netcodeSolutions)
{
foreach (var hosting in hostingSolutions)
{
var packages = allPackages.GetPackagesForSelection(netcode, hosting);
Assert.NotNull(packages);
Assert.AreEqual(referencePackageCount, packages.Length);
}
}
}
[TestCase(PossibleSolution.DS)]
[TestCase(PossibleSolution.LS)]
[TestCase(PossibleSolution.DA)]
[TestCase(PossibleSolution.CloudCode)]
public void RecommendationData_AllHostingOverridesExistInNetcodeData(PossibleSolution hostingModel)
{
var data = RecommenderSystemDataObject.instance.RecommenderSystemData;
var hostingOverrides = data.SolutionsByType[hostingModel].RecommendedPackages;
var netcodePackages = data.SolutionsByType[PossibleSolution.NGO].RecommendedPackages;
foreach (var package in hostingOverrides)
{
var index = Array.FindIndex(netcodePackages, p => p.PackageId == package.PackageId);
Assert.True(index > -1, $"Did not find package {package.PackageId} in Netcode packages for {hostingModel}");
}
}
[TestCase(PossibleSolution.NGO)]
[TestCase(PossibleSolution.N4E)]
[TestCase(PossibleSolution.CustomNetcode)]
[TestCase(PossibleSolution.NoNetcode)]
public void RecommendationData_NetcodeSolutionsHaveRecommendationDataForAllPackages(PossibleSolution netcode)
{
var data = RecommenderSystemDataObject.instance.RecommenderSystemData;
var packagesForNetcode = data.SolutionsByType[netcode].RecommendedPackages;
var solutionPackages = data.RecommendedSolutions.Where(e => e.MainPackageId != null).Select(e => e.MainPackageId).ToArray();
foreach (var package in data.Packages)
{
if(solutionPackages.Contains(package.Id))
continue;
var index = Array.FindIndex(packagesForNetcode, p => p.PackageId == package.Id);
Assert.True(index > -1, $"Did not find package {package.Id} in packages of {netcode}");
}
}
// Regression test: selecting back and forth between netcode options should not change whether some solutions
// are compatible or not
[Test]
public void TestAdaptRecommendationToNetcodeSelection_SelectBackAndForthDoesNotChangeRecommendations()
{
var recommendation = UtilsForRecommendationTests.ComputeRecommendationForPreset(Preset.Adventure);
var selectedNetcode = RecommendationUtils.GetSelectedNetcode(recommendation);
Assert.AreEqual(PossibleSolution.NGO, selectedNetcode.Solution, "Wrong test setup: expected NGO to be selected");
AssertCompatibilityInView(true, PossibleSolution.DA, recommendation.ServerArchitectureOptions, "Wrong test setup: expected DA to be compatible");
UtilsForRecommendationTests.SimulateSelectionChange(PossibleSolution.N4E, recommendation.NetcodeOptions);
RecommenderSystem.AdaptRecommendationToNetcodeSelection(recommendation);
Assert.AreEqual(PossibleSolution.N4E, RecommendationUtils.GetSelectedNetcode(recommendation).Solution, "Wrong test setup: expected N4E to be selected");
AssertCompatibilityInView(false, PossibleSolution.DA, recommendation.ServerArchitectureOptions,"Wrong test setup: expected DA to be incompatible with N4E");
UtilsForRecommendationTests.SimulateSelectionChange(PossibleSolution.NGO, recommendation.NetcodeOptions);
RecommenderSystem.AdaptRecommendationToNetcodeSelection(recommendation);
Assert.AreEqual(PossibleSolution.NGO, RecommendationUtils.GetSelectedNetcode(recommendation).Solution, "Wrong test setup: expected NGO to be selected");
AssertCompatibilityInView(true, PossibleSolution.DA, recommendation.ServerArchitectureOptions, "Fail: expected DA to still be compatible with NGO after selecting other solution");
}
[Test]
public void TestAdaptIncompatibility_AllValuesMatch()
{
bool ngoDaCompatibleBefore = AreCompatible(PossibleSolution.NGO, PossibleSolution.DA);
Assert.True(ngoDaCompatibleBefore, "Wrong test setup: expected NGO to be compatible with DA");
var ngoSolBefore = GetSolution(PossibleSolution.NGO);
Assert.AreEqual(-1, Array.FindIndex(ngoSolBefore.IncompatibleSolutions, s => s.Solution == PossibleSolution.DA), "Wrong test setup: expected DA to be in the incompatible list of NGO");
RecommenderSystemDataObject.instance.RecommenderSystemData.UpdateIncompatibility(PossibleSolution.NGO,
PossibleSolution.DA, false, "test");
bool ngoDaCompatibleAfter = AreCompatible(PossibleSolution.NGO, PossibleSolution.DA);
Assert.False(ngoDaCompatibleAfter);
var ngoSolAfter = GetSolution(PossibleSolution.NGO);
Assert.AreNotEqual(-1, Array.FindIndex(ngoSolAfter.IncompatibleSolutions, s => s.Solution == PossibleSolution.DA));
RecommenderSystemDataObject.instance.RecommenderSystemData.UpdateIncompatibility(PossibleSolution.NGO, PossibleSolution.DA, true);
bool ngoDaCompatibleEnd = AreCompatible(PossibleSolution.NGO, PossibleSolution.DA);
Assert.True(ngoDaCompatibleEnd, "Wrong test setup: expected NGO to be compatible with DA");
var ngoSolEnd = GetSolution(PossibleSolution.NGO);
Assert.AreEqual(-1, Array.FindIndex(ngoSolEnd.IncompatibleSolutions, s => s.Solution == PossibleSolution.DA));
}
static bool AreCompatible(PossibleSolution netcode, PossibleSolution hostingModel)
=> RecommenderSystemDataObject.instance.RecommenderSystemData.IsHostingModelCompatibleWithNetcode(
PossibleSolution.NGO, PossibleSolution.DA, out _);
static RecommendedSolution GetSolution(PossibleSolution type)
=> RecommenderSystemDataObject.instance.RecommenderSystemData.SolutionsByType[type];
static void AssertNoNetcodeIsTheLastNetcodeRecommendation(RecommendationViewData recommendation)
{
var netcodeSolutionCount = recommendation.NetcodeOptions.Length;
Assert.AreEqual(4, netcodeSolutionCount);
var lastSolution = recommendation.NetcodeOptions[netcodeSolutionCount - 1];
Assert.AreEqual(k_NoNetcodeTitle, lastSolution.Title);
Assert.False(lastSolution.Selected);
}
static void AssertCompatibilityInView(bool expectedCompatible, PossibleSolution hostingModel, RecommendedSolutionViewData[] hostingSolutions, string msg)
{
foreach (var hosting in hostingSolutions)
{
if(hosting.Solution == hostingModel)
{
if(expectedCompatible)
Assert.AreNotEqual(RecommendationType.Incompatible, hosting.RecommendationType, msg);
else
Assert.AreEqual(RecommendationType.Incompatible, hosting.RecommendationType, msg);
}
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 284da2a38ea043d69e1a38fd8a22bec9
timeCreated: 1700470994

View File

@@ -0,0 +1,80 @@
using System.Collections;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Center.Recommendations;
using Unity.Multiplayer.Center.Window.UI.RecommendationView;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UIElements;
namespace Unity.MultiplayerCenterTests
{
internal class SectionHeaderTests
{
[SetUp]
public void Setup()
{
var sectionHeader = new SectionHeader("Test");
var dummyWindow = EditorWindow.GetWindow<DummyWindow>();
dummyWindow.rootVisualElement.Clear();
dummyWindow.rootVisualElement.Add(sectionHeader);
}
SectionHeader GetSectionHeader()
{
return EditorWindow.GetWindow<DummyWindow>().rootVisualElement.Q<SectionHeader>();
}
IEnumerator WaitUntilDropdownIsShown()
{
DropdownField dropDown = null;
var maxWait = 100;
// wait for UI to be ready
while (dropDown == null || maxWait-- > 0)
{
dropDown = GetSectionHeader().Q<DropdownField>();
yield return null;
}
Assert.NotNull(dropDown, "Dropdown could not be found");
}
[UnityTest]
public IEnumerator SectionHeader_AppendsRecommendationText()
{
yield return WaitUntilDropdownIsShown();
var dropDown = GetSectionHeader().Q<DropdownField>();
var recommendation = UtilsForRecommendationTests.GetSomeRecommendation();
var serverChoices = recommendation.ServerArchitectureOptions;
var serverChoice = serverChoices.First(sol => sol.RecommendationType == RecommendationType.MainArchitectureChoice);
GetSectionHeader().UpdateData(recommendation.ServerArchitectureOptions);
Assert.AreEqual(serverChoice.Title +" - Recommended", dropDown.value);
}
[UnityTest]
public IEnumerator SectionHeader_IgnoresIncompatibleSolutions()
{
yield return WaitUntilDropdownIsShown();
var dropDown = GetSectionHeader().Q<DropdownField>();
var recommendation = UtilsForRecommendationTests.GetSomeRecommendation();
var serverChoices = recommendation.ServerArchitectureOptions;
serverChoices.Last().RecommendationType = RecommendationType.Incompatible;
GetSectionHeader().UpdateData(recommendation.ServerArchitectureOptions);
Assert.AreEqual(serverChoices.Count(sol => sol.RecommendationType !=RecommendationType.Incompatible), dropDown.choices.Count);
}
[TearDown]
public void TearDown()
{
var dummyWindow = EditorWindow.GetWindow<DummyWindow>();
dummyWindow.rootVisualElement.Clear();
dummyWindow.Close();
}
class DummyWindow : EditorWindow { }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6cfa81557736450abfb73264580451c1
timeCreated: 1717420762

View File

@@ -0,0 +1,111 @@
using NUnit.Framework;
using Unity.Multiplayer.Center.Analytics;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Common.Analytics;
using UnityEngine;
using Unity.Multiplayer.Center.Window;
using UnityEditor;
using UnityEngine.UIElements;
namespace Unity.MultiplayerCenterTests
{
[TestFixture]
class TabGroupTests
{
TabGroup m_TabGroup;
RecommendationTabView m_RecommendationTabView;
GettingStartedTabView m_GettingStartedTabView;
private class AnalyticsMock : IMultiplayerCenterAnalytics
{
public void SendInstallationEvent(AnswerData data, Preset preset, Package[] packages) {}
public void SendRecommendationEvent(AnswerData data, Preset preset){}
public void SendGettingStartedInteractionEvent(string targetPackageId, string sectionId, InteractionDataType type, string displayName) {}
}
private class MockTabEnabled : ITabView
{
public string Name => "MockTabEnabled";
public VisualElement RootVisualElement { get; set; }
public void SetVisible(bool visible)
{
RootVisualElement.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
}
public void Refresh() { }
public void Clear() { }
public IMultiplayerCenterAnalytics MultiplayerCenterAnalytics { get; set; }
}
private class MockTabDisabled : ITabView
{
public string Name => "MockTabDisbled";
public VisualElement RootVisualElement { get; set; }
public void SetVisible(bool visible)
{
RootVisualElement.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
}
public bool IsEnabled => false;
public void Refresh() { }
public void Clear() { }
public IMultiplayerCenterAnalytics MultiplayerCenterAnalytics { get; set; }
}
[SetUp]
public void SetUp()
{
m_RecommendationTabView = new RecommendationTabView();
m_GettingStartedTabView = new GettingStartedTabView();
m_TabGroup = new TabGroup(new AnalyticsMock(), new ITabView[] {m_RecommendationTabView, m_GettingStartedTabView, new MockTabEnabled(), new MockTabDisabled()});
}
[Test]
public void TabGroup_CreateTabs_4TabViews()
{
m_TabGroup.CreateTabs();
Assert.AreEqual(4, m_TabGroup.ViewCount);
}
[Test]
public void TabGroup_CreateTabs_SelectsTabFromUserPreferences()
{
m_TabGroup.CreateTabs();
var currentTabFromEditorPrefs = EditorPrefs.GetInt(PlayerSettings.productName + "_MultiplayerCenter_TabIndex", 0);
Assert.AreEqual(currentTabFromEditorPrefs, m_TabGroup.CurrentTab);
}
[Test]
public void TabGroup_SelectDeactivatedTab_SelectsFirstTab()
{
m_TabGroup.CreateTabs();
m_TabGroup.SetSelected(3);
Assert.AreEqual(0, m_TabGroup.CurrentTab);
}
[Test]
public void TabGroup_AnalyticsIsPropagatedToAllViews()
{
Assert.NotNull(m_RecommendationTabView.MultiplayerCenterAnalytics);
Assert.NotNull(m_GettingStartedTabView.MultiplayerCenterAnalytics);
Assert.AreEqual(m_RecommendationTabView.MultiplayerCenterAnalytics, m_GettingStartedTabView.MultiplayerCenterAnalytics);
Assert.AreEqual(m_RecommendationTabView.MultiplayerCenterAnalytics.GetType(), typeof(AnalyticsMock));
}
[TearDown]
public void TearDown()
{
if (m_TabGroup != null)
{
m_TabGroup.Clear();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 470a2d15035f44b7b2888a0db0704bff
timeCreated: 1709910098

View File

@@ -0,0 +1,26 @@
{
"name": "Unity.Multiplayer.Center.Editor.Tests",
"rootNamespace": "",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"Unity.Multiplayer.Center.Editor",
"Unity.Multiplayer.Center.Common",
"Unity.Multiplayer.Center.Integrations"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 787ec048daec145b580d1134da5dd278
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Questionnaire;
using UnityEngine;
namespace Unity.MultiplayerCenterTests
{
[TestFixture]
internal class UserChoicesMigrationTests
{
[OneTimeSetUp]
public void OneTimeSetup()
{
// Copy user choices to temp file to restore after tests.
UtilsForMultiplayerCenterTests.CopyUserChoicesToTempFile();
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
// Restore user choices after tests.
UtilsForMultiplayerCenterTests.RestoreUserChoicesFromTempFile();
}
[Test]
public void TestMigration_Pre1_2To1_2_RemovesCompetitivenessButNothingElse()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
questionnaire.Version = "1.2";
var userAnswers = new AnswerData()
{
Answers = new List<AnsweredQuestion>()
{
new () { QuestionId = "CostSensitivity", Answers = new List<string>() {"BestMargin"}},
new () { QuestionId = "Pace", Answers = new List<string>() {"Slow"}},
new () { QuestionId = "Competitiveness", Answers = new List<string>() {"Competitive"}},
new () { QuestionId = "PlayerCount", Answers = new List<string>() {"2"}}
}
};
UserChoicesObject.instance.UserAnswers = userAnswers;
UserChoicesObject.instance.QuestionnaireVersion = null;
var foundCompetitive = Logic.TryGetAnswerByQuestionId(userAnswers, "Competitiveness", out var answer);
Assert.True(foundCompetitive);
Assert.NotNull(answer);
var errorsBefore = Logic.ValidateAnswers(questionnaire, UserChoicesObject.instance.UserAnswers);
Assert.IsNotEmpty(errorsBefore);
Logic.MigrateUserChoices(questionnaire, UserChoicesObject.instance);
var foundCompetitiveAfterMigration = Logic.TryGetAnswerByQuestionId(userAnswers, "Competitiveness", out var answerAfterMigration);
Assert.False(foundCompetitiveAfterMigration);
Assert.Null(answerAfterMigration);
Assert.NotNull(UserChoicesObject.instance.QuestionnaireVersion);
Assert.AreEqual(3, UserChoicesObject.instance.UserAnswers.Answers.Count);
var errors = Logic.ValidateAnswers(questionnaire, UserChoicesObject.instance.UserAnswers);
Assert.IsEmpty(errors);
}
[Test]
public void TestMigration_1_2To1_3_ChangesMediumPaceToSlow()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
questionnaire.Version = "1.3";
var userAnswers = new AnswerData()
{
Answers = new List<AnsweredQuestion>()
{
new () { QuestionId = "CostSensitivity", Answers = new List<string>() {"BestMargin"}},
new () { QuestionId = "Pace", Answers = new List<string>() {"Medium"}},
new () { QuestionId = "PlayerCount", Answers = new List<string>() {"2"}}
}
};
UserChoicesObject.instance.UserAnswers = userAnswers;
UserChoicesObject.instance.QuestionnaireVersion = "1.2";
var foundPace = Logic.TryGetAnswerByQuestionId(userAnswers, "Pace", out var answer);
Assert.True(foundPace);
Assert.NotNull(answer);
Logic.MigrateUserChoices(questionnaire, UserChoicesObject.instance);
var foundPaceAfterMigration = Logic.TryGetAnswerByQuestionId(userAnswers, "Pace", out var answerAfterMigration);
Assert.True(foundPaceAfterMigration);
Assert.NotNull(answerAfterMigration);
Assert.AreEqual("Slow", answerAfterMigration.Answers[0]);
var errors = Logic.ValidateAnswers(questionnaire, UserChoicesObject.instance.UserAnswers);
Assert.IsEmpty(errors);
}
[Test]
public void TestMigration_SameVersion_RemovesNothing()
{
var questionnaire = UtilsForRecommendationTests.GetProjectQuestionnaire();
var userAnswers = new AnswerData()
{
Answers = new List<AnsweredQuestion>()
{
new () { QuestionId = "CostSensitivity", Answers = new List<string>() {"BestMargin"}},
new () { QuestionId = "Pace", Answers = new List<string>() {"Slow"}},
new () { QuestionId = "PlayerCount", Answers = new List<string>() {"2"}}
}
};
UserChoicesObject.instance.UserAnswers = userAnswers;
UserChoicesObject.instance.QuestionnaireVersion = "1.3";
Logic.MigrateUserChoices(questionnaire, UserChoicesObject.instance);
Assert.AreEqual("1.3", UserChoicesObject.instance.QuestionnaireVersion);
Assert.AreEqual(3, UserChoicesObject.instance.UserAnswers.Answers.Count);
var errors = Logic.ValidateAnswers(questionnaire, UserChoicesObject.instance.UserAnswers);
Assert.IsEmpty(errors);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 15513aed5d41411c887ccfe14c103337
timeCreated: 1715328871

View File

@@ -0,0 +1,70 @@
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Center.Recommendations;
namespace Unity.MultiplayerCenterTests
{
/// <summary>
/// Cheap checks on some user visible text.
/// </summary>
[TestFixture]
internal class UserVisibleTextTests
{
static readonly string[] k_Verbs = {"is", "offers", "can", "costs", "would", "should", "works", "tends", "enables"};
[Test]
public void AllScoreImpacts_ShouldHaveANonEmptyReason()
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
foreach (var question in questionnaireData.Questions)
{
foreach (var answer in question.Choices)
{
for (var index = 0; index < answer.ScoreImpacts.Length; index++)
{
var scoreImpact = answer.ScoreImpacts[index];
Assert.False(string.IsNullOrEmpty(scoreImpact.Comment),
$"Comment is empty for question {question.Id} answer {answer.Id} impact at index {index}");
}
}
}
}
[Test]
public void AllScoreImpacts_StartWithAVerbAndDoNotEndWithADot()
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
foreach (var question in questionnaireData.Questions)
{
foreach (var answer in question.Choices)
{
for (var index = 0; index < answer.ScoreImpacts.Length; index++)
{
var comment = answer.ScoreImpacts[index].Comment;
var firstWord = comment.Split(' ')[0];
CollectionAssert.Contains(k_Verbs, firstWord,
$"Comment '{comment}' does not start with a verb for question {question.Id} answer {answer.Id} impact at index {index}");
Assert.False(comment.EndsWith("."),
$"Comment '{comment}' should not end with a dot for question {question.Id} answer {answer.Id} impact at index {index}");
}
}
}
}
[Test]
public void AllSolutionsData_DoNotHaveAVerbBeforeDynamicText()
{
var data = RecommenderSystemDataObject.instance.RecommenderSystemData;
const string dynamicKeyword = Scoring.DynamicKeyword;
foreach (var solution in data.RecommendedSolutions)
{
Assert.True(solution.ShortDescription.Contains(dynamicKeyword),
$"Solution {solution.Type} description does not contain dynamic text '{dynamicKeyword}'");
var wordBeforeDynamic = solution.ShortDescription.Split(dynamicKeyword)[0].Split(' ').Last();
Assert.False(k_Verbs.Contains(wordBeforeDynamic),
$"Solution {solution.Type} description starts with a verb '{wordBeforeDynamic}' before dynamic text '{dynamicKeyword}'");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c3d3226643b94888b02e25de0a9de55f
timeCreated: 1716996547

View File

@@ -0,0 +1,79 @@
using System.IO;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Center.Window;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Unity.MultiplayerCenterTests
{
internal static class UtilsForGettingStartedTabTests
{
public static void OpenGettingStartedTab()
{
var window = EditorWindow.GetWindow<MultiplayerCenterWindow>();
window.CurrentTabTest = 1;
}
public static VisualElement GetSection(string sectionName)
{
return EditorWindow.GetWindow<MultiplayerCenterWindow>().rootVisualElement.Q(sectionName);
}
public static bool CheckErrorLogged(LogType type)
{
return type == LogType.Error || type == LogType.Exception;
}
public static bool DeleteSetupDirectoryIfExists(string directoryPath)
{
directoryPath = directoryPath.TrimEnd('/');
var metaFilePath = $"{directoryPath}.meta";
if(File.Exists(metaFilePath)) // In case the directory is not accessible, Delete will throw an exception
{
File.Delete(metaFilePath);
}
if (Directory.Exists(directoryPath))
{
Directory.Delete(directoryPath, true);
return true;
}
return false;
}
public static void AssertGameObjectHasNoMissingScripts(GameObject gameObject)
{
var components = gameObject.GetComponents<Component>();
for (var index = 0; index < components.Length; index++)
{
var component = components[index];
Assert.IsNotNull(component, $"GameObject {gameObject.name} has missing script (component index: {index})");
}
}
/// <summary>
/// Returns true if a SettingsPage (right side of the SettingsWindow) has content.
/// This is useful to test if calling SettingsService.OpenProjectSettings("Your setting")
/// makes the window show content. In case your path does not open settings, the right side
/// will be empty and false will be returned.
/// Caution: This will only work for SettingsPages created with UI-Toolkit.
/// </summary>
/// <returns> True if settings are shown on the right side of the SettingsWindow, false if no settings window is
/// shown, or the content on the right is empty.</returns>
public static bool SettingsWindowRightSideHasContent()
{
var windows = Resources.FindObjectsOfTypeAll(typeof(EditorWindow)) as EditorWindow[];
var projectSettingsWindow = windows.FirstOrDefault(window => window.GetType().FullName == "UnityEditor.ProjectSettingsWindow");
if (projectSettingsWindow == null)
return false;
var settingsPanel = projectSettingsWindow.rootVisualElement.Q<VisualElement>(className: "settings-panel");
if (settingsPanel == null)
return false;
return settingsPanel.Children().First().childCount > 0;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c37c4c45cacc46edb43b8c32617b3e34
timeCreated: 1715074033

View File

@@ -0,0 +1,143 @@
using System;
using System.IO;
using NUnit.Framework;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Questionnaire;
using Unity.Multiplayer.Center.Window;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Unity.MultiplayerCenterTests
{
/// <summary>
/// This is meant to be a utility class for MultiplayerCenter tests (including the project tests).
/// </summary>
internal static class UtilsForMultiplayerCenterTests
{
public static void CloseMultiplayerCenterWindow()
{
var allWindows = Resources.FindObjectsOfTypeAll<MultiplayerCenterWindow>();
if (allWindows.Length <= 0) return;
foreach (var window in allWindows)
{
window.Close();
UnityEngine.Object.DestroyImmediate(window);
}
}
public static void ResetUserChoices()
{
UserChoicesObject.instance.UserAnswers = new AnswerData();
UserChoicesObject.instance.Preset = new Preset();
UserChoicesObject.instance.SelectedSolutions = new SelectedSolutionsData();
UserChoicesObject.instance.Save();
}
public static void PopulateUserAnswersForPresetAndPlayerCount(Preset preset, int playerCount)
{
//TODO: also assert player count has proper value
Assert.AreNotEqual(Preset.None, preset);
UserChoicesObject.instance.Preset = preset;
Logic.Update(UserChoicesObject.instance.UserAnswers, CreatePlayerCountAnswer(playerCount));
UserChoicesObject.instance.Save();
var (resultAnswerData, recommendation) = Logic.ApplyPresetToAnswerData(
UserChoicesObject.instance.UserAnswers, preset, QuestionnaireObject.instance.Questionnaire);
Assert.NotNull(recommendation);
UserChoicesObject.instance.UserAnswers = resultAnswerData;
UserChoicesObject.instance.Save();
}
public static AnsweredQuestion CreatePlayerCountAnswer(int playerCount)
{
// simplified validation for the sake of the test
Assert.True(playerCount is 2 or 4 or 8, "Please use a player count of 2, 4 or 8.");
return new AnsweredQuestion() {QuestionId = "PlayerCount", Answers = new() {playerCount.ToString()}};
}
public static AnsweredQuestion GetAnsweredQuestionThatIsNotInAdventurePreset()
{
// Corresponds to answer "I do not know" for the question "Netcode Architecture"
return new AnsweredQuestion() { QuestionId = "NetcodeArchitecture", Answers = new() {"NoNetcode"}};
}
// Copy the UserChoices file to a temp file to be able to restore it after the tests.
public static void CopyUserChoicesToTempFile()
{
var sourceFilePath = GetUserChoicesFullFilePath();
var tempFilePath = GetUserChoicesTempFilePath();
// Can happen if the there are no UserChoices saved to disk yet.
if (!File.Exists(sourceFilePath))
{
// Save to have something to copy.
UserChoicesObject.instance.Save();
}
try
{
File.Copy(sourceFilePath, tempFilePath, true);
}
catch (Exception e)
{
Assert.Fail($"Could not create Temp File from {sourceFilePath} {e.Message}");
}
}
// Restore the UserChoices file from the temp file and delete the temp file.
public static void RestoreUserChoicesFromTempFile()
{
var sourceFilePath = GetUserChoicesFullFilePath();
var tempFilePath = GetUserChoicesTempFilePath();
try
{
File.Copy(tempFilePath,sourceFilePath, true);
File.Delete(tempFilePath);
}
catch (Exception e)
{
Assert.Fail($"Could not restore UserChoices from temp file {tempFilePath}, or could not delete temp file {tempFilePath} {e.Message}");
}
Object.DestroyImmediate(UserChoicesObject.instance);
}
public static void OpenTabByIndex(int tabIndex)
{
var multiplayerWindow = EditorWindow.GetWindow<MultiplayerCenterWindow>();
multiplayerWindow.CurrentTabTest = tabIndex;
}
public static void SetNetcodeSolutionToCustomNetcode(bool flag)
{
UserChoicesObject.instance.SelectedSolutions.SelectedNetcodeSolution = flag ? SelectedSolutionsData.NetcodeSolution.CustomNetcode : SelectedSolutionsData.NetcodeSolution.NGO;
}
public static void SetSelectedSolutionToDistributedAuthority(bool flag)
{
if (!flag)
{
UserChoicesObject.instance.SelectedSolutions.SelectedHostingModel = SelectedSolutionsData.HostingModel.ClientHosted;
return;
}
UserChoicesObject.instance.SelectedSolutions.SelectedNetcodeSolution = SelectedSolutionsData.NetcodeSolution.NGO;
UserChoicesObject.instance.SelectedSolutions.SelectedHostingModel = SelectedSolutionsData.HostingModel.DistributedAuthority;
}
static string GetUserChoicesFullFilePath()
{
var choicesObject = UserChoicesObject.instance;
var dirName = Path.GetDirectoryName(Application.dataPath);
return Path.Combine(dirName, choicesObject.FilePath);
}
static string GetUserChoicesTempFilePath()
{
return Path.ChangeExtension(GetUserChoicesFullFilePath(), ".tmp");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cfc5ccd4416341629caf73dd3e318aac
timeCreated: 1714987913

View File

@@ -0,0 +1,138 @@
using System;
using System.Linq;
using NUnit.Framework;
using Unity.Multiplayer.Center.Common;
using Unity.Multiplayer.Center.Questionnaire;
using Unity.Multiplayer.Center.Recommendations;
using UnityEngine;
namespace Unity.MultiplayerCenterTests
{
internal static class UtilsForRecommendationTests
{
public static QuestionnaireData GetProjectQuestionnaire()
{
// TODO: maybe accessing from disk is a better idea than using the singleton? (side effects)
var result = Clone(QuestionnaireObject.instance.Questionnaire);
// sanity checks
Assert.IsNotNull(result);
Assert.IsNotNull(result.Questions);
Assert.Greater(result.Questions.Length, 0);
Assert.False(result.Questions.Any(x => x.Choices.Length == 0));
return result;
}
public static AnswerData BuildAnswerMatching(QuestionnaireData questionnaireData)
{
var answerData = new AnswerData();
foreach (var question in questionnaireData.Questions)
{
var middleChoice = question.Choices[question.Choices.Length / 2 - 1];
var answeredQuestion = new AnsweredQuestion()
{
QuestionId = question.Id,
Answers = new(){middleChoice.Id}
};
Logic.Update(answerData, answeredQuestion);
}
return answerData;
}
public static RecommendationViewData GetSomeRecommendation()
{
var questionnaireData = GetProjectQuestionnaire();
var answerData = BuildAnswerMatching(questionnaireData);
return RecommenderSystem.GetRecommendation(questionnaireData, answerData);
}
public static void AssertRecommendedSolutionNotNull(RecommendedSolutionViewData solution, bool checkMainPackage = true)
{
Assert.NotNull(solution);
Assert.False(string.IsNullOrEmpty(solution.Title));
Assert.That(solution.RecommendationType is RecommendationType.MainArchitectureChoice
or RecommendationType.SecondArchitectureChoice or RecommendationType.NotRecommended or RecommendationType.Incompatible, $"Recommendation type: {solution.RecommendationType}");
if (checkMainPackage)
Assert.NotNull(solution.MainPackage);
}
public static void AssertAllRecommendedPackageNotNull(SolutionsToRecommendedPackageViewData allPackages)
{
foreach (var selection in allPackages.Selections)
{
var packages = allPackages.GetPackagesForSelection(selection);
var selectionString = $"Netcode {selection.Netcode} - Hosting {selection.HostingModel}";
Assert.NotNull(packages, selectionString);
CollectionAssert.IsNotEmpty(packages, selectionString);
foreach (var package in packages)
{
Assert.False(string.IsNullOrEmpty(package.Name), selectionString);
Assert.NotNull(string.IsNullOrEmpty(package.PackageId), $"{selectionString} - {package.Name}");
Assert.That(package.RecommendationType != RecommendationType.MainArchitectureChoice &&
package.RecommendationType != RecommendationType.SecondArchitectureChoice, $"{selectionString} - {package.Name}");
}
}
}
/// <summary>
/// All solutions should mention all packages, however the actual recommendations (RecommendationType) should be different.
/// This checks that all packages in solution 1 are also in solution 2, but that they don't all have the same recommendation.
/// Note: this a current requirement, but this might evolve. Adapt if necessary.
/// 2nd Note: With the introduction of the Multiplayer SDK this requirement changed for the server architecture options, but not for the netcode options.
/// </summary>
/// <param name="reco1">Solution 1</param>
/// <param name="reco2">Solution 2</param>
public static void AssertSamePackagesWithDifferentRecommendations(RecommendedPackageViewData[] reco1, RecommendedPackageViewData[] reco2, string msg)
{
Assert.AreEqual(reco1.Length, reco2.Length);
var allTheSame = true;
foreach (var p in reco1)
{
var matching = reco2.FirstOrDefault(x => x.PackageId == p.PackageId);
Assert.IsNotNull(matching);
if(matching.RecommendationType != p.RecommendationType)
allTheSame = false;
}
Assert.False(allTheSame, $"The two solutions have exactly the same recommended packages!");
}
public static T Clone<T>(T obj)
{
return JsonUtility.FromJson<T>(JsonUtility.ToJson(obj));
}
public static RecommendationViewData ComputeRecommendationForPreset(Preset preset, string playerCount = "4")
{
var questionnaireData = UtilsForRecommendationTests.GetProjectQuestionnaire();
var answerData = GetAnswerDataForPreset(questionnaireData, preset, playerCount);
var recommendation = RecommenderSystem.GetRecommendation(questionnaireData, answerData);
return recommendation;
}
public static AnswerData GetAnswerDataForPreset(QuestionnaireData questionnaireData, Preset preset, string playerCount)
{
if (preset == Preset.None)
return new AnswerData();
var indexOfPreset = Array.IndexOf(questionnaireData.PresetData.Presets, preset);
var matchingAnswers = questionnaireData.PresetData.Answers[indexOfPreset].Clone();
var playerCountAnswer = new AnsweredQuestion()
{
QuestionId = "PlayerCount",
Answers = new() {playerCount}
};
Logic.Update(matchingAnswers, playerCountAnswer);
return matchingAnswers;
}
public static void SimulateSelectionChange(PossibleSolution newSelectedSolution, RecommendedSolutionViewData[] solutionViewDatas)
{
foreach (var solution in solutionViewDatas)
{
solution.Selected = solution.Solution == newSelectedSolution;
if(solution.Selected && solution.RecommendationType == RecommendationType.Incompatible)
Assert.Fail("Selected incompatible solution");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ca560c7d2a0d4453a50ec4f704ce9ce1
timeCreated: 1700473715

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8e4d240cf158245a9945c4df01d83bc1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;
#if ENABLE_INPUT_SYSTEM && NEW_INPUT_SYSTEM_INSTALLED
using UnityEngine.InputSystem;
#endif
// This package does not contain any runtim components, therefore
// this test is only a placeholder.
[TestFixture]
class RuntimeExampleTest {
[SetUp]
public void SetUp()
{
#if ENABLE_INPUT_SYSTEM && NEW_INPUT_SYSTEM_INSTALLED
if (Keyboard.current == null)
{
InputSystem.AddDevice<Keyboard>();
}
#endif
}
[Test]
public void PlayModeSampleTestSimplePasses()
{
// Use the Assert class to test conditions.
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7456ea4ba2dcd48d3909b82e610f2048
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
{
"name": "Unity.Multiplayer.Center.Tests",
"rootNamespace": "",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"Unity.InputSystem"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [
{
"name": "com.unity.inputsystem",
"expression": "1.4.0",
"define": "NEW_INPUT_SYSTEM_INSTALLED"
}
],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2664430aff4254d79887d32c3fc1e221
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: