Files
AssetUsageAudit/Source/AssetUsageAuditTests/Private/MigrateDestination.spec.cpp
T
Admin 000cc5735c fix: correct authorship attribution to MagentaDolphin
The plugin was authored outside studio work but carried NextGenium
attribution in the .uplugin descriptor and in every source header.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 17:35:48 +07:00

159 lines
6.0 KiB
C++

// MagentaDolphin 2026. Asset Usage Audit.
#include "AssetUsagePaths.h"
#include "HAL/FileManager.h"
#include "Misc/App.h"
#include "Misc/AutomationTest.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#if WITH_DEV_AUTOMATION_TESTS
/**
* The Migrate destination check.
*
* Exists because the engine validates the destination only after MigratePackages has been called,
* and reports the refusal to the Output Log alone - so a bad folder presented as a button that did
* nothing at all. These specs pin the two rules to real folders on disk rather than to a reading of
* the engine source.
*/
BEGIN_DEFINE_SPEC(MigrateDestinationSpec,
"AssetUsageAudit.MigrateDestination",
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
FString ScratchDir;
void MakeScratchDir(const TCHAR* Suffix)
{
ScratchDir = FPaths::ConvertRelativePathToFull(
FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("AssetUsageAuditTests"), Suffix));
IFileManager::Get().DeleteDirectory(*ScratchDir, false, true);
IFileManager::Get().MakeDirectory(*ScratchDir, true);
}
void RemoveScratchDir()
{
if (!ScratchDir.IsEmpty())
{
IFileManager::Get().DeleteDirectory(*ScratchDir, false, true);
}
}
/** Build <Scratch>/<ProjectName>/Content plus a marker file beside it. */
FString MakeFakeProject(const TCHAR* MarkerFileName)
{
const FString ProjectRoot = FPaths::Combine(ScratchDir, TEXT("FakeProject"));
const FString ContentDir = FPaths::Combine(ProjectRoot, TEXT("Content"));
IFileManager::Get().MakeDirectory(*ContentDir, true);
if (MarkerFileName)
{
FFileHelper::SaveStringToFile(TEXT("{}"), *FPaths::Combine(ProjectRoot, MarkerFileName));
}
return ContentDir;
}
END_DEFINE_SPEC(MigrateDestinationSpec)
void MigrateDestinationSpec::Define()
{
Describe("Rejecting a destination Migrate would refuse", [this]()
{
AfterEach([this]() { RemoveScratchDir(); });
It("rejects an empty path", [this]()
{
TestFalse(TEXT("has a reason"), AssetUsagePaths::ValidateMigrateDestination(FString()).IsEmpty());
});
It("rejects a folder that is not called Content", [this]()
{
// The project's own Saved/AssetUsageAudit is the default for every other layout, so
// this is the exact path a user arrives with after switching to Migrate.
MakeScratchDir(TEXT("MigrateNotContent"));
const FString Reason = AssetUsagePaths::ValidateMigrateDestination(ScratchDir);
TestFalse(TEXT("rejected"), Reason.IsEmpty());
TestTrue(TEXT("says the folder must be Content"), Reason.Contains(TEXT("Content")));
});
It("rejects a Content folder with no project or plugin beside it", [this]()
{
// Passes the name check and still aborts: Migrate derives the destination mount point
// from what sits next to the Content folder.
MakeScratchDir(TEXT("MigrateOrphanContent"));
const FString ContentDir = MakeFakeProject(nullptr);
TestFalse(TEXT("rejected"), AssetUsagePaths::ValidateMigrateDestination(ContentDir).IsEmpty());
});
It("rejects a Content folder whose parent holds several plugins", [this]()
{
MakeScratchDir(TEXT("MigrateManyPlugins"));
const FString ContentDir = MakeFakeProject(TEXT("First.uplugin"));
FFileHelper::SaveStringToFile(TEXT("{}"),
*FPaths::Combine(FPaths::GetPath(ContentDir), TEXT("Second.uplugin")));
const FString Reason = AssetUsagePaths::ValidateMigrateDestination(ContentDir);
TestFalse(TEXT("rejected"), Reason.IsEmpty());
TestTrue(TEXT("explains the ambiguity"), Reason.Contains(TEXT("several")));
});
});
Describe("Accepting a destination Migrate would use", [this]()
{
AfterEach([this]() { RemoveScratchDir(); });
It("accepts a Content folder next to a .uproject", [this]()
{
MakeScratchDir(TEXT("MigrateGoodProject"));
const FString ContentDir = MakeFakeProject(TEXT("FakeProject.uproject"));
TestTrue(TEXT("accepted"), AssetUsagePaths::ValidateMigrateDestination(ContentDir).IsEmpty());
});
It("accepts a Content folder next to exactly one .uplugin", [this]()
{
// Plugin content is a legitimate migrate target; the engine derives the mount point
// from the plugin name.
MakeScratchDir(TEXT("MigrateGoodPlugin"));
const FString ContentDir = MakeFakeProject(TEXT("Only.uplugin"));
TestTrue(TEXT("accepted"), AssetUsagePaths::ValidateMigrateDestination(ContentDir).IsEmpty());
});
It("accepts a trailing slash and backslashes alike", [this]()
{
// A path pasted from Explorer arrives with backslashes; one typed by hand often ends
// in a slash. Neither should read as "not a Content folder".
MakeScratchDir(TEXT("MigrateSlashes"));
const FString ContentDir = MakeFakeProject(TEXT("FakeProject.uproject"));
TestTrue(TEXT("trailing slash"), AssetUsagePaths::ValidateMigrateDestination(ContentDir + TEXT("/")).IsEmpty());
TestTrue(TEXT("backslashes"), AssetUsagePaths::ValidateMigrateDestination(ContentDir.Replace(TEXT("/"), TEXT("\\"))).IsEmpty());
});
It("accepts this project's own Content folder", [this]()
{
// The strongest available check that the rule matches reality: the running project is
// a real, valid migrate target by construction.
const FString ProjectContent = FPaths::ConvertRelativePathToFull(FPaths::ProjectContentDir());
TestTrue(TEXT("accepted"), AssetUsagePaths::ValidateMigrateDestination(ProjectContent).IsEmpty());
});
});
}
#endif // WITH_DEV_AUTOMATION_TESTS