From 582175c3a429e36e4d58798693518c0fffc3508d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jun 2022 22:57:52 +0900 Subject: [PATCH 01/12] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 2ef3bb2dd1..584fe0a3ef 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ab690b22a6..ff223f5107 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index a84fc40a45..b8a4aca02e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + From 13108155d3e4ef0ca2e19d520e9ddfcf2c8a4b95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jun 2022 03:43:56 +0900 Subject: [PATCH 02/12] Fix crash on visual esting `TestSceneChatOverlay` due to null user --- osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index d2d9b9a9e5..247ea52648 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -39,7 +39,8 @@ namespace osu.Game.Tests.Visual.Online private TestChatOverlay chatOverlay; private ChannelManager channelManager; - private APIUser testUser; + private readonly APIUser testUser = new APIUser { Username = "test user", Id = 5071479 }; + private Channel[] testChannels; private Channel testChannel1 => testChannels[0]; @@ -51,7 +52,6 @@ namespace osu.Game.Tests.Visual.Online [SetUp] public void SetUp() => Schedule(() => { - testUser = new APIUser { Username = "test user", Id = 5071479 }; testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray(); Child = new DependencyProvidingContainer From 46cbbf5c6eb240b416ee24445ed8aa5beeead54a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jun 2022 04:45:02 +0900 Subject: [PATCH 03/12] Fix flaky song select placeholder test by changing ruleset post-display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was failing occasionally due to the beatmap present operation causing the test's ruleset change to undo. ```csharp TearDown : System.TimeoutException : "wait for placeholder visible" timed out --TearDown at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0() at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition) --- End of stack trace from previous location --- at osu.Framework.Testing.TestSceneTestRunner.TestRunner.RunTestBlocking(TestScene test) at osu.Game.Tests.Visual.OsuTestScene.OsuTestSceneTestRunner.RunTestBlocking(TestScene test) in /opt/buildagent/work/ecd860037212ac52/osu.Game/Tests/Visual/OsuTestScene.cs:line 503 at osu.Framework.Testing.TestScene.RunTestsFromNUnit() ------- Stdout: ------- [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ’จ Class: TestScenePlaySongSelect [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ถ Test: TestPlaceholderConvertSetting [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #1 exit all screens [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #2 reset defaults [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #3 delete all beatmaps [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #4 change ruleset to 2 [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #5 import test map for ruleset 0 [database] 2022-06-29 10:36:45 [verbose]: [efc1a] Beginning import from unknown... [database] 2022-06-29 10:36:45 [verbose]: [efc1a] Import successfully completed! [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #6 wait for imported to arrive in carousel [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #7 change convert setting [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #8 create song select [runtime] 2022-06-29 10:36:45 [verbose]: ScreenTestScene screen changed โ†’ TestScenePlaySongSelect+TestSongSelect [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ“บ ScreenTestScene(OsuScreenStack)#338(depth:1) loading TestScenePlaySongSelect+TestSongSelect#263 [runtime] 2022-06-29 10:36:45 [verbose]: decoupled ruleset transferred ("" -> "osu!catch") [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #9 wait for present [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ“บ ScreenTestScene(OsuScreenStack)#338(depth:1) entered TestScenePlaySongSelect+TestSongSelect#263 [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ“บ BackgroundScreenStack#328(depth:1) loading BackgroundScreenBeatmap#338 [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ“บ BackgroundScreenStack#328(depth:1) entered BackgroundScreenBeatmap#338 [runtime] 2022-06-29 10:36:45 [verbose]: Song select updating selection with beatmap:null ruleset:fruits [runtime] 2022-06-29 10:36:45 [verbose]: Song select changing beatmap from "please load a beatmap! - no beatmaps available!" to "null" [runtime] 2022-06-29 10:36:45 [verbose]: Song select working beatmap updated to Some Artist 0 - Some Song (set id 6224) ece7b702-895c-4f15-892f-05e4ff5e9a24 (Some Guy 3) [Normal 6224000 (length 0:50, bpm 106.7)] [runtime] 2022-06-29 10:36:45 [verbose]: Song select updating selection with beatmap:null ruleset:osu [runtime] 2022-06-29 10:36:45 [verbose]: decoupled ruleset transferred ("osu!catch" -> "osu!") [runtime] 2022-06-29 10:36:45 [verbose]: Song select updating selection with beatmap:74232aa2-2a1f-4920-b643-e85976838251 ruleset:osu [runtime] 2022-06-29 10:36:45 [verbose]: Song select decided to ensurePlayingSelected [runtime] 2022-06-29 10:36:45 [verbose]: Game-wide working beatmap updated to Some Artist 0 - Some Song (set id 6224) ece7b702-895c-4f15-892f-05e4ff5e9a24 (Some Guy 3) [Normal 6224000 (length 0:50, bpm 106.7)] [runtime] 2022-06-29 10:36:45 [debug]: Focus changed from nothing to SeekLimitedSearchTextBox. [network] 2022-06-29 10:36:45 [verbose]: Failing request osu.Game.Online.API.Requests.GetBeatmapRequest (System.InvalidOperationException: DummyAPIAccess cannot process this request.) [runtime] 2022-06-29 10:36:45 [verbose]: โœ”๏ธ 16 repetitions [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #10 wait for carousel loaded [runtime] 2022-06-29 10:36:45 [verbose]: ๐Ÿ”ธ Step #11 wait for placeholder visible [runtime] 2022-06-29 10:36:55 [verbose]: ๐Ÿ’ฅ Failed (on attempt 1,459) [runtime] 2022-06-29 10:36:55 [verbose]: โณ Currently loading components (0) [runtime] 2022-06-29 10:36:55 [verbose]: ๐Ÿงต Task schedulers [runtime] 2022-06-29 10:36:55 [verbose]: LoadComponentsAsync (standard) concurrency:4 running:0 pending:0 [runtime] 2022-06-29 10:36:55 [verbose]: LoadComponentsAsync (long load) concurrency:4 running:0 pending:0 [runtime] 2022-06-29 10:36:55 [verbose]: ๐ŸŽฑ Thread pool [runtime] 2022-06-29 10:36:55 [verbose]: worker: min 1 max 32,767 available 32,766 [runtime] 2022-06-29 10:36:55 [verbose]: completion: min 1 max 1,000 available 1,000 [runtime] 2022-06-29 10:36:55 [debug]: Focus on "SeekLimitedSearchTextBox" no longer valid as a result of unfocusIfNoLongerValid. [runtime] 2022-06-29 10:36:55 [debug]: Focus changed from SeekLimitedSearchTextBox to nothing. ``` --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6d881555da..6896b442e0 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -100,12 +100,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestPlaceholderConvertSetting() { - changeRuleset(2); addRulesetImportStep(0); AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false)); createSongSelect(); + changeRuleset(2); + AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible); AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType().First().TriggerClick()); From 8171deec494d8b1a2f9794453bb54f8d88821ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 29 Jun 2022 22:38:06 +0200 Subject: [PATCH 04/12] Attempt to fix editor navigation test failures again First of all, stop relying that `InputManager` will successfully press the gameplay test button, because it won't if it's obscured by the notification overlay. Closing the overlay in a loop doesn't work because there's a chance that a notification is posted, then all overlays are closed, then another notification is posted, and so the button still isn't clickable. Instead, use the `TestGameplay()` method directly. Secondly, the notifications will still block `EditorPlayerLoader` from transitioning to `EditorPlayer`. Here we have no other choice than to aggressively dismiss notifications every spin of the relevant until step and hope we eventually progress to `EditorPlayer`. --- .../Editing/TestSceneEditorNavigation.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs index a21ef6f897..327d581e37 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorNavigation.cs @@ -6,18 +6,14 @@ using NUnit.Framework; using osu.Framework.Extensions; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Select; using osu.Game.Tests.Resources; -using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { @@ -36,20 +32,21 @@ namespace osu.Game.Tests.Visual.Editing () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.IsLoaded); - AddUntilStep("wait for completion notification", () => Game.Notifications.ChildrenOfType().Count() == 1); - AddStep("dismiss notifications", () => Game.Notifications.Hide()); AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo); AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); - AddStep("test gameplay", () => + AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay()); + + AddUntilStep("wait for player", () => { - var testGameplayButton = this.ChildrenOfType().Single(); - InputManager.MoveMouseTo(testGameplayButton); - InputManager.Click(MouseButton.Left); + // notifications may fire at almost any inopportune time and cause annoying test failures. + // relentlessly attempt to dismiss any and all interfering overlays, which includes notifications. + // this is theoretically not foolproof, but it's the best that can be done here. + Game.CloseAllOverlays(); + return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded; }); - AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded); AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield())); From c5d31f50cd4053adedaa4787edc0fcb1222bbc6e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Jun 2022 11:21:15 +0900 Subject: [PATCH 05/12] Attempt to fix flaky TestSceneAutoplay test --- .../Visual/Gameplay/TestSceneAutoplay.cs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 019bfe322e..47c8dc0f8d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -6,10 +6,10 @@ using System.ComponentModel; using System.Linq; using osu.Framework.Testing; -using osu.Game.Beatmaps.Timing; using osu.Game.Graphics.Containers; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.Break; @@ -31,19 +31,20 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { + // It doesn't matter which ruleset is used - this beatmap is only used for reference. + var beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); - seekToBreak(0); + + seekTo(beatmap.Beatmap.Breaks[0].StartTime); AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType().Single().AccuracyDisplay.Current.Value == 1); + AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); - seekToBreak(0); - seekToBreak(1); - - AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); - + seekTo(beatmap.Beatmap.HitObjects[^1].GetEndTime()); AddUntilStep("results displayed", () => getResultsScreen()?.IsLoaded == true); AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100); @@ -58,12 +59,18 @@ namespace osu.Game.Tests.Visual.Gameplay ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen; } - private void seekToBreak(int breakIndex) + private void seekTo(double time) { - AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime)); - AddUntilStep("wait for seek to complete", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= destBreak().StartTime); + AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex); + // Prevent test timeouts by seeking in 10 second increments. + for (double t = 0; t < time; t += 10000) + { + double expectedTime = t; + AddUntilStep($"current time >= {t}", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= expectedTime); + } + + AddUntilStep("wait for seek to complete", () => Player.DrawableRuleset.FrameStableClock.CurrentTime >= time); } } } From edc4ace17e4f703b6ee0b362e943388d25262748 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Jun 2022 11:52:29 +0900 Subject: [PATCH 06/12] Attach DT with NC, SD with PF, and Cinema with Autoplay --- osu.Game/Rulesets/Ruleset.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index e098472999..c1ec6c30ef 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets break; case ModPerfect: - value |= LegacyMods.Perfect; + value |= LegacyMods.Perfect | LegacyMods.SuddenDeath; break; case ModSuddenDeath: @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets break; case ModNightcore: - value |= LegacyMods.Nightcore; + value |= LegacyMods.Nightcore | LegacyMods.DoubleTime; break; case ModDoubleTime: @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets break; case ModCinema: - value |= LegacyMods.Cinema; + value |= LegacyMods.Cinema | LegacyMods.Autoplay; break; case ModAutoplay: From 6c2d02fefdd5498553ca65b7546f717546e06c0d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Jun 2022 12:37:03 +0900 Subject: [PATCH 07/12] Fix tests --- .../CatchLegacyModConversionTest.cs | 15 +++++++++------ .../ManiaLegacyModConversionTest.cs | 9 ++++++--- .../OsuLegacyModConversionTest.cs | 9 ++++++--- .../TaikoLegacyModConversionTest.cs | 15 +++++++++------ 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index c65c9df9f9..b9d6f28228 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -24,21 +24,24 @@ namespace osu.Game.Rulesets.Catch.Tests new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } }, new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, - new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, - new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } }, - new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } }, new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } }; + [TestCaseSource(nameof(catch_mod_mapping))] + [TestCase(LegacyMods.Cinema, new[] { typeof(CatchModCinema) })] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })] + [TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })] + [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })] + [TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })] + [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })] + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + [TestCaseSource(nameof(catch_mod_mapping))] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })] - public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); - - [TestCaseSource(nameof(catch_mod_mapping))] public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new CatchRuleset(); diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index accae29ffe..9dee861e66 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -23,10 +23,8 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } }, new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } }, - new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } }, - new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } }, new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } }, new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } }, new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } }, @@ -34,7 +32,6 @@ namespace osu.Game.Rulesets.Mania.Tests new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } }, new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } }, new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } }, - new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } }, new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } }, new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } }, new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } }, @@ -45,12 +42,18 @@ namespace osu.Game.Rulesets.Mania.Tests }; [TestCaseSource(nameof(mania_mod_mapping))] + [TestCase(LegacyMods.Cinema, new[] { typeof(ManiaModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })] + [TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })] + [TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); [TestCaseSource(nameof(mania_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })] + [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })] + [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new ManiaRuleset(); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index db9872b152..01d83b55e6 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -25,24 +25,27 @@ namespace osu.Game.Rulesets.Osu.Tests new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } }, new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } }, - new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } }, new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, - new object[] { LegacyMods.Perfect, new[] { typeof(OsuModPerfect) } }, - new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } }, new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } }, new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } }; [TestCaseSource(nameof(osu_mod_mapping))] + [TestCase(LegacyMods.Cinema, new[] { typeof(OsuModCinema) })] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })] + [TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })] + [TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); [TestCaseSource(nameof(osu_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })] + [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })] + [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })] public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new OsuRuleset(); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index 3553cb27dc..c86f8cb8d2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -24,22 +24,25 @@ namespace osu.Game.Rulesets.Taiko.Tests new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } }, new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } }, new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } }, - new object[] { LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) } }, new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } }, new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } }, - new object[] { LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) } }, new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, - new object[] { LegacyMods.Cinema, new[] { typeof(TaikoModCinema) } }, new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } }; + [TestCaseSource(nameof(taiko_mod_mapping))] + [TestCase(LegacyMods.Cinema, new[] { typeof(TaikoModCinema) })] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })] + [TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })] + [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })] + [TestCase(LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) })] + [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })] + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + [TestCaseSource(nameof(taiko_mod_mapping))] [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })] - public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); - - [TestCaseSource(nameof(taiko_mod_mapping))] public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); protected override Ruleset CreateRuleset() => new TaikoRuleset(); From c6520de749daeaaf6b7e1e8962d7c46d25491aa4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 30 Jun 2022 14:24:49 +0900 Subject: [PATCH 08/12] Ensure PlaylistItem beatmap is not null --- osu.Game/Online/Rooms/PlaylistItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index a0711d9ded..2213311c67 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -72,7 +72,7 @@ namespace osu.Game.Online.Rooms /// In many cases, this will *not* contain any usable information apart from OnlineID. /// [JsonIgnore] - public IBeatmapInfo Beatmap { get; set; } = null!; + public IBeatmapInfo Beatmap { get; private set; } [JsonIgnore] public IBindable Valid => valid; @@ -81,6 +81,7 @@ namespace osu.Game.Online.Rooms [JsonConstructor] private PlaylistItem() + : this(new APIBeatmap()) { } From 0abb400f6400e8ceca40bbf34950aa1671a06eb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jun 2022 15:14:25 +0900 Subject: [PATCH 09/12] Add reproduction of transaction not being exited correctly --- osu.Game.Tests/Database/RealmLiveTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index 00a667521d..fd1f564f59 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -91,6 +91,25 @@ namespace osu.Game.Tests.Database Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); } + [Test] + public void TestTransactionRolledBackOnException() + { + RunTestWithRealm((realm, _) => + { + var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()); + + realm.Run(r => r.Write(_ => r.Add(beatmap))); + + var liveBeatmap = beatmap.ToLive(realm); + + Assert.Throws(() => liveBeatmap.PerformWrite(l => throw new InvalidOperationException())); + Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden)); + + liveBeatmap.PerformWrite(l => l.Hidden = true); + Assert.IsTrue(liveBeatmap.PerformRead(l => l.Hidden)); + }); + } + [Test] public void TestScopedReadWithoutContext() { From 78d86fd3ffef5f61b06ea557ab777a0990174762 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jun 2022 15:11:22 +0900 Subject: [PATCH 10/12] Fix `PerformWrite` not rolling back transaction on exception --- osu.Game/Database/RealmLive.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 72de747807..9c871a3929 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -104,9 +104,12 @@ namespace osu.Game.Database PerformRead(t => { - var transaction = t.Realm.BeginWrite(); - perform(t); - transaction.Commit(); + using (var transaction = t.Realm.BeginWrite()) + { + perform(t); + transaction.Commit(); + } + RealmLiveStatistics.WRITES.Value++; }); } From 356c0501ec25596ed62dc0c8ae0c4af3cf56878f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jun 2022 15:40:44 +0900 Subject: [PATCH 11/12] Guard again potential nulls in `RealmNamedFileUsage` Hopefully help in figuring out https://sentry.ppy.sh/organizations/ppy/issues/3679/?project=2&query=user%3A%22id%3A10078484%22 --- osu.Game/Models/RealmNamedFileUsage.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs index 0f6f439d73..c4310c4edb 100644 --- a/osu.Game/Models/RealmNamedFileUsage.cs +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -1,6 +1,7 @@ ๏ปฟ// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using JetBrains.Annotations; using osu.Framework.Testing; using osu.Game.Database; @@ -19,8 +20,8 @@ namespace osu.Game.Models public RealmNamedFileUsage(RealmFile file, string filename) { - File = file; - Filename = filename; + File = file ?? throw new ArgumentNullException(nameof(file)); + Filename = filename ?? throw new ArgumentNullException(nameof(filename)); } [UsedImplicitly] From 10934450cc2e35ccd02a70278ec073c433057881 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jun 2022 16:02:46 +0900 Subject: [PATCH 12/12] Fix directory cleanup occurring too early during realm tests --- .../NonVisual/CustomDataDirectoryTest.cs | 47 +++++++------------ 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index a803974d30..216bd0fd3c 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -44,8 +44,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestCustomDirectory() { - string customPath = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) using (var host = new CustomTestHeadlessGameHost()) { using (var storageConfig = new StorageConfigManager(host.InitialStorage)) @@ -63,7 +62,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); } } } @@ -71,8 +69,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestSubDirectoryLookup() { - string customPath = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) using (var host = new CustomTestHeadlessGameHost()) { using (var storageConfig = new StorageConfigManager(host.InitialStorage)) @@ -97,7 +94,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); } } } @@ -105,8 +101,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigration() { - string customPath = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) using (var host = new CustomTestHeadlessGameHost()) { try @@ -173,7 +168,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); } } } @@ -181,9 +175,8 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationBetweenTwoTargets() { - string customPath = prepareCustomPath(); - string customPath2 = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) + using (prepareCustomPath(out string customPath2)) using (var host = new CustomTestHeadlessGameHost()) { try @@ -205,8 +198,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); - cleanupPath(customPath2); } } } @@ -214,8 +205,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToSameTargetFails() { - string customPath = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) using (var host = new CustomTestHeadlessGameHost()) { try @@ -228,7 +218,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); } } } @@ -236,9 +225,8 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationFailsOnExistingData() { - string customPath = prepareCustomPath(); - string customPath2 = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) + using (prepareCustomPath(out string customPath2)) using (var host = new CustomTestHeadlessGameHost()) { try @@ -267,8 +255,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); - cleanupPath(customPath2); } } } @@ -276,8 +262,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToNestedTargetFails() { - string customPath = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) using (var host = new CustomTestHeadlessGameHost()) { try @@ -298,7 +283,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); } } } @@ -306,8 +290,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestMigrationToSeeminglyNestedTarget() { - string customPath = prepareCustomPath(); - + using (prepareCustomPath(out string customPath)) using (var host = new CustomTestHeadlessGameHost()) { try @@ -328,7 +311,6 @@ namespace osu.Game.Tests.NonVisual finally { host.Exit(); - cleanupPath(customPath); } } } @@ -343,14 +325,17 @@ namespace osu.Game.Tests.NonVisual return path; } - private static string prepareCustomPath() => Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}"); + private static IDisposable prepareCustomPath(out string path) + { + path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, $"custom-path-{Guid.NewGuid()}"); + return new InvokeOnDisposal(path, cleanupPath); + } private static void cleanupPath(string path) { try { - if (Directory.Exists(path)) - Directory.Delete(path, true); + if (Directory.Exists(path)) Directory.Delete(path, true); } catch {