Unit Tests: Coverage Reporting and Github Actions Integration (#131)

* Initial test suite

* Fix some build warnings

* Implemented working unit tests with coverage

* Reduced some warnings

* Fix a mac-specific issue

* Add 2 additional unit test classes.

* Multiple new unit tests

* Some fixins

* Remove auto-generated file

* WIP on hiding ignored tests

* Added list of debug hotkeys

* Remove old website

* Remove empty file

* Add more unit tests

* Fix bug where arrows would nudge BF

* Fix bug where ctrl/alt would flash capsules

* Fixed bug where bf-old easter egg broke

* Remove duplicate lines

* More test-related stuff

* Some code cleanup

* Add mocking and a test assets folder

* More TESTS!

* Update Hmm...

* Update artist on Monster

* More minor fixes to individual functions

* 1.38% unit test coverage!

* Even more tests? :O

* More unit test work

* Rework migration for BaseRegistry

* gameover fix

* Fix an issue with Lime

* Fix issues with version parsing on data files

* 100 total unit tests!

* Added even MORE unit tests!

* Additional test tweaks :3

* Fixed tests on windows by updating libraries.

* A bunch of smaller syntax tweaks.

* New crash handler catches and logs critical errors!

* Chart editor now has null safety enabled.

* Null safety on all tests

* New Level data test

* Generate proper code coverage reports!

* Disable null safety on ChartEditorState for unit testing

* Update openfl to use latest fixes for crash reporting

* Added unit test to Github Workflow

* Updated unit tests to compile with null safety enabled by inlining assertions.

* Added coverage gutters as a recommended extension

* Impreovements to tests involving exceptions

* Disable a few incomplete tests.

* Add scripts for building unit coverage reports on linux

---------

Co-authored-by: Cameron Taylor <cameron.taylor.ninja@gmail.com>
This commit is contained in:
Eric 2023-08-30 18:31:59 -04:00 committed by GitHub
parent 3828179218
commit 279277b18c
38 changed files with 583 additions and 121 deletions

View file

@ -58,3 +58,18 @@ jobs:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/debug/windows/bin
target: win
test-unit-win:
needs: create-nightly-win
runs-on: windows-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v3
with:
submodules: 'recursive'
- uses: ./.github/actions/setup-haxeshit
- name: Run unit tests
run: |
cd ./tests/unit/
./start-win-native.bat

View file

@ -6,6 +6,7 @@
"vshaxe.hxcpp-debugger", // CPP debugging
"openfl.lime-vscode-extension", // Lime integration
"esbenp.prettier-vscode", // JSON formatting
"redhat.vscode-xml" // XML formatting
"redhat.vscode-xml", // XML formatting
"ryanluker.vscode-coverage-gutters" // Highlight code coverage
]
}

View file

@ -117,5 +117,12 @@
"args": ["-debug", "-watch"]
}
],
"cmake.configureOnOpen": false
"cmake.configureOnOpen": false,
"coverage-gutters.coverageFileNames": [
"lcov.info",
"cov.xml",
"coverage.xml",
"jacoco.xml",
"coverage.cobertura.xml"
]
}

View file

@ -257,7 +257,7 @@ class ChartEditorDialogHandler
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
@:haxe.warning("-WVarInit")
@:haxe.warning("-WVarInit") // Hide the warning about the onDropFile handler.
public static function openUploadInstDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable);

View file

@ -85,8 +85,8 @@ using Lambda;
* @author MasterEric
*/
// Give other classes access to private instance fields
// @:nullSafety(Loose) // Enable this while developing, then disable to keep unit tests functional!
@:nullSafety
@:allow(funkin.ui.debug.charting.ChartEditorCommand)
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
@:allow(funkin.ui.debug.charting.ChartEditorThemeHandler)

View file

@ -0,0 +1,37 @@
package funkin.util.macro;
#if macro
using funkin.util.tools.ArrayTools;
#end
/**
* A macro to make fields inline.
*/
class InlineMacro
{
/**
* For the given class, find the (static?) field with the given name and make it inline.
* @param field
* @param isStatic
*/
public static macro function makeInline(field:String, isStatic:Bool = false):Array<haxe.macro.Expr.Field>
{
var pos:haxe.macro.Expr.Position = haxe.macro.Context.currentPos();
// The FlxBasic class. We can add new properties to this class.
var cls:haxe.macro.Type.ClassType = haxe.macro.Context.getLocalClass().get();
// The fields of the FlxClass.
var fields:Array<haxe.macro.Expr.Field> = haxe.macro.Context.getBuildFields();
// Find the field with the given name.
var targetField:Null<haxe.macro.Expr.Field> = fields.find(function(f) return f.name == field
&& (MacroUtil.isFieldStatic(f) == isStatic));
// If the field was not found, throw an error.
if (targetField == null) haxe.macro.Context.error("Field " + field + " not found in class " + cls.name, pos);
// Add the inline access modifier to the field.
targetField.access.push(AInline);
return fields;
}
}

View file

@ -99,6 +99,11 @@ class MacroUtil
return null;
}
public static function isFieldStatic(field:haxe.macro.Expr.Field):Bool
{
return field.access.contains(AStatic);
}
/**
* Converts a value to an equivalent macro expression.
*/

View file

@ -48,3 +48,9 @@ There are two parameters:
### `testDestroy()`
`testDestroy()` tests whether an `IFlxDestroyable` can safely be `destroy()`ed more than once (null reference errors are fairly common here). For this, `destroyable` has to be set during `before()` of the test class.
### Null Safety
Append each test class with `@:nullSafety` to prevent crash bugs while developing.
Note that `Assert.isNotNull(target)` is considered a vlid

View file

@ -0,0 +1,47 @@
{
"version": "1.0.0",
"name": "SHOULD FAIL TO PARSE",
"titleAsset": "",
"props": [
{
"assetPath": "storymenu/props/gf",
"scale": 1.0,
"danceEvery": 2,
"offsets": [80, 80],
"animations": [
{
"name": "danceLeft",
"prefix": "idle0",
"frameIndices": [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
},
{
"name": "danceRight",
"prefix": "idle0",
"frameIndices": [
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29
]
}
]
},
{
"assetPath": "storymenu/props/bf",
"scale": 1.0,
"danceEvery": 2,
"offsets": [150, 80],
"animations": [
{
"name": "idle",
"prefix": "idle0",
"frameRate": 24
},
{
"name": "confirm",
"prefix": "confirm0",
"frameRate": 24
}
]
}
],
"background": "#F9CF51",
"songs": ["tutorial"]
}

View file

@ -37,6 +37,9 @@
<!-- This macro allows addition of new functionality to existing Flixel. -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
<!-- Macros to satisfy null safety (null safety can't check nested functions, so assertions must be inlined) -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.InlineMacro.makeInline(\'fail\', true))', 'massive.munit.Assert')" />
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.InlineMacro.makeInline(\'isNotNull\', true))', 'massive.munit.Assert')" />
<!-- Assets -->
<assets path="assets/preload" rename="assets" exclude="*.ogg" if="web" />
@ -80,12 +83,22 @@
<!-- Test defines -->
<set name="no-custom-backend" />
<set name="unit-test" />
<!--<haxedef name="no-inline" />-->
<haxedef name="FLX_UNIT_TEST" />
<haxedef name="FLX_RECORD" />
<!-- Manually set up code coverage -->
<!-- Clean up the output -->
<haxedef name="no-traces" />
<!--
-->
<haxedef name="ignore-inline" />
<haxeflag name="-w" value="-WDeprecated" />
<!-- Manually set up code coverage (because munit report and lime test are mutually exclusive) -->
<haxeflag name="--macro" value="mcover.MCover.coverage(['funkin'],['../../source', 'source/'],[''])" />
<haxelib name="mcover" />
<haxedef name="MCOVER" />
<haxeflag name="--macro" value="mcover.MCover.coverage(['funkin'],['../../source', 'source/'],[''])" />
<haxedef name="safeMode"/>
<haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" />
<haxedef name="HXCPP_STACK_TRACE" />
</project>

View file

@ -0,0 +1,4 @@
#!/bin/bash
cd ./report/
genhtml -o ./html/ ./lcov.info

View file

@ -10,6 +10,7 @@ using flixel.util.FlxArrayUtil;
/**
* @see https://github.com/HaxeFlixel/flixel/tree/dev/tests/unit
*/
@:nullSafety
class FunkinAssert
{
/**
@ -21,15 +22,17 @@ class FunkinAssert
* @param margin The allowed margin of error between the expected and actual values.
* @param info Info on the position this function was called from. Magic value, passed automatically.
*/
public static function areNear(expected:Float, actual:Float, margin:Float = 0.001, ?info:PosInfos):Void
public static function areNear(expected:Float, ?actual:Float, margin:Float = 0.001, ?info:PosInfos):Void
{
if (actual == null) Assert.fail('Value [$actual] is null, and cannot be compared to [$expected]', info);
if (areNearHelper(expected, actual)) Assert.assertionCount++;
else
Assert.fail('Value [$actual] is not within [$margin] of [$expected]', info);
}
public static function rectsNear(expected:FlxRect, actual:FlxRect, margin:Float = 0.001, ?info:PosInfos):Void
public static function rectsNear(expected:FlxRect, ?actual:FlxRect, margin:Float = 0.001, ?info:PosInfos):Void
{
if (actual == null) Assert.fail('Value [$actual] is null, and cannot be compared to [$expected]', info);
var areNear = areNearHelper(expected.x, actual.x, margin)
&& areNearHelper(expected.y, actual.y, margin)
&& areNearHelper(expected.width, actual.width, margin)
@ -45,33 +48,83 @@ class FunkinAssert
return actual >= expected - margin && actual <= expected + margin;
}
public static function arraysEqual<T>(expected:Array<T>, actual:Array<T>, ?info:PosInfos):Void
public static function arraysEqual<T>(expected:Array<T>, ?actual:Array<T>, ?info:PosInfos):Void
{
if (actual == null) Assert.fail('Value [$actual] is null, and cannot be compared to [$expected]', info);
if (expected.equals(actual)) Assert.assertionCount++;
else
Assert.fail('\nExpected\n ${expected}\nbut was\n ${actual}\n', info);
}
public static function arraysNotEqual<T>(expected:Array<T>, actual:Array<T>, ?info:PosInfos):Void
public static function arraysNotEqual<T>(expected:Array<T>, ?actual:Array<T>, ?info:PosInfos):Void
{
if (actual == null) Assert.fail('Value [$actual] is null, and cannot be compared to [$expected]', info);
if (!expected.equals(actual)) Assert.assertionCount++;
else
Assert.fail('\nValue\n ${actual}\nwas equal to\n ${expected}\n', info);
}
public static function pointsEqual(expected:FlxPoint, actual:FlxPoint, ?msg:String, ?info:PosInfos)
public static function pointsEqual(expected:FlxPoint, ?actual:FlxPoint, ?msg:String, ?info:PosInfos)
{
if (actual == null) Assert.fail('Value [$actual] is null, and cannot be compared to [$expected]', info);
if (expected.equals(actual)) Assert.assertionCount++;
else if (msg != null) Assert.fail(msg, info);
else
Assert.fail("Value [" + actual + "] was not equal to expected value [" + expected + "]", info);
}
public static function pointsNotEqual(expected:FlxPoint, actual:FlxPoint, ?msg:String, ?info:PosInfos)
public static function pointsNotEqual(expected:FlxPoint, ?actual:FlxPoint, ?msg:String, ?info:PosInfos)
{
if (actual == null) Assert.fail('Value [$actual] is null, and cannot be compared to [$expected]', info);
if (!expected.equals(actual)) Assert.assertionCount++;
else if (msg != null) Assert.fail(msg, info);
else
Assert.fail("Value [" + actual + "] was equal to value [" + expected + "]", info);
}
/**
* Execute `targetFunc`, expecting it to throw an exception.
* If it doesn't, or if the exception doesn't validate against the provided `predicate`, fail.
*/
public static function validateThrows(targetFunc:Void->Void, predicate:Dynamic->Bool, ?info:PosInfos)
{
try
{
targetFunc();
Assert.fail("Expected exception to be thrown, got no failure.", info);
}
catch (e:Dynamic)
{
if (predicate(e))
{
Assert.assertionCount++;
}
else
{
Assert.fail('Expected exception to match predicate, but failed (got ${e})', info);
}
}
}
/**
* Execute `targetFunc`, expecting it to throw a `json2object.Error.CustomFunctionException` with a message matching `expected`.
* I made this its own function since it's the most common specific use case of `validateThrows`.
*/
public static function validateThrowsJ2OCustom(targetFunc:Void->Void, expected:String, ?info:PosInfos)
{
var predicate:Dynamic->Bool = function(err:Dynamic):Bool {
if (!Std.isOfType(err, json2object.Error)) Assert.fail('Expected error of type json2object.Error, got ${Type.typeof(err)}');
switch (err)
{
case json2object.Error.CustomFunctionException(msg, pos):
if (msg != expected) Assert.fail('Expected message [${expected}], got [${msg}].');
default:
Assert.fail('Expected error of type CustomFunctionException, got [${err}].');
}
return true;
};
validateThrows(targetFunc, predicate, info);
}
}

View file

@ -11,6 +11,7 @@ import massive.munit.Assert;
/**
* @see https://github.com/HaxeFlixel/flixel/tree/dev/tests/unit
*/
@:nullSafety
class FunkinTest
{
public static final MS_PER_STEP:Float = 1.0 / 60.0 * 1000;
@ -19,7 +20,7 @@ class FunkinTest
static inline var TICKS_PER_FRAME:UInt = 25;
static var totalSteps:UInt = 0;
var destroyable:IFlxDestroyable;
var destroyable:Null<IFlxDestroyable> = null;
public function new() {}

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.DateUtil;
@:nullSafety
class MockTest extends FunkinTest
{
public function new()
@ -45,16 +46,12 @@ class MockTest extends FunkinTest
// If not, a VerificationException will be thrown and the test will fail.
mockatoo.Mockatoo.verify(mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false), times(1));
try
{
FunkinAssert.validateThrows(function() {
// Attempt to verify the method was called.
// This should FAIL, since we didn't call the method.
mockatoo.Mockatoo.verify(mockAnim.addByIndices("testAnim", "blablabla", [], "", 24, false, false, false), times(1));
Assert.fail("Mocking function should have thrown but didn't.");
}
catch (_:mockatoo.exception.VerificationException)
{
// Expected.
}
mockatoo.Mockatoo.verify(mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false), times(1));
}, function(err) {
return Std.isOfType(err, mockatoo.exception.VerificationException);
});
}
}

View file

@ -6,11 +6,14 @@ import flixel.FlxState;
import massive.munit.TestRunner;
import massive.munit.client.HTTPClient;
import massive.munit.client.SummaryReportClient;
import funkin.util.logging.CrashHandler;
import funkin.util.FileUtil;
/**
* Auto generated Test Application.
* Refer to munit command line tool for more information (haxelib run munit)
*/
@:nullSafety
class TestMain
{
/**
@ -18,6 +21,8 @@ class TestMain
*/
static final INCLUDE_IGNORED_REPORT:Bool = false;
static final COVERAGE_FOLDER:String = "../../../report";
static function main()
{
new TestMain();
@ -25,34 +30,46 @@ class TestMain
public function new()
{
// Flixel was not designed for unit testing so we can only have one instance for now.
Lib.current.stage.addChild(new FlxGame(640, 480, FlxState, 60, 60, true));
try
{
CrashHandler.initialize();
var suites = new Array<Class<massive.munit.TestSuite>>();
suites.push(TestSuite);
// Flixel was not designed for unit testing so we can only have one instance for now.
Lib.current.stage.addChild(new FlxGame(640, 480, FlxState, 60, 60, true));
#if MCOVER
// Print individual test results alongside coverage results for each test class,
// as well as a final coverage report for the entire test suite.
var innerClient = new massive.munit.client.RichPrintClient(INCLUDE_IGNORED_REPORT);
var client = new mcover.coverage.munit.client.MCoverPrintClient(innerClient);
// Print final test results alongside detailed coverage results for the test suite.
var httpClient = new HTTPClient(new mcover.coverage.munit.client.MCoverSummaryReportClient());
// NOTE: You can also create a custom ICoverageTestResultClient implementation
var suites = new Array<Class<massive.munit.TestSuite>>();
suites.push(TestSuite);
mcover.coverage.MCoverage.getLogger().addClient(new mcover.coverage.client.CodecovJsonPrintClient());
#else
// Print individual test results.
var client = new massive.munit.client.RichPrintClient(INCLUDE_IGNORED_REPORT);
// Print final test suite results.
var httpClient = new HTTPClient(new SummaryReportClient());
#end
#if MCOVER
// Print individual test results alongside coverage results for each test class,
// as well as a final coverage report for the entire test suite.
var innerClient = new massive.munit.client.RichPrintClient(INCLUDE_IGNORED_REPORT);
var client = new mcover.coverage.munit.client.MCoverPrintClient(innerClient);
// Print final test results alongside detailed coverage results for the test suite.
var httpClient = new HTTPClient(new mcover.coverage.munit.client.MCoverSummaryReportClient());
// NOTE: You can also create a custom ICoverageTestResultClient implementation
var runner = new TestRunner(client);
runner.addResultClient(httpClient);
// Output coverage in LCOV format.
FileUtil.createDirIfNotExists(COVERAGE_FOLDER);
mcover.coverage.MCoverage.getLogger().addClient(new mcover.coverage.client.LcovPrintClient("Funkin' Coverage Report", '${COVERAGE_FOLDER}/lcov.info'));
#else
// Print individual test results.
var client = new massive.munit.client.RichPrintClient(INCLUDE_IGNORED_REPORT);
// Print final test suite results.
var httpClient = new HTTPClient(new SummaryReportClient());
#end
runner.completionHandler = completionHandler;
runner.run(suites);
var runner = new TestRunner(client);
runner.addResultClient(httpClient);
runner.completionHandler = completionHandler;
runner.run(suites);
}
catch (e)
{
trace('UNCAUGHT EXCEPTION');
trace(e);
}
}
/**

View file

@ -7,10 +7,11 @@ import funkin.play.song.SongData.SongTimeChange;
import funkin.util.Constants;
import massive.munit.Assert;
@:nullSafety
@:access(funkin.Conductor)
class ConductorTest extends FunkinTest
{
var conductorState:ConductorState;
var conductorState:Null<ConductorState> = null;
@Before
function before()
@ -54,6 +55,9 @@ class ConductorTest extends FunkinTest
@Test
function testUpdate():Void
{
var currentConductorState:Null<ConductorState> = conductorState;
Assert.isNotNull(currentConductorState);
Assert.areEqual(0, Conductor.songPosition);
step(); // 1
@ -72,15 +76,15 @@ class ConductorTest extends FunkinTest
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(8 / 9, Conductor.currentStepTime);
Assert.areEqual(0, conductorState.beatsHit);
Assert.areEqual(0, conductorState.stepsHit);
Assert.areEqual(0, currentConductorState.beatsHit);
Assert.areEqual(0, currentConductorState.stepsHit);
step(); // 9
Assert.areEqual(0, conductorState.beatsHit);
Assert.areEqual(1, conductorState.stepsHit);
conductorState.beatsHit = 0;
conductorState.stepsHit = 0;
Assert.areEqual(0, currentConductorState.beatsHit);
Assert.areEqual(1, currentConductorState.stepsHit);
currentConductorState.beatsHit = 0;
currentConductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 9, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
@ -89,10 +93,10 @@ class ConductorTest extends FunkinTest
step(35 - 9); // 35
Assert.areEqual(0, conductorState.beatsHit);
Assert.areEqual(2, conductorState.stepsHit);
conductorState.beatsHit = 0;
conductorState.stepsHit = 0;
Assert.areEqual(0, currentConductorState.beatsHit);
Assert.areEqual(2, currentConductorState.stepsHit);
currentConductorState.beatsHit = 0;
currentConductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 35, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
@ -101,10 +105,10 @@ class ConductorTest extends FunkinTest
step(); // 36
Assert.areEqual(1, conductorState.beatsHit);
Assert.areEqual(1, conductorState.stepsHit);
conductorState.beatsHit = 0;
conductorState.stepsHit = 0;
Assert.areEqual(1, currentConductorState.beatsHit);
Assert.areEqual(1, currentConductorState.stepsHit);
currentConductorState.beatsHit = 0;
currentConductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 36, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);

View file

@ -7,6 +7,7 @@ import funkin.data.BaseRegistry;
import funkin.util.SortUtil;
import funkin.util.VersionUtil;
@:nullSafety
@:access(funkin.data.BaseRegistry)
class BaseRegistryTest extends FunkinTest
{
@ -49,6 +50,7 @@ class BaseRegistryTest extends FunkinTest
// Ensure blablabla got parsed correctly.
var blablabla = MyTypeRegistry.instance.fetchEntry("blablabla");
Assert.isNotNull(blablabla);
Assert.areEqual(blablabla.id, "blablabla");
Assert.areEqual(blablabla._data.version, "1.0.0");
Assert.areEqual(blablabla._data.name, "blablabla API");

View file

@ -0,0 +1,146 @@
package funkin.data.level;
import funkin.data.level.LevelRegistry;
import funkin.ui.story.Level;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import massive.munit.util.Timer;
@:nullSafety
@:access(funkin.ui.story.Level)
@:access(funkin.data.level.LevelRegistry)
class LevelRegistryTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass():Void
{
LevelRegistry.instance.loadEntries();
}
@AfterClass
public function afterClass():Void {}
@Before
public function setup():Void {}
@After
public function tearDown():Void {}
@Test
public function testValid():Void
{
Assert.isNotNull(LevelRegistry.instance);
}
@Test
public function testParseEntryData():Void
{
var result:Null<LevelData> = LevelRegistry.instance.parseEntryData("test");
Assert.isNotNull(result);
Assert.areEqual("1.0.0", result.version);
Assert.areEqual("TEACHING TIME", result.name);
Assert.areEqual("storymenu/titles/tutorial", result.titleAsset);
Assert.areEqual(2, result.props.length);
Assert.areEqual("storymenu/props/gf", result.props[0].assetPath);
Assert.areEqual(1.0, result.props[0].scale);
Assert.areEqual(2, result.props[0].danceEvery);
Assert.areEqual([80, 80], result.props[0].offsets);
var anims = result.props[0].animations;
Assert.isNotNull(anims);
Assert.areEqual(2, anims.length);
var anim0 = anims[0];
Assert.isNotNull(anim0);
Assert.areEqual("danceLeft", anim0.name);
Assert.areEqual("idle0", anim0.prefix);
Assert.areEqual([30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], anim0.frameIndices);
var anim1 = anims[1];
Assert.isNotNull(anim1);
Assert.areEqual("danceRight", anim1.name);
Assert.areEqual("idle0", anim1.prefix);
Assert.areEqual([15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], anim1.frameIndices);
Assert.areEqual("storymenu/props/bf", result.props[1].assetPath);
Assert.areEqual(1.0, result.props[1].scale);
Assert.areEqual(2, result.props[1].danceEvery);
Assert.areEqual([150, 80], result.props[1].offsets);
anims = result.props[1].animations;
Assert.isNotNull(anims);
Assert.areEqual(2, anims.length);
anim0 = anims[0];
Assert.isNotNull(anim0);
Assert.areEqual("idle", anim0.name);
Assert.areEqual("idle0", anim0.prefix);
Assert.areEqual(24, anim0.frameRate);
anim1 = anims[1];
Assert.isNotNull(anim1);
Assert.areEqual("confirm", anim1.name);
Assert.areEqual("confirm0", anim1.prefix);
Assert.areEqual(24, anim1.frameRate);
Assert.areEqual("#F9CF51", result.background);
Assert.areEqual(["tutorial"], result.songs);
}
@Test
public function testCreateEntry():Void
{
var result:Null<Level> = LevelRegistry.instance.createEntry("test");
Assert.isNotNull(result);
Assert.areEqual("Level(test)", result.toString());
Assert.areEqual("TEACHING TIME", result.getTitle());
Assert.areEqual(true, result.isUnlocked());
Assert.areEqual(true, result.isVisible());
}
@Test
public function testFetchEntry():Void
{
var result:Null<Level> = LevelRegistry.instance.fetchEntry("test");
Assert.isNotNull(result);
Assert.areEqual("Level(test)", result.toString());
Assert.areEqual("TEACHING TIME", result.getTitle());
Assert.areEqual(true, result.isUnlocked());
Assert.areEqual(true, result.isVisible());
}
@Test
@Ignore("Requires redoing validation.")
public function testCreateEntryBlankPath():Void
{
FunkinAssert.validateThrows(function() {
var result:Null<Level> = LevelRegistry.instance.createEntry("blankpathtest");
}, function(err) {
return err == "Could not parse level data for id: blankpathtest";
});
}
@Test
@Ignore("Requires redoing validation.")
public function testFetchBadEntry():Void
{
var result:Null<Level> = LevelRegistry.instance.fetchEntry("blablabla");
Assert.isNull(result);
var result2:Null<Level> = LevelRegistry.instance.fetchEntry("blankpathtest");
Assert.isNull(result2);
}
}

View file

@ -6,6 +6,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import massive.munit.util.Timer;
@:nullSafety
@:access(funkin.play.notes.notestyle.NoteStyle)
@:access(funkin.data.notestyle.NoteStyleRegistry)
class NoteStyleRegistryTest extends FunkinTest
@ -16,48 +17,57 @@ class NoteStyleRegistryTest extends FunkinTest
}
@BeforeClass
public function beforeClass()
public function beforeClass():Void
{
NoteStyleRegistry.instance.loadEntries();
}
@AfterClass
public function afterClass() {}
public function afterClass():Void {}
@Before
public function setup() {}
public function setup():Void {}
@After
public function tearDown() {}
public function tearDown():Void {}
@Test
public function testValid()
public function testValid():Void
{
Assert.isNotNull(NoteStyleRegistry.instance);
}
@Test
public function testParseEntryData()
public function testParseEntryData():Void
{
var result:NoteStyleData = NoteStyleRegistry.instance.parseEntryData("test2");
var result:Null<NoteStyleData> = NoteStyleRegistry.instance.parseEntryData("test2");
Assert.isNotNull(result);
Assert.areEqual(result.version, "1.0.0");
Assert.areEqual(result.name, "Test2");
Assert.areEqual(result.author, "Eric");
Assert.areEqual(result.fallback, "funkin");
Assert.areEqual(result.assets.note.assetPath, "shared:coolstuff");
Assert.areEqual(result.assets.note.scale, 1.8);
Assert.areEqual(result.assets.note.data.left.prefix, "noteLeft1");
Assert.areEqual(result.assets.note.data.down.prefix, "noteDown3");
Assert.areEqual(result.assets.note.data.up.prefix, "noteUp2");
Assert.areEqual(result.assets.note.data.right.prefix, "noteRight4");
Assert.isNotNull(result.assets);
var note:Null<NoteStyleData.NoteStyleAssetData<NoteStyleData.NoteStyleData_Note>> = result.assets.note;
Assert.isNotNull(note);
Assert.areEqual(note.assetPath, "shared:coolstuff");
Assert.areEqual(note.scale, 1.8);
Assert.areEqual(note.data.left.prefix, "noteLeft1");
Assert.areEqual(note.data.down.prefix, "noteDown3");
Assert.areEqual(note.data.up.prefix, "noteUp2");
Assert.areEqual(note.data.right.prefix, "noteRight4");
}
@Test
public function testFetchEntry()
public function testFetchEntry():Void
{
var result:NoteStyle = NoteStyleRegistry.instance.fetchEntry("test2");
var result:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry("test2");
Assert.isNotNull(result);
Assert.areEqual(result.toString(), "NoteStyle(test2)");
Assert.areEqual(result.getName(), "Test2");
@ -66,15 +76,15 @@ class NoteStyleRegistryTest extends FunkinTest
}
@Test
public function testFetchBadEntry()
public function testFetchBadEntry():Void
{
var result:NoteStyle = NoteStyleRegistry.instance.fetchEntry("blablabla");
var result:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry("blablabla");
Assert.areEqual(result, null);
Assert.isNull(result);
}
@Test
public function testFetchDefault()
public function testFetchDefault():Void
{
var nsrMock:NoteStyleRegistry = mock(NoteStyleRegistry);

View file

@ -34,7 +34,9 @@ class NoteStyleTest extends FunkinTest
@Ignore("This test doesn't work, crashes when the project has 2 mocks of the same class???")
public function testBuildNoteSprite()
{
var target:NoteStyle = NoteStyleRegistry.instance.fetchEntry("funkin");
var target:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry("funkin");
Assert.isNotNull(target);
var mockNoteSprite:NoteSprite = mock(NoteSprite);
// var mockAnim = mock(FlxAnimationController);
@ -48,8 +50,11 @@ class NoteStyleTest extends FunkinTest
@Test
public function testFallbackBehavior()
{
var target1:NoteStyle = NoteStyleRegistry.instance.fetchEntry("funkin");
var target2:NoteStyle = NoteStyleRegistry.instance.fetchEntry("test2");
var target1:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry("funkin");
var target2:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry("test2");
Assert.isNotNull(target1);
Assert.isNotNull(target2);
Assert.areEqual("funkin", target1.id);
Assert.areEqual("test2", target2.id);
@ -63,7 +68,6 @@ class NoteStyleTest extends FunkinTest
// Overridden fields are different.
Assert.areEqual("arrows", target1.getNoteAssetPath(false));
Assert.areEqual("coolstuff", target2.getNoteAssetPath(false));
Assert.areEqual("shared:arrows", target1.getNoteAssetPath(true));
Assert.areEqual("shared:coolstuff", target2.getNoteAssetPath(true));

View file

@ -6,6 +6,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.BezierUtil;
@:nullSafety
@:access(funkin.util.BezierUtil)
class BezierUtilTest extends FunkinTest
{

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.ClipboardUtil;
@:nullSafety
@:access(funkin.util.ClipboardUtil)
class ClipboardUtilTest extends FunkinTest
{

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.DateUtil;
@:nullSafety
@:access(funkin.util.DateUtil)
class DateUtilTest extends FunkinTest
{

View file

@ -13,6 +13,7 @@ typedef FooBar =
c:Int
};
@:nullSafety
@:access(funkin.util.SerializerUtil)
class SerializerUtilTest extends FunkinTest
{

View file

@ -9,6 +9,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.SortUtil;
@:nullSafety
@:access(funkin.util.SortUtil)
class SortUtilTest extends FunkinTest
{

View file

@ -57,8 +57,26 @@ class VersionUtilTest extends FunkinTest
{
var jsonStr:String = "{ \"version\": \"3.1.0\" }";
var version:thx.semver.Version = VersionUtil.getVersionFromJSON(jsonStr);
var version:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(jsonStr);
Assert.isNotNull(version);
Assert.areEqual("3.1.0", version.toString());
}
@Test
public function testGetVersionFromJSONBad()
{
var jsonStr:String = "{ \"version\": \"bleh\" }";
Assert.throws(String, function() {
var version:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(jsonStr);
});
var jsonStr2:String = "{ \"blah\": \"3.1.0\" }";
var version2:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(jsonStr2);
Assert.isNull(version2);
}
}

View file

@ -6,6 +6,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.assets.DataAssets;
@:nullSafety
@:access(funkin.util.assets.DataAssets)
class DataAssetsTest extends FunkinTest
{

View file

@ -9,6 +9,7 @@ import massive.munit.async.AsyncFactory;
import funkin.util.DateUtil;
import flixel.FlxSprite;
@:nullSafety
@:access(funkin.util.assets.FlxAnimationUtil)
class FlxAnimationUtilTest extends FunkinTest
{

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.ArrayTools;
@:nullSafety
@:access(funkin.util.tools.ArrayTools)
class ArraySortToolsTest extends FunkinTest
{
@ -54,21 +55,30 @@ class ArraySortToolsTest extends FunkinTest
// Just make sure these don't crash.
ArraySortTools.mergeSort([], compare);
}
@Test
@:nullSafety(Off)
public function testMergeSortNull()
{
var testArray:Array<Int> = [5, 4, 3, 2, 1];
function compare(a:Int, b:Int)
{
return a - b;
}
// Just make sure these don't crash.
ArraySortTools.mergeSort(null, compare);
ArraySortTools.mergeSort([], null);
ArraySortTools.mergeSort(null, null);
// Make sure these throw an exception.
try
{
FunkinAssert.validateThrows(function() {
ArraySortTools.mergeSort(testArray, null);
Assert.fail("Function should have thrown an exception.");
}
catch (e)
{
Assert.areEqual("No comparison function provided.", e);
}
}, function(err) {
return err == 'No comparison function provided.';
});
}
@Test
@ -97,6 +107,18 @@ class ArraySortToolsTest extends FunkinTest
Assert.areEqual(testArray2[1], 6);
Assert.areEqual(testArray2[2], 9);
Assert.areEqual(testArray2[3], 12);
}
@Test
@:nullSafety(Off)
public function testQuickSortNull()
{
var testArray:Array<Int> = [5, 4, 3, 2, 1];
function compare(a:Int, b:Int)
{
return a - b;
}
// Just make sure these don't crash.
ArraySortTools.quickSort([], compare);
@ -105,16 +127,11 @@ class ArraySortToolsTest extends FunkinTest
ArraySortTools.quickSort(null, null);
// Make sure these throw an exception.
try
{
FunkinAssert.validateThrows(function() {
ArraySortTools.quickSort(testArray, null);
Assert.fail("Function should have thrown an exception.");
}
catch (e)
{
Assert.areEqual("No comparison function provided.", e);
}
}, function(err) {
return err == 'No comparison function provided.';
});
}
@Test
@ -143,6 +160,18 @@ class ArraySortToolsTest extends FunkinTest
Assert.areEqual(testArray2[1], 6);
Assert.areEqual(testArray2[2], 9);
Assert.areEqual(testArray2[3], 12);
}
@Test
@:nullSafety(Off)
public function testInsertionSortNull()
{
var testArray:Array<Int> = [5, 4, 3, 2, 1];
function compare(a:Int, b:Int)
{
return a - b;
}
// Just make sure these don't crash.
ArraySortTools.insertionSort([], compare);
@ -151,15 +180,10 @@ class ArraySortToolsTest extends FunkinTest
ArraySortTools.insertionSort(null, null);
// Make sure these throw an exception.
try
{
FunkinAssert.validateThrows(function() {
ArraySortTools.insertionSort(testArray, null);
Assert.fail("Function should have thrown an exception.");
}
catch (e)
{
Assert.areEqual("No comparison function provided.", e);
}
}, function(err) {
return err == 'No comparison function provided.';
});
}
}

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.ArrayTools;
@:nullSafety
@:access(funkin.util.tools.ArrayTools)
class ArrayToolsTest extends FunkinTest
{

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.IteratorTools;
@:nullSafety
@:access(funkin.util.tools.IteratorTools)
class IteratorToolsTest extends FunkinTest
{

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.MapTools;
@:nullSafety
@:access(funkin.util.tools.MapTools)
class MapToolsTest extends FunkinTest
{

View file

@ -5,6 +5,7 @@ import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.StringTools;
@:nullSafety
@:access(funkin.util.tools.StringTools)
class StringToolsTest extends FunkinTest
{

View file

@ -0,0 +1,3 @@
#!/bin/bash
haxe test-cpp.hxml

View file

@ -0,0 +1,3 @@
#!/bin/bash
haxe test-web.hxml

View file

@ -0,0 +1,3 @@
REM Launches the unit tests for the native target on Windows.
haxe test-web.hxml

View file

@ -1,11 +1,6 @@
# Updates TestSuite.hx to include all tests
#-cmd haxelib run munit gen
# Actually performs the tests
#-cmd haxelib run munit test -debug -coverage
# -debug may or may not be needed
# -coverage adds code coverage reporting
# Legacy style. Doesn't give detailed coverage reports,
# but it works without crashing.
-cmd haxelib run munit gen
-cmd haxelib run lime test cpp
# Actually performs the tests
# Lime is used for compatibility reasons, and build flags in `project.xml` ensure coverage is enabled
-cmd haxelib run lime test cpp -debug

View file

@ -1,3 +1,39 @@
## CPP
--next
--verbose
--debug
-main TestMain
-cpp build/cpp_test
# Funkin' deps
-lib lime
-lib openfl
-lib flixel
-lib flixel-addons
-lib flixel-ui
-lib hscript
-lib polymod
-lib haxeui-core
-lib haxeui-flixel
-lib flxanimate
-lib hxCodec
-lib thx.semver
-lib json2object
-lib tink_json
# Test deps
-lib munit
-lib hamcrest
-lib mcover
-lib mockatoo
# Class paths
-cp source
-cp ../../source
# Flixel macros
--remap flash:openfl
--macro flixel.system.macros.FlxDefines.run()
# Funkin' macros
--macro addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')
## JavaScript HTML5
--next
-js build/js_test.js