diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1130887 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Paige Marincak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..74cba36 --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# gms2-test +Unit Testing Framework for Gamemaker Studio 2.3+ + +## Table of Contents + + 1. [Requirements](#requirements) + 2. [Download](#download) + - [Local Package](#local-package) + - [Marketplace](#marketplace) + 3. [Maintainers & Contribution](#maintainers--contribution) + 4. [Samples](#samples) + 5. [API](#api) + - [Helpers](#helpers) + - [Matchers](#matchers) + - [Test Definitions](#test-definitions) + 6. [Unit Testing Resources](#unit-testing-resources) + +## Requirements + +|| Supported Version | +|--|--| +| Gamemaker Studio 2 | v23.1.1.160 | + +## Download +### Local Package + + 1. Download the [exported local package](https://github.com/pmarincak/gms2-test/blob/master/export/package.gmltest.yymps) + 2. Select Tools > Import Local Package + 3. Navigate to the saved package and select import + 4. Import all resources + +### Marketplace + + TBD + +## Maintainers & Contribution +This package is maintained by [Paige Marincak](https://twitter.com/paigemarincak/). To contribute, please fork the repo and make pull requests. + +## Samples +Samples can be viewed [here](https://github.com/pmarincak/gms2-test/tree/master/samples). + +## API +### Helpers +#### gmltest_start +Start running the unit tests + +#### gmltest_set_deterministic +Sets the seed to a static value or a random value. Can be toggled. + + @param {Bool} deterministic Whether to set the seed to a static value or not + +### Matchers +#### gmltest_expect_eq +Expects that the actual value is equal to the expected value + + @param {*} expected + @param {*} actual + +#### gmltest_expect_false +Expects that the provided value is false + + @param {*} value + +#### gmltest_expect_true +Expects that the provided value is true + + @param {*} value + +#### gmltest_expect_gt +Expects that the actual value is greater than the expected value + + @param {*} expected + @param {*} actual + +#### gmltest_expect_lt +Expects that the actual value is less than the expected value + + @param {*} expected + @param {*} actual + +#### gmltest_expect_neq +Expects that the actual value is not equal to the expected value + + @param {*} expected + @param {*} actual + +#### gmltest_expect_not_null +Expects that the provided value is not null + + @param {*} value + +#### gmltest_expect_null +Expects that the provided value is null + + @param {*} value + +### Test Definitions +#### GMLTest_Harness +Base struct that all harnesses should extend from + + ///@description Called before the execution of the test. + /// Use this function to setup your fixtures and parameterized tests. + function setup(){ + // Override this function + } + + ///@description Called after the execution of the test. + /// Use this function to clean up your fixtures and parameterized tests. + function tear_down(){ + // Override this function + } +#### test +Register a basic test with a name and a function to execute + + @param {String} name The name of the test to be logged to the console + @param {Function} fn The function to be executed +#### xtest +Disable a registered basic test that has a name and a function to execute + + @param {String} name The name of the test to be logged to the console + @param {Function} fn The function to be executed +#### test_f +Register a fixture test with a harness, name and a function to execute + + @param {Struct} harness The struct to use as the harness when the test executes + @param {String} name The name of the test to be logged to the console + @param {Function} fn The function to be executed + +#### xtest_f +Disable a registered fixture test that has a harness, name and a function to execute + + @param {Struct} harness The struct to use as the harness when the test executes + @param {String} name The name of the test to be logged to the console + @param {Function} fn The function to be executed + +#### test_p +Register a parameterized test with a harness, name, array of parameters, and a function to execute + + @param {Struct} harness The struct to use as the harness when the test executes + @param {String} name The name of the test to be logged to the console + @param {Array} array An array containing a list of parameters to be executed using the same provided function + @param {Function} fn The function to be executed which takes one parameter + +#### xtest_p +Disable a registered parameterized test that has a harness, name, array of parameters, and a function to execute + + @param {Struct} harness The struct to use as the harness when the test executes + @param {String} name The name of the test to be logged to the console + @param {Array} array An array containing a list of parameters to be executed using the same provided function + @param {Function} fn The function to be executed which takes one parameter + +## Unit Testing Resources +The following are a list of resources that can assist you with writing your unit tests. + +- [What is unit testing?](https://en.wikipedia.org/wiki/Unit_testing) +- [What is a fixture?](https://en.wikipedia.org/wiki/Test_fixture) +- [Beginner's Guide to Unit Testing](https://www.codementor.io/@wbsimms/unit-testing-foundations-programming-beginners-du107q81d) diff --git a/export/package.gmltest.yymps b/export/package.gmltest.yymps new file mode 100644 index 0000000..7093279 Binary files /dev/null and b/export/package.gmltest.yymps differ diff --git a/samples/BasicSample/BasicSample.yyp b/samples/BasicSample/BasicSample.yyp new file mode 100644 index 0000000..c204207 --- /dev/null +++ b/samples/BasicSample/BasicSample.yyp @@ -0,0 +1,50 @@ +{ + "resources": [ + {"id":{"name":"GMLTest_Helpers","path":"scripts/GMLTest_Helpers/GMLTest_Helpers.yy",},"order":0,}, + {"id":{"name":"GMLTest_Private","path":"scripts/GMLTest_Private/GMLTest_Private.yy",},"order":3,}, + {"id":{"name":"GMLTest_Tests","path":"scripts/GMLTest_Tests/GMLTest_Tests.yy",},"order":4,}, + {"id":{"name":"GMLTest_Manager","path":"scripts/GMLTest_Manager/GMLTest_Manager.yy",},"order":1,}, + {"id":{"name":"StandardTests","path":"scripts/StandardTests/StandardTests.yy",},"order":0,}, + {"id":{"name":"GMLTest_Matchers","path":"scripts/GMLTest_Matchers/GMLTest_Matchers.yy",},"order":2,}, + {"id":{"name":"ParameterizedTests","path":"scripts/ParameterizedTests/ParameterizedTests.yy",},"order":2,}, + {"id":{"name":"FixtureTests","path":"scripts/FixtureTests/FixtureTests.yy",},"order":1,}, + {"id":{"name":"MyObject","path":"objects/MyObject/MyObject.yy",},"order":0,}, + {"id":{"name":"Room1","path":"rooms/Room1/Room1.yy",},"order":1,}, + ], + "Options": [ + {"name":"HTML5","path":"options/html5/options_html5.yy",}, + {"name":"Linux","path":"options/linux/options_linux.yy",}, + {"name":"macOS","path":"options/mac/options_mac.yy",}, + {"name":"Main","path":"options/main/options_main.yy",}, + {"name":"Windows","path":"options/windows/options_windows.yy",}, + ], + "isDnDProject": false, + "isEcma": false, + "tutorialPath": "", + "configs": { + "name": "Default", + "children": [], + }, + "RoomOrder": [ + {"name":"Room1","path":"rooms/Room1/Room1.yy",}, + ], + "Folders": [ + {"folderPath":"folders/GMLTest.yy","order":1,"resourceVersion":"1.0","name":"GMLTest","tags":[],"resourceType":"GMFolder",}, + {"folderPath":"folders/Sample Unit Tests.yy","order":2,"resourceVersion":"1.0","name":"Sample Unit Tests","tags":[],"resourceType":"GMFolder",}, + {"folderPath":"folders/Supporting Resources.yy","order":4,"resourceVersion":"1.0","name":"Supporting Resources","tags":[],"resourceType":"GMFolder",}, + ], + "AudioGroups": [ + {"targets":461609314234257646,"resourceVersion":"1.0","name":"audiogroup_default","resourceType":"GMAudioGroup",}, + ], + "TextureGroups": [ + {"isScaled":true,"autocrop":true,"border":2,"mipsToGenerate":0,"targets":461609314234257646,"resourceVersion":"1.0","name":"Default","resourceType":"GMTextureGroup",}, + ], + "IncludedFiles": [], + "MetaData": { + "IDEVersion": "23.1.1.168", + }, + "resourceVersion": "1.3", + "name": "BasicSample", + "tags": [], + "resourceType": "GMProject", +} \ No newline at end of file diff --git a/samples/BasicSample/objects/MyObject/Create_0.gml b/samples/BasicSample/objects/MyObject/Create_0.gml new file mode 100644 index 0000000..3c4d7d4 --- /dev/null +++ b/samples/BasicSample/objects/MyObject/Create_0.gml @@ -0,0 +1 @@ +name = "hello"; \ No newline at end of file diff --git a/samples/BasicSample/objects/MyObject/MyObject.yy b/samples/BasicSample/objects/MyObject/MyObject.yy new file mode 100644 index 0000000..d16844d --- /dev/null +++ b/samples/BasicSample/objects/MyObject/MyObject.yy @@ -0,0 +1,33 @@ +{ + "spriteId": null, + "solid": false, + "visible": false, + "spriteMaskId": null, + "persistent": false, + "parentObjectId": null, + "physicsObject": false, + "physicsSensor": false, + "physicsShape": 1, + "physicsGroup": 1, + "physicsDensity": 0.5, + "physicsRestitution": 0.1, + "physicsLinearDamping": 0.1, + "physicsAngularDamping": 0.1, + "physicsFriction": 0.2, + "physicsStartAwake": true, + "physicsKinematic": false, + "physicsShapePoints": [], + "eventList": [ + {"isDnD":false,"eventNum":0,"eventType":0,"collisionObjectId":null,"parent":{"name":"MyObject","path":"objects/MyObject/MyObject.yy",},"resourceVersion":"1.0","name":"","tags":[],"resourceType":"GMEvent",}, + ], + "properties": [], + "overriddenProperties": [], + "parent": { + "name": "Supporting Resources", + "path": "folders/Supporting Resources.yy", + }, + "resourceVersion": "1.0", + "name": "MyObject", + "tags": [], + "resourceType": "GMObject", +} \ No newline at end of file diff --git a/samples/BasicSample/options/html5/options_html5.yy b/samples/BasicSample/options/html5/options_html5.yy new file mode 100644 index 0000000..876b777 --- /dev/null +++ b/samples/BasicSample/options/html5/options_html5.yy @@ -0,0 +1,34 @@ +{ + "option_html5_browser_title": "Created with GameMaker Studio 2", + "option_html5_version": "1.0.0.0", + "option_html5_foldername": "html5game", + "option_html5_outputname": "index.html", + "option_html5_splash_png": "${base_options_dir}\\html5\\splash.png", + "option_html5_usesplash": false, + "option_html5_outputdebugtoconsole": false, + "option_html5_display_cursor": true, + "option_html5_localrunalert": true, + "option_html5_index": "", + "option_html5_loadingbar": "", + "option_html5_jsprepend": "", + "option_html5_icon": "${base_options_dir}\\html5\\fav.ico", + "option_html5_allow_fullscreen": true, + "option_html5_interpolate_pixels": true, + "option_html5_centregame": false, + "option_html5_usebuiltinparticles": true, + "option_html5_usebuiltinfont": true, + "option_html5_webgl": 2, + "option_html5_scale": 0, + "option_html5_texture_page": "2048x2048", + "option_html5_use_facebook": false, + "option_html5_facebook_id": "", + "option_html5_facebook_app_display_name": "", + "option_html5_flurry_enable": false, + "option_html5_flurry_id": "", + "option_html5_google_analytics_enable": false, + "option_html5_google_tracking_id": "", + "resourceVersion": "1.0", + "name": "HTML5", + "tags": [], + "resourceType": "GMHtml5Options", +} \ No newline at end of file diff --git a/samples/BasicSample/options/linux/options_linux.yy b/samples/BasicSample/options/linux/options_linux.yy new file mode 100644 index 0000000..356a804 --- /dev/null +++ b/samples/BasicSample/options/linux/options_linux.yy @@ -0,0 +1,25 @@ +{ + "option_linux_display_name": "Created with GameMaker Studio 2", + "option_linux_version": "1.0.0.0", + "option_linux_maintainer_email": "", + "option_linux_homepage": "http://www.yoyogames.com", + "option_linux_short_desc": "", + "option_linux_long_desc": "", + "option_linux_splash_screen": "${base_options_dir}\\linux\\splash\\splash.png", + "option_linux_display_splash": false, + "option_linux_icon": "${base_options_dir}\\linux\\icons\\64.png", + "option_linux_start_fullscreen": false, + "option_linux_allow_fullscreen": false, + "option_linux_interpolate_pixels": true, + "option_linux_display_cursor": true, + "option_linux_sync": false, + "option_linux_resize_window": false, + "option_linux_scale": 0, + "option_linux_texture_page": "2048x2048", + "option_linux_enable_steam": false, + "option_linux_disable_sandbox": false, + "resourceVersion": "1.0", + "name": "Linux", + "tags": [], + "resourceType": "GMLinuxOptions", +} \ No newline at end of file diff --git a/samples/BasicSample/options/mac/options_mac.yy b/samples/BasicSample/options/mac/options_mac.yy new file mode 100644 index 0000000..78bf670 --- /dev/null +++ b/samples/BasicSample/options/mac/options_mac.yy @@ -0,0 +1,32 @@ +{ + "option_mac_display_name": "Created with GameMaker Studio 2", + "option_mac_app_id": "com.company.game", + "option_mac_version": "1.0.0.0", + "option_mac_output_dir": "~/gamemakerstudio2", + "option_mac_team_id": "", + "option_mac_signing_identity": "Developer ID Application:", + "option_mac_copyright": "", + "option_mac_splash_png": "${base_options_dir}\\mac\\splash\\splash.png", + "option_mac_icon_png": "${base_options_dir}\\mac\\icons\\1024.png", + "option_mac_menu_dock": false, + "option_mac_display_cursor": true, + "option_mac_start_fullscreen": false, + "option_mac_allow_fullscreen": false, + "option_mac_interpolate_pixels": true, + "option_mac_vsync": false, + "option_mac_resize_window": false, + "option_mac_enable_retina": false, + "option_mac_scale": 0, + "option_mac_texture_page": "2048x2048", + "option_mac_build_app_store": false, + "option_mac_allow_incoming_network": false, + "option_mac_allow_outgoing_network": false, + "option_mac_app_category": "Games", + "option_mac_enable_steam": false, + "option_mac_disable_sandbox": false, + "option_mac_apple_sign_in": false, + "resourceVersion": "1.0", + "name": "macOS", + "tags": [], + "resourceType": "GMMacOptions", +} \ No newline at end of file diff --git a/samples/BasicSample/options/main/options_main.yy b/samples/BasicSample/options/main/options_main.yy new file mode 100644 index 0000000..dd028dd --- /dev/null +++ b/samples/BasicSample/options/main/options_main.yy @@ -0,0 +1,18 @@ +{ + "option_gameguid": "c260e3c6-b8f0-49ce-9e21-d70dcb990fff", + "option_game_speed": 60, + "option_mips_for_3d_textures": false, + "option_draw_colour": 4294967295, + "option_window_colour": 255, + "option_steam_app_id": "0", + "option_sci_usesci": false, + "option_author": "", + "option_lastchanged": "", + "addon_amazon_apis": "", + "addon_google_play_services": "", + "option_spine_licence": false, + "resourceVersion": "1.1", + "name": "Main", + "tags": [], + "resourceType": "GMMainOptions", +} \ No newline at end of file diff --git a/samples/BasicSample/options/windows/options_windows.yy b/samples/BasicSample/options/windows/options_windows.yy new file mode 100644 index 0000000..7fb7dad --- /dev/null +++ b/samples/BasicSample/options/windows/options_windows.yy @@ -0,0 +1,35 @@ +{ + "option_windows_display_name": "Created with GameMaker Studio 2", + "option_windows_executable_name": "${project_name}.exe", + "option_windows_version": "1.0.0.0", + "option_windows_company_info": "YoYo Games Ltd", + "option_windows_product_info": "Created with GameMaker Studio 2", + "option_windows_copyright_info": "", + "option_windows_description_info": "A GameMaker Studio 2 Game", + "option_windows_display_cursor": true, + "option_windows_icon": "${base_options_dir}\\windows\\icons\\icon.ico", + "option_windows_save_location": 0, + "option_windows_splash_screen": "${base_options_dir}\\windows\\splash\\splash.png", + "option_windows_use_splash": false, + "option_windows_start_fullscreen": false, + "option_windows_allow_fullscreen_switching": false, + "option_windows_interpolate_pixels": false, + "option_windows_vsync": false, + "option_windows_resize_window": false, + "option_windows_borderless": false, + "option_windows_scale": 0, + "option_windows_copy_exe_to_dest": false, + "option_windows_sleep_margin": 10, + "option_windows_texture_page": "2048x2048", + "option_windows_installer_finished": "${base_options_dir}\\windows\\installer\\finished.bmp", + "option_windows_installer_header": "${base_options_dir}\\windows\\installer\\header.bmp", + "option_windows_license": "${base_options_dir}\\windows\\installer\\license.txt", + "option_windows_nsis_file": "${base_options_dir}\\windows\\installer\\nsis_script.nsi", + "option_windows_enable_steam": false, + "option_windows_disable_sandbox": false, + "option_windows_steam_use_alternative_launcher": false, + "resourceVersion": "1.0", + "name": "Windows", + "tags": [], + "resourceType": "GMWindowsOptions", +} \ No newline at end of file diff --git a/samples/BasicSample/rooms/Room1/Room1.yy b/samples/BasicSample/rooms/Room1/Room1.yy new file mode 100644 index 0000000..4bf30b7 --- /dev/null +++ b/samples/BasicSample/rooms/Room1/Room1.yy @@ -0,0 +1,51 @@ +{ + "isDnd": false, + "volume": 1.0, + "parentRoom": null, + "views": [ + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, + ], + "layers": [ + {"spriteId":null,"colour":4278190080,"x":0,"y":0,"htiled":false,"vtiled":false,"hspeed":0.0,"vspeed":0.0,"stretch":false,"animationFPS":15.0,"animationSpeedType":0,"userdefinedAnimFPS":false,"visible":true,"depth":0,"userdefinedDepth":false,"inheritLayerDepth":false,"inheritLayerSettings":false,"gridX":32,"gridY":32,"layers":[],"hierarchyFrozen":false,"resourceVersion":"1.0","name":"Background","tags":[],"resourceType":"GMRBackgroundLayer",}, + ], + "inheritLayers": false, + "creationCodeFile": "${project_dir}\\rooms\\Room1\\RoomCreationCode.gml", + "inheritCode": false, + "instanceCreationOrder": [], + "inheritCreationOrder": false, + "sequenceId": null, + "roomSettings": { + "inheritRoomSettings": false, + "Width": 1366, + "Height": 768, + "persistent": false, + }, + "viewSettings": { + "inheritViewSettings": false, + "enableViews": false, + "clearViewBackground": false, + "clearDisplayBuffer": true, + }, + "physicsSettings": { + "inheritPhysicsSettings": false, + "PhysicsWorld": false, + "PhysicsWorldGravityX": 0.0, + "PhysicsWorldGravityY": 10.0, + "PhysicsWorldPixToMetres": 0.1, + }, + "parent": { + "name": "Supporting Resources", + "path": "folders/Supporting Resources.yy", + }, + "resourceVersion": "1.0", + "name": "Room1", + "tags": [], + "resourceType": "GMRoom", +} \ No newline at end of file diff --git a/samples/BasicSample/rooms/Room1/RoomCreationCode.gml b/samples/BasicSample/rooms/Room1/RoomCreationCode.gml new file mode 100644 index 0000000..9b4bad4 --- /dev/null +++ b/samples/BasicSample/rooms/Room1/RoomCreationCode.gml @@ -0,0 +1 @@ +gmltest_start(); \ No newline at end of file diff --git a/samples/BasicSample/scripts/FixtureTests/FixtureTests.gml b/samples/BasicSample/scripts/FixtureTests/FixtureTests.gml new file mode 100644 index 0000000..cf751a7 --- /dev/null +++ b/samples/BasicSample/scripts/FixtureTests/FixtureTests.gml @@ -0,0 +1,28 @@ +///@description A custom fixture that tests MyObject +function MyFixture() : GMLTest_Harness() constructor { + + instance = noone; + + function setup(){ + // Before we execute the test let's create the instance we will be testing + // This way we don't have to duplicate code everywhere to do our tests + instance = instance_create_depth(0,0,0, MyObject); + } + + function tear_down(){ + // After we are done testing let's clean up and destroy the instance to keep things clean + instance_destroy(instance); + } + +} + +///@description Fixture test showing basic usage +test_f(MyFixture, "NotVisibleByDefault", function(){ + /// The function has access to the variables declared in MyFixture + gmltest_expect_false(instance.visible); +}); + +///@description Disabled fixture test showing basic usage +xtest_f(MyFixture, "NameIsHelloByDefault", function(){ + gmltest_expect_eq("hello", instance.name); +}); \ No newline at end of file diff --git a/samples/BasicSample/scripts/FixtureTests/FixtureTests.yy b/samples/BasicSample/scripts/FixtureTests/FixtureTests.yy new file mode 100644 index 0000000..1db3eab --- /dev/null +++ b/samples/BasicSample/scripts/FixtureTests/FixtureTests.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "Sample Unit Tests", + "path": "folders/Sample Unit Tests.yy", + }, + "resourceVersion": "1.0", + "name": "FixtureTests", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Helpers/GMLTest_Helpers.gml b/samples/BasicSample/scripts/GMLTest_Helpers/GMLTest_Helpers.gml new file mode 100644 index 0000000..78cc4c9 --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Helpers/GMLTest_Helpers.gml @@ -0,0 +1,15 @@ +///@description Start running the unit tests +function gmltest_start() { + global.GMLTestManager.execute(); +} + +///@description Sets the seed to a static value or a random value. Can be toggled. +///@param {Bool} deterministic Whether to set the seed to a static value or not +function gmltest_set_deterministic(deterministic) { + if (deterministic){ + random_set_seed(0); + } + else{ + random_set_seed(global.GMLTestManager._seed); + } +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Helpers/GMLTest_Helpers.yy b/samples/BasicSample/scripts/GMLTest_Helpers/GMLTest_Helpers.yy new file mode 100644 index 0000000..6f57a5b --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Helpers/GMLTest_Helpers.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "GMLTest", + "path": "folders/GMLTest.yy", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Helpers", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Manager/GMLTest_Manager.gml b/samples/BasicSample/scripts/GMLTest_Manager/GMLTest_Manager.gml new file mode 100644 index 0000000..6248bcc --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Manager/GMLTest_Manager.gml @@ -0,0 +1,150 @@ +///@description Struct used to manage and execute all registered tests +function GMLTest_Manager() constructor { + _tests = ds_list_create(); + _failCount = 0; + _disabledCount = 0; + _testCount = 0; + _seed = random_get_seed(); + + ///@description Destroys data held by the manager. + /// Should be called after tests are finished executing. + _destroy = function() { + ds_list_destroy(_tests); + } + + ///@description Get the status string for whether there was a pass or a fail + ///@param {Bool} passed + _get_status_string = function(passed){ + return passed ? "PASSED" : "FAILED"; + } + + ///@description Run a standard test + ///@param {Struct} test + _run_test = function (test){ + var passed = true; + var testName = test.get_name(); + _gmltest_log_status("RUN", testName); + _testCount++; + + try { + test._fn(); + } catch (e){ + passed = false; + _handleException(e); + } + + var statusString = _get_status_string(passed); + _gmltest_log_status(statusString, testName); + } + + ///@description Run a fixture test + ///@param {Struct} test + _run_fixture_test = function (test){ + var passed = true; + var testName = test.get_name(); + _gmltest_log_status("RUN", testName); + _testCount++; + + var harness = new test._harness(); + harness.setup(); + var fn = method(harness, test._fn); + try { + fn(); + } catch (e){ + passed = false; + _handleException(e); + } + harness.tear_down(); + delete harness; + + var statusString = _get_status_string(passed); + _gmltest_log_status(statusString, testName); + } + + ///@description Run a parameterized test + ///@param {Struct} test + _run_parameter_test = function (test){ + for (var i = 0; i < array_length(test._array); i++){ + var passed = true; + var testName = test.get_name() + "::" + string(i); + _gmltest_log_status("RUN", testName); + _testCount++; + + var harness = new test._harness(); + harness.setup(); + var fn = method(harness, test._fn); + try { + fn(test._array[i]); + } catch (e){ + passed = false; + _handleException(e); + } + harness.tear_down(); + delete harness; + + var statusString = _get_status_string(passed); + _gmltest_log_status(statusString, testName); + } + } + + ///@description Handles any exceptions thrown during the execution of the test + ///@param {Struct} e + _handleException = function (e){ + _failCount++; + show_debug_message(e.message); + // If we threw an exception and it wasn't because an expect failed, we should log the callstack to the user + if (!variable_struct_exists(e, "expectFailed")){ + _gmltest_log_callstack(e.stacktrace); + } + } + + ///@description Execute the provided test struct + ///@param {Struct} test + _execute_test = function (test) { + if (test._disabled){ + _disabledCount++; + _gmltest_log_status("DISABLED", test.get_name()); + return; + } + + if (test._harness == noone){ + _run_test(test); + }else if (test._array == noone){ + _run_fixture_test(test); + } + else { + _run_parameter_test(test); + } + } + + ///@description Execute all registered tests + execute = function () { + var testCount = ds_list_size(_tests); + var startTime = current_time; + + for (var i = 0; i < testCount; i++){ + var test = ds_list_find_value(_tests, i); + _execute_test(test); + } + + var endTime = current_time; + var timeToRun = endTime - startTime; + + show_debug_message("-------------------------"); + show_debug_message("RAN " + string(_testCount) + " TESTS IN " + string(timeToRun) + "MS."); + if (_disabledCount > 0){ + show_debug_message("DISABLED TESTS: " + string(_disabledCount)); + } + if (_failCount > 0){ + show_debug_message("FAILED TESTS: " + string(_failCount)); + } + + _destroy(); + } + + ///@description Adds a test to this manager + ///@param {Struct} test + add_test = function(test){ + ds_list_add(_tests, test); + } +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Manager/GMLTest_Manager.yy b/samples/BasicSample/scripts/GMLTest_Manager/GMLTest_Manager.yy new file mode 100644 index 0000000..0aaab35 --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Manager/GMLTest_Manager.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "GMLTest", + "path": "folders/GMLTest.yy", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Manager", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Matchers/GMLTest_Matchers.gml b/samples/BasicSample/scripts/GMLTest_Matchers/GMLTest_Matchers.gml new file mode 100644 index 0000000..ad0f4ff --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Matchers/GMLTest_Matchers.gml @@ -0,0 +1,51 @@ +///@description Expects that the actual value is equal to the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_eq(expected, actual) { + _gmltest_throw_result(expected, actual, expected == actual); +} + +///@description Expects that the provided value is false +///@param {*} value +function gmltest_expect_false(value) { + gmltest_expect_eq(false, value); +} + +///@description Expects that the provided value is true +///@param {*} value +function gmltest_expect_true(value) { + gmltest_expect_eq(true, value); +} + +///@description Expects that the actual value is greater than the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_gt(expected, actual) { + _gmltest_throw_result(expected, actual, expected < actual); +} + +///@description Expects that the actual value is less than the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_lt(expected, actual) { + _gmltest_throw_result(expected, actual, expected > actual); +} + +///@description Expects that the actual value is not equal to the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_neq(expected, actual) { + _gmltest_throw_result(expected, actual, expected != actual); +} + +///@description Expects that the provided value is not null +///@param {*} value +function gmltest_expect_not_null(value) { + _gmltest_throw_result("not null", value, !_gmltest_is_null(value)); +} + +///@description Expects that the provided value is null +///@param {*} value +function gmltest_expect_null(value) { + _gmltest_throw_result("null", value, _gmltest_is_null(value)); +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Matchers/GMLTest_Matchers.yy b/samples/BasicSample/scripts/GMLTest_Matchers/GMLTest_Matchers.yy new file mode 100644 index 0000000..87372fe --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Matchers/GMLTest_Matchers.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "GMLTest", + "path": "folders/GMLTest.yy", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Matchers", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Private/GMLTest_Private.gml b/samples/BasicSample/scripts/GMLTest_Private/GMLTest_Private.gml new file mode 100644 index 0000000..6f71413 --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Private/GMLTest_Private.gml @@ -0,0 +1,65 @@ +///@description Creates the GMLTestManager if it does not exist +function _gmltest_create_manager() { + if (!variable_global_exists("GMLTestManager")){ + global.GMLTestManager = new GMLTest_Manager(); + } +} + +///@description Throws an exception if the provided values do not match +///@param {*} expected +///@param {*} actual +///@param {Bool} matches +function _gmltest_throw_result(expected, actual, matches) { + if (!matches){ + var errorMessage = "Expected [" + string(expected) + "] Actual [" + string(actual) + "]"; + throw({message: errorMessage, stacktrace: debug_get_callstack(), expectFailed: true}); + } +} + +#region Typechecking + +///@desc Checks if the provided value is an int or not +///@param {*} value The value to check +///@returns {Bool} True if int, false otherwise +function _gmltest_is_int(value) { + if (is_undefined(value) || !is_numeric(value)){ + return false; + } + + var remainder = value % 1; + return remainder == 0; +} + +///@desc Checks if the provided value is null or not +///@param {*} value The value to check +///@returns {Bool} True if null, false otherwise +function _gmltest_is_null(value){ + var result = is_undefined(value) || !_gmltest_is_int(value) || value == noone || value < 0 || !instance_exists(value); + return result; +} + +#endregion + +#region Logging + +///@description Logs the provided callstack to the console +///@param {Array} callstack +function _gmltest_log_callstack(callstack) { + show_debug_message("Callstack:"); + show_debug_message("-------------------------"); + for (var i = 0; i < array_length(callstack); i++){ + show_debug_message(callstack[i]); + } +} + +///@description Logs the provided status for the given test name to the console +///@param {String} status +///@param {String} testName +function _gmltest_log_status(status, testName){ + show_debug_message("[" + status + "] " + testName); +} + +#endregion + + + diff --git a/samples/BasicSample/scripts/GMLTest_Private/GMLTest_Private.yy b/samples/BasicSample/scripts/GMLTest_Private/GMLTest_Private.yy new file mode 100644 index 0000000..1eadcb2 --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Private/GMLTest_Private.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "GMLTest", + "path": "folders/GMLTest.yy", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Private", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Tests/GMLTest_Tests.gml b/samples/BasicSample/scripts/GMLTest_Tests/GMLTest_Tests.gml new file mode 100644 index 0000000..280ad39 --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Tests/GMLTest_Tests.gml @@ -0,0 +1,95 @@ +///@description Base struct that all harnesses should extend from +function GMLTest_Harness() constructor { + + ///@description Called before the execution of the test. + /// Use this function to setup your fixtures and parameterized tests. + function setup(){ + // Override this function + } + + ///@description Called after the execution of the test. + /// Use this function to clean up your fixtures and parameterized tests. + function tear_down(){ + // Override this function + } +} + +///@description Test struct used to hold the registered test data for later execution +function _GMLTest_Test() constructor { + _name = ""; + _harness = noone; + _fn = noone; + _disabled = false; + _array = noone; + + function get_name(){ + var result = ""; + if (_harness != noone){ + var temp = new _harness(); + result = instanceof(temp) + "::"; + delete temp; + } + result += _name; + return result; + } +} + +///@description Register a basic test with a name and a function to execute +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function test(name, fn){ + _gmltest_create_manager(); + var temp = new _GMLTest_Test(); + temp._fn = fn; + temp._name = name; + global.GMLTestManager.add_test(temp); + return temp; +} + +///@description Disable a registered basic test that has a name and a function to execute +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function xtest(name, fn){ + var temp = test(name, fn); + temp._disabled = true; +} + +///@description Register a fixture test with a harness, name and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function test_f(harness, name, fn){ + var temp = test(name, fn); + temp._harness = harness; + return temp; +} + +///@description Disable a registered fixture test that has a harness, name and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function xtest_f(harness, name, fn){ + var temp = test_f(harness, name, fn); + temp._disabled = true; +} + +///@description Register a parameterized test with a harness, name, array of parameters, and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Array} array An array containing a list of parameters to be executed using the same provided function +///@param {Function} fn The function to be executed which takes one parameter +function test_p(harness, name, array, fn) { + var temp = test_f(harness, name, fn); + temp._array = array; + return temp; +} + +///@description Disable a registered parameterized test that has a harness, name, array of parameters, and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Array} array An array containing a list of parameters to be executed using the same provided function +///@param {Function} fn The function to be executed which takes one parameter +function xtest_p(harness, name, array, fn) { + var temp = test_p(harness, name, array, fn); + temp._disabled = true; +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/GMLTest_Tests/GMLTest_Tests.yy b/samples/BasicSample/scripts/GMLTest_Tests/GMLTest_Tests.yy new file mode 100644 index 0000000..a0a351b --- /dev/null +++ b/samples/BasicSample/scripts/GMLTest_Tests/GMLTest_Tests.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "GMLTest", + "path": "folders/GMLTest.yy", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Tests", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/ParameterizedTests/ParameterizedTests.gml b/samples/BasicSample/scripts/ParameterizedTests/ParameterizedTests.gml new file mode 100644 index 0000000..c9b1b05 --- /dev/null +++ b/samples/BasicSample/scripts/ParameterizedTests/ParameterizedTests.gml @@ -0,0 +1,13 @@ +///@description Parameterized test showing basic usage +/// We don't need any custom setup so we will use the basic harness +test_p(GMLTest_Harness, "IsNumericTest", [1, 2, -1, 0], function(p){ + // The value of p is equal to 1, 2, -1, or 0 + // This function will be called each time for each value to test the values + gmltest_expect_true(is_numeric(p)); +}); + +///@description Disabled parameterized test showing basic usage +/// If this test was run this would cause a failure, but it won't because it is disabled +xtest_p(GMLTest_Harness, "IsUndefinedTest",[undefined, noone, "hello"], function(p){ + gmltest_expect_eq(undefined, p); +}); \ No newline at end of file diff --git a/samples/BasicSample/scripts/ParameterizedTests/ParameterizedTests.yy b/samples/BasicSample/scripts/ParameterizedTests/ParameterizedTests.yy new file mode 100644 index 0000000..a0d7e5b --- /dev/null +++ b/samples/BasicSample/scripts/ParameterizedTests/ParameterizedTests.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "Sample Unit Tests", + "path": "folders/Sample Unit Tests.yy", + }, + "resourceVersion": "1.0", + "name": "ParameterizedTests", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/samples/BasicSample/scripts/StandardTests/StandardTests.gml b/samples/BasicSample/scripts/StandardTests/StandardTests.gml new file mode 100644 index 0000000..ea5897a --- /dev/null +++ b/samples/BasicSample/scripts/StandardTests/StandardTests.gml @@ -0,0 +1,10 @@ +///@description Standard test showing basic usage +test("Basic Standard Test", function(){ + gmltest_expect_true(true); +}); + +///@description Standard disabled test showing basic usage +/// If this test was run this would cause a failure, but it won't because it is disabled +xtest("Basic Disabled Test", function(){ + gmltest_expect_true(false); +}); \ No newline at end of file diff --git a/samples/BasicSample/scripts/StandardTests/StandardTests.yy b/samples/BasicSample/scripts/StandardTests/StandardTests.yy new file mode 100644 index 0000000..7b86d1d --- /dev/null +++ b/samples/BasicSample/scripts/StandardTests/StandardTests.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "Sample Unit Tests", + "path": "folders/Sample Unit Tests.yy", + }, + "resourceVersion": "1.0", + "name": "StandardTests", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/src/gms2-test/gms2-test.yyp b/src/gms2-test/gms2-test.yyp new file mode 100644 index 0000000..37c0da0 --- /dev/null +++ b/src/gms2-test/gms2-test.yyp @@ -0,0 +1,39 @@ +{ + "resources": [ + {"id":{"name":"GMLTest_Tests","path":"scripts/GMLTest_Tests/GMLTest_Tests.yy",},"order":4,}, + {"id":{"name":"GMLTest_Manager","path":"scripts/GMLTest_Manager/GMLTest_Manager.yy",},"order":2,}, + {"id":{"name":"GMLTest_Helpers","path":"scripts/GMLTest_Helpers/GMLTest_Helpers.yy",},"order":3,}, + {"id":{"name":"GMLTest_Private","path":"scripts/GMLTest_Private/GMLTest_Private.yy",},"order":5,}, + {"id":{"name":"GMLTest_Matchers","path":"scripts/GMLTest_Matchers/GMLTest_Matchers.yy",},"order":1,}, + ], + "Options": [ + {"name":"HTML5","path":"options/html5/options_html5.yy",}, + {"name":"Linux","path":"options/linux/options_linux.yy",}, + {"name":"macOS","path":"options/mac/options_mac.yy",}, + {"name":"Main","path":"options/main/options_main.yy",}, + {"name":"Windows","path":"options/windows/options_windows.yy",}, + ], + "isDnDProject": false, + "isEcma": false, + "tutorialPath": "", + "configs": { + "name": "Default", + "children": [], + }, + "RoomOrder": [], + "Folders": [], + "AudioGroups": [ + {"targets":461609314234257646,"resourceVersion":"1.0","name":"audiogroup_default","resourceType":"GMAudioGroup",}, + ], + "TextureGroups": [ + {"isScaled":true,"autocrop":true,"border":2,"mipsToGenerate":0,"targets":461609314234257646,"resourceVersion":"1.0","name":"Default","resourceType":"GMTextureGroup",}, + ], + "IncludedFiles": [], + "MetaData": { + "IDEVersion": "23.1.1.168", + }, + "resourceVersion": "1.3", + "name": "gms2-test", + "tags": [], + "resourceType": "GMProject", +} \ No newline at end of file diff --git a/src/gms2-test/options/html5/options_html5.yy b/src/gms2-test/options/html5/options_html5.yy new file mode 100644 index 0000000..876b777 --- /dev/null +++ b/src/gms2-test/options/html5/options_html5.yy @@ -0,0 +1,34 @@ +{ + "option_html5_browser_title": "Created with GameMaker Studio 2", + "option_html5_version": "1.0.0.0", + "option_html5_foldername": "html5game", + "option_html5_outputname": "index.html", + "option_html5_splash_png": "${base_options_dir}\\html5\\splash.png", + "option_html5_usesplash": false, + "option_html5_outputdebugtoconsole": false, + "option_html5_display_cursor": true, + "option_html5_localrunalert": true, + "option_html5_index": "", + "option_html5_loadingbar": "", + "option_html5_jsprepend": "", + "option_html5_icon": "${base_options_dir}\\html5\\fav.ico", + "option_html5_allow_fullscreen": true, + "option_html5_interpolate_pixels": true, + "option_html5_centregame": false, + "option_html5_usebuiltinparticles": true, + "option_html5_usebuiltinfont": true, + "option_html5_webgl": 2, + "option_html5_scale": 0, + "option_html5_texture_page": "2048x2048", + "option_html5_use_facebook": false, + "option_html5_facebook_id": "", + "option_html5_facebook_app_display_name": "", + "option_html5_flurry_enable": false, + "option_html5_flurry_id": "", + "option_html5_google_analytics_enable": false, + "option_html5_google_tracking_id": "", + "resourceVersion": "1.0", + "name": "HTML5", + "tags": [], + "resourceType": "GMHtml5Options", +} \ No newline at end of file diff --git a/src/gms2-test/options/linux/options_linux.yy b/src/gms2-test/options/linux/options_linux.yy new file mode 100644 index 0000000..356a804 --- /dev/null +++ b/src/gms2-test/options/linux/options_linux.yy @@ -0,0 +1,25 @@ +{ + "option_linux_display_name": "Created with GameMaker Studio 2", + "option_linux_version": "1.0.0.0", + "option_linux_maintainer_email": "", + "option_linux_homepage": "http://www.yoyogames.com", + "option_linux_short_desc": "", + "option_linux_long_desc": "", + "option_linux_splash_screen": "${base_options_dir}\\linux\\splash\\splash.png", + "option_linux_display_splash": false, + "option_linux_icon": "${base_options_dir}\\linux\\icons\\64.png", + "option_linux_start_fullscreen": false, + "option_linux_allow_fullscreen": false, + "option_linux_interpolate_pixels": true, + "option_linux_display_cursor": true, + "option_linux_sync": false, + "option_linux_resize_window": false, + "option_linux_scale": 0, + "option_linux_texture_page": "2048x2048", + "option_linux_enable_steam": false, + "option_linux_disable_sandbox": false, + "resourceVersion": "1.0", + "name": "Linux", + "tags": [], + "resourceType": "GMLinuxOptions", +} \ No newline at end of file diff --git a/src/gms2-test/options/mac/options_mac.yy b/src/gms2-test/options/mac/options_mac.yy new file mode 100644 index 0000000..78bf670 --- /dev/null +++ b/src/gms2-test/options/mac/options_mac.yy @@ -0,0 +1,32 @@ +{ + "option_mac_display_name": "Created with GameMaker Studio 2", + "option_mac_app_id": "com.company.game", + "option_mac_version": "1.0.0.0", + "option_mac_output_dir": "~/gamemakerstudio2", + "option_mac_team_id": "", + "option_mac_signing_identity": "Developer ID Application:", + "option_mac_copyright": "", + "option_mac_splash_png": "${base_options_dir}\\mac\\splash\\splash.png", + "option_mac_icon_png": "${base_options_dir}\\mac\\icons\\1024.png", + "option_mac_menu_dock": false, + "option_mac_display_cursor": true, + "option_mac_start_fullscreen": false, + "option_mac_allow_fullscreen": false, + "option_mac_interpolate_pixels": true, + "option_mac_vsync": false, + "option_mac_resize_window": false, + "option_mac_enable_retina": false, + "option_mac_scale": 0, + "option_mac_texture_page": "2048x2048", + "option_mac_build_app_store": false, + "option_mac_allow_incoming_network": false, + "option_mac_allow_outgoing_network": false, + "option_mac_app_category": "Games", + "option_mac_enable_steam": false, + "option_mac_disable_sandbox": false, + "option_mac_apple_sign_in": false, + "resourceVersion": "1.0", + "name": "macOS", + "tags": [], + "resourceType": "GMMacOptions", +} \ No newline at end of file diff --git a/src/gms2-test/options/main/options_main.yy b/src/gms2-test/options/main/options_main.yy new file mode 100644 index 0000000..eb5e02b --- /dev/null +++ b/src/gms2-test/options/main/options_main.yy @@ -0,0 +1,18 @@ +{ + "option_gameguid": "6a097336-f958-466e-993e-283195d73725", + "option_game_speed": 60, + "option_mips_for_3d_textures": false, + "option_draw_colour": 4294967295, + "option_window_colour": 255, + "option_steam_app_id": "0", + "option_sci_usesci": false, + "option_author": "", + "option_lastchanged": "", + "addon_amazon_apis": "", + "addon_google_play_services": "", + "option_spine_licence": false, + "resourceVersion": "1.1", + "name": "Main", + "tags": [], + "resourceType": "GMMainOptions", +} \ No newline at end of file diff --git a/src/gms2-test/options/windows/options_windows.yy b/src/gms2-test/options/windows/options_windows.yy new file mode 100644 index 0000000..7fb7dad --- /dev/null +++ b/src/gms2-test/options/windows/options_windows.yy @@ -0,0 +1,35 @@ +{ + "option_windows_display_name": "Created with GameMaker Studio 2", + "option_windows_executable_name": "${project_name}.exe", + "option_windows_version": "1.0.0.0", + "option_windows_company_info": "YoYo Games Ltd", + "option_windows_product_info": "Created with GameMaker Studio 2", + "option_windows_copyright_info": "", + "option_windows_description_info": "A GameMaker Studio 2 Game", + "option_windows_display_cursor": true, + "option_windows_icon": "${base_options_dir}\\windows\\icons\\icon.ico", + "option_windows_save_location": 0, + "option_windows_splash_screen": "${base_options_dir}\\windows\\splash\\splash.png", + "option_windows_use_splash": false, + "option_windows_start_fullscreen": false, + "option_windows_allow_fullscreen_switching": false, + "option_windows_interpolate_pixels": false, + "option_windows_vsync": false, + "option_windows_resize_window": false, + "option_windows_borderless": false, + "option_windows_scale": 0, + "option_windows_copy_exe_to_dest": false, + "option_windows_sleep_margin": 10, + "option_windows_texture_page": "2048x2048", + "option_windows_installer_finished": "${base_options_dir}\\windows\\installer\\finished.bmp", + "option_windows_installer_header": "${base_options_dir}\\windows\\installer\\header.bmp", + "option_windows_license": "${base_options_dir}\\windows\\installer\\license.txt", + "option_windows_nsis_file": "${base_options_dir}\\windows\\installer\\nsis_script.nsi", + "option_windows_enable_steam": false, + "option_windows_disable_sandbox": false, + "option_windows_steam_use_alternative_launcher": false, + "resourceVersion": "1.0", + "name": "Windows", + "tags": [], + "resourceType": "GMWindowsOptions", +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Helpers/GMLTest_Helpers.gml b/src/gms2-test/scripts/GMLTest_Helpers/GMLTest_Helpers.gml new file mode 100644 index 0000000..78cc4c9 --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Helpers/GMLTest_Helpers.gml @@ -0,0 +1,15 @@ +///@description Start running the unit tests +function gmltest_start() { + global.GMLTestManager.execute(); +} + +///@description Sets the seed to a static value or a random value. Can be toggled. +///@param {Bool} deterministic Whether to set the seed to a static value or not +function gmltest_set_deterministic(deterministic) { + if (deterministic){ + random_set_seed(0); + } + else{ + random_set_seed(global.GMLTestManager._seed); + } +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Helpers/GMLTest_Helpers.yy b/src/gms2-test/scripts/GMLTest_Helpers/GMLTest_Helpers.yy new file mode 100644 index 0000000..545fa7f --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Helpers/GMLTest_Helpers.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "gms2-test", + "path": "gms2-test.yyp", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Helpers", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Manager/GMLTest_Manager.gml b/src/gms2-test/scripts/GMLTest_Manager/GMLTest_Manager.gml new file mode 100644 index 0000000..6248bcc --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Manager/GMLTest_Manager.gml @@ -0,0 +1,150 @@ +///@description Struct used to manage and execute all registered tests +function GMLTest_Manager() constructor { + _tests = ds_list_create(); + _failCount = 0; + _disabledCount = 0; + _testCount = 0; + _seed = random_get_seed(); + + ///@description Destroys data held by the manager. + /// Should be called after tests are finished executing. + _destroy = function() { + ds_list_destroy(_tests); + } + + ///@description Get the status string for whether there was a pass or a fail + ///@param {Bool} passed + _get_status_string = function(passed){ + return passed ? "PASSED" : "FAILED"; + } + + ///@description Run a standard test + ///@param {Struct} test + _run_test = function (test){ + var passed = true; + var testName = test.get_name(); + _gmltest_log_status("RUN", testName); + _testCount++; + + try { + test._fn(); + } catch (e){ + passed = false; + _handleException(e); + } + + var statusString = _get_status_string(passed); + _gmltest_log_status(statusString, testName); + } + + ///@description Run a fixture test + ///@param {Struct} test + _run_fixture_test = function (test){ + var passed = true; + var testName = test.get_name(); + _gmltest_log_status("RUN", testName); + _testCount++; + + var harness = new test._harness(); + harness.setup(); + var fn = method(harness, test._fn); + try { + fn(); + } catch (e){ + passed = false; + _handleException(e); + } + harness.tear_down(); + delete harness; + + var statusString = _get_status_string(passed); + _gmltest_log_status(statusString, testName); + } + + ///@description Run a parameterized test + ///@param {Struct} test + _run_parameter_test = function (test){ + for (var i = 0; i < array_length(test._array); i++){ + var passed = true; + var testName = test.get_name() + "::" + string(i); + _gmltest_log_status("RUN", testName); + _testCount++; + + var harness = new test._harness(); + harness.setup(); + var fn = method(harness, test._fn); + try { + fn(test._array[i]); + } catch (e){ + passed = false; + _handleException(e); + } + harness.tear_down(); + delete harness; + + var statusString = _get_status_string(passed); + _gmltest_log_status(statusString, testName); + } + } + + ///@description Handles any exceptions thrown during the execution of the test + ///@param {Struct} e + _handleException = function (e){ + _failCount++; + show_debug_message(e.message); + // If we threw an exception and it wasn't because an expect failed, we should log the callstack to the user + if (!variable_struct_exists(e, "expectFailed")){ + _gmltest_log_callstack(e.stacktrace); + } + } + + ///@description Execute the provided test struct + ///@param {Struct} test + _execute_test = function (test) { + if (test._disabled){ + _disabledCount++; + _gmltest_log_status("DISABLED", test.get_name()); + return; + } + + if (test._harness == noone){ + _run_test(test); + }else if (test._array == noone){ + _run_fixture_test(test); + } + else { + _run_parameter_test(test); + } + } + + ///@description Execute all registered tests + execute = function () { + var testCount = ds_list_size(_tests); + var startTime = current_time; + + for (var i = 0; i < testCount; i++){ + var test = ds_list_find_value(_tests, i); + _execute_test(test); + } + + var endTime = current_time; + var timeToRun = endTime - startTime; + + show_debug_message("-------------------------"); + show_debug_message("RAN " + string(_testCount) + " TESTS IN " + string(timeToRun) + "MS."); + if (_disabledCount > 0){ + show_debug_message("DISABLED TESTS: " + string(_disabledCount)); + } + if (_failCount > 0){ + show_debug_message("FAILED TESTS: " + string(_failCount)); + } + + _destroy(); + } + + ///@description Adds a test to this manager + ///@param {Struct} test + add_test = function(test){ + ds_list_add(_tests, test); + } +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Manager/GMLTest_Manager.yy b/src/gms2-test/scripts/GMLTest_Manager/GMLTest_Manager.yy new file mode 100644 index 0000000..027318b --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Manager/GMLTest_Manager.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "gms2-test", + "path": "gms2-test.yyp", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Manager", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Matchers/GMLTest_Matchers.gml b/src/gms2-test/scripts/GMLTest_Matchers/GMLTest_Matchers.gml new file mode 100644 index 0000000..ad0f4ff --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Matchers/GMLTest_Matchers.gml @@ -0,0 +1,51 @@ +///@description Expects that the actual value is equal to the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_eq(expected, actual) { + _gmltest_throw_result(expected, actual, expected == actual); +} + +///@description Expects that the provided value is false +///@param {*} value +function gmltest_expect_false(value) { + gmltest_expect_eq(false, value); +} + +///@description Expects that the provided value is true +///@param {*} value +function gmltest_expect_true(value) { + gmltest_expect_eq(true, value); +} + +///@description Expects that the actual value is greater than the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_gt(expected, actual) { + _gmltest_throw_result(expected, actual, expected < actual); +} + +///@description Expects that the actual value is less than the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_lt(expected, actual) { + _gmltest_throw_result(expected, actual, expected > actual); +} + +///@description Expects that the actual value is not equal to the expected value +///@param {*} expected +///@param {*} actual +function gmltest_expect_neq(expected, actual) { + _gmltest_throw_result(expected, actual, expected != actual); +} + +///@description Expects that the provided value is not null +///@param {*} value +function gmltest_expect_not_null(value) { + _gmltest_throw_result("not null", value, !_gmltest_is_null(value)); +} + +///@description Expects that the provided value is null +///@param {*} value +function gmltest_expect_null(value) { + _gmltest_throw_result("null", value, _gmltest_is_null(value)); +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Matchers/GMLTest_Matchers.yy b/src/gms2-test/scripts/GMLTest_Matchers/GMLTest_Matchers.yy new file mode 100644 index 0000000..0f81b4e --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Matchers/GMLTest_Matchers.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "gms2-test", + "path": "gms2-test.yyp", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Matchers", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Private/GMLTest_Private.gml b/src/gms2-test/scripts/GMLTest_Private/GMLTest_Private.gml new file mode 100644 index 0000000..6f71413 --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Private/GMLTest_Private.gml @@ -0,0 +1,65 @@ +///@description Creates the GMLTestManager if it does not exist +function _gmltest_create_manager() { + if (!variable_global_exists("GMLTestManager")){ + global.GMLTestManager = new GMLTest_Manager(); + } +} + +///@description Throws an exception if the provided values do not match +///@param {*} expected +///@param {*} actual +///@param {Bool} matches +function _gmltest_throw_result(expected, actual, matches) { + if (!matches){ + var errorMessage = "Expected [" + string(expected) + "] Actual [" + string(actual) + "]"; + throw({message: errorMessage, stacktrace: debug_get_callstack(), expectFailed: true}); + } +} + +#region Typechecking + +///@desc Checks if the provided value is an int or not +///@param {*} value The value to check +///@returns {Bool} True if int, false otherwise +function _gmltest_is_int(value) { + if (is_undefined(value) || !is_numeric(value)){ + return false; + } + + var remainder = value % 1; + return remainder == 0; +} + +///@desc Checks if the provided value is null or not +///@param {*} value The value to check +///@returns {Bool} True if null, false otherwise +function _gmltest_is_null(value){ + var result = is_undefined(value) || !_gmltest_is_int(value) || value == noone || value < 0 || !instance_exists(value); + return result; +} + +#endregion + +#region Logging + +///@description Logs the provided callstack to the console +///@param {Array} callstack +function _gmltest_log_callstack(callstack) { + show_debug_message("Callstack:"); + show_debug_message("-------------------------"); + for (var i = 0; i < array_length(callstack); i++){ + show_debug_message(callstack[i]); + } +} + +///@description Logs the provided status for the given test name to the console +///@param {String} status +///@param {String} testName +function _gmltest_log_status(status, testName){ + show_debug_message("[" + status + "] " + testName); +} + +#endregion + + + diff --git a/src/gms2-test/scripts/GMLTest_Private/GMLTest_Private.yy b/src/gms2-test/scripts/GMLTest_Private/GMLTest_Private.yy new file mode 100644 index 0000000..62ce150 --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Private/GMLTest_Private.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "gms2-test", + "path": "gms2-test.yyp", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Private", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Tests/GMLTest_Tests.gml b/src/gms2-test/scripts/GMLTest_Tests/GMLTest_Tests.gml new file mode 100644 index 0000000..280ad39 --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Tests/GMLTest_Tests.gml @@ -0,0 +1,95 @@ +///@description Base struct that all harnesses should extend from +function GMLTest_Harness() constructor { + + ///@description Called before the execution of the test. + /// Use this function to setup your fixtures and parameterized tests. + function setup(){ + // Override this function + } + + ///@description Called after the execution of the test. + /// Use this function to clean up your fixtures and parameterized tests. + function tear_down(){ + // Override this function + } +} + +///@description Test struct used to hold the registered test data for later execution +function _GMLTest_Test() constructor { + _name = ""; + _harness = noone; + _fn = noone; + _disabled = false; + _array = noone; + + function get_name(){ + var result = ""; + if (_harness != noone){ + var temp = new _harness(); + result = instanceof(temp) + "::"; + delete temp; + } + result += _name; + return result; + } +} + +///@description Register a basic test with a name and a function to execute +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function test(name, fn){ + _gmltest_create_manager(); + var temp = new _GMLTest_Test(); + temp._fn = fn; + temp._name = name; + global.GMLTestManager.add_test(temp); + return temp; +} + +///@description Disable a registered basic test that has a name and a function to execute +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function xtest(name, fn){ + var temp = test(name, fn); + temp._disabled = true; +} + +///@description Register a fixture test with a harness, name and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function test_f(harness, name, fn){ + var temp = test(name, fn); + temp._harness = harness; + return temp; +} + +///@description Disable a registered fixture test that has a harness, name and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Function} fn The function to be executed +function xtest_f(harness, name, fn){ + var temp = test_f(harness, name, fn); + temp._disabled = true; +} + +///@description Register a parameterized test with a harness, name, array of parameters, and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Array} array An array containing a list of parameters to be executed using the same provided function +///@param {Function} fn The function to be executed which takes one parameter +function test_p(harness, name, array, fn) { + var temp = test_f(harness, name, fn); + temp._array = array; + return temp; +} + +///@description Disable a registered parameterized test that has a harness, name, array of parameters, and a function to execute +///@param {Struct} harness The struct to use as the harness when the test executes +///@param {String} name The name of the test to be logged to the console +///@param {Array} array An array containing a list of parameters to be executed using the same provided function +///@param {Function} fn The function to be executed which takes one parameter +function xtest_p(harness, name, array, fn) { + var temp = test_p(harness, name, array, fn); + temp._disabled = true; +} \ No newline at end of file diff --git a/src/gms2-test/scripts/GMLTest_Tests/GMLTest_Tests.yy b/src/gms2-test/scripts/GMLTest_Tests/GMLTest_Tests.yy new file mode 100644 index 0000000..5bcf5a9 --- /dev/null +++ b/src/gms2-test/scripts/GMLTest_Tests/GMLTest_Tests.yy @@ -0,0 +1,12 @@ +{ + "isDnD": false, + "isCompatibility": false, + "parent": { + "name": "gms2-test", + "path": "gms2-test.yyp", + }, + "resourceVersion": "1.0", + "name": "GMLTest_Tests", + "tags": [], + "resourceType": "GMScript", +} \ No newline at end of file