From 3828179218d26f0e8adb9bab89786e22072a8f2c Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 30 Aug 2023 18:29:09 -0400 Subject: [PATCH 1/7] Chart Editor: Several bug fixes (#139) * Fix a crash bug caused by unhandled null value. * Fix bug where note preview wouldn't update on song load * Fix note preview not updating when loading inst, fixed BF health icon positioning --- .../ui/debug/charting/ChartEditorDialogHandler.hx | 6 ++++++ .../ui/debug/charting/ChartEditorNoteSprite.hx | 2 ++ .../funkin/ui/debug/charting/ChartEditorState.hx | 14 ++++++++------ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index 63dc8bd92..a93e9ec73 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -833,6 +833,9 @@ class ChartEditorDialogHandler songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import'); songChartData.set(variation, songChartDataVariation); + state.notePreviewDirty = true; + state.notePreviewViewportBoundsDirty = true; + state.noteDisplayDirty = true; // Tell the user the load was successful. NotificationManager.instance.addNotification( @@ -858,6 +861,9 @@ class ChartEditorDialogHandler songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import'); songChartData.set(variation, songChartDataVariation); + state.notePreviewDirty = true; + state.notePreviewViewportBoundsDirty = true; + state.noteDisplayDirty = true; // Tell the user the load was successful. NotificationManager.instance.addNotification( diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx index 0adbf1a20..1c440f6ed 100644 --- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx +++ b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx @@ -132,6 +132,8 @@ class ChartEditorNoteSprite extends FlxSprite public function updateNotePosition(?origin:FlxObject) { + if (this.noteData == null) return; + var cursorColumn:Int = this.noteData.data; if (cursorColumn < 0) cursorColumn = 0; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 83c052050..86c0a11eb 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -569,7 +569,6 @@ class ChartEditorState extends HaxeUIState /** * Whether the note preview graphic needs to be FULLY rebuilt. - * The Bitmap can be modified by individual commands without using this. */ var notePreviewDirty:Bool = true; @@ -1317,7 +1316,7 @@ class ChartEditorState extends HaxeUIState healthIconBF = new HealthIcon(currentSongCharacterPlayer); healthIconBF.autoUpdate = false; healthIconBF.size.set(0.5, 0.5); - healthIconBF.x = gridTiledSprite.x + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; + healthIconBF.x = gridTiledSprite.x + gridTiledSprite.width + 15; healthIconBF.y = gridTiledSprite.y + 5; healthIconBF.flipX = true; add(healthIconBF); @@ -1797,7 +1796,7 @@ class ChartEditorState extends HaxeUIState if (healthIconBF != null) { // Base X position to the right of the grid. - var baseHealthIconXPos:Float = gridTiledSprite?.x ?? 0.0 + GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + 15; + var baseHealthIconXPos:Float = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 15); // Will be 0 when not bopping. When bopping, will increase to push the icon left. var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); healthIconBF.x = baseHealthIconXPos - healthIconOffset; @@ -2718,12 +2717,12 @@ class ChartEditorState extends HaxeUIState trace('Creating new Note... (${renderedNotes.members.length})'); noteSprite.parentState = this; - // Setting note data resets position relative to the grid so we fix that. - noteSprite.updateNotePosition(renderedNotes); - // The note sprite handles animation playback and positioning. noteSprite.noteData = noteData; + // Setting note data resets position relative to the grid so we fix that. + noteSprite.updateNotePosition(renderedNotes); + // Add hold notes that are now visible (and not already displayed). if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1) { @@ -3869,6 +3868,9 @@ class ChartEditorState extends HaxeUIState scrollPositionInPixels = 0; playheadPositionInPixels = 0; + notePreviewDirty = true; + notePreviewViewportBoundsDirty = true; + noteDisplayDirty = true; moveSongToScrollPosition(); } From 279277b18c1c98d49cf3ae59dca4897849865d9c Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 30 Aug 2023 18:31:59 -0400 Subject: [PATCH 2/7] 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 --- .github/workflows/build-shit.yml | 15 ++ .vscode/extensions.json | 3 +- .vscode/settings.json | 9 +- .../charting/ChartEditorDialogHandler.hx | 2 +- .../ui/debug/charting/ChartEditorState.hx | 2 +- source/funkin/util/macro/InlineMacro.hx | 37 +++++ source/funkin/util/macro/MacroUtil.hx | 5 + tests/unit/README.md | 6 + .../preload/data/levels/blankpathtest.json | 47 ++++++ tests/unit/project.xml | 19 ++- tests/unit/report-linux.sh | 4 + tests/unit/source/FunkinAssert.hx | 65 +++++++- tests/unit/source/FunkinTest.hx | 3 +- tests/unit/source/MockTest.hx | 15 +- tests/unit/source/TestMain.hx | 63 +++++--- tests/unit/source/funkin/ConductorTest.hx | 34 ++-- .../source/funkin/data/BaseRegistryTest.hx | 2 + .../funkin/data/level/LevelRegistryTest.hx | 146 ++++++++++++++++++ .../data/notestyle/NoteStyleRegistryTest.hx | 48 +++--- .../play/notes/notestyle/NoteStyleTest.hx | 12 +- .../unit/source/funkin/util/BezierUtilTest.hx | 1 + .../source/funkin/util/ClipboardUtilTest.hx | 1 + tests/unit/source/funkin/util/DateUtilTest.hx | 1 + .../source/funkin/util/SerializerUtilTest.hx | 1 + tests/unit/source/funkin/util/SortUtilTest.hx | 1 + .../source/funkin/util/VersionUtilTest.hx | 20 ++- .../funkin/util/assets/DataAssetsTest.hx | 1 + .../util/assets/FlxAnimationUtilTest.hx | 1 + .../funkin/util/tools/ArraySortToolsTest.hx | 78 ++++++---- .../funkin/util/tools/ArrayToolsTest.hx | 1 + .../funkin/util/tools/IteratorToolsTest.hx | 1 + .../source/funkin/util/tools/MapToolsTest.hx | 1 + .../funkin/util/tools/StringToolsTest.hx | 1 + tests/unit/start-linux-native.sh | 3 + tests/unit/start-linux-web.sh | 3 + tests/unit/start-win-web.bat | 3 + tests/unit/test-cpp.hxml | 13 +- tests/unit/test.hxml-old | 36 +++++ 38 files changed, 583 insertions(+), 121 deletions(-) create mode 100644 source/funkin/util/macro/InlineMacro.hx create mode 100644 tests/unit/assets/preload/data/levels/blankpathtest.json create mode 100644 tests/unit/report-linux.sh create mode 100644 tests/unit/source/funkin/data/level/LevelRegistryTest.hx create mode 100644 tests/unit/start-linux-native.sh create mode 100644 tests/unit/start-linux-web.sh create mode 100644 tests/unit/start-win-web.bat 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 From 874c57bde3967a45d4a0b1b3ffd882a782af2b2f Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 30 Aug 2023 18:32:51 -0400 Subject: [PATCH 3/7] GitHub Actions: Release builds (#140) * 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 * GitHub Actions now creates release builds (for improved performance when testing) --------- Co-authored-by: Cameron Taylor --- .github/workflows/build-shit.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index b031a90c0..ed509b44d 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -30,12 +30,12 @@ jobs: - name: Build game run: | sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev - haxelib run lime build html5 -debug --times + haxelib run lime build html5 -release --times ls - uses: ./.github/actions/upload-itch with: butler-key: ${{ secrets.BUTLER_API_KEY}} - build-dir: export/debug/html5/bin + build-dir: export/release/html5/bin target: html5 create-nightly-win: needs: check_date @@ -51,12 +51,12 @@ jobs: - uses: ./.github/actions/setup-haxeshit - name: Build game run: | - haxelib run lime build windows -debug -DNO_REDIRECT_ASSETS_FOLDER + haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER dir - uses: ./.github/actions/upload-itch with: butler-key: ${{ secrets.BUTLER_API_KEY}} - build-dir: export/debug/windows/bin + build-dir: export/release/windows/bin target: win test-unit-win: needs: create-nightly-win From dd4403d673e0954e4456bfbcc4e8c0d233dd1061 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 4 Sep 2023 22:06:52 -0400 Subject: [PATCH 4/7] Update hmm.json to update flixel-addons (#146) * Update hmm.json to include new flixel-addons * flixel-addons update --------- Co-authored-by: Cameron Taylor --- hmm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmm.json b/hmm.json index a3226281b..922e6f599 100644 --- a/hmm.json +++ b/hmm.json @@ -18,7 +18,7 @@ "name": "flixel-addons", "type": "git", "dir": null, - "ref": "f107166de3e830648e8fbf3da5526d4b94aa7dfc", + "ref": "c8c41e26d463aaf2edc0582fb23b6e228235bd16", "url": "https://github.com/EliteMasterEric/flixel-addons" }, { From d4e601ebaa5a307d178d203d90164894359b0b62 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 4 Sep 2023 22:10:24 -0400 Subject: [PATCH 5/7] Update action.yml (#147) --- .github/actions/setup-haxeshit/action.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml index ec8ed52d8..38a504442 100644 --- a/.github/actions/setup-haxeshit/action.yml +++ b/.github/actions/setup-haxeshit/action.yml @@ -10,14 +10,6 @@ runs: run: | haxelib config shell: bash - - name: Restore/create existing haxelib cache for faster downloads - uses: actions/cache@v3 - id: cache-haxelib-windows - with: - # wha? - key: cache-haxelib-${{ runner.os }}-${{ hashFiles('**/hmm.json')}} - path: | - .haxelib/ - name: Installing Haxe lol run: | haxe -version From bb09cc19e7a88b1da2ac95b60448524689f9a08a Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 4 Sep 2023 22:18:45 -0400 Subject: [PATCH 6/7] HTML5: Several fixes to asset loading (#136) * Update Polymod to fix async script loading. * Got into the Main Menu but playstate doesn't work * Got into PlayState but GF script breaks! --------- Co-authored-by: Cameron Taylor --- hmm.json | 26 +++++++++++++------------ source/funkin/InitState.hx | 5 ++++- source/funkin/modding/PolymodHandler.hx | 1 + 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/hmm.json b/hmm.json index 922e6f599..e2670420a 100644 --- a/hmm.json +++ b/hmm.json @@ -23,8 +23,10 @@ }, { "name": "flixel-ui", - "type": "haxelib", - "version": "2.5.0" + "type": "git", + "dir": null, + "ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15", + "url": "https://github.com/HaxeFlixel/flixel-ui" }, { "name": "flxanimate", @@ -57,6 +59,13 @@ "ref": "be0b18553189a55fd42821026618a18615b070e3", "url": "https://github.com/haxeui/haxeui-flixel" }, + { + "name": "hmm", + "type": "git", + "dir": null, + "ref": "d514d7786cabf18b90e60fcee38399fd44c2ddfb", + "url": "https://github.com/andywhite37/hmm" + }, { "name": "hscript", "type": "haxelib", @@ -93,7 +102,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "558798adc5bf0e82d70fef589a59ce88892e0b5b", + "ref": "f195121ebec688b417e38ab115185c8d93c349d3", "url": "https://github.com/EliteMasterEric/lime" }, { @@ -128,14 +137,14 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "1591a6c5f1f72e65d711f7e17e8055df41424d94", + "ref": "ef43deb2c68d8a4bcd73abfbd77324fc8220d0c1", "url": "https://github.com/EliteMasterEric/openfl" }, { "name": "polymod", "type": "git", "dir": null, - "ref": "4bcd614103469af79a320898b823d1df8a55c3de", + "ref": "e8a07b81e3bc535238ad8649e38f5d43c46f1b65", "url": "https://github.com/larsiusprime/polymod" }, { @@ -147,13 +156,6 @@ "name": "tink_json", "type": "haxelib", "version": "0.11.0" - }, - { - "name": "hmm", - "type": "git", - "dir": null, - "ref": "d514d7786cabf18b90e60fcee38399fd44c2ddfb", - "url": "https://github.com/andywhite37/hmm" } ] } diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 6c465be9c..82a357ae9 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -1,5 +1,6 @@ package funkin; +import flixel.FlxState; import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond; import flixel.addons.transition.TransitionData; @@ -33,8 +34,10 @@ import Discord.DiscordClient; * The initialization state has several functions: * - Calls code to set up the game, including loading saves and parsing game data. * - Chooses whether to start via debug or via launching normally. + * + * It should not contain any sprites or rendering. */ -class InitState extends FlxTransitionableState +class InitState extends FlxState { /** * Perform a bunch of game setup, then immediately transition to the title screen. diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 3f5752acc..47afb0a30 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -114,6 +114,7 @@ class PolymodHandler // Parse hxc files and register the scripted classes in them. useScriptedClasses: true, + loadScriptsAsync: #if html5 true #else false #end, }); if (loadedModList == null) From 12c8362321472a4b204e3eab569fba46eb02c446 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 12 Sep 2023 18:38:55 -0400 Subject: [PATCH 7/7] Revert OpenFL to the version which has no crash handler but un-breaks the HaxeUI style. --- hmm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmm.json b/hmm.json index e2670420a..50cc68851 100644 --- a/hmm.json +++ b/hmm.json @@ -137,7 +137,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "ef43deb2c68d8a4bcd73abfbd77324fc8220d0c1", + "ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08", "url": "https://github.com/EliteMasterEric/openfl" }, {