Implement most Android tests

mac
Miepee 3 years ago
parent 1cb85c49cf
commit 1d1e9dc493

@ -23,6 +23,7 @@ public static class Core
{ {
Windows, Windows,
Linux, Linux,
Mac Mac,
Android
} }
} }

@ -65,7 +65,7 @@ public static class HelperMethods
DirectoryCopy(subDir.FullName, tempPath); DirectoryCopy(subDir.FullName, tempPath);
} }
} }
/// <summary> /// <summary>
/// Loads an <see cref="Image"/> via filepath, resizes it via Nearest Neighbor to a specified dimension, and then saves it to a specified path. /// Loads an <see cref="Image"/> via filepath, resizes it via Nearest Neighbor to a specified dimension, and then saves it to a specified path.
/// </summary> /// </summary>
@ -80,12 +80,12 @@ public static class HelperMethods
/// </example> /// </example>
public static void SaveAndroidIcon(string iconPath, int dimensions, string filePath) public static void SaveAndroidIcon(string iconPath, int dimensions, string filePath)
{ {
Image picture = Image.Load(iconPath);
// Most am2r cover is pixelart, hence why NN is used. Hermite would probably be a decent alternative too though. // Most am2r cover is pixelart, hence why NN is used. Hermite would probably be a decent alternative too though.
using Image picture = Image.Load(iconPath);
picture.Mutate(x => x.Resize(dimensions, dimensions, KnownResamplers.NearestNeighbor)); picture.Mutate(x => x.Resize(dimensions, dimensions, KnownResamplers.NearestNeighbor));
picture.SaveAsPng(filePath); picture.SaveAsPng(filePath);
} }
/// <summary> /// <summary>
/// Calculates the SHA256 hash of a specified file. /// Calculates the SHA256 hash of a specified file.
/// </summary> /// </summary>

@ -354,12 +354,12 @@ public abstract class RawMods : ModsBase
UndertaleData gmData = UndertaleIO.Read(fs, outputDelegate.SendOutput, outputDelegate.SendOutput); UndertaleData gmData = UndertaleIO.Read(fs, outputDelegate.SendOutput, outputDelegate.SendOutput);
modName = gmData.GeneralInfo.DisplayName.Content; modName = gmData.GeneralInfo.DisplayName.Content;
} }
modName = modName.Replace(" ", "").Replace(":", ""); modName = modName.Replace(" ", "").Replace(":", "").Replace(".", "");
// rules for name: A-Z, a-z, digits, underscore and needs to start with letters // rules for name: A-Z, a-z, digits, underscore and needs to start with letters
Regex nameReg = new Regex(@"^[a-zA-Z][a-zA-Z0-9_]*$"); Regex nameReg = new Regex(@"^[a-zA-Z][a-zA-Z0-9_]*$");
if (!nameReg.Match(modName).Success) if (!nameReg.Match(modName).Success)
throw new InvalidDataException("The display name " + modName + " is invalid! The name has to start with letters (a-z), and can only contain letters, digits, space, colon and underscore!"); throw new InvalidDataException("The display name \"" + modName + "\" is invalid! The name has to start with letters (a-z/A-Z), and can only contain letters, digits, space, colon and underscore!");
outputDelegate.SendOutput("Replace Android save directory..."); outputDelegate.SendOutput("Replace Android save directory...");

@ -1,4 +1,6 @@
using System.Diagnostics;
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.InteropServices;
using AM2RPortHelperLib; using AM2RPortHelperLib;
using UndertaleModLib; using UndertaleModLib;
using Xunit; using Xunit;
@ -204,6 +206,7 @@ public class RawModsTests : IDisposable
Assert.True(origFiles.SequenceEqual(newFiles)); Assert.True(origFiles.SequenceEqual(newFiles));
break; break;
} }
default: throw new Exception("unwritten test case for new os?");
} }
// If we didn't specify any, there should be no subdirectories at the end // If we didn't specify any, there should be no subdirectories at the end
@ -302,6 +305,7 @@ public class RawModsTests : IDisposable
Assert.True(origFiles.SequenceEqual(newFiles)); Assert.True(origFiles.SequenceEqual(newFiles));
break; break;
} }
default: throw new Exception("unwritten test case for new os?");
} }
// There should be exactly one subdir here // There should be exactly one subdir here
@ -317,37 +321,6 @@ public class RawModsTests : IDisposable
//Otherwise there should be our stuff //Otherwise there should be our stuff
Assert.True(File.Exists(newExtract + "/assets/" + deepSuffix.ToLower() + origInput.ToLower())); Assert.True(File.Exists(newExtract + "/assets/" + deepSuffix.ToLower() + origInput.ToLower()));
} }
[Theory]
[InlineData("./GameWin.zip")]
[InlineData("./GameLin.zip")]
[InlineData("./GameMac.zip")]
public void CheckThatLinuxPortHasProperIcons(string inputZip)
{
var outputZip = testTempDir + Guid.NewGuid();
var newExtract = testTempDir + Guid.NewGuid() + "/";
// With default icons
void CheckIconsWithPath(string? path)
{
File.Delete(outputZip);
RawMods.PortToLinux(inputZip, outputZip, path, path);
if (Directory.Exists(newExtract))
Directory.Delete(newExtract, true);
ZipFile.ExtractToDirectory(outputZip, newExtract);
var newIcon = File.ReadAllBytes(newExtract + "/assets/icon.png");
var newSplash = File.ReadAllBytes(newExtract + "/assets/splash.png");
Directory.CreateDirectory(libTempDir);
var oldIcon = File.ReadAllBytes(RawMods.GetProperPathToBuiltinIcons(nameof(Resources.icon), path));
var oldSplash = File.ReadAllBytes(RawMods.GetProperPathToBuiltinIcons(nameof(Resources.splash), path));
Assert.True(newIcon.SequenceEqual(oldIcon));
Assert.True(newSplash.SequenceEqual(oldSplash));
}
CheckIconsWithPath(null);
CheckIconsWithPath(inputZip);
}
#endregion #endregion
@ -459,6 +432,7 @@ public class RawModsTests : IDisposable
Assert.True(origFiles.SequenceEqual(newFiles)); Assert.True(origFiles.SequenceEqual(newFiles));
break; break;
} }
default: throw new Exception("unwritten test case for new os?");
} }
// There should be exactly one subdir here and it should end with .app // There should be exactly one subdir here and it should end with .app
@ -479,12 +453,235 @@ public class RawModsTests : IDisposable
Assert.True(File.Exists(newExtract + "/" + appDir.Name + "/Contents/Resources/" + deepSuffix.ToLower() + origInput.ToLower())); Assert.True(File.Exists(newExtract + "/" + appDir.Name + "/Contents/Resources/" + deepSuffix.ToLower() + origInput.ToLower()));
} }
#endregion
#region PortInvalidZips
[Theory] [Theory]
[InlineData("./GameWin.zip")] [InlineData(Core.ModOS.Windows)]
[InlineData("./GameLin.zip")] [InlineData(Core.ModOS.Linux)]
[InlineData("./GameMac.zip")] [InlineData(Core.ModOS.Mac)]
public void CheckThatMacPortHasProperIcons(string inputZip) [InlineData(Core.ModOS.Android)]
public void PortInvalidZipsToOS(Core.ModOS os)
{
Action<string?, string?> function = os switch
{
Core.ModOS.Windows => (input, outputFile) => RawMods.PortToWindows(input, outputFile),
Core.ModOS.Linux => (input, outputFile) => RawMods.PortToLinux(input, outputFile),
Core.ModOS.Mac => (input, outputFile) => RawMods.PortToMac(input, outputFile),
Core.ModOS.Android => (input, outputFile) => RawMods.PortToAndroid(input, outputFile),
_ => throw new Exception("This should not have happened! new unhandled data!")
};
Assert.Throws<ArgumentNullException>(() => function.Invoke(null, "/foo"));
Assert.Throws<FileNotFoundException>(() => function.Invoke("/foo", "/foo"));
Assert.Throws<ArgumentOutOfRangeException>(() => function.Invoke("./GameLin.zip", null));
}
#endregion
#region PortToAndroid
[Theory]
[InlineData("./GameWin.zip", false, false, false, false)]
[InlineData("./GameLin.zip", false, false, false, false)]
[InlineData("./GameMac.zip", false, false, false, false)]
[InlineData("./GameWin.zip", true, true, true, true)]
[InlineData("./GameLin.zip", true, true, true, true)]
[InlineData("./GameMac.zip", true, true, true, true)]
public void PortZipToAndroid(string inputZip, bool useSubdirectories, bool createWorkingDirectoryBeforeHand, bool useCustomSave, bool useInternet)
{ {
var origMod = RawMods.GetModOSOfRawZip(inputZip);
var outputZip = testTempDir + Guid.NewGuid();
var origExtract = testTempDir + Guid.NewGuid();
var newExtract = testTempDir + Guid.NewGuid() + "/";
var deepSuffix = "Foobar/Foobar/Foo/Blag/";
var origInput = inputZip;
if (useSubdirectories)
{
string archiveDeepSuffix = deepSuffix;
if (origMod == Core.ModOS.Linux)
archiveDeepSuffix = "assets/" + deepSuffix;
else if (origMod == Core.ModOS.Mac)
archiveDeepSuffix = "AM2R.app/Contents/Resources/" + deepSuffix;
File.Copy(inputZip, testTempDir + inputZip + "_modified");
inputZip = testTempDir + inputZip + "_modified";
using ZipArchive archive = ZipFile.Open(inputZip, ZipArchiveMode.Update);
archive.CreateEntry(archiveDeepSuffix + origInput);
}
if (createWorkingDirectoryBeforeHand)
Directory.CreateDirectory(libTempDir + Path.GetFileNameWithoutExtension(inputZip));
RawMods.PortToAndroid(inputZip, outputZip, null, null, useCustomSave, useInternet);
// HACK: STORE'd files aren't compressed, thus the compressed size is the same as the normal
using (var archive = ZipFile.OpenRead(outputZip))
{
var entry = archive.GetEntry("assets/coolsong.ogg");
Assert.Equal(entry.Length, entry.CompressedLength);
}
ZipFile.ExtractToDirectory(inputZip, origExtract);
ZipFile.ExtractToDirectory(outputZip, newExtract);
switch (origMod)
{
case Core.ModOS.Windows:
{
// File contents should be same between the zips except for all files being lowered now, data file being different, runner+dll not existing now, and splash being new
var origFiles = new DirectoryInfo(origExtract).GetFiles().Select(f => f.Name.ToLower()).ToList();
origFiles.Remove("am2r.exe");
origFiles.Remove("d3dx9_43.dll");
origFiles.Remove("data.win");
origFiles.Add("game.droid");
origFiles.Remove("am2r.exe");
origFiles.Add("splash.png");
origFiles.Sort();
var newFiles = new DirectoryInfo(newExtract + "/assets").GetFiles().Select(f => f.Name).ToList();
newFiles.Sort();
Assert.True(origFiles.SequenceEqual(newFiles));
break;
}
case Core.ModOS.Linux:
{
// File contents should be same between the zips except for data file being different
var origFiles = new DirectoryInfo(origExtract + "/assets").GetFiles().Select(f => f.Name).ToList();
origFiles.Remove("game.unx");
origFiles.Add("game.droid");
origFiles.Sort();
var newFiles = new DirectoryInfo(newExtract + "/assets").GetFiles().Select(f => f.Name).ToList();
newFiles.Sort();
Assert.True(origFiles.SequenceEqual(newFiles));
break;
}
case Core.ModOS.Mac:
{
// File contents should be the same between the zips except for data file being different and extra mac files
var origFiles = new DirectoryInfo(origExtract + "/AM2R.app/Contents/Resources").GetFiles().Select(f => f.Name).ToList();
origFiles.Remove("game.ios");
origFiles.Add("game.droid");
origFiles.Remove("gamecontrollerdb.txt");
origFiles.Remove("yoyorunner.config");
origFiles.Sort();
var newFiles = new DirectoryInfo(newExtract + "/assets").GetFiles().Select(f => f.Name).ToList();
newFiles.Sort();
Assert.True(origFiles.SequenceEqual(newFiles));
break;
}
default: throw new Exception("unwritten test case for new os?");
}
// TODO: check save folder - probably needs to be done by decompiling again. If one does this, then the "useInternet" check below should also get redone
if (useCustomSave)
{
}
// HACK: ugly af, but works
if (useInternet)
{
Assert.Contains("android.permission." +
"INTERNETandroid." +
"permission.BLUETOOTH",
File.ReadAllText(newExtract + "/AndroidManifest.xml"));
}
// there should be four subdirs in root
Assert.Equal(4, new DirectoryInfo(newExtract).GetDirectories().Length);
// If we didn't specify any, there should be no subdirectories at the end in asset folder
if (!useSubdirectories)
{
Assert.Empty(new DirectoryInfo(newExtract + "/assets/").GetDirectories());
return;
}
//Otherwise there should be our stuff
Assert.True(File.Exists(newExtract + "/assets/" + deepSuffix.ToLower() + origInput.ToLower()));
// TODO: check whether final signature is correct?
}
[Theory]
[InlineData("./GameWin.zip", "")]
[InlineData("./GameWin.zip", "фыва")]
[InlineData("./GameLin.zip", "")]
[InlineData("./GameLin.zip", "фыва")]
[InlineData("./GameMac.zip", "")]
[InlineData("./GameMac.zip", "фыва")]
public void HandleInvalidAndroidDisplayNames(string inputZip, string nameToTest)
{
var origMod = RawMods.GetModOSOfRawZip(inputZip);
var outputZip = testTempDir + Guid.NewGuid();
string assetFile = "data.win";
if (origMod == Core.ModOS.Linux)
assetFile = "assets/game.unx";
else if (origMod == Core.ModOS.Mac)
assetFile = "AM2R.app/Contents/Resources/game.ios";
File.Copy(inputZip, testTempDir + inputZip + "_modified");
inputZip = testTempDir + inputZip + "_modified";
using (ZipArchive archive = ZipFile.Open(inputZip, ZipArchiveMode.Update))
{
var file = archive.GetEntry(assetFile);
var outputFile = testTempDir + Guid.NewGuid();
file.ExtractToFile(outputFile);
// Read data file and change display name
{
UndertaleData gmData;
using (FileStream fs = new FileInfo(outputFile).OpenRead())
{
gmData = UndertaleIO.Read(fs);
var newName = gmData.Strings.MakeString(nameToTest);
gmData.GeneralInfo.DisplayName = newName;
}
using (FileStream fs = new FileInfo(outputFile).OpenWrite())
{
UndertaleIO.Write(fs, gmData);
}
}
file.Delete();
archive.CreateEntryFromFile(outputFile, assetFile);
}
Assert.Throws<InvalidDataException>(() => RawMods.PortToAndroid(inputZip, outputZip, null, null, true));
}
#endregion
#region Check proper icons after porting
[Theory]
[InlineData("./GameWin.zip", Core.ModOS.Linux)]
[InlineData("./GameLin.zip", Core.ModOS.Linux)]
[InlineData("./GameMac.zip", Core.ModOS.Linux)]
[InlineData("./GameWin.zip", Core.ModOS.Mac)]
[InlineData("./GameLin.zip", Core.ModOS.Mac)]
[InlineData("./GameMac.zip", Core.ModOS.Mac)]
public void CheckThatUnixPortHasProperIcons(string inputZip, Core.ModOS os)
{
const string icon = "icon.png";
const string splash = "splash.png";
string assetSuffix;
Action<string, string, string?, string?> function;
switch (os)
{
case Core.ModOS.Linux:
assetSuffix = "/assets/";
function = (inp, outp, ic, spl) => RawMods.PortToLinux(inp, outp, ic, spl);
break;
case Core.ModOS.Mac:
assetSuffix = "/AM2R.app/Contents/Resources/";
function = (inp, outp, ic, spl) => RawMods.PortToMac(inp, outp, ic, spl);
break;
default: throw new Exception("was called with unimplemented os");
}
var outputZip = testTempDir + Guid.NewGuid(); var outputZip = testTempDir + Guid.NewGuid();
var newExtract = testTempDir + Guid.NewGuid() + "/"; var newExtract = testTempDir + Guid.NewGuid() + "/";
@ -492,12 +689,12 @@ public class RawModsTests : IDisposable
void CheckIconsWithPath(string? path) void CheckIconsWithPath(string? path)
{ {
File.Delete(outputZip); File.Delete(outputZip);
RawMods.PortToMac(inputZip, outputZip, path, path); function.Invoke(inputZip, outputZip, path, path);
if (Directory.Exists(newExtract)) if (Directory.Exists(newExtract))
Directory.Delete(newExtract, true); Directory.Delete(newExtract, true);
ZipFile.ExtractToDirectory(outputZip, newExtract); ZipFile.ExtractToDirectory(outputZip, newExtract);
var newIcon = File.ReadAllBytes(newExtract + "/AM2R.app/Contents/Resources/icon.png"); var newIcon = File.ReadAllBytes(newExtract + assetSuffix + icon);
var newSplash = File.ReadAllBytes(newExtract + "/AM2R.app/Contents/Resources/splash.png"); var newSplash = File.ReadAllBytes(newExtract + assetSuffix + splash);
Directory.CreateDirectory(libTempDir); Directory.CreateDirectory(libTempDir);
var oldIcon = File.ReadAllBytes(RawMods.GetProperPathToBuiltinIcons(nameof(Resources.icon), path)); var oldIcon = File.ReadAllBytes(RawMods.GetProperPathToBuiltinIcons(nameof(Resources.icon), path));
var oldSplash = File.ReadAllBytes(RawMods.GetProperPathToBuiltinIcons(nameof(Resources.splash), path)); var oldSplash = File.ReadAllBytes(RawMods.GetProperPathToBuiltinIcons(nameof(Resources.splash), path));
@ -510,43 +707,75 @@ public class RawModsTests : IDisposable
CheckIconsWithPath(inputZip); CheckIconsWithPath(inputZip);
} }
#endregion // TODO: see skip reason
[Theory(Skip = "Currently buggy, due to probably an apktool bug.")]
#region PortInvalidZips [InlineData("./GameWin.zip")]
[InlineData("./GameLin.zip")]
[Theory] [InlineData("./GameMac.zip")]
[InlineData(Core.ModOS.Windows)] public void CheckThatAndroidHasProperIcons(string inputZip)
[InlineData(Core.ModOS.Linux)]
[InlineData(Core.ModOS.Mac)]
public void PortInvalidZipsToOS(Core.ModOS os)
{ {
Action<string?, string?> function = os switch const string splash = "splash.png";
string assetSuffix = "/assets/";
Dictionary<string, int> resPaths = new Dictionary<string, int>
{ {
Core.ModOS.Windows => (input, outputFile) => RawMods.PortToWindows(input, outputFile), {"/res/drawable/icon.png", 96},
Core.ModOS.Linux => (input, outputFile) => RawMods.PortToLinux(input, outputFile), {"/res/drawable-hdpi-v4/icon.png", 72},
Core.ModOS.Mac => (input, outputFile) => RawMods.PortToMac(input, outputFile), {"/res/drawable-ldpi-v4/icon.png", 36},
_ => throw new Exception("This should not have happened! new unhandled data!") {"/res/drawable-mdpi-v4/icon.png", 48},
{"/res/drawable-xhdpi-v4/icon.png", 96},
{"/res/drawable-xxhdpi-v4/icon.png", 144},
{"/res/drawable-xxxhdpi-v4/icon.png", 192}
}; };
Assert.Throws<ArgumentNullException>(() => function.Invoke(null, "/foo")); var outputZip = testTempDir + Guid.NewGuid();
Assert.Throws<FileNotFoundException>(() => function.Invoke("/foo", "/foo")); var newExtract = testTempDir + Guid.NewGuid() + "/";
Assert.Throws<ArgumentOutOfRangeException>(() => function.Invoke("./GameLin.zip", null));
// With default icons
void CheckIconsWithPath(string? path)
{
File.Delete(outputZip);
RawMods.PortToAndroid(inputZip, outputZip, path, path);
if (Directory.Exists(newExtract))
Directory.Delete(newExtract, true);
ZipFile.ExtractToDirectory(outputZip, newExtract);
var newSplash = File.ReadAllBytes(newExtract + assetSuffix + splash);
Directory.CreateDirectory(libTempDir);
var oldSplash = File.ReadAllBytes(RawMods.GetProperPathToBuiltinIcons(nameof(Resources.splash), path));
Assert.True(newSplash.SequenceEqual(oldSplash));
foreach (var kvp in resPaths)
{
var sizedPath = testTempDir + "/" + Guid.NewGuid() +".png";
var newIcon = File.ReadAllBytes(newExtract + kvp.Key);
var oldIconPath = RawMods.GetProperPathToBuiltinIcons(nameof(Resources.icon), path);
HelperMethods.SaveAndroidIcon(oldIconPath, kvp.Value, sizedPath);
var oldIcon = File.ReadAllBytes(sizedPath);
Assert.True(newIcon.SequenceEqual(oldIcon));
}
}
CheckIconsWithPath(null);
CheckIconsWithPath(inputZip);
} }
#endregion #endregion
#region Make sure porting methods work when called in succession #region Make sure porting methods work when called in succession
[Theory] [Theory]
[InlineData("./GameWin.zip", Core.ModOS.Windows)] [InlineData("./GameWin.zip", Core.ModOS.Windows)]
[InlineData("./GameWin.zip", Core.ModOS.Linux)] [InlineData("./GameWin.zip", Core.ModOS.Linux)]
[InlineData("./GameWin.zip", Core.ModOS.Mac)] [InlineData("./GameWin.zip", Core.ModOS.Mac)]
[InlineData("./GameWin.zip", Core.ModOS.Android)]
[InlineData("./GameLin.zip", Core.ModOS.Windows)] [InlineData("./GameLin.zip", Core.ModOS.Windows)]
[InlineData("./GameLin.zip", Core.ModOS.Linux)] [InlineData("./GameLin.zip", Core.ModOS.Linux)]
[InlineData("./GameLin.zip", Core.ModOS.Mac)] [InlineData("./GameLin.zip", Core.ModOS.Mac)]
[InlineData("./GameLin.zip", Core.ModOS.Android)]
[InlineData("./GameMac.zip", Core.ModOS.Windows)] [InlineData("./GameMac.zip", Core.ModOS.Windows)]
[InlineData("./GameMac.zip", Core.ModOS.Linux)] [InlineData("./GameMac.zip", Core.ModOS.Linux)]
[InlineData("./GameMac.zip", Core.ModOS.Mac)] [InlineData("./GameMac.zip", Core.ModOS.Mac)]
[InlineData("./GameMac.zip", Core.ModOS.Android)]
public void TestPortToOSMultipleTimes(string input, Core.ModOS os) public void TestPortToOSMultipleTimes(string input, Core.ModOS os)
{ {
Action<string?, string?> function = os switch Action<string?, string?> function = os switch
@ -554,6 +783,7 @@ public class RawModsTests : IDisposable
Core.ModOS.Windows => (inputFile, outputFile) => RawMods.PortToWindows(inputFile, outputFile), Core.ModOS.Windows => (inputFile, outputFile) => RawMods.PortToWindows(inputFile, outputFile),
Core.ModOS.Linux => (inputFile, outputFile) => RawMods.PortToLinux(inputFile, outputFile), Core.ModOS.Linux => (inputFile, outputFile) => RawMods.PortToLinux(inputFile, outputFile),
Core.ModOS.Mac => (inputFile, outputFile) => RawMods.PortToMac(inputFile, outputFile), Core.ModOS.Mac => (inputFile, outputFile) => RawMods.PortToMac(inputFile, outputFile),
Core.ModOS.Android => (inputFile, outputFile) => RawMods.PortToAndroid(inputFile, outputFile),
_ => throw new Exception("This should not have happened! new unhandled data!") _ => throw new Exception("This should not have happened! new unhandled data!")
}; };
@ -566,6 +796,4 @@ public class RawModsTests : IDisposable
} }
#endregion #endregion
// TODO: write tests for porttoandroid
} }
Loading…
Cancel
Save