1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-11-21 22:33:26 +00:00

Unit Test Suite (#119)

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

* Set versions for flixel-ui and hamcrest

---------

Co-authored-by: Cameron Taylor <cameron.taylor.ninja@gmail.com>
This commit is contained in:
Eric 2023-08-22 04:27:30 -04:00 committed by GitHub
parent ab34bbdcee
commit 42d8d55067
87 changed files with 3123 additions and 290 deletions

View file

@ -1 +0,0 @@
funkin.me

View file

@ -0,0 +1,31 @@
# Funkin' Debug Hotkeys
`F4` (EVERYWHERE) - Leave Current State and move to Main Menu
`F5` (EVERYWHERE) - Hot Reload Data Files
`Y` (Title Screen) - WOAH
`~` (Main Menu) - Access Debug Menu
`U` (Play) - Open Stage Editor State
`H` (Play) - Show/Hide HUD
`1` (Play) - End Song
`2` (Play) - Add 10% Health
`3` (Play) - Subtract 5% Health
`7` (Play) - (NOT WORKING) Open Chart Editor
`8` (Play) - Open Animation Editor
`9` (Play) - (Easter Egg) Classic Health Icon
`PGUP`/`Fn+Up` (Play) - Skip Forward In Time
`PGDN`/`Fn+Down` (Play) - 🦃 That's right, we're going to go BACK IN TIME
`F` (Freeplay Menu) - Move to Favorites
`P` (Freeplay Menu) - Switch to Pico (probably doesn't work)
`T` (Freeplay Menu) - Start typing in search bar
`Q` (Freeplay Menu) - Back one letter
`E` (Freeplay Menu) - Forward one letter
`Arrows` (Stage Editor) - Move Prop
`Ctrl-Z` (Stage Editor) - Undo
`Y` (Stage Editor) - Leave Stage Editor
`H` (Pause Menu) - Hide the Pause Menu UI (good for screenshots!)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

View file

@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<!--
@brandybuizel
@ninja_muffin2.4
-->
<!--Let's get meta bois-->
<head>
<meta charset="utf-8">
<title>Friday Night Funkin'</title>
<meta name="description" content="A dope ass rhythm game">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="styles.css" rel="stylesheet" type="text/css">
<!-- <link href="website/mm.ico" rel="icon" type="image/x-icon"> -->
<link rel="shortcut icon" type="image/png" href="../art/icon.png" />
</head>
<body>
<!--Header-->
<header>
</header>
<marquee width="1920" scrollamount="2" scrolldelay="10" truespeed="truespeed">Friday Night Funkin</marquee>
<!--Hot tortilla wrap-->
<div id="wrapper">
<h1>Friday Night Funkin' - Rhythm game extraordinaire</h1>
<div id="coolervidwrapper">
<div class='videoWrapper'>
<iframe width="560" height="349" src="https://www.youtube.com/embed/HMNKUo3CCpU" frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
</div>
<div class="coolBox hovertext" id="twitter">
<p class="description">Friday Night Funkin' is a dope ass rhythm game.</p>
<p class='description'>It is made by <a href="https://twitter.com/ninja_muffin99">ninjamuffin99 (programmer)</a>, <a
href="https://twitter.com/PhantomArcade3K">PhantomArcade (animator)</a>, <a
href="https://twitter.com/evilsk8r">evilsk8r (artist)</a>,
and <a href="https://twitter.com/kawaisprite">Kawaisprite (musician)</a> originally for Ludum Dare 47.</p>
</div>
<div class="linktext hovertext" id="ng">
<a href="https://www.newgrounds.com/portal/view/770371">Play demo on Newgrounds</a>
</div>
<div class='linktext hovertext' id="itch">
<a href="https://ninja-muffin24.itch.io/funkin">Support the game on Itch.io</a>
</div>
</div>
</body>
</html>

View file

@ -1,7 +0,0 @@
this is for funkin website stuff
if you're compiling game, you can ignore this folder completely!
if not, dawg check out [funkin.me](https://funkin.me)
note that i probably won't accept any major pull requests for this
cuz I wanna make the website as a nice lil project to teach myself HTML and CSS and all that!!

View file

@ -1,97 +0,0 @@
.coolBox {
background: #1a1a1aCC;
margin: 40px 4vw;
padding: 25px;
font-size: 120%;
border-radius: 10px;
}
#wrapper
{
padding: 20px;
margin: auto;
}
marquee {
margin: auto;
text-align: center;
display: inline-block;
}
h1 {
text-align: center;
}
body {
background-image: url('img/skin-funkin-cardbordtoast.jpg');
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
color:white;
background-color: #000;
font-family: Arial, Helvetica, sans-serif;
}
a { color:inherit;
text-decoration: none;}
.hovertext a{
color:white;
text-shadow: 0px 0px #00000077;
transition: color 1s, text-shadow 1s;
}
.hovertext a:hover
{
text-shadow: 2px 2px #00000077;
color: #ffb50e;
}
.linktext {
text-align: center;
display:block;
font-size: 130%;
padding-bottom: 10px;
}
.linktext a {
font-weight: bold;
}
.linktext a:hover {
border-radius: 5px;
}
#itch a:hover
{
color: #fa5c5c;
}
#twitter a:hover
{
color: #00acee;
}
#coolervidwrapper {
padding-left: min(12em, 75%);
padding-right: min(12em, 75%);
min-width: 30%;
}
.videoWrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
max-block-size: 1400px;
}
.videoWrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}

View file

@ -24,7 +24,7 @@
{
"name": "flixel-ui",
"type": "haxelib",
"version": "2.4.0"
"version": "2.5.0"
},
{
"name": "flxanimate",
@ -38,6 +38,11 @@
"type": "haxelib",
"version": "3.5.0"
},
{
"name": "hamcrest",
"type": "haxelib",
"version": "3.0.0"
},
{
"name": "haxeui-core",
"type": "git",
@ -92,12 +97,33 @@
"url": "https://github.com/EliteMasterEric/lime"
},
{
"name": "mockatoo",
"name": "mconsole",
"type": "git",
"dir": null,
"ref": "master",
"url": "https://github.com/massive-oss/mconsole"
},
{
"name": "mcover",
"type": "git",
"dir": "src",
"ref": "master",
"url": "https://github.com/massive-oss/mcover"
},
{
"name": "mockatoo",
"type": "git",
"dir": "src",
"ref": "master",
"url": "https://github.com/EliteMasterEric/mockatoo"
},
{
"name": "munit",
"type": "git",
"dir": "src",
"ref": "master",
"url": "https://github.com/EliteMasterEric/MassiveUnit"
},
{
"name": "openfl",
"type": "git",
@ -121,6 +147,13 @@
"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

@ -180,9 +180,15 @@ class Conductor
*/
public static function forceBPM(?bpm:Float = null)
{
if (bpm != null) trace('[CONDUCTOR] Forcing BPM to ' + bpm);
if (bpm != null)
{
trace('[CONDUCTOR] Forcing BPM to ${bpm}');
}
else
trace('[CONDUCTOR] Resetting BPM to default');
{
// trace('[CONDUCTOR] Resetting BPM to default');
}
Conductor.bpmOverride = bpm;
}
@ -272,9 +278,9 @@ class Conductor
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
{
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
currentTimeChange.beatTime = prevTimeChange.beatTime
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC)
+ 0.01;
currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
4);
}
}
}
@ -282,7 +288,10 @@ class Conductor
timeChanges.push(currentTimeChange);
}
trace('Done mapping time changes: ' + timeChanges);
if (timeChanges.length > 0)
{
trace('Done mapping time changes: ${timeChanges}' + timeChanges);
}
// Update currentStepTime
Conductor.update(Conductor.songPosition);

View file

@ -50,7 +50,7 @@ enum Control
#end
}
@:enum
enum
abstract Action(String) to String from String
{
var UI_UP = "ui_up";

View file

@ -492,22 +492,6 @@ class FreeplayState extends MusicBeatSubState
}
}
// if (regexp != null)
// tempSongs = songs.filter(item -> regexp.match(item.songName));
// tempSongs.sort(function(a, b):Int
// {
// var tempA = a.songName.toUpperCase();
// var tempB = b.songName.toUpperCase();
// if (tempA < tempB)
// return -1;
// else if (tempA > tempB)
// return 1;
// else
// return 0;
// });
for (i in 0...tempSongs.length)
{
var funnyMenu:SongMenuItem = new SongMenuItem(FlxG.width, (i * 150) + 160, tempSongs[i].songName);

View file

@ -2,6 +2,7 @@ package funkin.data;
import openfl.Assets;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import haxe.Constraints.Constructible;
/**
@ -24,16 +25,23 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
final entries:Map<String, T>;
/**
* The version rule to use when loading entries.
* If the entry's version does not match this rule, migration is needed.
*/
final versionRule:thx.semver.VersionRule;
// public abstract static final instance:BaseRegistry<T, J> = new BaseRegistry<>();
/**
* @param registryId A readable ID for this registry, used when logging.
* @param dataFilePath The path (relative to `assets/data`) to search for JSON files.
*/
public function new(registryId:String, dataFilePath:String)
public function new(registryId:String, dataFilePath:String, versionRule:thx.semver.VersionRule = null)
{
this.registryId = registryId;
this.dataFilePath = dataFilePath;
this.versionRule = versionRule == null ? "1.0.x" : versionRule;
this.entries = new Map<String, T>();
}
@ -125,6 +133,13 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
return 'Registry(' + registryId + ', ${countEntries()} entries)';
}
public function fetchEntryVersion(id:String):Null<thx.semver.Version>
{
var entryStr:String = loadEntryFile(id);
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
return entryVersion;
}
function log(message:String):Void
{
trace('[' + registryId + '] ' + message);
@ -154,10 +169,36 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class annd
* NOTE: Must be implemented on the implementation class.
*/
public abstract function parseEntryData(id:String):Null<J>;
/**
* Read, parse, and validate the JSON data and produce the corresponding data object,
* accounting for old versions of the data.
*
* NOTE: Extend this function to handle migration.
*/
public function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<J>
{
// If a version rule is not specified, do not check against it.
if (versionRule == null || VersionUtil.validateVersion(version, versionRule))
{
return parseEntryData(id);
}
else
{
throw '[${registryId}] Entry ${id} does not support migration.';
}
// Example:
// if (VersionUtil.validateVersion(version, "0.1.x")) {
// return parseEntryData_v0_1_x(id);
// } else {
// super.parseEntryDataWithMigration(id, version);
// }
}
/**
* Retrieve the list of scripted class names to load.
* @return An array of scripted class names.

View file

@ -11,13 +11,15 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final LEVEL_DATA_VERSION:String = "1.0.0";
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0";
public static final LEVEL_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final instance:LevelRegistry = new LevelRegistry();
public function new()
{
super('LEVEL', 'levels');
super('LEVEL', 'levels', LEVEL_DATA_VERSION_RULE);
}
/**
@ -34,7 +36,7 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
if (parser.errors.length > 0)
{
trace('Failed to parse entry data: ${id}');
trace('[${registryId}] Failed to parse entry data: ${id}');
for (error in parser.errors)
{
trace(error);

View file

@ -11,7 +11,9 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateNoteStyleData()` function.
*/
public static final NOTE_STYLE_DATA_VERSION:String = "1.0.0";
public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.0.0";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final DEFAULT_NOTE_STYLE_ID:String = "funkin";
@ -19,7 +21,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
public function new()
{
super('NOTESTYLE', 'notestyles');
super('NOTESTYLE', 'notestyles', NOTE_STYLE_DATA_VERSION_RULE);
}
public function fetchDefault():NoteStyle
@ -43,7 +45,7 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
if (parser.errors.length > 0)
{
trace('Failed to parse entry data: ${id}');
trace('[${registryId}] Failed to parse entry data: ${id}');
for (error in parser.errors)
{
trace(error);

View file

@ -40,27 +40,6 @@ class DJBoyfriend extends FlxSprite
{
super.update(elapsed);
if (FlxG.keys.justPressed.LEFT)
{
animOffsets["confirm"] = [animOffsets["confirm"][0] + 1, animOffsets["confirm"][1]];
applyAnimOffset();
}
else if (FlxG.keys.justPressed.RIGHT)
{
animOffsets["confirm"] = [animOffsets["confirm"][0] - 1, animOffsets["confirm"][1]];
applyAnimOffset();
}
else if (FlxG.keys.justPressed.UP)
{
animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] + 1];
applyAnimOffset();
}
else if (FlxG.keys.justPressed.DOWN)
{
animOffsets["confirm"] = [animOffsets["confirm"][0], animOffsets["confirm"][1] - 1];
applyAnimOffset();
}
switch (currentState)
{
case Intro:

View file

@ -103,9 +103,6 @@ class SongMenuItem extends FlxSpriteGroup
y = CoolUtil.coolLerp(y, targetPos.y, 0.4);
}
if (FlxG.keys.justPressed.ALT) selected = false;
if (FlxG.keys.justPressed.CONTROL) selected = true;
super.update(elapsed);
}

View file

@ -24,7 +24,7 @@ class PolymodHandler
* Bug fixes increment the patch version, new features increment the minor version.
* Changes that break old mods increment the major version.
*/
static final API_VERSION = "0.1.0";
static final API_VERSION:String = "0.1.0";
/**
* Where relative to the executable that mods are located.

View file

@ -67,19 +67,26 @@ class ModuleHandler
{
modulePriorityOrder = moduleCache.keys().array();
modulePriorityOrder.sort(function(a:String, b:String):Int {
var aModule:Module = moduleCache.get(a);
var bModule:Module = moduleCache.get(b);
modulePriorityOrder.sort(sortByPriority);
}
if (aModule.priority != bModule.priority)
{
return aModule.priority - bModule.priority;
}
else
{
return SortUtil.alphabetically(a, b);
}
});
/**
* Given two module IDs, sort them by priority.
* @return 1 or -1 depending on which module has a higher priority.
*/
static function sortByPriority(a:String, b:String)
{
var aModule:Module = moduleCache.get(a);
var bModule:Module = moduleCache.get(b);
if (aModule.priority != bModule.priority)
{
return aModule.priority - bModule.priority;
}
else
{
return SortUtil.alphabetically(a, b);
}
}
public static function getModule(moduleId:String):Module

View file

@ -146,13 +146,13 @@ class HealthIcon extends FlxSprite
*/
public function toggleOldIcon():Void
{
if (characterId == 'beta')
if (characterId == 'bf-old')
{
characterId = PlayState.instance.currentPlayerId;
}
else
{
characterId = 'beta';
characterId = 'bf-old';
}
}

View file

@ -666,14 +666,6 @@ class PlayState extends MusicBeatSubState
super.update(elapsed);
if (FlxG.keys.justPressed.U)
{
// hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
disableKeys = true;
persistentUpdate = false;
openSubState(new StageOffsetSubState());
}
updateHealthBar();
updateScoreText();

View file

@ -382,7 +382,7 @@ class CharacterDataParser
input.version = CHARACTER_DATA_VERSION;
}
if (!VersionUtil.validateVersion(input.version, CHARACTER_DATA_VERSION_RULE))
if (!VersionUtil.validateVersionStr(input.version, CHARACTER_DATA_VERSION_RULE))
{
trace('ERROR: Could not load character data for "$id": bad version (got ${input.version}, expected ${CHARACTER_DATA_VERSION_RULE})');
return null;

View file

@ -5,7 +5,7 @@ import flixel.group.FlxSpriteGroup;
import flixel.util.FlxColor;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.system.FlxSound;
import flixel.sound.FlxSound;
import funkin.util.SortUtil;
import flixel.util.FlxSort;
import funkin.modding.events.ScriptEvent;
@ -18,7 +18,7 @@ import flixel.addons.display.FlxPieDial;
/**
* A high-level handler for dialogue.
*
*
* This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
*/
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
@ -299,7 +299,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
/**
* Dispatch an event to attempt to advance the conversation.
* This is done once at the start of the conversation, and once whenever the user presses CONFIRM to advance the conversation.
*
*
* The broadcast event may be cancelled by modules or ScriptedConversations. This will prevent the conversation from actually advancing.
* This is useful if you want to manually play an animation or something.
*/
@ -365,7 +365,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
/**
* Dispatch an event to attempt to immediately end the conversation.
*
*
* The broadcast event may be cancelled by modules or ScriptedConversations. This will prevent the conversation from being cancelled.
* This is useful if you want to prevent an animation from being skipped or something.
*/
@ -567,7 +567,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
/**
* As this event is dispatched to the Conversation, it is also dispatched to the active speaker.
* @param event
* @param event
*/
function propagateEvent(event:ScriptEvent):Void
{

View file

@ -104,6 +104,10 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
if (noteFrames == null) {
throw 'Could not load note frames for note style: $id';
}
noteFrames.parent.persist = true;
return noteFrames;
@ -161,7 +165,8 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.holdNote?.assetPath;
// TODO: figure out why ?. didn't work here
var rawPath:Null<String> = (_data?.assets?.holdNote == null) ? null : _data?.assets?.holdNote?.assetPath;
return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath;
}
@ -299,6 +304,6 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
public function _fetchData(id:String):Null<NoteStyleData>
{
return NoteStyleRegistry.instance.parseEntryData(id);
return NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
}
}

View file

@ -32,7 +32,7 @@ class SongMigrator
{
if (jsonData.version != null)
{
if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE))
if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE))
{
trace('Song (${songId}) metadata version (${jsonData.version}) is valid and up-to-date.');
@ -70,7 +70,7 @@ class SongMigrator
{
if (jsonData.version)
{
if (VersionUtil.validateVersion(jsonData.version, CHART_VERSION_RULE))
if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE))
{
trace('Song (${songId}) chart version (${jsonData.version}) is valid and up-to-date.');

View file

@ -217,7 +217,7 @@ class StageDataParser
return null;
}
if (!VersionUtil.validateVersion(input.version, STAGE_DATA_VERSION_RULE))
if (!VersionUtil.validateVersionStr(input.version, STAGE_DATA_VERSION_RULE))
{
trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
return null;

View file

@ -257,6 +257,7 @@ class ChartEditorDialogHandler
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
@:haxe.warning("-WVarInit")
public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
{
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable);
@ -416,6 +417,7 @@ class ChartEditorDialogHandler
* @param state The ChartEditorState instance.
* @return The dialog to open.
*/
@:haxe.warning("-WVarInit")
public static function openSongMetadataDialog(state:ChartEditorState):Dialog
{
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false);

View file

@ -95,7 +95,7 @@ class ChartEditorEventSprite extends FlxSprite
public function correctAnimationName(name:String):String
{
if (this.animation.exists(name)) return name;
trace('Warning: Invalid animation name "' + name + '" for song event. Using "${DEFAULT_EVENT}"');
trace('Warning: Invalid animation name "${name}" for song event. Using "${DEFAULT_EVENT}"');
return DEFAULT_EVENT;
}

View file

@ -15,7 +15,7 @@ import flixel.addons.transition.FlxTransitionableState;
import flixel.input.keyboard.FlxKey;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.system.FlxSound;
import flixel.sound.FlxSound;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.tweens.misc.VarTween;

View file

@ -181,6 +181,6 @@ class Level implements IRegistryEntry<LevelData>
public function _fetchData(id:String):Null<LevelData>
{
return LevelRegistry.instance.parseEntryData(id);
return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id));
}
}

View file

@ -15,7 +15,7 @@ class BezierUtil
/**
* Linearly interpolate between three values.
* Depending on p, 0 = a, 0.5 = b, 1 = c, 0.25 = halfway between a and c, etc.
* Depending on p, 0 = a, 0.5 = b, 1 = c, 0.25 = halfway between a and b, etc.
*/
static inline function mix3(p:Float, a:Float, b:Float, c:Float):Float
{
@ -32,11 +32,6 @@ class BezierUtil
return mix2(p, mix4(p, a, b, c, d), mix4(p, b, c, d, e));
}
static inline function mix6(p:Float, a:Float, b:Float, c:Float, d:Float, e:Float, f:Float):Float
{
return mix2(p, mix5(p, a, b, c, d, e), mix5(p, b, c, d, e, f));
}
/**
* A bezier curve with two points.
* This is really just linear interpolation but whatever.

View file

@ -88,12 +88,17 @@ class Constants
public static final COLOR_HEALTH_BAR_GREEN:FlxColor = 0xFF66FF33;
/**
* The base colors of the notes.
* The base colors used by notes.
*/
public static final COLOR_NOTES:Array<FlxColor> = [0xFFFF22AA, 0xFF00EEFF, 0xFF00CC00, 0xFFCC1111];
public static var COLOR_NOTES:Array<FlxColor> = [
0xFFFF22AA, // left (0)
0xFF00EEFF, // down (1)
0xFF00CC00, // up (2)
0xFFCC1111 // right (3)
];
/**
* STAGE DEFAULTS
* GAME DEFAULTS
*/
// ==============================

View file

@ -2,9 +2,10 @@ package funkin.util;
class DateUtil
{
public static function generateTimestamp():String
public static function generateTimestamp(?date:Date = null):String
{
var date = Date.now();
if (date == null) date = Date.now();
return
'${date.getFullYear()}-${Std.string(date.getMonth() + 1).lpad('0', 2)}-${Std.string(date.getDate()).lpad('0', 2)}-${Std.string(date.getHours()).lpad('0', 2)}-${Std.string(date.getMinutes()).lpad('0', 2)}-${Std.string(date.getSeconds()).lpad('0', 2)}';
}

View file

@ -1 +0,0 @@
package funkin.util;

View file

@ -11,26 +11,21 @@ typedef ScoreInput =
var t:Int; // Start timestamp
}
/**
* A class of functions dedicated to serializing and deserializing data.
*/
class SerializerUtil
{
static final INDENT_CHAR = "\t";
/**
* Convert a Haxe object to a JSON string.
**/
*/
public static function toJSON(input:Dynamic, ?pretty:Bool = true):String
{
return Json.stringify(input, replacer, pretty ? INDENT_CHAR : null);
}
/**
* Convert a JSON string to a Haxe object of the chosen type.
*/
public static function fromJSONTyped<T>(input:String, type:Class<T>):T
{
return cast Json.parse(input);
}
/**
* Convert a JSON string to a Haxe object.
*/
@ -76,14 +71,19 @@ class SerializerUtil
if (Std.isOfType(value, String)) return value;
// Stringify Version objects.
var valueVersion:thx.semver.Version = cast value;
var result = '${valueVersion.major}.${valueVersion.minor}.${valueVersion.patch}';
if (valueVersion.hasPre) result += '-${valueVersion.pre}';
if (valueVersion.hasBuild) result += '+${valueVersion.build}';
return result;
return serializeVersion(cast value);
}
// Else, return the value as-is.
return value;
}
static inline function serializeVersion(value:thx.semver.Version):String
{
var result = '${value.major}.${value.minor}.${value.patch}';
if (value.hasPre) result += '-${value.pre}';
// TODO: Merge fix for version.hasBuild
if (value.build.length > 0) result += '+${value.build}';
return result;
}
}

View file

@ -14,13 +14,11 @@ class VersionUtil
* Checks that a given verison number satisisfies a given version rule.
* Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports.
*/
public static function validateVersion(version:String, versionRule:String):Bool
public static function validateVersion(version:thx.semver.Version, versionRule:thx.semver.VersionRule):Bool
{
try
{
var v:Version = version; // Perform a cast.
var vr:VersionRule = versionRule; // Perform a cast.
return v.satisfies(vr);
return version.satisfies(versionRule);
}
catch (e)
{
@ -28,4 +26,38 @@ class VersionUtil
return false;
}
}
/**
* Checks that a given verison number satisisfies a given version rule.
* Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports.
*/
public static function validateVersionStr(version:String, versionRule:String):Bool
{
try
{
var version:thx.semver.Version = version;
var versionRule:thx.semver.VersionRule = versionRule;
return version.satisfies(versionRule);
}
catch (e)
{
trace('[VERSIONUTIL] Invalid semantic version: ${version}');
return false;
}
}
/**
* Get and parse the semantic version from a JSON string.
* @param input The JSON string to parse.
* @return The semantic version, or null if it could not be parsed.
*/
public static function getVersionFromJSON(input:String):Null<thx.semver.Version>
{
var parsed = SerializerUtil.fromJSON(input);
if (parsed == null) return null;
if (parsed.version == null) return null;
var versionStr:String = parsed.version; // Dynamic -> String cast
var version:thx.semver.Version = versionStr; // Implicit, not explicit, cast.
return version;
}
}

View file

@ -19,7 +19,7 @@ class FlxAnimationUtil
if (anim.frameIndices != null && anim.frameIndices.length > 0)
{
// trace('addByIndices(${anim.name}, ${anim.prefix}, ${anim.frameIndices}, ${frameRate}, ${looped}, ${flipX}, ${flipY})');
target.animation.addByIndices(anim.name, anim.prefix, anim.frameIndices, "", frameRate, looped, flipX, flipY);
target.animation.addByIndices(anim.name, anim.prefix, anim.frameIndices, '', frameRate, looped, flipX, flipY);
// trace('RESULT:${target.animation.getAnimationList()}');
}
else

View file

@ -108,6 +108,9 @@ class ArraySortTools
input[i] = input[j];
input[j] = temp;
}
// Don't expect to get here.
return -1;
}
/**

View file

@ -27,6 +27,7 @@ class MapTools
/**
* Return a list of keys from the map (as an array, rather than an iterator).
* TODO: Rename this?
*/
public static function keyValues<K, T>(map:Map<K, T>):Array<K>
{

View file

@ -51,6 +51,7 @@ class StringTools
/**
* Parses the string data as JSON and returns the resulting object.
* This is here so you can use `string.parseJSON()` when `using StringTools`.
*
* @return The parsed object.
*/

7
tests/unit/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
bin/
# auto-generated
source/TestSuite.hx
.temp/
report/
build/

7
tests/unit/.munit Normal file
View file

@ -0,0 +1,7 @@
version=2.3.5
src=source
bin=bin
report=report
hxml=test.hxml
classPaths=../../source
coveragePackages=funkin

13
tests/unit/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,13 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Lime",
"type": "lime",
"request": "launch"
}
]
}

50
tests/unit/README.md Normal file
View file

@ -0,0 +1,50 @@
Flixel Unit Tests
-----------------
This is a unit test project using [munit](https://github.com/massiveinteractive/MassiveUnit). It's good practice to add tests for fixed bugs or new features.
TODO Make sure the unit tests are automatically run on GitHub Actions.
There's a 1:1 mapping between `.hx` files in `source/` and the unit test project - tests for `funkin.Conductor` go into `funkin.ConductorTest` etc.
### Building
Run one of the `test-*.hxml` files in this directory to run the tests on that specific target, e.g. `haxe test-cpp.hxml`. Currently supported are:
- `web` (HTML5)
- `cpp` (Native)
Alternatively, this can be done from within Visual Studio Code - (`F1` -> `Tasks: Run Task` -> Choose the target to test).
#### Adding Tests
- Run `haxelib run munit create com.FooBarTest -for com.Foo`
- Use `@:allow(full.package.name.ClassName)` to allow a test class to call private functions.
- Use `mockatoo.Mockatoo.mock(ClassName)` to mock a class. See [Mockatoo docs](https://github.com/misprintt/mockatoo).
#### Functions
- `@Before` functions are named `before()`
- Each `@Test` function starts with `test` and describes what exactly it tests. This can lead to long function names like `FlxEmitter#testStartShouldNotReviveMembers()` and serves as self-documentation.
- Another thing that helps with self-documentation is adding a comment for tests that are related an issue on GitHub.
```haxe
@Test // #1203
function testColorWithAlphaComparison()
```
### `FunkinTest` base class
Test classes extend `FunkinTest`, which is a base class with some utility functions for testing.
### `step()`
`step()` advances the `FlxGame` exactly one step. This is useful for tests that depend on game time advancing / `FlxGame#step()` being executed, such as physics of `add()`ed objects, state switches, or just time passing for tweens or timers.
There are two parameters:
- `steps` - specifies the amount of steps to advance (defaults to 1)
- `callback` - an optional callback function that is executed after each step
### `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.

View file

@ -0,0 +1,47 @@
{
"version": "1.0.0",
"name": "TEACHING TIME",
"titleAsset": "storymenu/titles/tutorial",
"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

@ -0,0 +1,6 @@
{
"version": "1.0.0",
"id": "blablabla",
"name": "blablabla API",
"data": []
}

View file

@ -0,0 +1,15 @@
{
"version": "1.0.0",
"id": "fizzbuzz",
"name": "nodejs-express-mongoose-demo",
"data": [
{
"foo": "bar",
"bar": "foo"
},
{
"foo": "baz",
"bar": "baz"
}
]
}

View file

@ -0,0 +1,5 @@
{
"version": "0.1.0",
"id": 1,
"name": "My First App"
}

View file

@ -0,0 +1,4 @@
{
"version": "3.0.0",
"info": "stuff"
}

View file

@ -0,0 +1,59 @@
{
"version": "1.0.0",
"name": "Funkin'",
"author": "PhantomArcade",
"fallback": null,
"assets": {
"note": {
"assetPath": "shared:arrows",
"scale": 1.0,
"isPixel": true,
"data": {
"left": { "prefix": "noteLeft" },
"down": { "prefix": "noteDown" },
"up": { "prefix": "noteUp" },
"right": { "prefix": "noteRight" }
}
},
"noteStrumline": {
"assetPath": "shared:arrows",
"scale": 1.0,
"offsets": [28, 32],
"isPixel": true,
"data": {
"leftStatic": { "prefix": "staticLeft0" },
"leftPress": { "prefix": "pressedLeft0" },
"leftConfirm": { "prefix": "confirmLeft0" },
"leftConfirmHold": { "prefix": "confirmLeft0" },
"downStatic": { "prefix": "staticDown0" },
"downPress": { "prefix": "pressedDown0" },
"downConfirm": { "prefix": "confirmDown0" },
"downConfirmHold": { "prefix": "confirmDown0" },
"upStatic": { "prefix": "staticUp0" },
"upPress": { "prefix": "pressedUp0" },
"upConfirm": { "prefix": "confirmUp0" },
"upConfirmHold": { "prefix": "confirmUp0" },
"rightStatic": { "prefix": "staticRight0" },
"rightPress": { "prefix": "pressedRight0" },
"rightConfirm": { "prefix": "confirmRight0" },
"rightConfirmHold": { "prefix": "confirmRight0" }
}
},
"holdNote": {
"assetPath": "NOTE_hold_assets",
"data": {}
},
"noteSplash": {
"assetPath": "",
"data": {
"enabled": true
}
},
"holdNoteCover": {
"assetPath": "",
"data": {
"enabled": true
}
}
}
}

View file

@ -0,0 +1,18 @@
{
"version": "1.0.0",
"name": "Test2",
"author": "Eric",
"fallback": "funkin",
"assets": {
"note": {
"assetPath": "shared:coolstuff",
"scale": 1.8,
"data": {
"left": { "prefix": "noteLeft1" },
"down": { "prefix": "noteDown3" },
"up": { "prefix": "noteUp2" },
"right": { "prefix": "noteRight4" }
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<TextureAtlas imagePath="arrows.png">
<SubTexture name="staticLeft0001" x="0" y="0" width="17" height="17"/>
<SubTexture name="staticDown0001" x="17" y="0" width="17" height="17"/>
<SubTexture name="staticUp0001" x="34" y="0" width="17" height="17"/>
<SubTexture name="staticRight0001" x="51" y="0" width="17" height="17"/>
<SubTexture name="noteLeft0001" x="0" y="17" width="17" height="17"/>
<SubTexture name="noteDown0001" x="17" y="17" width="17" height="17"/>
<SubTexture name="noteUp0001" x="34" y="17" width="17" height="17"/>
<SubTexture name="noteRight0001" x="51" y="17" width="17" height="17"/>
<SubTexture name="pressedLeft0001" x="0" y="17" width="17" height="17"/>
<SubTexture name="pressedDown0001" x="17" y="17" width="17" height="17"/>
<SubTexture name="pressedUp0001" x="34" y="17" width="17" height="17"/>
<SubTexture name="pressedRight0001" x="51" y="17" width="17" height="17"/>
<SubTexture name="pressedLeft0002" x="0" y="34" width="17" height="17"/>
<SubTexture name="pressedDown0002" x="17" y="34" width="17" height="17"/>
<SubTexture name="pressedUp0002" x="34" y="34" width="17" height="17"/>
<SubTexture name="pressedRight0002" x="51" y="34" width="17" height="17"/>
<SubTexture name="confirmLeft0001" x="0" y="51" width="17" height="17"/>
<SubTexture name="confirmDown0001" x="17" y="51" width="17" height="17"/>
<SubTexture name="confirmUp0001" x="34" y="51" width="17" height="17"/>
<SubTexture name="confirmRight0001" x="51" y="51" width="17" height="17"/>
<SubTexture name="confirmLeft0002" x="0" y="68" width="17" height="17"/>
<SubTexture name="confirmDown0002" x="17" y="68" width="17" height="17"/>
<SubTexture name="confirmUp0002" x="34" y="68" width="17" height="17"/>
<SubTexture name="confirmRight0002" x="51" y="68" width="17" height="17"/>
</TextureAtlas>

91
tests/unit/project.xml Normal file
View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<project
xmlns="http://lime.software/project/1.0.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://lime.software/project/1.0.2 http://lime.software/xsd/project-1.0.2.xsd">
<meta title="FunkinUnitTests" version="1.0.0" />
<app file="TestMain" main="TestMain" />
<!-- Test sources -->
<source path="source" />
<!-- Funkin' sources -->
<source path="../../source" />
<!-- Funkin' dependencies -->
<haxelib name="lime" /> <!-- Game engine -->
<haxelib name="openfl" /> <!-- Game engine -->
<haxelib name="flixel" /> <!-- Game engine -->
<haxelib name="flixel-addons" /> <!-- Additional utilities for Flixel -->
<haxelib name="hscript" /> <!-- Scripting -->
<haxelib name="flixel-ui" /> <!-- UI framework (deprecate this? -->
<haxelib name="haxeui-core" /> <!-- UI framework -->
<haxelib name="haxeui-flixel" /> <!-- Integrate HaxeUI with Flixel -->
<haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" /> <!-- Video playback -->
<haxelib name="thx.semver" /> <!-- Semantic version handling -->
<haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="tink_json" /> <!-- JSON parsing -->
<!-- Test dependencies -->
<haxelib name="munit" /> <!-- Unit test execution -->
<haxelib name="mcover" /> <!-- Code coverage -->
<haxelib name="mockatoo" /> <!-- Mocking -->
<haxelib name="hamcrest" /> <!-- Assertions/matching -->
<!-- This macro allows addition of new functionality to existing Flixel. -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
<!-- Assets -->
<assets path="assets/preload" rename="assets" exclude="*.ogg" if="web" />
<assets path="assets/preload" rename="assets" exclude="*.mp3" unless="web" />
<!--
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3" unless="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg" if="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3" unless="web" />
<library name="songs" preload="true" />
<library name="shared" preload="true" />
<library name="tutorial" preload="true" />
<library name="week1" preload="true" />
<library name="week2" preload="true" />
<library name="week3" preload="true" />
<library name="week4" preload="true" />
<library name="week5" preload="true" />
<library name="week6" preload="true" />
<library name="week7" preload="true" />
<library name="weekend1" preload="true" />
-->
<!-- 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 -->
<haxelib name="mcover" />
<haxedef name="MCOVER" />
<haxeflag name="--macro" value="mcover.MCover.coverage(['funkin'],['../../source', 'source/'],[''])" />
</project>

View file

@ -0,0 +1,77 @@
package;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import haxe.PosInfos;
import massive.munit.Assert;
using flixel.util.FlxArrayUtil;
/**
* @see https://github.com/HaxeFlixel/flixel/tree/dev/tests/unit
*/
class FunkinAssert
{
/**
* Assert if `expected` is within `margin` of `actual`, and fail if not.
* Useful for comparting Float values.
*
* @param expected The expected value of the test.
* @param actual The actual value of the test.
* @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
{
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
{
var areNear = areNearHelper(expected.x, actual.x, margin)
&& areNearHelper(expected.y, actual.y, margin)
&& areNearHelper(expected.width, actual.width, margin)
&& areNearHelper(expected.height, actual.height, margin);
if (areNear) Assert.assertionCount++;
else
Assert.fail('Value [$actual] is not within [$margin] of [$expected]', info);
}
static function areNearHelper(expected:Float, actual:Float, margin:Float = 0.001):Bool
{
return actual >= expected - margin && actual <= expected + margin;
}
public static function arraysEqual<T>(expected:Array<T>, actual:Array<T>, ?info:PosInfos):Void
{
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
{
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)
{
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)
{
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);
}
}

View file

@ -0,0 +1,105 @@
package;
import openfl.utils.Assets;
import openfl.errors.Error;
import flixel.FlxG;
import flixel.FlxState;
import flixel.tweens.FlxTween;
import flixel.util.FlxDestroyUtil.IFlxDestroyable;
import massive.munit.Assert;
/**
* @see https://github.com/HaxeFlixel/flixel/tree/dev/tests/unit
*/
class FunkinTest
{
public static final MS_PER_STEP:Float = 1.0 / 60.0 * 1000;
// approx. amount of ticks at 60 fps
static inline var TICKS_PER_FRAME:UInt = 25;
static var totalSteps:UInt = 0;
var destroyable:IFlxDestroyable;
public function new() {}
@After
@:access(flixel)
function after()
{
// Redefine how the game gets the time during tests.
FlxG.game.getTimer = function() {
return totalSteps * TICKS_PER_FRAME;
}
// make sure we have the same starting conditions for each test
resetGame();
}
/**
* Advance the game simulation.
* @param steps The amount to advance the game by.
* @param callback A function to call after each step.
*/
@:access(flixel)
function step(steps:UInt = 1, ?callback:Void->Void)
{
for (i in 0...steps)
{
FlxG.game.step();
if (callback != null) callback();
totalSteps++;
}
}
function resetGame()
{
FlxG.resetGame();
step();
}
function switchState(nextState:FlxState)
{
FlxG.switchState(nextState);
step();
}
function resetState()
{
FlxG.resetState();
step();
}
@Test
public function testAssert()
{
Assert.areEqual(4, 2 + 2);
}
@Test
function testDestroy()
{
if (destroyable == null)
{
return;
}
try
{
destroyable.destroy();
destroyable.destroy();
}
catch (e:Error)
{
Assert.fail(e.message);
}
}
function finishTween(tween:FlxTween)
{
while (!tween.finished)
{
step();
}
}
}

View file

@ -0,0 +1,60 @@
package;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.DateUtil;
class MockTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testMock()
{
// Test that mocking works.
var mockSprite = mockatoo.Mockatoo.mock(flixel.FlxSprite);
var mockAnim = mockatoo.Mockatoo.mock(flixel.animation.FlxAnimationController);
mockSprite.animation = mockAnim;
var animData:funkin.data.animation.AnimationData =
{
name: "testAnim",
prefix: "blablabla"
};
mockSprite.animation.addByPrefix("testAnim", "blablabla", 24, false, false, false);
// Verify that the method was called once.
// 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
{
// 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.
}
}
}

View file

@ -0,0 +1,77 @@
package;
import openfl.Lib;
import flixel.FlxGame;
import flixel.FlxState;
import massive.munit.TestRunner;
import massive.munit.client.HTTPClient;
import massive.munit.client.SummaryReportClient;
/**
* Auto generated Test Application.
* Refer to munit command line tool for more information (haxelib run munit)
*/
class TestMain
{
/**
* If true, include a report with each ignored test and their descriptions.
*/
static final INCLUDE_IGNORED_REPORT:Bool = false;
static function main()
{
new 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));
var suites = new Array<Class<massive.munit.TestSuite>>();
suites.push(TestSuite);
#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
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
var runner = new TestRunner(client);
runner.addResultClient(httpClient);
runner.completionHandler = completionHandler;
runner.run(suites);
}
/**
* updates the background color and closes the current browser
* for flash and html targets (useful for continuos integration servers)
*/
function completionHandler(successful:Bool):Void
{
try
{
#if flash
openfl.external.ExternalInterface.call("testResult", successful);
#elseif js
js.Lib.eval("testResult(" + successful + ");");
#elseif sys
Sys.exit(successful ? 0 : 1);
#end
}
// if run from outside browser can get error which we can ignore
catch (e:Dynamic) {}
}
}

View file

@ -0,0 +1,30 @@
package funkin.play;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.play.PlayState;
@:access(funkin.play.PlayState)
class Template extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function test() {}
}

View file

@ -0,0 +1,107 @@
package flixel;
import massive.munit.Assert;
@:access(flixel.FlxG)
class FlxGTest extends FunkinTest
{
@Test function testVERSIONNull():Void
Assert.isNotNull(FlxG.VERSION);
@Test function testGameNull():Void
Assert.isNotNull(FlxG.game);
@Test function testStageNull():Void
Assert.isNotNull(FlxG.stage);
@Test function testStateNull():Void
Assert.isNotNull(FlxG.state);
@Test function testWorldBoundsNull():Void
Assert.isNotNull(FlxG.worldBounds);
@Test function testSaveNull():Void
Assert.isNotNull(FlxG.save);
#if FLX_MOUSE
@Test function testMouseNull():Void
Assert.isNotNull(FlxG.mouse);
#end
#if FLX_TOUCH
@Test function testTouchNull():Void
Assert.isNotNull(FlxG.touches);
#end
#if FLX_POINTER_INPUT
@Test function testSwipesNull():Void
Assert.isNotNull(FlxG.swipes);
#end
#if FLX_KEYBOARD
@Test function testKeysNull():Void
Assert.isNotNull(FlxG.keys);
#end
#if FLX_GAMEPAD
@Test function testGamepadsNull():Void
Assert.isNotNull(FlxG.gamepads);
#end
#if android
@Test function testAndroidNull():Void
Assert.isNotNull(FlxG.android);
#end
#if js
@Test function testHtml5Null():Void
Assert.isNotNull(FlxG.html5);
#end
@Test function testInputsNull():Void
Assert.isNotNull(FlxG.inputs);
@Test function testConsoleNull():Void
Assert.isNotNull(FlxG.console);
@Test function testLogNull():Void
Assert.isNotNull(FlxG.log);
@Test function testWatchNull():Void
Assert.isNotNull(FlxG.watch);
@Test function testDebuggerNull():Void
Assert.isNotNull(FlxG.debugger);
@Test function testVcrNull():Void
Assert.isNotNull(FlxG.vcr);
@Test function testBitmapNull():Void
Assert.isNotNull(FlxG.bitmap);
@Test function testCamerasNull():Void
Assert.isNotNull(FlxG.cameras);
@Test function testPluginsNull():Void
Assert.isNotNull(FlxG.plugins);
#if FLX_SOUND_SYSTEM
@Test function testSoundNull():Void
Assert.isNotNull(FlxG.sound);
#end
@Test function testScaleModeNull():Void
Assert.isNotNull(FlxG.scaleMode);
@Test
function testDefaultWidth():Void
{
Assert.areEqual(640, FlxG.width);
}
@Test
function testDefaultHeight():Void
{
Assert.areEqual(480, FlxG.height);
}
}

View file

@ -0,0 +1,550 @@
package funkin;
import flixel.FlxG;
import flixel.FlxState;
import funkin.Conductor;
import funkin.play.song.SongData.SongTimeChange;
import funkin.util.Constants;
import massive.munit.Assert;
@:access(funkin.Conductor)
class ConductorTest extends FunkinTest
{
var conductorState:ConductorState;
@Before
function before()
{
resetGame();
// The ConductorState will advance the conductor when step() is called.
FlxG.switchState(conductorState = new ConductorState());
Conductor.reset();
}
@Test
function testDefaultValues():Void
{
// NOTE: Expected value comes first.
Assert.areEqual([], Conductor.timeChanges);
Assert.areEqual(null, Conductor.currentTimeChange);
Assert.areEqual(0, Conductor.songPosition);
Assert.areEqual(Constants.DEFAULT_BPM, Conductor.bpm);
Assert.areEqual(null, Conductor.bpmOverride);
Assert.areEqual(600, Conductor.beatLengthMs);
Assert.areEqual(4, Conductor.timeSignatureNumerator);
Assert.areEqual(4, Conductor.timeSignatureDenominator);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
Assert.areEqual(0.0, Conductor.currentStepTime);
Assert.areEqual(150, Conductor.stepLengthMs);
}
/**
* Tests implementation of `update()`, and how it affects
* `currentBeat`, `currentStep`, `currentStepTime`, and the `beatHit` and `stepHit` signals.
*/
@Test
function testUpdate():Void
{
Assert.areEqual(0, Conductor.songPosition);
step(); // 1
var BPM_100_STEP_TIME = 1 / 9;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(1 / 9, Conductor.currentStepTime);
step(7); // 8
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 8, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(8 / 9, Conductor.currentStepTime);
Assert.areEqual(0, conductorState.beatsHit);
Assert.areEqual(0, conductorState.stepsHit);
step(); // 9
Assert.areEqual(0, conductorState.beatsHit);
Assert.areEqual(1, conductorState.stepsHit);
conductorState.beatsHit = 0;
conductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 9, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(1, Conductor.currentStep);
FunkinAssert.areNear(1.0, Conductor.currentStepTime);
step(35 - 9); // 35
Assert.areEqual(0, conductorState.beatsHit);
Assert.areEqual(2, conductorState.stepsHit);
conductorState.beatsHit = 0;
conductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 35, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(3, Conductor.currentStep);
FunkinAssert.areNear(3.0 + 8 / 9, Conductor.currentStepTime);
step(); // 36
Assert.areEqual(1, conductorState.beatsHit);
Assert.areEqual(1, conductorState.stepsHit);
conductorState.beatsHit = 0;
conductorState.stepsHit = 0;
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 36, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);
Assert.areEqual(4, Conductor.currentStep);
FunkinAssert.areNear(4.0, Conductor.currentStepTime);
step(50 - 36); // 50
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 50, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);
Assert.areEqual(5, Conductor.currentStep);
FunkinAssert.areNear(5.555555, Conductor.currentStepTime);
step(49); // 99
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 99, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(11, Conductor.currentStep);
FunkinAssert.areNear(11.0, Conductor.currentStepTime);
step(1); // 100
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 100, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(11, Conductor.currentStep);
FunkinAssert.areNear(11.111111, Conductor.currentStepTime);
}
@Test
function testUpdateForcedBPM():Void
{
Conductor.forceBPM(60);
Assert.areEqual(0, Conductor.songPosition);
// 60 beats per minute = 1 beat per second
// 1 beat per second = 1/60 beats per frame = 4/60 steps per frame
step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(4 / 60, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step
step(14 - 1); // 14
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 14, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(1.0 - 4 / 60, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step
step(); // 15
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(1, Conductor.currentStep);
FunkinAssert.areNear(1.0, Conductor.currentStepTime); // 1/60 of 1 beat = 4/60 of 1 step
step(45 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(3, Conductor.currentStep);
FunkinAssert.areNear(4.0 - 4 / 60, Conductor.currentStepTime);
step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);
Assert.areEqual(4, Conductor.currentStep);
FunkinAssert.areNear(4.0, Conductor.currentStepTime);
step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);
Assert.areEqual(4, Conductor.currentStep);
FunkinAssert.areNear(4.0 + 4 / 60, Conductor.currentStepTime);
}
@Test
function testSingleTimeChange():Void
{
// Start the song with a BPM of 120.
var songTimeChanges:Array<SongTimeChange> = [
{
t: 0,
b: 0,
bpm: 120,
n: 4,
d: 4,
bt: [4, 4, 4, 4]
}, // 120 bpm starting 0 sec/0 beats
];
Conductor.mapTimeChanges(songTimeChanges);
// All should be at 0.
FunkinAssert.areNear(0, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
// 120 beats per minute = 2 beat per second
// 2 beat per second = 2/60 beats per frame = 16/120 steps per frame
step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
step(15 - 1); // 15
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 15, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(2, Conductor.currentStep);
FunkinAssert.areNear(2.0, Conductor.currentStepTime); // 2/60 of 1 beat = 8/60 of 1 step
step(45 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);
Assert.areEqual(7, Conductor.currentStep);
FunkinAssert.areNear(7.0 + 104 / 120, Conductor.currentStepTime);
step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(8, Conductor.currentStep);
FunkinAssert.areNear(8.0, Conductor.currentStepTime);
step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(8, Conductor.currentStep);
FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime);
}
@Test
function testDoubleTimeChange():Void
{
// Start the song with a BPM of 120.
var songTimeChanges:Array<SongTimeChange> = [
{
t: 0,
b: 0,
bpm: 120,
n: 4,
d: 4,
bt: [4, 4, 4, 4]
}, // 120 bpm starting 0 sec/0 beats
{
t: 3000,
b: 6,
bpm: 90,
n: 4,
d: 4,
bt: [4, 4, 4, 4]
} // 90 bpm starting 3 sec/6 beats
];
Conductor.mapTimeChanges(songTimeChanges);
// All should be at 0.
FunkinAssert.areNear(0, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
// 120 beats per minute = 2 beat per second
// 2 beat per second = 2/60 beats per frame = 16/120 steps per frame
step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step
step(60 - 1 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);
Assert.areEqual(7, Conductor.currentStep);
FunkinAssert.areNear(7.0 + 104 / 120, Conductor.currentStepTime);
step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(8, Conductor.currentStep);
FunkinAssert.areNear(8.0, Conductor.currentStepTime);
step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(8, Conductor.currentStep);
FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime);
step(179 - 61); // 179
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.songPosition);
Assert.areEqual(5, Conductor.currentBeat);
Assert.areEqual(23, Conductor.currentStep);
FunkinAssert.areNear(23.0 + 52 / 60, Conductor.currentStepTime);
step(); // 180 (3 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.songPosition);
Assert.areEqual(6, Conductor.currentBeat);
Assert.areEqual(24, Conductor.currentStep);
FunkinAssert.areNear(24.0, Conductor.currentStepTime);
step(); // 181 (3 + 1/60 seconds)
// BPM has switched to 90!
// 90 beats per minute = 1.5 beat per second
// 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame
// = 12/120 steps per frame
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.songPosition);
Assert.areEqual(6, Conductor.currentBeat);
Assert.areEqual(24, Conductor.currentStep);
FunkinAssert.areNear(24.0 + 12 / 120, Conductor.currentStepTime);
step(59); // 240 (4 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.songPosition);
Assert.areEqual(7, Conductor.currentBeat);
Assert.areEqual(30, Conductor.currentStep);
FunkinAssert.areNear(30.0, Conductor.currentStepTime);
step(); // 241 (4 + 1/60 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.songPosition);
Assert.areEqual(7, Conductor.currentBeat);
Assert.areEqual(30, Conductor.currentStep);
FunkinAssert.areNear(30.0 + 12 / 120, Conductor.currentStepTime);
}
@Test
function testTripleTimeChange():Void
{
// Start the song with a BPM of 120, then move to 90, then move to 180.
var songTimeChanges:Array<SongTimeChange> = [
{
t: 0,
b: null,
bpm: 120,
n: 4,
d: 4,
bt: [4, 4, 4, 4]
}, // 120 bpm starting 0 sec/0 beats
{
t: 3000,
b: null,
bpm: 90,
n: 4,
d: 4,
bt: [4, 4, 4, 4]
}, // 90 bpm starting 3 sec/6 beats
{
t: 6000,
b: null,
bpm: 180,
n: 4,
d: 4,
bt: [4, 4, 4, 4]
} // 90 bpm starting 3 sec/6 beats
];
Conductor.mapTimeChanges(songTimeChanges);
// Verify time changes.
Assert.areEqual(3, Conductor.timeChanges.length);
FunkinAssert.areNear(0, Conductor.timeChanges[0].beatTime);
FunkinAssert.areNear(6, Conductor.timeChanges[1].beatTime);
FunkinAssert.areNear(10.5, Conductor.timeChanges[2].beatTime);
// All should be at 0.
FunkinAssert.areNear(0, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(0.0, Conductor.currentStepTime); // 2/120 of 1 beat = 8/120 of 1 step
// 120 beats per minute = 2 beat per second
// 2 beat per second = 2/60 beats per frame = 16/120 steps per frame
step(); // Advances time 1/60 of 1 second.
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 1, Conductor.songPosition);
Assert.areEqual(0, Conductor.currentBeat);
Assert.areEqual(0, Conductor.currentStep);
FunkinAssert.areNear(16 / 120, Conductor.currentStepTime); // 4/120 of 1 beat = 16/120 of 1 step
step(60 - 1 - 1); // 59
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 59, Conductor.songPosition);
Assert.areEqual(1, Conductor.currentBeat);
Assert.areEqual(7, Conductor.currentStep);
FunkinAssert.areNear(7 + 104 / 120, Conductor.currentStepTime);
step(); // 60
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 60, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(8, Conductor.currentStep);
FunkinAssert.areNear(8.0, Conductor.currentStepTime);
step(); // 61
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 61, Conductor.songPosition);
Assert.areEqual(2, Conductor.currentBeat);
Assert.areEqual(8, Conductor.currentStep);
FunkinAssert.areNear(8.0 + 8 / 60, Conductor.currentStepTime);
step(179 - 61); // 179
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 179, Conductor.songPosition);
Assert.areEqual(5, Conductor.currentBeat);
Assert.areEqual(23, Conductor.currentStep);
FunkinAssert.areNear(23.0 + 52 / 60, Conductor.currentStepTime);
step(); // 180 (3 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 180, Conductor.songPosition);
Assert.areEqual(6, Conductor.currentBeat);
Assert.areEqual(24, Conductor.currentStep); // 23.999 => 24
FunkinAssert.areNear(24.0, Conductor.currentStepTime);
step(); // 181 (3 + 1/60 seconds)
// BPM has switched to 90!
// 90 beats per minute = 1.5 beat per second
// 1.5 beat per second = 1.5/60 beats per frame = 3/120 beats per frame
// = 12/120 steps per frame
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 181, Conductor.songPosition);
Assert.areEqual(6, Conductor.currentBeat);
Assert.areEqual(24, Conductor.currentStep);
FunkinAssert.areNear(24.0 + 12 / 120, Conductor.currentStepTime);
step(60 - 1 - 1); // 240 (4 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 239, Conductor.songPosition);
Assert.areEqual(7, Conductor.currentBeat);
Assert.areEqual(29, Conductor.currentStep);
FunkinAssert.areNear(29.0 + 108 / 120, Conductor.currentStepTime);
step(); // 240 (4 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 240, Conductor.songPosition);
Assert.areEqual(7, Conductor.currentBeat);
Assert.areEqual(30, Conductor.currentStep);
FunkinAssert.areNear(30.0, Conductor.currentStepTime);
step(); // 241 (4 + 1/60 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 241, Conductor.songPosition);
Assert.areEqual(7, Conductor.currentBeat);
Assert.areEqual(30, Conductor.currentStep);
FunkinAssert.areNear(30.0 + 12 / 120, Conductor.currentStepTime);
step(359 - 241); // 359 (5 + 59/60 seconds)
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 359, Conductor.songPosition);
Assert.areEqual(10, Conductor.currentBeat);
Assert.areEqual(41, Conductor.currentStep);
FunkinAssert.areNear(41 + 108 / 120, Conductor.currentStepTime);
step(); // 360
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 360, Conductor.songPosition);
Assert.areEqual(10, Conductor.currentBeat);
Assert.areEqual(42, Conductor.currentStep); // 41.999
FunkinAssert.areNear(42.0, Conductor.currentStepTime);
step(); // 361
// BPM has switched to 180!
// 180 beats per minute = 3 beat per second
// 3 beat per second = 3/60 beats per frame
// = 12/60 steps per frame
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 361, Conductor.songPosition);
Assert.areEqual(10, Conductor.currentBeat);
Assert.areEqual(42, Conductor.currentStep);
FunkinAssert.areNear(42.0 + 12 / 60, Conductor.currentStepTime);
step(); // 362
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 362, Conductor.songPosition);
Assert.areEqual(10, Conductor.currentBeat);
Assert.areEqual(42, Conductor.currentStep);
FunkinAssert.areNear(42.0 + 24 / 60, Conductor.currentStepTime);
step(3); // 365
FunkinAssert.areNear(FunkinTest.MS_PER_STEP * 365, Conductor.songPosition);
Assert.areEqual(10, Conductor.currentBeat);
Assert.areEqual(43, Conductor.currentStep); // 42.999 => 42
FunkinAssert.areNear(43.0, Conductor.currentStepTime);
}
}
class ConductorState extends FlxState
{
public var beatsHit:Int = 0;
public var stepsHit:Int = 0;
public function new()
{
super();
}
function beatHit():Void
{
beatsHit += 1;
}
function stepHit():Void
{
stepsHit += 1;
}
public override function create():Void
{
super.create();
Conductor.beatHit.add(this.beatHit);
Conductor.stepHit.add(this.stepHit);
}
public override function destroy():Void
{
super.destroy();
Conductor.beatHit.remove(this.beatHit);
Conductor.stepHit.remove(this.stepHit);
}
public override function update(elapsed:Float)
{
super.update(elapsed);
// On each step, increment the Conductor as though the song was playing.
Conductor.update(Conductor.songPosition + elapsed * Constants.MS_PER_SEC);
}
}

View file

@ -0,0 +1,228 @@
package funkin.data;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.data.BaseRegistry;
import funkin.util.SortUtil;
import funkin.util.VersionUtil;
@:access(funkin.data.BaseRegistry)
class BaseRegistryTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testMyTypeRegistry()
{
// This shouldn't crash.
MyTypeRegistry.instance.loadEntries();
// Ensure all entries were loaded.
var entryList = MyTypeRegistry.instance.listEntryIds();
entryList.sort(SortUtil.alphabetically);
Assert.areEqual(entryList, [
"blablabla",
"fizzbuzz",
"foobar",
// "junk"
]);
// Ensure this one is not in the list.
Assert.areEqual(entryList.indexOf("junk"), -1);
// Ensure blablabla got parsed correctly.
var blablabla = MyTypeRegistry.instance.fetchEntry("blablabla");
Assert.areEqual(blablabla.id, "blablabla");
Assert.areEqual(blablabla._data.version, "1.0.0");
Assert.areEqual(blablabla._data.name, "blablabla API");
}
}
typedef MyTypeData =
{
/**
* The version number of the data schema.
* When making changes to the note style data format, this should be incremented,
* and a migration function should be added to handle old versions.
*/
@:default(funkin.data.BaseRegistryTest.MyTypeRegistry.DATA_VERSION)
var version:String;
var id:String;
var name:String;
var data:Array<MySubTypeData>;
}
typedef MySubTypeData =
{
var foo:String;
var bar:String;
};
typedef MyTypeData_v0_1_x =
{
var version:String;
var id:Int;
var name:String;
};
class MyType implements IRegistryEntry<MyTypeData>
{
/**
* The ID of the mytype.
*/
public final id:String;
/**
* Mytype data as parsed from the JSON file.
*/
public final _data:MyTypeData;
/**
* @param id The ID of the JSON file to parse.
*/
public function new(id:String)
{
this.id = id;
_data = _fetchData(id);
if (_data == null)
{
throw 'Could not parse mytype data for id: $id';
}
}
public function destroy():Void {}
public function toString():String
{
return 'MyType($id)';
}
public function _fetchData(id:String):Null<MyTypeData>
{
return MyTypeRegistry.instance.parseEntryDataWithMigration(id, MyTypeRegistry.instance.fetchEntryVersion(id));
}
public function getSubData():Array<MySubTypeData>
{
return _data.data;
}
}
class MyTypeRegistry extends BaseRegistry<MyType, MyTypeData>
{
/**
* The current version string for the note style data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateMyTypeData()` function.
*/
public static final DATA_VERSION:String = "1.0.0";
public static final instance:MyTypeRegistry = new MyTypeRegistry();
public function new()
{
super('MYTYPE', 'mytype');
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryData(id:String):Null<MyTypeData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<MyTypeData>();
var jsonStr:String = loadEntryFile(id);
parser.fromJson(jsonStr);
if (parser.errors.length > 0)
{
trace('[${registryId}] Failed to parse entry data: ${id}');
for (error in parser.errors)
{
trace(error);
}
return null;
}
return parser.value;
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryData_v0_1_x(id:String):Null<MyTypeData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<MyTypeData_v0_1_x>();
var jsonStr:String = loadEntryFile(id);
parser.fromJson(jsonStr);
if (parser.errors.length > 0)
{
trace('[${registryId}] Failed to parse entry data: ${id}');
for (error in parser.errors)
{
trace(error);
}
return null;
}
var oldData:MyTypeData_v0_1_x = parser.value;
var result:MyTypeData =
{
version: DATA_VERSION,
id: '${oldData.id}',
name: oldData.name,
data: []
};
return result;
}
public override function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<MyTypeData>
{
if (VersionUtil.validateVersion(version, "0.1.x"))
{
trace('Migrating mytype data from ${version} to ${DATA_VERSION}');
return parseEntryData_v0_1_x(id);
}
else
{
trace('Parsing mytype data with version ${version}');
return super.parseEntryDataWithMigration(id, version);
}
}
function createScriptedEntry(clsName:String):MyType
{
return null;
}
function getScriptedClassNames():Array<String>
{
return [];
}
}

View file

@ -0,0 +1,90 @@
package funkin.data.notestyle;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import massive.munit.util.Timer;
@:access(funkin.play.notes.notestyle.NoteStyle)
@:access(funkin.data.notestyle.NoteStyleRegistry)
class NoteStyleRegistryTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass()
{
NoteStyleRegistry.instance.loadEntries();
}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testValid()
{
Assert.isNotNull(NoteStyleRegistry.instance);
}
@Test
public function testParseEntryData()
{
var result:NoteStyleData = NoteStyleRegistry.instance.parseEntryData("test2");
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");
}
@Test
public function testFetchEntry()
{
var result:NoteStyle = NoteStyleRegistry.instance.fetchEntry("test2");
Assert.areEqual(result.toString(), "NoteStyle(test2)");
Assert.areEqual(result.getName(), "Test2");
Assert.areEqual(result.getAuthor(), "Eric");
Assert.areEqual(result.getFallbackID(), "funkin");
}
@Test
public function testFetchBadEntry()
{
var result:NoteStyle = NoteStyleRegistry.instance.fetchEntry("blablabla");
Assert.areEqual(result, null);
}
@Test
public function testFetchDefault()
{
var nsrMock:NoteStyleRegistry = mock(NoteStyleRegistry);
nsrMock.fetchDefault().callsRealMethod();
// Perform the call.
nsrMock.fetchDefault();
// Verify the underlying call.
nsrMock.fetchEntry(NoteStyleRegistry.DEFAULT_NOTE_STYLE_ID).verify(times(1));
}
}

View file

@ -0,0 +1,77 @@
package funkin.play.notes.notestyle;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.animation.FlxAnimationController;
@:access(funkin.play.notes.notestyle.NoteStyle)
class NoteStyleTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass()
{
NoteStyleRegistry.instance.loadEntries();
}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
@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 mockNoteSprite:NoteSprite = mock(NoteSprite);
// var mockAnim = mock(FlxAnimationController);
// mockNoteSprite.animation = mockAnim;
target.buildNoteSprite(mockNoteSprite);
Assert.areEqual(mockNoteSprite.frames, []);
}
@Test
public function testFallbackBehavior()
{
var target1:NoteStyle = NoteStyleRegistry.instance.fetchEntry("funkin");
var target2:NoteStyle = NoteStyleRegistry.instance.fetchEntry("test2");
Assert.areEqual("funkin", target1.id);
Assert.areEqual("test2", target2.id);
Assert.areEqual("Funkin'", target1.getName());
Assert.areEqual("Test2", target2.getName());
Assert.isNull(target1.getFallbackID());
Assert.areEqual(target1.id, target2.getFallbackID());
// 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));
// Unspecified fields use the fallback.
// Should NOT return null!
Assert.areEqual("assets/images/NOTE_hold_assets.png", target1.getHoldNoteAssetPath(false));
Assert.areEqual("assets/images/NOTE_hold_assets.png", target2.getHoldNoteAssetPath(false));
Assert.areEqual("NOTE_hold_assets", target1.getHoldNoteAssetPath(true));
}
}

View file

@ -0,0 +1,137 @@
package funkin.util;
import flixel.math.FlxPoint;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.BezierUtil;
@:access(funkin.util.BezierUtil)
class BezierUtilTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testBezier2()
{
var point1:FlxPoint = FlxPoint.get(1, 1);
var point2:FlxPoint = FlxPoint.get(2, 2);
var result:FlxPoint = BezierUtil.bezier2(0.5, point1, point2);
Assert.areEqual(result.x, 1.5);
Assert.areEqual(result.y, 1.5);
result = BezierUtil.bezier2(0.25, point1, point2);
Assert.areEqual(result.x, 1.25);
Assert.areEqual(result.y, 1.25);
result = BezierUtil.bezier2(0.75, point1, point2);
Assert.areEqual(result.x, 1.75);
Assert.areEqual(result.y, 1.75);
}
@Test
public function testBezier3()
{
var point1:FlxPoint = FlxPoint.get(1, 1);
var point2:FlxPoint = FlxPoint.get(2, 2);
var point3:FlxPoint = FlxPoint.get(3, 3);
var result:FlxPoint = BezierUtil.bezier3(0.5, point1, point2, point3);
Assert.areEqual(result.x, 2);
Assert.areEqual(result.y, 2);
result = BezierUtil.bezier3(0.25, point1, point2, point3);
Assert.areEqual(result.x, 1.5);
Assert.areEqual(result.y, 1.5);
result = BezierUtil.bezier3(0.75, point1, point2, point3);
Assert.areEqual(result.x, 2.5);
Assert.areEqual(result.y, 2.5);
result = BezierUtil.bezier3(0.5, point1, point2, point3);
Assert.areEqual(result.x, 2);
Assert.areEqual(result.y, 2);
result = BezierUtil.bezier3(0.6, point1, point2, point3);
Assert.areEqual(result.x, 2.2);
Assert.areEqual(result.y, 2.2);
}
@Test
public function testBezier4()
{
var point1:FlxPoint = FlxPoint.get(1, 1);
var point2:FlxPoint = FlxPoint.get(2, 2);
var point3:FlxPoint = FlxPoint.get(3, 3);
var point4:FlxPoint = FlxPoint.get(4, 4);
var result:FlxPoint = BezierUtil.bezier4(0.5, point1, point2, point3, point4);
Assert.areEqual(result.x, 2.5);
Assert.areEqual(result.y, 2.5);
result = BezierUtil.bezier4(0.25, point1, point2, point3, point4);
Assert.areEqual(result.x, 1.75);
Assert.areEqual(result.y, 1.75);
result = BezierUtil.bezier4(0.75, point1, point2, point3, point4);
Assert.areEqual(result.x, 3.25);
Assert.areEqual(result.y, 3.25);
}
@Test
public function testBezier5()
{
var point1:FlxPoint = FlxPoint.get(1, 1);
var point2:FlxPoint = FlxPoint.get(2, 2);
var point3:FlxPoint = FlxPoint.get(3, 3);
var point4:FlxPoint = FlxPoint.get(4, 4);
var point5:FlxPoint = FlxPoint.get(5, 5);
var result:FlxPoint = BezierUtil.bezier5(0.5, point1, point2, point3, point4, point5);
Assert.areEqual(result.x, 3);
Assert.areEqual(result.y, 3);
result = BezierUtil.bezier5(0.25, point1, point2, point3, point4, point5);
Assert.areEqual(result.x, 2);
Assert.areEqual(result.y, 2);
result = BezierUtil.bezier5(0.75, point1, point2, point3, point4, point5);
Assert.areEqual(result.x, 4);
Assert.areEqual(result.y, 4);
result = BezierUtil.bezier5(0.5, point1, point2, point3, point4, point5);
Assert.areEqual(result.x, 3);
Assert.areEqual(result.y, 3);
}
}

View file

@ -0,0 +1,71 @@
package funkin.util;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.ClipboardUtil;
@:access(funkin.util.ClipboardUtil)
class ClipboardUtilTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testGetSetClipboard()
{
var testString = "test string";
ClipboardUtil.setClipboard(testString);
var clipboardString = ClipboardUtil.getClipboard();
Assert.areEqual(testString, clipboardString);
}
@Ignore("This test doesn't work, Lime issue?")
@Test
public function testAddRemoveListener()
{
ClipboardUtil.addListener(onClipboardChange);
var testString = "test string";
ClipboardUtil.setClipboard(testString);
var clipboardString = ClipboardUtil.getClipboard();
Assert.areEqual(testString, clipboardString);
step();
// TODO: Fix issue where this test fails
Assert.areEqual(1, count);
ClipboardUtil.removeListener(onClipboardChange);
var testString2 = "test string 2";
ClipboardUtil.setClipboard(testString2);
var clipboardString2 = ClipboardUtil.getClipboard();
Assert.areEqual(testString2, clipboardString2);
Assert.areEqual(1, count);
}
var count:Int = 0;
function onClipboardChange()
{
count += 1;
}
}

View file

@ -0,0 +1,35 @@
package funkin.util;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.DateUtil;
@:access(funkin.util.DateUtil)
class DateUtilTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testGenerateTimestamp()
{
var date:Date = new Date(2020, 10 - 1, 31, 3, 0, 0);
var timestamp:String = DateUtil.generateTimestamp(date);
Assert.areEqual("2020-10-31-03-00-00", timestamp);
}
}

View file

@ -0,0 +1,87 @@
package funkin.util;
import haxe.io.Bytes;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.SerializerUtil;
typedef FooBar =
{
a:Int,
b:Int,
c:Int
};
@:access(funkin.util.SerializerUtil)
class SerializerUtilTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testToJSON()
{
var object = {a: 1, b: 2, c: 3};
var json = SerializerUtil.toJSON(object);
Assert.areEqual('{' + '\n\t"a": 1,' + '\n\t"b": 2,' + '\n\t"c": 3' + '\n}', json);
}
@Test
public function testFromJSON()
{
var json = '{' + '\n\t"a": 1,' + '\n\t"b": 2,' + '\n\t"c": 3' + '\n}';
var object = SerializerUtil.fromJSON(json);
Assert.areEqual(1, object.a);
Assert.areEqual(2, object.b);
Assert.areEqual(3, object.c);
}
@Test
public function testFromJSONBytes()
{
var json = '{' + '\n\t"a": 1,' + '\n\t"b": 2,' + '\n\t"c": 3' + '\n}';
var bytes = Bytes.ofString(json);
var object = SerializerUtil.fromJSONBytes(bytes);
Assert.areEqual(1, object.a);
Assert.areEqual(2, object.b);
Assert.areEqual(3, object.c);
}
@Test
public function testReplacer()
{
var version:thx.semver.Version = '1.0.0-beta';
Assert.areEqual(1, version.major);
Assert.areEqual(0, version.minor);
Assert.areEqual(0, version.patch);
Assert.areEqual(true, version.hasPre);
Assert.areEqual('beta', version.pre);
// Assert.areEqual(false, version.hasBuild);
Assert.areEqual('', version.build);
var formatted = SerializerUtil.replacer('version', version);
Assert.areEqual('1.0.0-beta', formatted);
var test2 = SerializerUtil.toJSON({version: version});
Assert.areEqual('{' + '\n\t"version": "1.0.0-beta"' + '\n}', test2);
}
}

View file

@ -0,0 +1,63 @@
package funkin.util;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.util.FlxSort;
import funkin.play.song.SongData.SongNoteData;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.SortUtil;
@:access(funkin.util.SortUtil)
class SortUtilTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testAlphabetically()
{
var arr:Array<String> = ["b", "a", "c"];
arr.sort(SortUtil.alphabetically);
Assert.areEqual(["a", "b", "c"], arr);
}
@Test
public function testByZIndex()
{
var arr:Array<FlxObject> = [new FlxSprite(), new FlxObject(), new FlxSprite()];
arr[0].zIndex = 2000;
arr[1].zIndex = 1000;
arr[2].zIndex = 3000;
arr.sort(SortUtil.byZIndex.bind(FlxSort.ASCENDING));
Assert.areEqual(1000, arr[0].zIndex);
Assert.areEqual(2000, arr[1].zIndex);
Assert.areEqual(3000, arr[2].zIndex);
arr.sort(SortUtil.byZIndex.bind(FlxSort.DESCENDING));
Assert.areEqual(3000, arr[0].zIndex);
Assert.areEqual(2000, arr[1].zIndex);
Assert.areEqual(1000, arr[2].zIndex);
}
}

View file

@ -0,0 +1,64 @@
package funkin.util;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.VersionUtil;
@:access(funkin.util.VersionUtil)
class VersionUtilTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testValidateVersionStr()
{
Assert.areEqual(true, VersionUtil.validateVersionStr("1.0.0", "1.0.0"));
Assert.areEqual(false, VersionUtil.validateVersionStr("ehe", "test"));
}
@Test
public function testValidateVersion()
{
var version:thx.semver.Version = "1.0.0"; // implicit cast
var versionRule:thx.semver.VersionRule = "1.0.0"; // implicit cast
Assert.areEqual(true, VersionUtil.validateVersion(version, versionRule));
var versionRule2:thx.semver.VersionRule = ">=3.1.0"; // implicit cast
var version1:thx.semver.Version = "3.0.0";
var version2:thx.semver.Version = "3.1.1";
var version3:thx.semver.Version = "4.2.0";
Assert.areEqual(false, VersionUtil.validateVersion(version1, versionRule2));
Assert.areEqual(true, VersionUtil.validateVersion(version2, versionRule2));
Assert.areEqual(true, VersionUtil.validateVersion(version3, versionRule2));
}
@Test
public function testGetVersionFromJSON()
{
var jsonStr:String = "{ \"version\": \"3.1.0\" }";
var version:thx.semver.Version = VersionUtil.getVersionFromJSON(jsonStr);
Assert.areEqual("3.1.0", version.toString());
}
}

View file

@ -0,0 +1,42 @@
package funkin.util.assets;
import openfl.utils.Assets;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.assets.DataAssets;
@:access(funkin.util.assets.DataAssets)
class DataAssetsTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testBuildDataPath()
{
Assert.areEqual('assets/data/test.json', DataAssets.buildDataPath('test.json'));
}
@Test
public function listDataFilesInPath()
{
var expected = ['blablabla', 'test1', 'test2'];
Assert.areEqual(expected, DataAssets.listDataFilesInPath('test/'));
}
}

View file

@ -0,0 +1,118 @@
package funkin.util.assets;
import funkin.util.assets.FlxAnimationUtil;
import flixel.animation.FlxAnimationController;
import funkin.data.animation.AnimationData;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.DateUtil;
import flixel.FlxSprite;
@:access(funkin.util.assets.FlxAnimationUtil)
class FlxAnimationUtilTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testAddAtlasAnimation()
{
// Build a mock child class of FlxSprite
var mockSprite = mock(FlxSprite);
var mockAnim = mock(FlxAnimationController);
mockSprite.animation = mockAnim;
var animData:AnimationData =
{
name: "testAnim",
prefix: "blablabla"
};
FlxAnimationUtil.addAtlasAnimation(mockSprite, animData);
// Verify that the method was called once.
// If not, a VerificationException will be thrown and the test will fail.
mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false).verify(times(1));
// Verify there were no other functions called.
mockAnim.verifyZeroInteractions();
mockSprite.verifyZeroInteractions();
var animData2:AnimationData =
{
name: "testAnim2",
prefix: "blablabla2",
frameIndices: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
frameRate: 12,
flipX: true,
flipY: true,
looped: true
};
FlxAnimationUtil.addAtlasAnimation(mockSprite, animData2);
try
{
mockAnim.addByIndices("testAnim2", "blablabla2", cast anyIterator, "", 12, true, true, true).verify(times(1));
}
catch (e)
{
trace('CAUGHT EXCEPTION');
trace(e);
}
mockAnim.verifyZeroInteractions();
mockSprite.verifyZeroInteractions();
}
@Test
public function testAddAtlasAnimations()
{
// Build a mock child class of FlxSprite
var mockSprite = mock(FlxSprite);
var mockAnim = mock(FlxAnimationController);
mockSprite.animation = mockAnim;
var animData:Array<AnimationData> = [
{
name: "testAnim",
prefix: "blablabla"
},
{
name: "testAnim2",
prefix: "blablabla2"
},
{
name: "testAnim3",
prefix: "blablabla3"
}
];
FlxAnimationUtil.addAtlasAnimations(mockSprite, animData);
// Verify that the method was called once.
// If not, a VerificationException will be thrown and the test will fail.
mockAnim.addByPrefix("testAnim", "blablabla", 24, false, false, false).verify(times(1));
mockAnim.addByPrefix("testAnim2", "blablabla2", 24, false, false, false).verify(times(1));
mockAnim.addByPrefix("testAnim3", "blablabla3", 24, false, false, false).verify(times(1));
// Verify there were no other functions called.
mockAnim.verifyZeroInteractions();
mockSprite.verifyZeroInteractions();
}
}

View file

@ -0,0 +1,165 @@
package funkin.util.tools;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.ArrayTools;
@:access(funkin.util.tools.ArrayTools)
class ArraySortToolsTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testMergeSort()
{
var testArray:Array<Int> = [5, 4, 3, 2, 1];
function compare(a:Int, b:Int)
{
return a - b;
}
ArraySortTools.mergeSort(testArray, compare);
Assert.areEqual(testArray[0], 1);
Assert.areEqual(testArray[1], 2);
Assert.areEqual(testArray[2], 3);
Assert.areEqual(testArray[3], 4);
Assert.areEqual(testArray[4], 5);
var testArray2:Array<Int> = [9, 6, 3, 12];
ArraySortTools.mergeSort(testArray2, compare);
Assert.areEqual(testArray2[0], 3);
Assert.areEqual(testArray2[1], 6);
Assert.areEqual(testArray2[2], 9);
Assert.areEqual(testArray2[3], 12);
// Just make sure these don't crash.
ArraySortTools.mergeSort([], compare);
ArraySortTools.mergeSort(null, compare);
ArraySortTools.mergeSort([], null);
ArraySortTools.mergeSort(null, null);
// Make sure these throw an exception.
try
{
ArraySortTools.mergeSort(testArray, null);
Assert.fail("Function should have thrown an exception.");
}
catch (e)
{
Assert.areEqual("No comparison function provided.", e);
}
}
@Test
public function testQuickSort()
{
var testArray:Array<Int> = [5, 4, 3, 2, 1];
function compare(a:Int, b:Int)
{
return a - b;
}
ArraySortTools.quickSort(testArray, compare);
Assert.areEqual(testArray[0], 1);
Assert.areEqual(testArray[1], 2);
Assert.areEqual(testArray[2], 3);
Assert.areEqual(testArray[3], 4);
Assert.areEqual(testArray[4], 5);
var testArray2:Array<Int> = [9, 6, 3, 12];
ArraySortTools.quickSort(testArray2, compare);
Assert.areEqual(testArray2[0], 3);
Assert.areEqual(testArray2[1], 6);
Assert.areEqual(testArray2[2], 9);
Assert.areEqual(testArray2[3], 12);
// Just make sure these don't crash.
ArraySortTools.quickSort([], compare);
ArraySortTools.quickSort(null, compare);
ArraySortTools.quickSort([], null);
ArraySortTools.quickSort(null, null);
// Make sure these throw an exception.
try
{
ArraySortTools.quickSort(testArray, null);
Assert.fail("Function should have thrown an exception.");
}
catch (e)
{
Assert.areEqual("No comparison function provided.", e);
}
}
@Test
public function testInsertionSort()
{
var testArray:Array<Int> = [5, 4, 3, 2, 1];
function compare(a:Int, b:Int)
{
return a - b;
}
ArraySortTools.insertionSort(testArray, compare);
Assert.areEqual(testArray[0], 1);
Assert.areEqual(testArray[1], 2);
Assert.areEqual(testArray[2], 3);
Assert.areEqual(testArray[3], 4);
Assert.areEqual(testArray[4], 5);
var testArray2:Array<Int> = [9, 6, 3, 12];
ArraySortTools.insertionSort(testArray2, compare);
Assert.areEqual(testArray2[0], 3);
Assert.areEqual(testArray2[1], 6);
Assert.areEqual(testArray2[2], 9);
Assert.areEqual(testArray2[3], 12);
// Just make sure these don't crash.
ArraySortTools.insertionSort([], compare);
ArraySortTools.insertionSort(null, compare);
ArraySortTools.insertionSort([], null);
ArraySortTools.insertionSort(null, null);
// Make sure these throw an exception.
try
{
ArraySortTools.insertionSort(testArray, null);
Assert.fail("Function should have thrown an exception.");
}
catch (e)
{
Assert.areEqual("No comparison function provided.", e);
}
}
}

View file

@ -0,0 +1,65 @@
package funkin.util.tools;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.ArrayTools;
@:access(funkin.util.tools.ArrayTools)
class ArrayToolsTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testUnique()
{
var testArray:Array<Int> = [1, 2, 3, 4, 5, 6, 10, 7, 8, 9, 3, 4, 5, 6, 11, 7, 8];
var uniqueArray:Array<Int> = ArrayTools.unique(testArray);
Assert.areEqual(uniqueArray.length, 11);
// Array order doesn't change
Assert.areEqual(uniqueArray[0], 1);
Assert.areEqual(uniqueArray[1], 2);
Assert.areEqual(uniqueArray[2], 3);
Assert.areEqual(uniqueArray[3], 4);
Assert.areEqual(uniqueArray[4], 5);
Assert.areEqual(uniqueArray[5], 6);
Assert.areEqual(uniqueArray[6], 10);
Assert.areEqual(uniqueArray[7], 7);
Assert.areEqual(uniqueArray[8], 8);
Assert.areEqual(uniqueArray[9], 9);
Assert.areEqual(uniqueArray[10], 11);
}
@Test
public function testFind()
{
function predicate(a:String):Bool
{
return a.startsWith("Hello");
}
var testArray:Array<String> = ["Foo", "Bar", "HelloWorld", "Baz", "HelloTest"];
var result = ArrayTools.find(testArray, predicate);
Assert.areEqual(result, "HelloWorld");
}
}

View file

@ -0,0 +1,59 @@
package funkin.util.tools;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.IteratorTools;
@:access(funkin.util.tools.IteratorTools)
class IteratorToolsTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testArray()
{
var iter = new MyStringIterator("HelloWorld");
var arr = IteratorTools.array(iter);
Assert.areEqual(["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"], arr);
}
}
class MyStringIterator
{
var s:String;
var i:Int;
public function new(s:String)
{
this.s = s;
i = 0;
}
public function hasNext()
{
return i < s.length;
}
public function next()
{
return s.charAt(i++);
}
}

View file

@ -0,0 +1,55 @@
package funkin.util.tools;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.MapTools;
@:access(funkin.util.tools.MapTools)
class MapToolsTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testSize()
{
var testMap:Map<String, String> = ["key1" => "value1", "key2" => "value2", "key3" => "value3"];
Assert.areEqual(3, MapTools.size(testMap));
}
@Test
public function testValues()
{
var testMap:Map<String, String> = ["key1" => "value1", "key2" => "value2", "key3" => "value3"];
var result:Array<String> = MapTools.values(testMap);
result.sort(SortUtil.alphabetically);
Assert.areEqual(["value1", "value2", "value3"], result);
}
@Test
public function testKeyValues()
{
var testMap:Map<String, String> = ["key1" => "value1", "key2" => "value2", "key3" => "value3"];
var result:Array<String> = MapTools.keyValues(testMap);
result.sort(SortUtil.alphabetically);
Assert.areEqual(["key1", "key2", "key3"], result);
}
}

View file

@ -0,0 +1,59 @@
package funkin.util.tools;
import massive.munit.util.Timer;
import massive.munit.Assert;
import massive.munit.async.AsyncFactory;
import funkin.util.tools.StringTools;
@:access(funkin.util.tools.StringTools)
class StringToolsTest extends FunkinTest
{
public function new()
{
super();
}
@BeforeClass
public function beforeClass() {}
@AfterClass
public function afterClass() {}
@Before
public function setup() {}
@After
public function tearDown() {}
@Test
public function testToTitleCase()
{
var input = "hello world";
Assert.areEqual("Hello World", StringTools.toTitleCase(input));
}
@Test
public function testToLowerKebabCase()
{
var input = "hello world";
Assert.areEqual("hello-world", StringTools.toLowerKebabCase(input));
}
@Test
public function testToUpperKebabCase()
{
var input = "hello world";
Assert.areEqual("HELLO-WORLD", StringTools.toUpperKebabCase(input));
}
@Test
public function testParseJSON()
{
var input = "{ \"hello\": \"world\" }";
Assert.areEqual({hello: "world"}, StringTools.parseJSON(input));
}
}

View file

@ -0,0 +1,21 @@
#if !macro
// Only import these when we aren't in a macro.
import funkin.util.Constants;
import funkin.Paths;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
// These are great.
using Lambda;
using StringTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.IteratorTools;
using funkin.util.tools.MapTools;
using funkin.util.tools.StringTools;
#end
// Testing-specific
// Mocking
import mockatoo.Mockatoo.*;
using mockatoo.Mockatoo;

3
tests/unit/start-mac-native.sh Executable file
View file

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

3
tests/unit/start-mac-web.sh Executable file
View file

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

View file

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

11
tests/unit/test-cpp.hxml Normal file
View file

@ -0,0 +1,11 @@
# 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

5
tests/unit/test-web.hxml Normal file
View file

@ -0,0 +1,5 @@
# Update the test runner classes
# TODO: This just opens the browser and doesn't do anything?
-cmd haxelib run munit gen
-cmd haxelib run lime build html5
-cmd haxelib run lime test html5

32
tests/unit/test.hxml Normal file
View file

@ -0,0 +1,32 @@
## CPP
--next
-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')

24
tests/unit/test.hxml-old Normal file
View file

@ -0,0 +1,24 @@
## JavaScript HTML5
--next
-js build/js_test.js
# Funkin' deps
-lib lime
-lib openfl
-lib flixel
-lib flixel-addons
-lib hscript
-lib flixel-ui
-lib haxeui-core
-lib haxeui-flixel
-lib polymod
-lib flxanimate
-lib hxCodec
-lib thx.semver
-lib json2object
-lib tink_json
# Test deps
-lib munit
-lib hamcrest
# Class paths
-cp source
-cp ../../source