Merge branch 'master' into realm-integration/live-queryable-fix

This commit is contained in:
Dean Herbert
2021-11-30 12:02:35 +09:00
10 changed files with 81 additions and 29 deletions

View File

@ -12,6 +12,7 @@ using NUnit.Framework;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
@ -474,7 +475,7 @@ namespace osu.Game.Tests.Database
} }
[Test] [Test]
public void TestImportThenDeleteThenImport() public void TestImportThenDeleteThenImportOptimisedPath()
{ {
RunTestWithRealmAsync(async (realmFactory, storage) => RunTestWithRealmAsync(async (realmFactory, storage) =>
{ {
@ -485,11 +486,39 @@ namespace osu.Game.Tests.Database
deleteBeatmapSet(imported, realmFactory.Context); deleteBeatmapSet(imported, realmFactory.Context);
Assert.IsTrue(imported.DeletePending);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context); var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending);
});
}
[Test]
public void TestImportThenDeleteThenImportNonOptimisedPath()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new NonOptimisedBeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
deleteBeatmapSet(imported, realmFactory.Context);
Assert.IsTrue(imported.DeletePending);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending);
}); });
} }
@ -867,5 +896,15 @@ namespace osu.Game.Tests.Database
Assert.Fail(failureMessage); Assert.Fail(failureMessage);
} }
public class NonOptimisedBeatmapImporter : BeatmapImporter
{
public NonOptimisedBeatmapImporter(RealmContextFactory realmFactory, Storage storage)
: base(realmFactory, storage)
{
}
protected override bool HasCustomHashFunction => true;
}
} }
} }

View File

@ -29,6 +29,22 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestAccessAfterAttach()
{
RunTestWithRealm((realmFactory, _) =>
{
var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
var liveBeatmap = beatmap.ToLive();
using (var context = realmFactory.CreateContext())
context.Write(r => r.Add(beatmap));
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
});
}
[Test] [Test]
public void TestAccessNonManaged() public void TestAccessNonManaged()
{ {
@ -51,7 +67,7 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realmFactory, _) =>
{ {
RealmLive<RealmBeatmap>? liveBeatmap = null; ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) using (var threadContext = realmFactory.CreateContext())
@ -80,7 +96,7 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realmFactory, _) =>
{ {
RealmLive<RealmBeatmap>? liveBeatmap = null; ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) using (var threadContext = realmFactory.CreateContext())
@ -121,7 +137,8 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realmFactory, _) =>
{ {
RealmLive<RealmBeatmap>? liveBeatmap = null; ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) using (var threadContext = realmFactory.CreateContext())
@ -159,7 +176,7 @@ namespace osu.Game.Tests.Database
{ {
RunTestWithRealm((realmFactory, _) => RunTestWithRealm((realmFactory, _) =>
{ {
RealmLive<RealmBeatmap>? liveBeatmap = null; ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
using (var threadContext = realmFactory.CreateContext()) using (var threadContext = realmFactory.CreateContext())
@ -192,7 +209,7 @@ namespace osu.Game.Tests.Database
using (var updateThreadContext = realmFactory.CreateContext()) using (var updateThreadContext = realmFactory.CreateContext())
{ {
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange); updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
RealmLive<RealmBeatmap>? liveBeatmap = null; ILive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {

View File

@ -288,9 +288,9 @@ namespace osu.Game.Beatmaps
#region Implementation of IModelFileManager<in BeatmapSetInfo,in BeatmapSetFileInfo> #region Implementation of IModelFileManager<in BeatmapSetInfo,in BeatmapSetFileInfo>
public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents, string filename = null) public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents)
{ {
beatmapModelManager.ReplaceFile(model, file, contents, filename); beatmapModelManager.ReplaceFile(model, file, contents);
} }
public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file) public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file)

View File

@ -453,13 +453,12 @@ namespace osu.Game.Database
/// <param name="model">The item to operate on.</param> /// <param name="model">The item to operate on.</param>
/// <param name="file">The existing file to be replaced.</param> /// <param name="file">The existing file to be replaced.</param>
/// <param name="contents">The new file contents.</param> /// <param name="contents">The new file contents.</param>
/// <param name="filename">An optional filename for the new file. Will use the previous filename if not specified.</param> public void ReplaceFile(TModel model, TFileModel file, Stream contents)
public void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null)
{ {
using (ContextFactory.GetForWrite()) using (ContextFactory.GetForWrite())
{ {
DeleteFile(model, file); DeleteFile(model, file);
AddFile(model, contents, filename ?? file.Filename); AddFile(model, contents, file.Filename);
} }
} }

View File

@ -15,8 +15,7 @@ namespace osu.Game.Database
/// <param name="model">The item to operate on.</param> /// <param name="model">The item to operate on.</param>
/// <param name="file">The existing file to be replaced.</param> /// <param name="file">The existing file to be replaced.</param>
/// <param name="contents">The new file contents.</param> /// <param name="contents">The new file contents.</param>
/// <param name="filename">An optional filename for the new file. Will use the previous filename if not specified.</param> void ReplaceFile(TModel model, TFileModel file, Stream contents);
void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null);
/// <summary> /// <summary>
/// Delete an existing file. /// Delete an existing file.

View File

@ -49,13 +49,13 @@ namespace osu.Game.Database
return mapper.Map<T>(item); return mapper.Map<T>(item);
} }
public static List<RealmLive<T>> ToLive<T>(this IEnumerable<T> realmList) public static List<ILive<T>> ToLive<T>(this IEnumerable<T> realmList)
where T : RealmObject, IHasGuidPrimaryKey where T : RealmObject, IHasGuidPrimaryKey
{ {
return realmList.Select(l => new RealmLive<T>(l)).ToList(); return realmList.Select(l => new RealmLive<T>(l)).Cast<ILive<T>>().ToList();
} }
public static RealmLive<T> ToLive<T>(this T realmObject) public static ILive<T> ToLive<T>(this T realmObject)
where T : RealmObject, IHasGuidPrimaryKey where T : RealmObject, IHasGuidPrimaryKey
{ {
return new RealmLive<T>(realmObject); return new RealmLive<T>(realmObject);

View File

@ -16,6 +16,7 @@ namespace osu.Game.Models
{ {
public RealmFile File { get; set; } = null!; public RealmFile File { get; set; } = null!;
// [Indexed] cannot be used on `EmbeddedObject`s as it only applies to top-level queries. May need to reconsider this if performance becomes a concern.
public string Filename { get; set; } = null!; public string Filename { get; set; } = null!;
public RealmNamedFileUsage(RealmFile file, string filename) public RealmNamedFileUsage(RealmFile file, string filename)

View File

@ -78,8 +78,8 @@ namespace osu.Game.Screens.Edit.Setup
using (var stream = info.OpenRead()) using (var stream = info.OpenRead())
{ {
if (oldFile != null) if (oldFile != null)
beatmaps.ReplaceFile(set, oldFile, stream, info.Name); beatmaps.DeleteFile(set, oldFile);
else
beatmaps.AddFile(set, stream, info.Name); beatmaps.AddFile(set, stream, info.Name);
} }
@ -105,8 +105,7 @@ namespace osu.Game.Screens.Edit.Setup
using (var stream = info.OpenRead()) using (var stream = info.OpenRead())
{ {
if (oldFile != null) if (oldFile != null)
beatmaps.ReplaceFile(set, oldFile, stream, info.Name); beatmaps.DeleteFile(set, oldFile);
else
beatmaps.AddFile(set, stream, info.Name); beatmaps.AddFile(set, stream, info.Name);
} }

View File

@ -171,7 +171,7 @@ namespace osu.Game.Skinning
var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename);
if (oldFile != null) if (oldFile != null)
skinModelManager.ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename); skinModelManager.ReplaceFile(skin.SkinInfo, oldFile, streamContent);
else else
skinModelManager.AddFile(skin.SkinInfo, streamContent, filename); skinModelManager.AddFile(skin.SkinInfo, streamContent, filename);
} }

View File

@ -294,12 +294,8 @@ namespace osu.Game.Stores
/// <remarks> /// <remarks>
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>. /// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
/// </remarks> /// </remarks>
protected virtual string ComputeHash(TModel item, ArchiveReader? reader = null) protected virtual string ComputeHash(TModel item)
{ {
if (reader != null)
// fast hashing for cases where the item's files may not be populated.
return computeHashFast(reader);
// for now, concatenate all hashable files in the set to create a unique hash. // for now, concatenate all hashable files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream(); MemoryStream hashable = new MemoryStream();
@ -374,7 +370,7 @@ namespace osu.Game.Stores
// TODO: look into rollback of file additions (or delayed commit). // TODO: look into rollback of file additions (or delayed commit).
item.Files.AddRange(createFileInfos(archive, Files, realm)); item.Files.AddRange(createFileInfos(archive, Files, realm));
item.Hash = ComputeHash(item, archive); item.Hash = ComputeHash(item);
// TODO: we may want to run this outside of the transaction. // TODO: we may want to run this outside of the transaction.
await Populate(item, archive, realm, cancellationToken).ConfigureAwait(false); await Populate(item, archive, realm, cancellationToken).ConfigureAwait(false);
@ -387,7 +383,9 @@ namespace osu.Game.Stores
if (CanReuseExisting(existing, item)) if (CanReuseExisting(existing, item))
{ {
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import."); LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
existing.DeletePending = false; existing.DeletePending = false;
transaction.Commit();
return existing.ToLive(); return existing.ToLive();
} }