Merge remote-tracking branch 'origin/master' into bugfix/chart-editor-playbar-buttons

This commit is contained in:
EliteMasterEric 2023-09-12 18:51:16 -04:00
commit 71e1fccce1
43 changed files with 623 additions and 153 deletions

View file

@ -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

View file

@ -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,10 +51,25 @@ 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
runs-on: windows-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v3
with:
submodules: 'recursive'
- uses: ./.github/actions/setup-haxeshit
- name: Run unit tests
run: |
cd ./tests/unit/
./start-win-native.bat

View file

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

View file

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

View file

@ -18,13 +18,15 @@
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "f107166de3e830648e8fbf3da5526d4b94aa7dfc",
"ref": "c8c41e26d463aaf2edc0582fb23b6e228235bd16",
"url": "https://github.com/EliteMasterEric/flixel-addons"
},
{
"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": "d33d489a137ff8fdece4994cf1302f0b6334ed08",
"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"
}
]
}

View file

@ -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.

View file

@ -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)

View file

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

View file

@ -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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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