2021-09-30 15:05:14 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using NUnit.Framework ;
using osu.Framework.Extensions ;
using osu.Framework.Extensions.ObjectExtensions ;
using osu.Framework.Logging ;
2021-11-29 05:48:28 +00:00
using osu.Framework.Platform ;
2021-09-30 15:05:14 +00:00
using osu.Game.Beatmaps ;
using osu.Game.Database ;
2021-11-19 07:07:55 +00:00
using osu.Game.Extensions ;
2021-09-30 15:05:14 +00:00
using osu.Game.IO.Archives ;
using osu.Game.Models ;
2021-11-12 06:48:38 +00:00
using osu.Game.Overlays.Notifications ;
2021-11-19 10:07:21 +00:00
using osu.Game.Rulesets ;
2021-09-30 15:05:14 +00:00
using osu.Game.Stores ;
using osu.Game.Tests.Resources ;
using Realms ;
using SharpCompress.Archives ;
using SharpCompress.Archives.Zip ;
using SharpCompress.Common ;
using SharpCompress.Writers.Zip ;
#nullable enable
namespace osu.Game.Tests.Database
{
[TestFixture]
public class BeatmapImporterTests : RealmTest
{
[Test]
public void TestImportBeatmapThenCleanup ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using ( var importer = new BeatmapModelManager ( realmFactory , storage ) )
2021-11-23 04:00:33 +00:00
using ( new RulesetStore ( realmFactory , storage ) )
2021-09-30 15:05:14 +00:00
{
2021-11-19 10:07:21 +00:00
ILive < BeatmapSetInfo > ? imported ;
2021-09-30 15:05:14 +00:00
using ( var reader = new ZipArchiveReader ( TestResources . GetTestBeatmapStream ( ) ) )
imported = await importer . Import ( reader ) ;
2021-11-19 10:07:21 +00:00
Assert . AreEqual ( 1 , realmFactory . Context . All < BeatmapSetInfo > ( ) . Count ( ) ) ;
2021-09-30 15:05:14 +00:00
Assert . NotNull ( imported ) ;
Debug . Assert ( imported ! = null ) ;
imported . PerformWrite ( s = > s . DeletePending = true ) ;
2021-11-19 10:07:21 +00:00
Assert . AreEqual ( 1 , realmFactory . Context . All < BeatmapSetInfo > ( ) . Count ( s = > s . DeletePending ) ) ;
2021-09-30 15:05:14 +00:00
}
} ) ;
Logger . Log ( "Running with no work to purge pending deletions" ) ;
2021-11-19 10:07:21 +00:00
RunTestWithRealm ( ( realmFactory , _ ) = > { Assert . AreEqual ( 0 , realmFactory . Context . All < BeatmapSetInfo > ( ) . Count ( ) ) ; } ) ;
2021-09-30 15:05:14 +00:00
}
[Test]
public void TestImportWhenClosed ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
await LoadOszIntoStore ( importer , realmFactory . Context ) ;
} ) ;
}
2021-11-22 06:30:11 +00:00
[Test]
public void TestAccessFileAfterImport ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-11-22 06:30:11 +00:00
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
var beatmap = imported . Beatmaps . First ( ) ;
var file = beatmap . File ;
Assert . NotNull ( file ) ;
Assert . AreEqual ( beatmap . Hash , file ! . File . Hash ) ;
} ) ;
}
2021-09-30 15:05:14 +00:00
[Test]
public void TestImportThenDelete ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
deleteBeatmapSet ( imported , realmFactory . Context ) ;
} ) ;
}
[Test]
public void TestImportThenDeleteFromStream ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? tempPath = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
2021-11-19 10:07:21 +00:00
ILive < BeatmapSetInfo > ? importedSet ;
2021-09-30 15:05:14 +00:00
using ( var stream = File . OpenRead ( tempPath ) )
{
importedSet = await importer . Import ( new ImportTask ( stream , Path . GetFileName ( tempPath ) ) ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
}
Assert . NotNull ( importedSet ) ;
Debug . Assert ( importedSet ! = null ) ;
Assert . IsTrue ( File . Exists ( tempPath ) , "Stream source file somehow went missing" ) ;
File . Delete ( tempPath ) ;
2021-11-19 10:07:21 +00:00
var imported = realmFactory . Context . All < BeatmapSetInfo > ( ) . First ( beatmapSet = > beatmapSet . ID = = importedSet . ID ) ;
2021-09-30 15:05:14 +00:00
deleteBeatmapSet ( imported , realmFactory . Context ) ;
} ) ;
}
[Test]
public void TestImportThenImport ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
var imported = 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.
Assert . IsTrue ( imported . ID = = importedSecondTime . ID ) ;
Assert . IsTrue ( imported . Beatmaps . First ( ) . ID = = importedSecondTime . Beatmaps . First ( ) . ID ) ;
checkBeatmapSetCount ( realmFactory . Context , 1 ) ;
checkSingleReferencedFileCount ( realmFactory . Context , 18 ) ;
} ) ;
}
[Test]
public void TestImportThenImportWithReZip ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
string extractedFolder = $"{temp}_extracted" ;
Directory . CreateDirectory ( extractedFolder ) ;
try
{
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
string hashBefore = hashFile ( temp ) ;
using ( var zip = ZipArchive . Open ( temp ) )
zip . WriteToDirectory ( extractedFolder ) ;
using ( var zip = ZipArchive . Create ( ) )
{
zip . AddAllFromDirectory ( extractedFolder ) ;
zip . SaveTo ( temp , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
}
// zip files differ because different compression or encoder.
Assert . AreNotEqual ( hashBefore , hashFile ( temp ) ) ;
var importedSecondTime = await importer . Import ( new ImportTask ( temp ) ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
Assert . NotNull ( importedSecondTime ) ;
Debug . Assert ( importedSecondTime ! = null ) ;
// but contents doesn't, so existing should still be used.
Assert . IsTrue ( imported . ID = = importedSecondTime . ID ) ;
Assert . IsTrue ( imported . Beatmaps . First ( ) . ID = = importedSecondTime . PerformRead ( s = > s . Beatmaps . First ( ) . ID ) ) ;
}
finally
{
Directory . Delete ( extractedFolder , true ) ;
}
} ) ;
}
[Test]
public void TestImportThenImportWithChangedHashedFile ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
string extractedFolder = $"{temp}_extracted" ;
Directory . CreateDirectory ( extractedFolder ) ;
try
{
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
await createScoreForBeatmap ( realmFactory . Context , imported . Beatmaps . First ( ) ) ;
using ( var zip = ZipArchive . Open ( temp ) )
zip . WriteToDirectory ( extractedFolder ) ;
// arbitrary write to hashed file
// this triggers the special BeatmapManager.PreImport deletion/replacement flow.
using ( var sw = new FileInfo ( Directory . GetFiles ( extractedFolder , "*.osu" ) . First ( ) ) . AppendText ( ) )
await sw . WriteLineAsync ( "// changed" ) ;
using ( var zip = ZipArchive . Create ( ) )
{
zip . AddAllFromDirectory ( extractedFolder ) ;
zip . SaveTo ( temp , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
}
var importedSecondTime = await importer . Import ( new ImportTask ( temp ) ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
// check the newly "imported" beatmap is not the original.
Assert . NotNull ( importedSecondTime ) ;
Debug . Assert ( importedSecondTime ! = null ) ;
Assert . IsTrue ( imported . ID ! = importedSecondTime . ID ) ;
Assert . IsTrue ( imported . Beatmaps . First ( ) . ID ! = importedSecondTime . PerformRead ( s = > s . Beatmaps . First ( ) . ID ) ) ;
}
finally
{
Directory . Delete ( extractedFolder , true ) ;
}
} ) ;
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public void TestImportThenImportWithChangedFile ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
string extractedFolder = $"{temp}_extracted" ;
Directory . CreateDirectory ( extractedFolder ) ;
try
{
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
using ( var zip = ZipArchive . Open ( temp ) )
zip . WriteToDirectory ( extractedFolder ) ;
// arbitrary write to non-hashed file
using ( var sw = new FileInfo ( Directory . GetFiles ( extractedFolder , "*.mp3" ) . First ( ) ) . AppendText ( ) )
await sw . WriteLineAsync ( "text" ) ;
using ( var zip = ZipArchive . Create ( ) )
{
zip . AddAllFromDirectory ( extractedFolder ) ;
zip . SaveTo ( temp , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
}
var importedSecondTime = await importer . Import ( new ImportTask ( temp ) ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
Assert . NotNull ( importedSecondTime ) ;
Debug . Assert ( importedSecondTime ! = null ) ;
// check the newly "imported" beatmap is not the original.
Assert . IsTrue ( imported . ID ! = importedSecondTime . ID ) ;
Assert . IsTrue ( imported . Beatmaps . First ( ) . ID ! = importedSecondTime . PerformRead ( s = > s . Beatmaps . First ( ) . ID ) ) ;
}
finally
{
Directory . Delete ( extractedFolder , true ) ;
}
} ) ;
}
[Test]
public void TestImportThenImportWithDifferentFilename ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
string extractedFolder = $"{temp}_extracted" ;
Directory . CreateDirectory ( extractedFolder ) ;
try
{
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
using ( var zip = ZipArchive . Open ( temp ) )
zip . WriteToDirectory ( extractedFolder ) ;
// change filename
var firstFile = new FileInfo ( Directory . GetFiles ( extractedFolder ) . First ( ) ) ;
firstFile . MoveTo ( Path . Combine ( firstFile . DirectoryName . AsNonNull ( ) , $"{firstFile.Name}-changed{firstFile.Extension}" ) ) ;
using ( var zip = ZipArchive . Create ( ) )
{
zip . AddAllFromDirectory ( extractedFolder ) ;
zip . SaveTo ( temp , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
}
var importedSecondTime = await importer . Import ( new ImportTask ( temp ) ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
Assert . NotNull ( importedSecondTime ) ;
Debug . Assert ( importedSecondTime ! = null ) ;
// check the newly "imported" beatmap is not the original.
Assert . IsTrue ( imported . ID ! = importedSecondTime . ID ) ;
Assert . IsTrue ( imported . Beatmaps . First ( ) . ID ! = importedSecondTime . PerformRead ( s = > s . Beatmaps . First ( ) . ID ) ) ;
}
finally
{
Directory . Delete ( extractedFolder , true ) ;
}
} ) ;
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public void TestImportCorruptThenImport ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
var firstFile = imported . Files . First ( ) ;
long originalLength ;
2021-11-19 07:07:55 +00:00
using ( var stream = storage . GetStream ( firstFile . File . GetStoragePath ( ) ) )
2021-09-30 15:05:14 +00:00
originalLength = stream . Length ;
2021-11-19 07:07:55 +00:00
using ( var stream = storage . GetStream ( firstFile . File . GetStoragePath ( ) , FileAccess . Write , FileMode . Create ) )
2021-09-30 15:05:14 +00:00
stream . WriteByte ( 0 ) ;
var importedSecondTime = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
2021-11-19 07:07:55 +00:00
using ( var stream = storage . GetStream ( firstFile . File . GetStoragePath ( ) ) )
2021-09-30 15:05:14 +00:00
Assert . AreEqual ( stream . Length , originalLength , "Corruption was not fixed on second import" ) ;
// 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 ) ;
checkBeatmapSetCount ( realmFactory . Context , 1 ) ;
checkSingleReferencedFileCount ( realmFactory . Context , 18 ) ;
} ) ;
}
2021-11-12 06:48:38 +00:00
[Test]
public void TestModelCreationFailureDoesntReturn ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-11-12 06:48:38 +00:00
var progressNotification = new ImportProgressNotification ( ) ;
var zipStream = new MemoryStream ( ) ;
using ( var zip = ZipArchive . Create ( ) )
zip . SaveTo ( zipStream , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
var imported = await importer . Import (
progressNotification ,
new ImportTask ( zipStream , string . Empty )
) ;
checkBeatmapSetCount ( realmFactory . Context , 0 ) ;
checkBeatmapCount ( realmFactory . Context , 0 ) ;
Assert . IsEmpty ( imported ) ;
Assert . AreEqual ( ProgressNotificationState . Cancelled , progressNotification . State ) ;
} ) ;
}
2021-09-30 15:05:14 +00:00
[Test]
public void TestRollbackOnFailure ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
int loggedExceptionCount = 0 ;
Logger . NewEntry + = l = >
{
if ( l . Target = = LoggingTarget . Database & & l . Exception ! = null )
Interlocked . Increment ( ref loggedExceptionCount ) ;
} ;
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
realmFactory . Context . Write ( ( ) = > imported . Hash + = "-changed" ) ;
checkBeatmapSetCount ( realmFactory . Context , 1 ) ;
checkBeatmapCount ( realmFactory . Context , 12 ) ;
checkSingleReferencedFileCount ( realmFactory . Context , 18 ) ;
2021-10-27 04:04:41 +00:00
string? brokenTempFilename = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
MemoryStream brokenOsu = new MemoryStream ( ) ;
MemoryStream brokenOsz = new MemoryStream ( await File . ReadAllBytesAsync ( brokenTempFilename ) ) ;
File . Delete ( brokenTempFilename ) ;
using ( var outStream = File . Open ( brokenTempFilename , FileMode . CreateNew ) )
using ( var zip = ZipArchive . Open ( brokenOsz ) )
{
zip . AddEntry ( "broken.osu" , brokenOsu , false ) ;
zip . SaveTo ( outStream , CompressionType . Deflate ) ;
}
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
try
{
await importer . Import ( new ImportTask ( brokenTempFilename ) ) ;
}
catch
{
}
checkBeatmapSetCount ( realmFactory . Context , 1 ) ;
checkBeatmapCount ( realmFactory . Context , 12 ) ;
checkSingleReferencedFileCount ( realmFactory . Context , 18 ) ;
Assert . AreEqual ( 1 , loggedExceptionCount ) ;
File . Delete ( brokenTempFilename ) ;
} ) ;
}
[Test]
2021-11-29 05:48:28 +00:00
public void TestImportThenDeleteThenImportOptimisedPath ( )
2021-09-30 15:05:14 +00:00
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
deleteBeatmapSet ( imported , realmFactory . Context ) ;
2021-11-29 05:48:28 +00:00
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 ) ;
} ) ;
}
[Test]
public void TestImportThenDeleteThenImportNonOptimisedPath ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
using var importer = new NonOptimisedBeatmapImporter ( realmFactory , storage ) ;
2021-11-24 03:16:08 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-11-29 05:48:28 +00:00
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
deleteBeatmapSet ( imported , realmFactory . Context ) ;
Assert . IsTrue ( imported . DeletePending ) ;
2021-09-30 15:05:14 +00:00
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 ) ;
2021-11-29 05:48:28 +00:00
Assert . IsFalse ( imported . DeletePending ) ;
Assert . IsFalse ( importedSecondTime . DeletePending ) ;
2021-09-30 15:05:14 +00:00
} ) ;
}
[Test]
public void TestImportThenDeleteThenImportWithOnlineIDsMissing ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
var imported = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
realmFactory . Context . Write ( ( ) = >
{
foreach ( var b in imported . Beatmaps )
2021-10-18 07:10:37 +00:00
b . OnlineID = - 1 ;
2021-09-30 15:05:14 +00:00
} ) ;
deleteBeatmapSet ( imported , realmFactory . Context ) ;
var importedSecondTime = await LoadOszIntoStore ( importer , realmFactory . Context ) ;
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
Assert . IsTrue ( imported . ID ! = importedSecondTime . ID ) ;
Assert . IsTrue ( imported . Beatmaps . First ( ) . ID ! = importedSecondTime . Beatmaps . First ( ) . ID ) ;
} ) ;
}
[Test]
public void TestImportWithDuplicateBeatmapIDs ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-11-19 10:07:21 +00:00
var metadata = new BeatmapMetadata
2021-09-30 15:05:14 +00:00
{
Artist = "SomeArtist" ,
2021-11-04 09:22:21 +00:00
Author =
{
Username = "SomeAuthor"
}
2021-09-30 15:05:14 +00:00
} ;
2021-11-19 10:07:21 +00:00
var ruleset = realmFactory . Context . All < RulesetInfo > ( ) . First ( ) ;
2021-09-30 15:05:14 +00:00
2021-11-19 10:07:21 +00:00
var toImport = new BeatmapSetInfo
2021-09-30 15:05:14 +00:00
{
OnlineID = 1 ,
Beatmaps =
{
2021-11-19 10:07:21 +00:00
new BeatmapInfo ( ruleset , new BeatmapDifficulty ( ) , metadata )
2021-09-30 15:05:14 +00:00
{
OnlineID = 2 ,
} ,
2021-11-19 10:07:21 +00:00
new BeatmapInfo ( ruleset , new BeatmapDifficulty ( ) , metadata )
2021-09-30 15:05:14 +00:00
{
OnlineID = 2 ,
2021-11-24 09:42:47 +00:00
Status = BeatmapOnlineStatus . Loved ,
2021-09-30 15:05:14 +00:00
}
}
} ;
var imported = await importer . Import ( toImport ) ;
Assert . NotNull ( imported ) ;
Debug . Assert ( imported ! = null ) ;
2021-10-18 07:10:37 +00:00
Assert . AreEqual ( - 1 , imported . PerformRead ( s = > s . Beatmaps [ 0 ] . OnlineID ) ) ;
Assert . AreEqual ( - 1 , imported . PerformRead ( s = > s . Beatmaps [ 1 ] . OnlineID ) ) ;
2021-09-30 15:05:14 +00:00
} ) ;
}
[Test]
public void TestImportWhenFileOpen ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
using ( File . OpenRead ( temp ) )
await importer . Import ( temp ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
File . Delete ( temp ) ;
Assert . IsFalse ( File . Exists ( temp ) , "We likely held a read lock on the file when we shouldn't" ) ;
} ) ;
}
[Test]
public void TestImportWithDuplicateHashes ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
string extractedFolder = $"{temp}_extracted" ;
Directory . CreateDirectory ( extractedFolder ) ;
try
{
using ( var zip = ZipArchive . Open ( temp ) )
zip . WriteToDirectory ( extractedFolder ) ;
using ( var zip = ZipArchive . Create ( ) )
{
zip . AddAllFromDirectory ( extractedFolder ) ;
zip . AddEntry ( "duplicate.osu" , Directory . GetFiles ( extractedFolder , "*.osu" ) . First ( ) ) ;
zip . SaveTo ( temp , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
}
await importer . Import ( temp ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
}
finally
{
Directory . Delete ( extractedFolder , true ) ;
}
} ) ;
}
[Test]
public void TestImportNestedStructure ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
string extractedFolder = $"{temp}_extracted" ;
string subfolder = Path . Combine ( extractedFolder , "subfolder" ) ;
Directory . CreateDirectory ( subfolder ) ;
try
{
using ( var zip = ZipArchive . Open ( temp ) )
zip . WriteToDirectory ( subfolder ) ;
using ( var zip = ZipArchive . Create ( ) )
{
zip . AddAllFromDirectory ( extractedFolder ) ;
zip . SaveTo ( temp , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
}
var imported = await importer . Import ( new ImportTask ( temp ) ) ;
Assert . NotNull ( imported ) ;
Debug . Assert ( imported ! = null ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
Assert . IsFalse ( imported . PerformRead ( s = > s . Files . Any ( f = > f . Filename . Contains ( "subfolder" ) ) ) , "Files contain common subfolder" ) ;
}
finally
{
Directory . Delete ( extractedFolder , true ) ;
}
} ) ;
}
[Test]
public void TestImportWithIgnoredDirectoryInArchive ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
string extractedFolder = $"{temp}_extracted" ;
string dataFolder = Path . Combine ( extractedFolder , "actual_data" ) ;
string resourceForkFolder = Path . Combine ( extractedFolder , "__MACOSX" ) ;
string resourceForkFilePath = Path . Combine ( resourceForkFolder , ".extracted" ) ;
Directory . CreateDirectory ( dataFolder ) ;
Directory . CreateDirectory ( resourceForkFolder ) ;
using ( var resourceForkFile = File . CreateText ( resourceForkFilePath ) )
{
await resourceForkFile . WriteLineAsync ( "adding content so that it's not empty" ) ;
}
try
{
using ( var zip = ZipArchive . Open ( temp ) )
zip . WriteToDirectory ( dataFolder ) ;
using ( var zip = ZipArchive . Create ( ) )
{
zip . AddAllFromDirectory ( extractedFolder ) ;
zip . SaveTo ( temp , new ZipWriterOptions ( CompressionType . Deflate ) ) ;
}
var imported = await importer . Import ( new ImportTask ( temp ) ) ;
Assert . NotNull ( imported ) ;
Debug . Assert ( imported ! = null ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realmFactory . Context ) ;
2021-09-30 15:05:14 +00:00
Assert . IsFalse ( imported . PerformRead ( s = > s . Files . Any ( f = > f . Filename . Contains ( "__MACOSX" ) ) ) , "Files contain resource fork folder, which should be ignored" ) ;
Assert . IsFalse ( imported . PerformRead ( s = > s . Files . Any ( f = > f . Filename . Contains ( "actual_data" ) ) ) , "Files contain common subfolder" ) ;
}
finally
{
Directory . Delete ( extractedFolder , true ) ;
}
} ) ;
}
[Test]
public void TestUpdateBeatmapInfo ( )
{
RunTestWithRealmAsync ( async ( realmFactory , storage ) = >
{
2021-12-14 10:47:11 +00:00
using var importer = new BeatmapModelManager ( realmFactory , storage ) ;
2021-11-23 04:00:33 +00:00
using var store = new RulesetStore ( realmFactory , storage ) ;
2021-09-30 15:05:14 +00:00
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
await importer . Import ( temp ) ;
// Update via the beatmap, not the beatmap info, to ensure correct linking
2021-11-19 10:07:21 +00:00
BeatmapSetInfo setToUpdate = realmFactory . Context . All < BeatmapSetInfo > ( ) . First ( ) ;
2021-09-30 15:05:14 +00:00
var beatmapToUpdate = setToUpdate . Beatmaps . First ( ) ;
realmFactory . Context . Write ( ( ) = > beatmapToUpdate . DifficultyName = "updated" ) ;
2021-11-19 10:07:21 +00:00
BeatmapInfo updatedInfo = realmFactory . Context . All < BeatmapInfo > ( ) . First ( b = > b . ID = = beatmapToUpdate . ID ) ;
2021-09-30 15:05:14 +00:00
Assert . That ( updatedInfo . DifficultyName , Is . EqualTo ( "updated" ) ) ;
} ) ;
}
2021-11-19 10:07:21 +00:00
public static async Task < BeatmapSetInfo ? > LoadQuickOszIntoOsu ( BeatmapImporter importer , Realm realm )
2021-09-30 15:05:14 +00:00
{
2021-10-27 04:04:41 +00:00
string? temp = TestResources . GetQuickTestBeatmapForImport ( ) ;
2021-09-30 15:05:14 +00:00
var importedSet = await importer . Import ( new ImportTask ( temp ) ) ;
Assert . NotNull ( importedSet ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realm ) ;
2021-09-30 15:05:14 +00:00
waitForOrAssert ( ( ) = > ! File . Exists ( temp ) , "Temporary file still exists after standard import" , 5000 ) ;
2021-11-19 10:07:21 +00:00
return realm . All < BeatmapSetInfo > ( ) . FirstOrDefault ( beatmapSet = > beatmapSet . ID = = importedSet ! . ID ) ;
2021-09-30 15:05:14 +00:00
}
2021-11-19 10:07:21 +00:00
public static async Task < BeatmapSetInfo > LoadOszIntoStore ( BeatmapImporter importer , Realm realm , string? path = null , bool virtualTrack = false )
2021-09-30 15:05:14 +00:00
{
2021-10-27 04:04:41 +00:00
string? temp = path ? ? TestResources . GetTestBeatmapForImport ( virtualTrack ) ;
2021-09-30 15:05:14 +00:00
var importedSet = await importer . Import ( new ImportTask ( temp ) ) ;
Assert . NotNull ( importedSet ) ;
Debug . Assert ( importedSet ! = null ) ;
2021-12-17 09:26:12 +00:00
EnsureLoaded ( realm ) ;
2021-09-30 15:05:14 +00:00
waitForOrAssert ( ( ) = > ! File . Exists ( temp ) , "Temporary file still exists after standard import" , 5000 ) ;
2021-11-19 10:07:21 +00:00
return realm . All < BeatmapSetInfo > ( ) . First ( beatmapSet = > beatmapSet . ID = = importedSet . ID ) ;
2021-09-30 15:05:14 +00:00
}
2021-11-19 10:07:21 +00:00
private void deleteBeatmapSet ( BeatmapSetInfo imported , Realm realm )
2021-09-30 15:05:14 +00:00
{
realm . Write ( ( ) = > imported . DeletePending = true ) ;
checkBeatmapSetCount ( realm , 0 ) ;
checkBeatmapSetCount ( realm , 1 , true ) ;
2021-11-19 10:07:21 +00:00
Assert . IsTrue ( realm . All < BeatmapSetInfo > ( ) . First ( _ = > true ) . DeletePending ) ;
2021-09-30 15:05:14 +00:00
}
2021-11-19 10:07:21 +00:00
private static Task createScoreForBeatmap ( Realm realm , BeatmapInfo beatmap )
2021-09-30 15:05:14 +00:00
{
// TODO: reimplement when we have score support in realm.
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
// {
2021-12-10 06:37:12 +00:00
// OnlineID = 2,
2021-09-30 15:05:14 +00:00
// Beatmap = beatmap,
// BeatmapInfoID = beatmap.ID
// }, new ImportScoreTest.TestArchiveReader());
return Task . CompletedTask ;
}
private static void checkBeatmapSetCount ( Realm realm , int expected , bool includeDeletePending = false )
{
Assert . AreEqual ( expected , includeDeletePending
2021-11-19 10:07:21 +00:00
? realm . All < BeatmapSetInfo > ( ) . Count ( )
: realm . All < BeatmapSetInfo > ( ) . Count ( s = > ! s . DeletePending ) ) ;
2021-09-30 15:05:14 +00:00
}
private static string hashFile ( string filename )
{
using ( var s = File . OpenRead ( filename ) )
return s . ComputeMD5Hash ( ) ;
}
private static void checkBeatmapCount ( Realm realm , int expected )
{
2021-11-19 10:07:21 +00:00
Assert . AreEqual ( expected , realm . All < BeatmapInfo > ( ) . Where ( _ = > true ) . ToList ( ) . Count ) ;
2021-09-30 15:05:14 +00:00
}
private static void checkSingleReferencedFileCount ( Realm realm , int expected )
{
int singleReferencedCount = 0 ;
foreach ( var f in realm . All < RealmFile > ( ) )
{
if ( f . BacklinksCount = = 1 )
singleReferencedCount + + ;
}
Assert . AreEqual ( expected , singleReferencedCount ) ;
}
2021-12-17 09:26:12 +00:00
internal static void EnsureLoaded ( Realm realm , int timeout = 60000 )
2021-09-30 15:05:14 +00:00
{
2021-11-19 10:07:21 +00:00
IQueryable < BeatmapSetInfo > ? resultSets = null ;
2021-09-30 15:05:14 +00:00
2021-11-30 05:12:49 +00:00
waitForOrAssert ( ( ) = >
2021-11-19 10:07:21 +00:00
{
realm . Refresh ( ) ;
return ( resultSets = realm . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & s . OnlineID = = 241526 ) ) . Any ( ) ;
} , @"BeatmapSet did not import to the database in allocated time." , timeout ) ;
2021-09-30 15:05:14 +00:00
// ensure we were stored to beatmap database backing...
Assert . IsTrue ( resultSets ? . Count ( ) = = 1 , $@"Incorrect result count found ({resultSets?.Count()} but should be 1)." ) ;
2021-11-19 10:07:21 +00:00
IEnumerable < BeatmapSetInfo > queryBeatmapSets ( ) = > realm . All < BeatmapSetInfo > ( ) . Where ( s = > ! s . DeletePending & & s . OnlineID = = 241526 ) ;
2021-09-30 15:05:14 +00:00
var set = queryBeatmapSets ( ) . First ( ) ;
// ReSharper disable once PossibleUnintendedReferenceComparison
2021-11-19 10:07:21 +00:00
IEnumerable < BeatmapInfo > queryBeatmaps ( ) = > realm . All < BeatmapInfo > ( ) . Where ( s = > s . BeatmapSet ! = null & & s . BeatmapSet = = set ) ;
2021-09-30 15:05:14 +00:00
2021-11-30 05:12:49 +00:00
Assert . AreEqual ( 12 , queryBeatmaps ( ) . Count ( ) , @"Beatmap count was not correct" ) ;
Assert . AreEqual ( 1 , queryBeatmapSets ( ) . Count ( ) , @"Beatmapset count was not correct" ) ;
2021-09-30 15:05:14 +00:00
2021-11-30 05:12:49 +00:00
int countBeatmapSetBeatmaps ;
int countBeatmaps ;
2021-09-30 15:05:14 +00:00
2021-11-30 05:12:49 +00:00
Assert . AreEqual (
countBeatmapSetBeatmaps = queryBeatmapSets ( ) . First ( ) . Beatmaps . Count ,
countBeatmaps = queryBeatmaps ( ) . Count ( ) ,
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps})." ) ;
2021-09-30 15:05:14 +00:00
2021-11-19 10:07:21 +00:00
foreach ( BeatmapInfo b in set . Beatmaps )
2021-09-30 15:05:14 +00:00
Assert . IsTrue ( set . Beatmaps . Any ( c = > c . OnlineID = = b . OnlineID ) ) ;
Assert . IsTrue ( set . Beatmaps . Count > 0 ) ;
}
private static void waitForOrAssert ( Func < bool > result , string failureMessage , int timeout = 60000 )
{
const int sleep = 200 ;
while ( timeout > 0 )
{
Thread . Sleep ( sleep ) ;
timeout - = sleep ;
if ( result ( ) )
return ;
}
Assert . Fail ( failureMessage ) ;
}
2021-11-29 05:48:28 +00:00
public class NonOptimisedBeatmapImporter : BeatmapImporter
{
public NonOptimisedBeatmapImporter ( RealmContextFactory realmFactory , Storage storage )
: base ( realmFactory , storage )
{
}
protected override bool HasCustomHashFunction = > true ;
}
2021-09-30 15:05:14 +00:00
}
}