diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 794457917..b031a90c0 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -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 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 483db9ea9..c018b89e9 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -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 ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 86ae2b643..80d2bf76a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" + ] } diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index a93e9ec73..e5b2d332c 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -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); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 86c0a11eb..f27fbb6c7 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -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) diff --git a/source/funkin/util/macro/InlineMacro.hx b/source/funkin/util/macro/InlineMacro.hx new file mode 100644 index 000000000..b0e7ed184 --- /dev/null +++ b/source/funkin/util/macro/InlineMacro.hx @@ -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 + { + 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.Context.getBuildFields(); + + // Find the field with the given name. + var targetField:Null = 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; + } +} diff --git a/source/funkin/util/macro/MacroUtil.hx b/source/funkin/util/macro/MacroUtil.hx index a121200ca..2e2c73279 100644 --- a/source/funkin/util/macro/MacroUtil.hx +++ b/source/funkin/util/macro/MacroUtil.hx @@ -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. */ diff --git a/tests/unit/README.md b/tests/unit/README.md index 00fed78e5..ae8d273bf 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -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 diff --git a/tests/unit/assets/preload/data/levels/blankpathtest.json b/tests/unit/assets/preload/data/levels/blankpathtest.json new file mode 100644 index 000000000..709012258 --- /dev/null +++ b/tests/unit/assets/preload/data/levels/blankpathtest.json @@ -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"] +} diff --git a/tests/unit/project.xml b/tests/unit/project.xml index ccfadce8c..63f164607 100644 --- a/tests/unit/project.xml +++ b/tests/unit/project.xml @@ -37,6 +37,9 @@ + + + @@ -80,12 +83,22 @@ - - + + + + + + + + - + + + + diff --git a/tests/unit/report-linux.sh b/tests/unit/report-linux.sh new file mode 100644 index 000000000..bc9ac2b76 --- /dev/null +++ b/tests/unit/report-linux.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cd ./report/ +genhtml -o ./html/ ./lcov.info diff --git a/tests/unit/source/FunkinAssert.hx b/tests/unit/source/FunkinAssert.hx index acfba6723..00c3a9e00 100644 --- a/tests/unit/source/FunkinAssert.hx +++ b/tests/unit/source/FunkinAssert.hx @@ -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(expected:Array, actual:Array, ?info:PosInfos):Void + public static function arraysEqual(expected:Array, ?actual:Array, ?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(expected:Array, actual:Array, ?info:PosInfos):Void + public static function arraysNotEqual(expected:Array, ?actual:Array, ?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); + } } diff --git a/tests/unit/source/FunkinTest.hx b/tests/unit/source/FunkinTest.hx index 7e0e0b26c..8f47a7d36 100644 --- a/tests/unit/source/FunkinTest.hx +++ b/tests/unit/source/FunkinTest.hx @@ -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 = null; public function new() {} diff --git a/tests/unit/source/MockTest.hx b/tests/unit/source/MockTest.hx index c1ae0a90d..308dbb45a 100644 --- a/tests/unit/source/MockTest.hx +++ b/tests/unit/source/MockTest.hx @@ -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); + }); } } diff --git a/tests/unit/source/TestMain.hx b/tests/unit/source/TestMain.hx index a3fbdb428..11eb87b33 100644 --- a/tests/unit/source/TestMain.hx +++ b/tests/unit/source/TestMain.hx @@ -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>(); - 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>(); + 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); + } } /** diff --git a/tests/unit/source/funkin/ConductorTest.hx b/tests/unit/source/funkin/ConductorTest.hx index 856c52186..0cfbe3960 100644 --- a/tests/unit/source/funkin/ConductorTest.hx +++ b/tests/unit/source/funkin/ConductorTest.hx @@ -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 = null; @Before function before() @@ -54,6 +55,9 @@ class ConductorTest extends FunkinTest @Test function testUpdate():Void { + var currentConductorState:Null = 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); diff --git a/tests/unit/source/funkin/data/BaseRegistryTest.hx b/tests/unit/source/funkin/data/BaseRegistryTest.hx index 3532c4403..31e44b6ed 100644 --- a/tests/unit/source/funkin/data/BaseRegistryTest.hx +++ b/tests/unit/source/funkin/data/BaseRegistryTest.hx @@ -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"); diff --git a/tests/unit/source/funkin/data/level/LevelRegistryTest.hx b/tests/unit/source/funkin/data/level/LevelRegistryTest.hx new file mode 100644 index 000000000..3d9cf5d29 --- /dev/null +++ b/tests/unit/source/funkin/data/level/LevelRegistryTest.hx @@ -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 = 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 = 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 = 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 = 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 = LevelRegistry.instance.fetchEntry("blablabla"); + Assert.isNull(result); + + var result2:Null = LevelRegistry.instance.fetchEntry("blankpathtest"); + Assert.isNull(result2); + } +} diff --git a/tests/unit/source/funkin/data/notestyle/NoteStyleRegistryTest.hx b/tests/unit/source/funkin/data/notestyle/NoteStyleRegistryTest.hx index ec33b37d7..8ae9cb31f 100644 --- a/tests/unit/source/funkin/data/notestyle/NoteStyleRegistryTest.hx +++ b/tests/unit/source/funkin/data/notestyle/NoteStyleRegistryTest.hx @@ -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 = 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> = 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 = 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 = NoteStyleRegistry.instance.fetchEntry("blablabla"); - Assert.areEqual(result, null); + Assert.isNull(result); } @Test - public function testFetchDefault() + public function testFetchDefault():Void { var nsrMock:NoteStyleRegistry = mock(NoteStyleRegistry); diff --git a/tests/unit/source/funkin/play/notes/notestyle/NoteStyleTest.hx b/tests/unit/source/funkin/play/notes/notestyle/NoteStyleTest.hx index 31e8e38ae..6b4b46c10 100644 --- a/tests/unit/source/funkin/play/notes/notestyle/NoteStyleTest.hx +++ b/tests/unit/source/funkin/play/notes/notestyle/NoteStyleTest.hx @@ -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 = 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 = NoteStyleRegistry.instance.fetchEntry("funkin"); + var target2:Null = 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)); diff --git a/tests/unit/source/funkin/util/BezierUtilTest.hx b/tests/unit/source/funkin/util/BezierUtilTest.hx index 90186c111..5ff2ade65 100644 --- a/tests/unit/source/funkin/util/BezierUtilTest.hx +++ b/tests/unit/source/funkin/util/BezierUtilTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/ClipboardUtilTest.hx b/tests/unit/source/funkin/util/ClipboardUtilTest.hx index 9efd96d34..311160d6b 100644 --- a/tests/unit/source/funkin/util/ClipboardUtilTest.hx +++ b/tests/unit/source/funkin/util/ClipboardUtilTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/DateUtilTest.hx b/tests/unit/source/funkin/util/DateUtilTest.hx index c8adb3824..35c7bab9c 100644 --- a/tests/unit/source/funkin/util/DateUtilTest.hx +++ b/tests/unit/source/funkin/util/DateUtilTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/SerializerUtilTest.hx b/tests/unit/source/funkin/util/SerializerUtilTest.hx index d2adc3350..6a0152376 100644 --- a/tests/unit/source/funkin/util/SerializerUtilTest.hx +++ b/tests/unit/source/funkin/util/SerializerUtilTest.hx @@ -13,6 +13,7 @@ typedef FooBar = c:Int }; +@:nullSafety @:access(funkin.util.SerializerUtil) class SerializerUtilTest extends FunkinTest { diff --git a/tests/unit/source/funkin/util/SortUtilTest.hx b/tests/unit/source/funkin/util/SortUtilTest.hx index 4720c3da6..1a39bf655 100644 --- a/tests/unit/source/funkin/util/SortUtilTest.hx +++ b/tests/unit/source/funkin/util/SortUtilTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/VersionUtilTest.hx b/tests/unit/source/funkin/util/VersionUtilTest.hx index 55848955a..517b37e5d 100644 --- a/tests/unit/source/funkin/util/VersionUtilTest.hx +++ b/tests/unit/source/funkin/util/VersionUtilTest.hx @@ -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 = 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 = VersionUtil.getVersionFromJSON(jsonStr); + }); + + var jsonStr2:String = "{ \"blah\": \"3.1.0\" }"; + + var version2:Null = VersionUtil.getVersionFromJSON(jsonStr2); + + Assert.isNull(version2); + } } diff --git a/tests/unit/source/funkin/util/assets/DataAssetsTest.hx b/tests/unit/source/funkin/util/assets/DataAssetsTest.hx index 7612d4706..b3df036e5 100644 --- a/tests/unit/source/funkin/util/assets/DataAssetsTest.hx +++ b/tests/unit/source/funkin/util/assets/DataAssetsTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/assets/FlxAnimationUtilTest.hx b/tests/unit/source/funkin/util/assets/FlxAnimationUtilTest.hx index 990d998f2..02b03055d 100644 --- a/tests/unit/source/funkin/util/assets/FlxAnimationUtilTest.hx +++ b/tests/unit/source/funkin/util/assets/FlxAnimationUtilTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/tools/ArraySortToolsTest.hx b/tests/unit/source/funkin/util/tools/ArraySortToolsTest.hx index 0bcef00b5..b9518151b 100644 --- a/tests/unit/source/funkin/util/tools/ArraySortToolsTest.hx +++ b/tests/unit/source/funkin/util/tools/ArraySortToolsTest.hx @@ -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 = [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 = [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 = [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.'; + }); } } diff --git a/tests/unit/source/funkin/util/tools/ArrayToolsTest.hx b/tests/unit/source/funkin/util/tools/ArrayToolsTest.hx index 68adb833b..df8d364f2 100644 --- a/tests/unit/source/funkin/util/tools/ArrayToolsTest.hx +++ b/tests/unit/source/funkin/util/tools/ArrayToolsTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/tools/IteratorToolsTest.hx b/tests/unit/source/funkin/util/tools/IteratorToolsTest.hx index deaea352b..2c5a406c6 100644 --- a/tests/unit/source/funkin/util/tools/IteratorToolsTest.hx +++ b/tests/unit/source/funkin/util/tools/IteratorToolsTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/tools/MapToolsTest.hx b/tests/unit/source/funkin/util/tools/MapToolsTest.hx index c3fe34461..1ada408b6 100644 --- a/tests/unit/source/funkin/util/tools/MapToolsTest.hx +++ b/tests/unit/source/funkin/util/tools/MapToolsTest.hx @@ -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 { diff --git a/tests/unit/source/funkin/util/tools/StringToolsTest.hx b/tests/unit/source/funkin/util/tools/StringToolsTest.hx index 215b5e402..ff2ee2dbd 100644 --- a/tests/unit/source/funkin/util/tools/StringToolsTest.hx +++ b/tests/unit/source/funkin/util/tools/StringToolsTest.hx @@ -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 { diff --git a/tests/unit/start-linux-native.sh b/tests/unit/start-linux-native.sh new file mode 100644 index 000000000..5110b410f --- /dev/null +++ b/tests/unit/start-linux-native.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +haxe test-cpp.hxml diff --git a/tests/unit/start-linux-web.sh b/tests/unit/start-linux-web.sh new file mode 100644 index 000000000..68e785b90 --- /dev/null +++ b/tests/unit/start-linux-web.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +haxe test-web.hxml diff --git a/tests/unit/start-win-web.bat b/tests/unit/start-win-web.bat new file mode 100644 index 000000000..27664f96b --- /dev/null +++ b/tests/unit/start-win-web.bat @@ -0,0 +1,3 @@ +REM Launches the unit tests for the native target on Windows. + +haxe test-web.hxml diff --git a/tests/unit/test-cpp.hxml b/tests/unit/test-cpp.hxml index 9379f841d..e25d92539 100644 --- a/tests/unit/test-cpp.hxml +++ b/tests/unit/test-cpp.hxml @@ -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 + diff --git a/tests/unit/test.hxml-old b/tests/unit/test.hxml-old index d86661fc8..e8a7d9bde 100644 --- a/tests/unit/test.hxml-old +++ b/tests/unit/test.hxml-old @@ -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