diff --git a/.github/workflows/beehave-ci.yml b/.github/workflows/beehave-ci.yml index 826b95d5..4816cb47 100644 --- a/.github/workflows/beehave-ci.yml +++ b/.github/workflows/beehave-ci.yml @@ -37,7 +37,7 @@ jobs: fail-fast: false max-parallel: 10 matrix: - godot-version: ["4.2.2", "4.3", "4.4"] + godot-version: ["4.5.1"] name: "🤖 CI on Godot ${{ matrix.godot-version }}" steps: @@ -45,7 +45,7 @@ jobs: uses: actions/checkout@v2 - name: Run GDUnit4 tests - uses: MikeSchulze/gdUnit4-action@v1.1.1 + uses: MikeSchulze/gdUnit4-action@v1.2.3 with: godot-version: ${{ matrix.godot-version }} godot-status: "stable" diff --git a/addons/beehave/debug/icons/horizontal_layout.svg.import b/addons/beehave/debug/icons/horizontal_layout.svg.import index 539e518f..030a49f0 100644 --- a/addons/beehave/debug/icons/horizontal_layout.svg.import +++ b/addons/beehave/debug/icons/horizontal_layout.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/horizontal_layout.svg-d2a7af351e44f9bf61d0c93 compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/debug/icons/port_bottom.svg.import b/addons/beehave/debug/icons/port_bottom.svg.import index 8845c5bf..73e68293 100644 --- a/addons/beehave/debug/icons/port_bottom.svg.import +++ b/addons/beehave/debug/icons/port_bottom.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/port_bottom.svg-e5c5c61b642a79ab9c2b66ff56603 compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/debug/icons/port_left.svg.import b/addons/beehave/debug/icons/port_left.svg.import index 7ea98278..2a0c5d3a 100644 --- a/addons/beehave/debug/icons/port_left.svg.import +++ b/addons/beehave/debug/icons/port_left.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/port_left.svg-69cd927c4db555f1edbb8d1f553ea2f compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/debug/icons/port_right.svg.import b/addons/beehave/debug/icons/port_right.svg.import index 20931cd8..ed576a46 100644 --- a/addons/beehave/debug/icons/port_right.svg.import +++ b/addons/beehave/debug/icons/port_right.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/port_right.svg-f760bd8be2dd613d0d3848c998c92a compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/debug/icons/port_top.svg.import b/addons/beehave/debug/icons/port_top.svg.import index dec78208..c8b5b768 100644 --- a/addons/beehave/debug/icons/port_top.svg.import +++ b/addons/beehave/debug/icons/port_top.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/port_top.svg-d1b336cdc6a0dd570305782a1e56f61d compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/debug/icons/vertical_layout.svg.import b/addons/beehave/debug/icons/vertical_layout.svg.import index 8ddcfca8..96f77690 100644 --- a/addons/beehave/debug/icons/vertical_layout.svg.import +++ b/addons/beehave/debug/icons/vertical_layout.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/vertical_layout.svg-1a08fee4b09812a05bcf3defb compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/action.svg.import b/addons/beehave/icons/action.svg.import index cf8a6123..7d95d2d3 100644 --- a/addons/beehave/icons/action.svg.import +++ b/addons/beehave/icons/action.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/action.svg-e8a91246d0ba9ba3cf84290d65648f06.c compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/blackboard.svg.import b/addons/beehave/icons/blackboard.svg.import index 46500581..40f3ed6e 100644 --- a/addons/beehave/icons/blackboard.svg.import +++ b/addons/beehave/icons/blackboard.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/blackboard.svg-18d4dfd4f6de558de250b67251ff1e compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/category_bt.svg.import b/addons/beehave/icons/category_bt.svg.import index c11e4f26..0c59c011 100644 --- a/addons/beehave/icons/category_bt.svg.import +++ b/addons/beehave/icons/category_bt.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/category_bt.svg-8537bebd1c5f62dca3d7ee7f17efe compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/category_composite.svg.import b/addons/beehave/icons/category_composite.svg.import index 0496273b..44cad8de 100644 --- a/addons/beehave/icons/category_composite.svg.import +++ b/addons/beehave/icons/category_composite.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/category_composite.svg-43f66e63a7ccfa5ac8ec6d compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/category_decorator.svg.import b/addons/beehave/icons/category_decorator.svg.import index 492f32e4..b37cace6 100644 --- a/addons/beehave/icons/category_decorator.svg.import +++ b/addons/beehave/icons/category_decorator.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/category_decorator.svg-79d598d6456f3272415624 compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/category_leaf.svg.import b/addons/beehave/icons/category_leaf.svg.import index 4ef9604c..67ca6610 100644 --- a/addons/beehave/icons/category_leaf.svg.import +++ b/addons/beehave/icons/category_leaf.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/category_leaf.svg-c740ecab6cfae632574ca5e39e4 compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/condition.svg.import b/addons/beehave/icons/condition.svg.import index ef590992..6b149190 100644 --- a/addons/beehave/icons/condition.svg.import +++ b/addons/beehave/icons/condition.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/condition.svg-57892684b10a64086f68c09c388b17e compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/failer.svg.import b/addons/beehave/icons/failer.svg.import index 989b556f..24862ae1 100644 --- a/addons/beehave/icons/failer.svg.import +++ b/addons/beehave/icons/failer.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/failer.svg-9a62b840e1eacc0437e7a67b14a302e4.c compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/inverter.svg.import b/addons/beehave/icons/inverter.svg.import index e9050a8d..4b57722e 100644 --- a/addons/beehave/icons/inverter.svg.import +++ b/addons/beehave/icons/inverter.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/inverter.svg-1f1b976d95de42c4ad99a92fa9a6c5d0 compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/limiter.svg.import b/addons/beehave/icons/limiter.svg.import index 7b56b08c..614f44f4 100644 --- a/addons/beehave/icons/limiter.svg.import +++ b/addons/beehave/icons/limiter.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/limiter.svg-b4c7646605c46f53c5e403fe21d8f584. compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/selector.svg.import b/addons/beehave/icons/selector.svg.import index ef7326de..1fd21969 100644 --- a/addons/beehave/icons/selector.svg.import +++ b/addons/beehave/icons/selector.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/selector.svg-78bccfc448bd1676b5a29bfde4b08e5b compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/selector_random.svg.import b/addons/beehave/icons/selector_random.svg.import index 6306f76c..b4d24e0a 100644 --- a/addons/beehave/icons/selector_random.svg.import +++ b/addons/beehave/icons/selector_random.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/selector_random.svg-d52fea1352c24483ecd9dc860 compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/selector_reactive.svg.import b/addons/beehave/icons/selector_reactive.svg.import index 12a8c5b5..9e2a7817 100644 --- a/addons/beehave/icons/selector_reactive.svg.import +++ b/addons/beehave/icons/selector_reactive.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/selector_reactive.svg-dd3b8fb8cd2ffe331605aaa compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/sequence.svg.import b/addons/beehave/icons/sequence.svg.import index 5dadbe23..c1ab6f87 100644 --- a/addons/beehave/icons/sequence.svg.import +++ b/addons/beehave/icons/sequence.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/sequence.svg-76e5600611900cc81e9ec286977b8c6a compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/sequence_random.svg.import b/addons/beehave/icons/sequence_random.svg.import index 3907462a..bde7dcc7 100644 --- a/addons/beehave/icons/sequence_random.svg.import +++ b/addons/beehave/icons/sequence_random.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/sequence_random.svg-58cee9098c622ef87db941279 compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/sequence_reactive.svg.import b/addons/beehave/icons/sequence_reactive.svg.import index ab0fa25e..887d75de 100644 --- a/addons/beehave/icons/sequence_reactive.svg.import +++ b/addons/beehave/icons/sequence_reactive.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/sequence_reactive.svg-7d384ca290f7934adb9e17d compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/succeeder.svg.import b/addons/beehave/icons/succeeder.svg.import index 0cb73341..6ab4093c 100644 --- a/addons/beehave/icons/succeeder.svg.import +++ b/addons/beehave/icons/succeeder.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/succeeder.svg-e5cf6f6e04b9b862b82fd2cb479272a compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/beehave/icons/tree.svg.import b/addons/beehave/icons/tree.svg.import index 9ac0308a..df4a4709 100644 --- a/addons/beehave/icons/tree.svg.import +++ b/addons/beehave/icons/tree.svg.import @@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/tree.svg-c0b20ed88b2fe300c0296f7236049076.cte compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -26,6 +28,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/addons/gdUnit4/GdUnitRunner.cfg b/addons/gdUnit4/GdUnitRunner.cfg index 73ff8df6..f6df0584 100644 --- a/addons/gdUnit4/GdUnitRunner.cfg +++ b/addons/gdUnit4/GdUnitRunner.cfg @@ -1 +1,24 @@ -{"included":{"res://test/beehave_tree_test.gd":["test_low_tick_rate"]},"server_port":31002,"skipped":{},"version":"1.0"} \ No newline at end of file +{ + "server_port": 31002, + "tests": [ + { + "@path": "res://addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd", + "@subpath": "", + "assembly_location": "", + "attribute_index": -1, + "display_name": "test_debugger_renders_correctly", + "fully_qualified_name": "test.debug.debugger_test.test_debugger_renders_correctly", + "guid": "cf8e08b6-c6f0480-6befcff-af403499e7", + "line_number": 16, + "metadata": { + + }, + "require_godot_runtime": true, + "source_file": "res://test/debug/debugger_test.gd", + "suite_name": "debugger_test", + "suite_resource_path": "res://test/debug/debugger_test.gd", + "test_name": "test_debugger_renders_correctly" + } + ], + "version": "5.0" +} \ No newline at end of file diff --git a/addons/gdUnit4/bin/GdUnitBuildTool.gd b/addons/gdUnit4/bin/GdUnitBuildTool.gd deleted file mode 100644 index 6e5d588c..00000000 --- a/addons/gdUnit4/bin/GdUnitBuildTool.gd +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env -S godot -s -extends SceneTree - -enum { - INIT, - PROCESSING, - EXIT -} - -const RETURN_SUCCESS = 0 -const RETURN_ERROR = 100 -const RETURN_WARNING = 101 - -var _console := CmdConsole.new() -var _cmd_options: = CmdOptions.new([ - CmdOption.new( - "-scp, --src_class_path", - "-scp ", - "The full class path of the source file.", - TYPE_STRING - ), - CmdOption.new( - "-scl, --src_class_line", - "-scl ", - "The selected line number to generate test case.", - TYPE_INT - ) -]) - -var _status := INIT -var _source_file :String = "" -var _source_line :int = -1 - - -func _init() -> void: - var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitBuildTool.gd") - var result := cmd_parser.parse(OS.get_cmdline_args()) - if result.is_error(): - show_options() - exit(RETURN_ERROR, result.error_message()); - return - - var cmd_options :Array[CmdCommand] = result.value() - for cmd in cmd_options: - if cmd.name() == '-scp': - _source_file = cmd.arguments()[0] - _source_file = ProjectSettings.localize_path(ProjectSettings.localize_path(_source_file)) - if cmd.name() == '-scl': - _source_line = int(cmd.arguments()[0]) - # verify required arguments - if _source_file == "": - exit(RETURN_ERROR, "missing required argument -scp ") - return - if _source_line == -1: - exit(RETURN_ERROR, "missing required argument -scl ") - return - _status = PROCESSING - - -func _idle(_delta :float) -> void: - if _status == PROCESSING: - var script := ResourceLoader.load(_source_file) as Script - if script == null: - exit(RETURN_ERROR, "Can't load source file %s!" % _source_file) - var result := GdUnitTestSuiteBuilder.create(script, _source_line) - if result.is_error(): - print_json_error(result.error_message()) - exit(RETURN_ERROR, result.error_message()) - return - _console.prints_color("Added testcase: %s" % result.value(), Color.CORNFLOWER_BLUE) - print_json_result(result.value() as Dictionary) - exit(RETURN_SUCCESS) - - -func exit(code :int, message :String = "") -> void: - _status = EXIT - if code == RETURN_ERROR: - if not message.is_empty(): - _console.prints_error(message) - _console.prints_error("Abnormal exit with %d" % code) - else: - _console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) - quit(code) - - -func print_json_result(result :Dictionary) -> void: - # convert back to system path - var path := ProjectSettings.globalize_path(result["path"] as String) - var json := 'JSON_RESULT:{"TestCases" : [{"line":%d, "path": "%s"}]}' % [result["line"], path] - prints(json) - - -func print_json_error(error :String) -> void: - prints('JSON_RESULT:{"Error" : "%s"}' % error) - - -func show_options() -> void: - _console.prints_color(" Usage:", Color.DARK_SALMON) - _console.prints_color(" build -scp -scl ", Color.DARK_SALMON) - _console.prints_color("-- Options ---------------------------------------------------------------------------------------", - Color.DARK_SALMON).new_line() - for option in _cmd_options.default_options(): - descripe_option(option) - - -func descripe_option(cmd_option :CmdOption) -> void: - _console.print_color(" %-40s" % str(cmd_option.commands()), Color.CORNFLOWER_BLUE) - _console.prints_color(cmd_option.description(), Color.LIGHT_GREEN) - if not cmd_option.help().is_empty(): - _console.prints_color("%-4s %s" % ["", cmd_option.help()], Color.DARK_TURQUOISE) - _console.new_line() diff --git a/addons/gdUnit4/bin/GdUnitBuildTool.gd.uid b/addons/gdUnit4/bin/GdUnitBuildTool.gd.uid deleted file mode 100644 index d9d05d2b..00000000 --- a/addons/gdUnit4/bin/GdUnitBuildTool.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bbi6074fuqd6u diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd b/addons/gdUnit4/bin/GdUnitCmdTool.gd index 21c7a47e..cae9138b 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd @@ -1,622 +1,13 @@ #!/usr/bin/env -S godot -s extends SceneTree -const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") - -#warning-ignore-all:return_value_discarded -class CLIRunner: - extends Node - - enum { - READY, - INIT, - RUN, - STOP, - EXIT - } - - const DEFAULT_REPORT_COUNT = 20 - const RETURN_SUCCESS = 0 - const RETURN_ERROR = 100 - const RETURN_ERROR_HEADLESS_NOT_SUPPORTED = 103 - const RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED = 104 - const RETURN_WARNING = 101 - - var _state := READY - var _test_suites_to_process: Array - var _executor :Variant - var _cs_executor :Variant - var _report: GdUnitHtmlReport - var _report_dir: String - var _report_max: int = DEFAULT_REPORT_COUNT - var _headless_mode_ignore := false - var _runner_config := GdUnitRunnerConfig.new() - var _runner_config_file := "" - var _debug_cmd_args: = PackedStringArray() - var _console := CmdConsole.new() - var _cmd_options := CmdOptions.new([ - CmdOption.new( - "-a, --add", - "-a ", - "Adds the given test suite or directory to the execution pipeline.", - TYPE_STRING - ), - CmdOption.new( - "-i, --ignore", - "-i ", - "Adds the given test suite or test case to the ignore list.", - TYPE_STRING - ), - CmdOption.new( - "-c, --continue", - "", - """By default GdUnit will abort checked first test failure to be fail fast, - instead of stop after first failure you can use this option to run the complete test set.""".dedent() - ), - CmdOption.new( - "-conf, --config", - "-conf [testconfiguration.cfg]", - "Run all tests by given test configuration. Default is 'GdUnitRunner.cfg'", - TYPE_STRING, - true - ), - CmdOption.new( - "-help", "", - "Shows this help message." - ), - CmdOption.new("--help-advanced", "", - "Shows advanced options." - ) - ], - [ - # advanced options - CmdOption.new( - "-rd, --report-directory", - "-rd ", - "Specifies the output directory in which the reports are to be written. The default is res://reports/.", - TYPE_STRING, - true - ), - CmdOption.new( - "-rc, --report-count", - "-rc ", - "Specifies how many reports are saved before they are deleted. The default is %s." % str(DEFAULT_REPORT_COUNT), - TYPE_INT, - true - ), - #CmdOption.new("--list-suites", "--list-suites [directory]", "Lists all test suites located in the given directory.", TYPE_STRING), - #CmdOption.new("--describe-suite", "--describe-suite ", "Shows the description of selected test suite.", TYPE_STRING), - CmdOption.new( - "--info", "", - "Shows the GdUnit version info" - ), - CmdOption.new( - "--selftest", "", - "Runs the GdUnit self test" - ), - CmdOption.new( - "--ignoreHeadlessMode", - "--ignoreHeadlessMode", - "By default, running GdUnit4 in headless mode is not allowed. You can switch off the headless mode check by set this property." - ), - ]) - - - func _ready() -> void: - _state = INIT - _report_dir = GdUnitFileAccess.current_dir() + "reports" - _executor = GdUnitTestSuiteExecutor.new() - # stop checked first test failure to fail fast - @warning_ignore("unsafe_cast") - (_executor as GdUnitTestSuiteExecutor).fail_fast(true) - if GdUnit4CSharpApiLoader.is_mono_supported(): - prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version()) - _cs_executor = GdUnit4CSharpApiLoader.create_executor(self) - var err := GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) - if err != OK: - prints("gdUnitSignals failed") - push_error("Error checked startup, can't connect executor for 'send_event'") - quit(RETURN_ERROR) - - - func _notification(what: int) -> void: - if what == NOTIFICATION_PREDELETE: - prints("Finallize .. done") - - - @warning_ignore("unsafe_method_access") - func _process(_delta :float) -> void: - match _state: - INIT: - init_gd_unit() - _state = RUN - RUN: - # all test suites executed - if _test_suites_to_process.is_empty(): - _state = STOP - else: - set_process(false) - # process next test suite - var test_suite: Node = _test_suites_to_process.pop_front() - - if _cs_executor != null and _cs_executor.IsExecutable(test_suite): - _cs_executor.Execute(test_suite) - await _cs_executor.ExecutionCompleted - else: - await _executor.execute(test_suite) - set_process(true) - STOP: - _state = EXIT - _on_gdunit_event(GdUnitStop.new()) - quit(report_exit_code(_report)) - - - func quit(code: int) -> void: - _cs_executor = null - GdUnitTools.dispose_all() - await GdUnitMemoryObserver.gc_on_guarded_instances() - await get_tree().physics_frame - get_tree().quit(code) - - - func set_report_dir(path: String) -> void: - _report_dir = ProjectSettings.globalize_path(GdUnitFileAccess.make_qualified_path(path)) - _console.prints_color( - "Set write reports to %s" % _report_dir, - Color.DEEP_SKY_BLUE - ) - - - func set_report_count(count: String) -> void: - var report_count := count.to_int() - if report_count < 1: - _console.prints_error( - "Invalid report history count '%s' set back to default %d" - % [count, DEFAULT_REPORT_COUNT] - ) - _report_max = DEFAULT_REPORT_COUNT - else: - _console.prints_color( - "Set report history count to %s" % count, - Color.DEEP_SKY_BLUE - ) - _report_max = report_count - - - func disable_fail_fast() -> void: - _console.prints_color( - "Disabled fail fast!", - Color.DEEP_SKY_BLUE - ) - @warning_ignore("unsafe_method_access") - _executor.fail_fast(false) - - - func run_self_test() -> void: - _console.prints_color( - "Run GdUnit4 self tests.", - Color.DEEP_SKY_BLUE - ) - disable_fail_fast() - _runner_config.self_test() - - - func show_version() -> void: - _console.prints_color( - "Godot %s" % Engine.get_version_info().get("string") as String, - Color.DARK_SALMON - ) - var config := ConfigFile.new() - config.load("addons/gdUnit4/plugin.cfg") - _console.prints_color( - "GdUnit4 %s" % config.get_value("plugin", "version") as String, - Color.DARK_SALMON - ) - quit(RETURN_SUCCESS) - - - func check_headless_mode() -> void: - _headless_mode_ignore = true - - - func show_options(show_advanced: bool = false) -> void: - _console.prints_color( - """ - Usage: - runtest -a - runtest -a -i - """.dedent(), - Color.DARK_SALMON - ).prints_color( - "-- Options ---------------------------------------------------------------------------------------", - Color.DARK_SALMON - ).new_line() - for option in _cmd_options.default_options(): - descripe_option(option) - if show_advanced: - _console.prints_color( - "-- Advanced options --------------------------------------------------------------------------", - Color.DARK_SALMON - ).new_line() - for option in _cmd_options.advanced_options(): - descripe_option(option) - - - func descripe_option(cmd_option: CmdOption) -> void: - _console.print_color( - " %-40s" % str(cmd_option.commands()), - Color.CORNFLOWER_BLUE - ) - _console.prints_color( - cmd_option.description(), - Color.LIGHT_GREEN - ) - if not cmd_option.help().is_empty(): - _console.prints_color( - "%-4s %s" % ["", cmd_option.help()], - Color.DARK_TURQUOISE - ) - _console.new_line() - - - func load_test_config(path := GdUnitRunnerConfig.CONFIG_FILE) -> void: - _console.print_color( - "Loading test configuration %s\n" % path, - Color.CORNFLOWER_BLUE - ) - _runner_config_file = path - _runner_config.load_config(path) - - - func show_help() -> void: - show_options() - quit(RETURN_SUCCESS) - - - func show_advanced_help() -> void: - show_options(true) - quit(RETURN_SUCCESS) - - - func get_cmdline_args() -> PackedStringArray: - if _debug_cmd_args.is_empty(): - return OS.get_cmdline_args() - return _debug_cmd_args - - - func init_gd_unit() -> void: - _console.prints_color( - """ - -------------------------------------------------------------------------------------------------- - GdUnit4 Comandline Tool - --------------------------------------------------------------------------------------------------""".dedent(), - Color.DARK_SALMON - ).new_line() - - var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") - var result := cmd_parser.parse(get_cmdline_args()) - if result.is_error(): - show_options() - _console.prints_error(result.error_message()) - _console.prints_error("Abnormal exit with %d" % RETURN_ERROR) - _state = STOP - quit(RETURN_ERROR) - return - if result.is_empty(): - show_help() - return - # build runner config by given commands - var commands :Array[CmdCommand] = [] - @warning_ignore("unsafe_cast") - commands.append_array(result.value() as Array) - result = ( - CmdCommandHandler.new(_cmd_options) - .register_cb("-help", show_help) - .register_cb("--help-advanced", show_advanced_help) - .register_cb("-a", _runner_config.add_test_suite) - .register_cbv("-a", _runner_config.add_test_suites) - .register_cb("-i", _runner_config.skip_test_suite) - .register_cbv("-i", _runner_config.skip_test_suites) - .register_cb("-rd", set_report_dir) - .register_cb("-rc", set_report_count) - .register_cb("--selftest", run_self_test) - .register_cb("-c", disable_fail_fast) - .register_cb("-conf", load_test_config) - .register_cb("--info", show_version) - .register_cb("--ignoreHeadlessMode", check_headless_mode) - .execute(commands) - ) - if result.is_error(): - _console.prints_error(result.error_message()) - _state = STOP - quit(RETURN_ERROR) - - if DisplayServer.get_name() == "headless": - if _headless_mode_ignore: - _console.prints_warning(""" - Headless mode is ignored by option '--ignoreHeadlessMode'" - - Please note that tests that use UI interaction do not work correctly in headless mode. - Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore - have no effect in the test! - """.dedent() - ).new_line() - else: - _console.prints_error(""" - Headless mode is not supported! - - Please note that tests that use UI interaction do not work correctly in headless mode. - Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore - have no effect in the test! - - You can run with '--ignoreHeadlessMode' to swtich off this check. - """.dedent() - ).prints_error( - "Abnormal exit with %d" % RETURN_ERROR_HEADLESS_NOT_SUPPORTED - ) - quit(RETURN_ERROR_HEADLESS_NOT_SUPPORTED) - return - - _test_suites_to_process = load_testsuites(_runner_config) - if _test_suites_to_process.is_empty(): - _console.prints_warning("No test suites found, abort test run!") - _console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) - _state = STOP - quit(RETURN_SUCCESS) - var total_test_count := _collect_test_case_count(_test_suites_to_process) - _on_gdunit_event(GdUnitInit.new(_test_suites_to_process.size(), total_test_count)) - - - func load_testsuites(config: GdUnitRunnerConfig) -> Array[Node]: - var test_suites_to_process: Array[Node] = [] - # Dictionary[String, Dictionary[String, PackedStringArray]] - var to_execute := config.to_execute() - # scan for the requested test suites - var ts_scanner := GdUnitTestSuiteScanner.new() - for as_resource_path in to_execute.keys() as Array[String]: - var selected_tests: PackedStringArray = to_execute.get(as_resource_path) - var scanned_suites := ts_scanner.scan(as_resource_path) - skip_test_case(scanned_suites, selected_tests) - test_suites_to_process.append_array(scanned_suites) - skip_suites(test_suites_to_process, config) - return test_suites_to_process - - - func skip_test_case(test_suites: Array[Node], test_case_names: Array[String]) -> void: - if test_case_names.is_empty(): - return - for test_suite in test_suites: - for test_case in test_suite.get_children(): - if not test_case_names.has(test_case.get_name()): - test_suite.remove_child(test_case) - test_case.free() - - - func skip_suites(test_suites: Array[Node], config: GdUnitRunnerConfig) -> void: - var skipped := config.skipped() - if skipped.is_empty(): - return - - for test_suite in test_suites: - # skipp c# testsuites for now - if test_suite.get_script() == null: - continue - skip_suite(test_suite, skipped) - - - # Dictionary[String, PackedStringArray] - func skip_suite(test_suite: Node, skipped: Dictionary) -> void: - var skipped_suites :Array = skipped.keys() - var suite_name := test_suite.get_name() - var test_suite_path: String = ( - test_suite.get_meta("ResourcePath") if test_suite.get_script() == null - else test_suite.get_script().resource_path - ) - for suite_to_skip: String in skipped_suites: - # if suite skipped by path or name - if ( - suite_to_skip == test_suite_path - or (suite_to_skip.is_valid_filename() and suite_to_skip == suite_name) - ): - var skipped_tests: PackedStringArray = skipped.get(suite_to_skip) - var skip_reason := "Excluded by configuration" - # if no tests skipped test the complete suite is skipped - if skipped_tests.is_empty(): - _console.prints_warning("Mark the entire test suite '%s' as skipped!" % test_suite_path) - @warning_ignore("unsafe_property_access") - test_suite.__is_skipped = true - @warning_ignore("unsafe_property_access") - test_suite.__skip_reason = skip_reason - else: - # skip tests - for test_to_skip in skipped_tests: - var test_case: _TestCase = test_suite.find_child(test_to_skip, true, false) - if test_case: - test_case.skip(true, skip_reason) - _console.prints_warning("Mark test case '%s':%s as skipped" % [suite_to_skip, test_to_skip]) - else: - _console.prints_error( - "Can't skip test '%s' checked test suite '%s', no test with given name exists!" - % [test_to_skip, suite_to_skip] - ) - - - func _collect_test_case_count(test_suites: Array[Node]) -> int: - var total: int = 0 - for test_suite in test_suites: - total += test_suite.get_child_count() - return total - - - # gdlint: disable=function-name - func PublishEvent(data: Dictionary) -> void: - _on_gdunit_event(GdUnitEvent.new().deserialize(data)) - - - func _on_gdunit_event(event: GdUnitEvent) -> void: - match event.type(): - GdUnitEvent.INIT: - _report = GdUnitHtmlReport.new(_report_dir, _report_max) - GdUnitEvent.STOP: - var report_path := _report.write() - _report.delete_history(_report_max) - JUnitXmlReport.new(_report._report_path, _report.iteration()).write(_report) - _console.prints_color( - build_executed_test_suite_msg(_report.suite_executed_count(), _report.suite_count()), - Color.DARK_SALMON - ).prints_color( - build_executed_test_case_msg(_report.test_executed_count(), _report.test_count()), - Color.DARK_SALMON - ).prints_color( - "Total time: %s" % LocalTime.elapsed(_report.duration()), - Color.DARK_SALMON - ).prints_color( - "Open Report at: file://%s" % report_path, - Color.CORNFLOWER_BLUE - ) - GdUnitEvent.TESTSUITE_BEFORE: - _report.add_testsuite_report(event.resource_path(), event.suite_name(), event.total_count()) - GdUnitEvent.TESTSUITE_AFTER: - _report.add_testsuite_reports( - event.resource_path(), - event.error_count(), - event.failed_count(), - event.orphan_nodes(), - event.elapsed_time(), - event.reports() - ) - GdUnitEvent.TESTCASE_BEFORE: - _report.add_testcase(event.resource_path(), event.suite_name(), event.test_name()) - GdUnitEvent.TESTCASE_AFTER: - _report.set_testcase_counters(event.resource_path(), - event.test_name(), - event.is_error(), - event.failed_count(), - event.orphan_nodes(), - event.is_skipped(), - event.is_flaky(), - event.elapsed_time()) - _report.add_testcase_reports(event.resource_path(), event.test_name(), event.reports()) - GdUnitEvent.TESTCASE_STATISTICS: - _report.update_testsuite_counters(event.resource_path(), event.is_error(), event.failed_count(), event.orphan_nodes(),\ - event.is_skipped(), event.is_flaky(), event.elapsed_time()) - print_status(event) - - - func build_executed_test_suite_msg(executed_count :int, total_count :int) -> String: - if executed_count == total_count: - return "Executed test suites: (%d/%d)" % [executed_count, total_count] - return "Executed test suites: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)] - - - func build_executed_test_case_msg(executed_count :int, total_count :int) -> String: - if executed_count == total_count: - return "Executed test cases: (%d/%d)" % [executed_count, total_count] - return "Executed test cases: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)] - - - func report_exit_code(report: GdUnitHtmlReport) -> int: - if report.error_count() + report.failure_count() > 0: - _console.prints_color("Exit code: %d" % RETURN_ERROR, Color.FIREBRICK) - return RETURN_ERROR - if report.orphan_count() > 0: - _console.prints_color("Exit code: %d" % RETURN_WARNING, Color.GOLDENROD) - return RETURN_WARNING - _console.prints_color("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) - return RETURN_SUCCESS - - - func print_status(event: GdUnitEvent) -> void: - match event.type(): - GdUnitEvent.TESTSUITE_BEFORE: - _console.prints_color( - "Run Test Suite %s " % event.resource_path(), - Color.ANTIQUE_WHITE - ) - GdUnitEvent.TESTCASE_BEFORE: - _console.print_color( - " Run Test: %s > %s :" % [event.resource_path(), event.test_name()], - Color.ANTIQUE_WHITE - ).prints_color( - "STARTED", - Color.FOREST_GREEN - ).save_cursor() - GdUnitEvent.TESTCASE_AFTER: - #_console.restore_cursor() - _console.print_color( - " Run Test: %s > %s :" % [event.resource_path(), event.test_name()], - Color.ANTIQUE_WHITE - ) - _print_status(event) - _print_failure_report(event.reports()) - GdUnitEvent.TESTSUITE_AFTER: - _print_failure_report(event.reports()) - _print_status(event) - _console.prints_color( - "Statistics: | %d tests cases | %d error | %d failed | %d flaky | %d skipped | %d orphans |\n" - % [ - _report.test_count(), - _report.error_count(), - _report.failure_count(), - _report.flaky_count(), - _report.skipped_count(), - _report.orphan_count() - ], - Color.ANTIQUE_WHITE - ) - - - func _print_failure_report(reports: Array[GdUnitReport]) -> void: - for report in reports: - if ( - report.is_failure() - or report.is_error() - or report.is_warning() - or report.is_skipped() - ): - _console.prints_color( - " Report:", - Color.DARK_TURQUOISE, CmdConsole.BOLD | CmdConsole.UNDERLINE - ) - var text := GdUnitTools.richtext_normalize(str(report)) - for line in text.split("\n"): - _console.prints_color(" %s" % line, Color.DARK_TURQUOISE) - _console.new_line() - - - func _print_status(event: GdUnitEvent) -> void: - if event.is_flaky() and event.is_success(): - var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) - _console.print_color("FLAKY (%d retries)" % retries, Color.GREEN_YELLOW, CmdConsole.BOLD | CmdConsole.ITALIC) - elif event.is_success(): - _console.print_color("PASSED", Color.FOREST_GREEN, CmdConsole.BOLD) - elif event.is_skipped(): - _console.print_color("SKIPPED", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.ITALIC) - elif event.is_failed() or event.is_error(): - var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) - if retries > 1: - _console.print_color("FAILED (retry %d)" % retries, Color.FIREBRICK, CmdConsole.BOLD) - else: - _console.print_color("FAILED", Color.FIREBRICK, CmdConsole.BOLD) - elif event.is_warning(): - _console.print_color("WARNING", Color.GOLDENROD, CmdConsole.BOLD | CmdConsole.UNDERLINE) - - _console.prints_color( - " %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE - ) - - -var _cli_runner :CLIRunner +var _cli_runner: GdUnitTestCIRunner func _initialize() -> void: - if Engine.get_version_info().hex < 0x40200: - prints("GdUnit4 requires a minimum of Godot 4.2.x Version!") - quit(CLIRunner.RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED) - return DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) - _cli_runner = CLIRunner.new() + _cli_runner = GdUnitTestCIRunner.new() root.add_child(_cli_runner) diff --git a/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid b/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid index 329e221d..69e43a57 100644 --- a/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid +++ b/addons/gdUnit4/bin/GdUnitCmdTool.gd.uid @@ -1 +1 @@ -uid://c5o3vrqjdy7lo +uid://drclt2mnlga5u diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd b/addons/gdUnit4/bin/GdUnitCopyLog.gd index 084ac72d..8b228051 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd @@ -82,9 +82,9 @@ func _process(_delta: float) -> bool: func set_current_report_path() -> void: # scan for latest report directory var iteration := GdUnitFileAccess.find_last_path_index( - _report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX + _report_root_path, GdUnitConstants.REPORT_DIR_PREFIX ) - _current_report_path = "%s/%s%d" % [_report_root_path, GdUnitHtmlReport.REPORT_DIR_PREFIX, iteration] + _current_report_path = "%s/%s%d" % [_report_root_path, GdUnitConstants.REPORT_DIR_PREFIX, iteration] func set_report_directory(path: String) -> void: @@ -125,9 +125,9 @@ func read_log_file_content(log_file: String) -> GdUnitResult: content += "" content = content\ .replace("", "")\ - .replace(CmdConsole.CSI_BOLD, "")\ - .replace(CmdConsole.CSI_ITALIC, "")\ - .replace(CmdConsole.CSI_UNDERLINE, "") + .replace(GdUnitCSIMessageWriter.CSI_BOLD, "")\ + .replace(GdUnitCSIMessageWriter.CSI_ITALIC, "")\ + .replace(GdUnitCSIMessageWriter.CSI_UNDERLINE, "") return GdUnitResult.success(content) @@ -145,11 +145,12 @@ func write_report(content: String, godot_log_file: String) -> GdUnitResult: func _update_index_html(godot_log_file: String) -> void: - var index_file := FileAccess.open("%s/index.html" % _current_report_path, FileAccess.READ_WRITE) + var index_path := "%s/index.html" % _current_report_path + var index_file := FileAccess.open(index_path, FileAccess.READ_WRITE) if index_file == null: push_error( - "Can't add log path to index.html. Error: %s" - % error_string(FileAccess.get_open_error()) + "Can't add log path '%s' to `%s`. Error: %s" + % [godot_log_file, index_path, error_string(FileAccess.get_open_error())] ) return var content := index_file.get_as_text()\ diff --git a/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid index 9f8aa7b6..2715b1b4 100644 --- a/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid +++ b/addons/gdUnit4/bin/GdUnitCopyLog.gd.uid @@ -1 +1 @@ -uid://6rvkp7nob1bs +uid://cdipopy0teh2q diff --git a/addons/gdUnit4/plugin.cfg b/addons/gdUnit4/plugin.cfg index e6b0163a..64bfc352 100644 --- a/addons/gdUnit4/plugin.cfg +++ b/addons/gdUnit4/plugin.cfg @@ -3,5 +3,5 @@ name="gdUnit4" description="Unit Testing Framework for Godot Scripts" author="Mike Schulze" -version="4.5.0" +version="6.0.1" script="plugin.gd" diff --git a/addons/gdUnit4/plugin.gd b/addons/gdUnit4/plugin.gd index 36a70588..822c4d56 100644 --- a/addons/gdUnit4/plugin.gd +++ b/addons/gdUnit4/plugin.gd @@ -1,36 +1,50 @@ @tool extends EditorPlugin -const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") -const GdUnitTestDiscoverGuard := preload("res://addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd") -const GdUnitConsole := preload("res://addons/gdUnit4/src/ui/GdUnitConsole.gd") - +# We need to define manually the slot id's, to be downwards compatible +const CONTEXT_SLOT_FILESYSTEM: int = 1 # EditorContextMenuPlugin.CONTEXT_SLOT_FILESYSTEM +const CONTEXT_SLOT_SCRIPT_EDITOR: int = 2 # EditorContextMenuPlugin.CONTEXT_SLOT_SCRIPT_EDITOR var _gd_inspector: Control -var _gd_console: GdUnitConsole -var _guard: GdUnitTestDiscoverGuard +var _gd_console: Control +var _gd_filesystem_context_menu: Variant +var _gd_scripteditor_context_menu: Variant func _enter_tree() -> void: + + var inferred_declaration: int = ProjectSettings.get_setting("debug/gdscript/warnings/inferred_declaration") + var exclude_addons: bool = ProjectSettings.get_setting("debug/gdscript/warnings/exclude_addons") + if !exclude_addons and inferred_declaration != 0: + printerr("GdUnit4: 'inferred_declaration' is set to Warning/Error!") + printerr("GdUnit4 is not 'inferred_declaration' save, you have to excluded addons (debug/gdscript/warnings/exclude_addons)") + printerr("Loading GdUnit4 Plugin failed.") + return + if check_running_in_test_env(): @warning_ignore("return_value_discarded") - CmdConsole.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!") + GdUnitCSIMessageWriter.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!") return - if Engine.get_version_info().hex < 0x40200: - prints("GdUnit4 plugin requires a minimum of Godot 4.2.x Version!") + + if Engine.get_version_info().hex < 0x40500: + prints("This GdUnit4 plugin version '%s' requires Godot version '4.5' or higher to run." % GdUnit4Version.current()) return GdUnitSettings.setup() # Install the GdUnit Inspector _gd_inspector = (load("res://addons/gdUnit4/src/ui/GdUnitInspector.tscn") as PackedScene).instantiate() + _add_context_menus() add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector) # Install the GdUnit Console _gd_console = (load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn") as PackedScene).instantiate() - var control := add_control_to_bottom_panel(_gd_console, "gdUnitConsole") + var control: Control = add_control_to_bottom_panel(_gd_console, "gdUnitConsole") + @warning_ignore("unsafe_method_access") await _gd_console.setup_update_notification(control) - if GdUnit4CSharpApiLoader.is_mono_supported(): + if GdUnit4CSharpApiLoader.is_api_loaded(): prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version()) + else: + prints("No GdUnit4Net found.") # Connect to be notified for script changes to be able to discover new tests - _guard = GdUnitTestDiscoverGuard.new() + GdUnitTestDiscoverGuard.instance() @warning_ignore("return_value_discarded") resource_saved.connect(_on_resource_saved) prints("Loading GdUnit4 Plugin success") @@ -42,19 +56,55 @@ func _exit_tree() -> void: if is_instance_valid(_gd_inspector): remove_control_from_docks(_gd_inspector) _gd_inspector.free() + _remove_context_menus() if is_instance_valid(_gd_console): remove_control_from_bottom_panel(_gd_console) _gd_console.free() - GdUnitTools.dispose_all(true) + var gdUnitTools: GDScript = load("res://addons/gdUnit4/src/core/GdUnitTools.gd") + @warning_ignore("unsafe_method_access") + gdUnitTools.dispose_all(true) prints("Unload GdUnit4 Plugin success") func check_running_in_test_env() -> bool: - var args := OS.get_cmdline_args() + var args: PackedStringArray = OS.get_cmdline_args() args.append_array(OS.get_cmdline_user_args()) return DisplayServer.get_name() == "headless" or args.has("--selftest") or args.has("--add") or args.has("-a") or args.has("--quit-after") or args.has("--import") +func _add_context_menus() -> void: + if Engine.get_version_info().hex >= 0x40400: + # With Godot 4.4 we have to use the 'add_context_menu_plugin' to register editor context menus + _gd_filesystem_context_menu = _preload_gdx_script("res://addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx") + call_deferred("add_context_menu_plugin", CONTEXT_SLOT_FILESYSTEM, _gd_filesystem_context_menu) + # the CONTEXT_SLOT_SCRIPT_EDITOR is adding to the script panel instead of script editor see https://github.com/godotengine/godot/pull/100556 + #_gd_scripteditor_context_menu = _preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx") + #call_deferred("add_context_menu_plugin", CONTEXT_SLOT_SCRIPT_EDITOR, _gd_scripteditor_context_menu) + # so we use the old hacky way to add the context menu + _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd").new()) + else: + # TODO Delete it if the minimum requirement for the plugin is set to Godot 4.4. + _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd").new()) + _gd_inspector.add_child(preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd").new()) + + +func _remove_context_menus() -> void: + if is_instance_valid(_gd_filesystem_context_menu): + call_deferred("remove_context_menu_plugin", _gd_filesystem_context_menu) + if is_instance_valid(_gd_scripteditor_context_menu): + call_deferred("remove_context_menu_plugin", _gd_scripteditor_context_menu) + + +func _preload_gdx_script(script_path: String) -> Variant: + var script: GDScript = GDScript.new() + script.source_code = GdUnitFileAccess.resource_as_string(script_path) + script.take_over_path(script_path) + var err :Error = script.reload() + if err != OK: + push_error("Can't create context menu %s, error: %s" % [script_path, error_string(err)]) + return script.new() + + func _on_resource_saved(resource: Resource) -> void: if resource is Script: - await _guard.discover(resource as Script) + await GdUnitTestDiscoverGuard.instance().discover(resource as Script) diff --git a/addons/gdUnit4/plugin.gd.uid b/addons/gdUnit4/plugin.gd.uid index ed0615db..30d555e6 100644 --- a/addons/gdUnit4/plugin.gd.uid +++ b/addons/gdUnit4/plugin.gd.uid @@ -1 +1 @@ -uid://hnrpbda7ppbw +uid://cryriduwrk2m diff --git a/addons/gdUnit4/runtest.cmd b/addons/gdUnit4/runtest.cmd index 0c8fbc5f..ad5da4d9 100644 --- a/addons/gdUnit4/runtest.cmd +++ b/addons/gdUnit4/runtest.cmd @@ -1,25 +1,62 @@ -@ECHO OFF -CLS +@echo off +setlocal enabledelayedexpansion -IF NOT DEFINED GODOT_BIN ( - ECHO "GODOT_BIN is not set." - ECHO "Please set the environment variable 'setx GODOT_BIN '" - EXIT /b -1 +:: Initialize variables +set "godot_binary=" +set "filtered_args=" + +:: Process all arguments +set "i=0" +:parse_args +if "%~1"=="" goto end_parse_args + +if "%~1"=="--godot_binary" ( + set "godot_binary=%~2" + shift + shift +) else ( + set "filtered_args=!filtered_args! %~1" + shift +) +goto parse_args +:end_parse_args + +:: If --godot_binary wasn't provided, fallback to environment variable +if "!godot_binary!"=="" ( + set "godot_binary=%GODOT_BIN%" ) -REM scan if Godot mono used and compile c# classes -for /f "tokens=5 delims=. " %%i in ('%GODOT_BIN% --version') do set GODOT_TYPE=%%i -IF "%GODOT_TYPE%" == "mono" ( - ECHO "Godot mono detected" - ECHO Compiling c# classes ... Please Wait - dotnet build --debug - ECHO done %errorlevel% +:: Check if we have a godot_binary value from any source +if "!godot_binary!"=="" ( + echo Godot binary path is not specified. + echo Please either: + echo - Set the environment variable: set GODOT_BIN=C:\path\to\godot.exe + echo - Or use the --godot_binary argument: --godot_binary C:\path\to\godot.exe + exit /b 1 ) -%GODOT_BIN% -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd %* -SET exit_code=%errorlevel% -%GODOT_BIN% --headless --quiet -s -d res://addons/gdUnit4/bin/GdUnitCopyLog.gd %* +:: Check if the Godot binary exists +if not exist "!godot_binary!" ( + echo Error: The specified Godot binary '!godot_binary!' does not exist. + exit /b 1 +) + +:: Get Godot version and check if it's a mono build +for /f "tokens=*" %%i in ('"!godot_binary!" --version') do set GODOT_VERSION=%%i +echo !GODOT_VERSION! | findstr /I "mono" >nul +if !errorlevel! equ 0 ( + echo Godot .NET detected + echo Compiling c# classes ... Please Wait + dotnet build --debug + echo done !errorlevel! +) -ECHO %exit_code% +:: Run the tests with the filtered arguments +"!godot_binary!" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd !filtered_args! +set exit_code=%ERRORLEVEL% +echo Run tests ends with %exit_code% -EXIT /B %exit_code% +:: Run the copy log command +"!godot_binary!" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd !filtered_args! > nul +set exit_code2=%ERRORLEVEL% +exit /b %exit_code% diff --git a/addons/gdUnit4/runtest.sh b/addons/gdUnit4/runtest.sh index 17ecb0dd..f0269efb 100644 --- a/addons/gdUnit4/runtest.sh +++ b/addons/gdUnit4/runtest.sh @@ -1,15 +1,62 @@ -#!/bin/sh +#!/bin/bash -if [ -z "$GODOT_BIN" ]; then - echo "'GODOT_BIN' is not set." - echo "Please set the environment variable 'export GODOT_BIN=/Applications/Godot.app/Contents/MacOS/Godot'" +# Check for command-line argument +godot_binary="" +filtered_args="" + +# Process all arguments with a more compatible approach +while [ $# -gt 0 ]; do + if [ "$1" = "--godot_binary" ] && [ $# -gt 1 ]; then + # Get the next argument as the value + godot_binary="$2" + shift 2 + else + # Keep non-godot_binary arguments for passing to Godot + filtered_args="$filtered_args $1" + shift + fi +done + +# If --godot_binary wasn't provided, fallback to environment variable +if [ -z "$godot_binary" ]; then + godot_binary="$GODOT_BIN" +fi + +# Check if we have a godot_binary value from any source +if [ -z "$godot_binary" ]; then + echo "Godot binary path is not specified." + echo "Please either:" + echo " - Set the environment variable: export GODOT_BIN=/path/to/godot" + echo " - Or use the --godot_binary argument: --godot_binary /path/to/godot" + exit 1 +fi + +# Check if the Godot binary exists and is executable +if [ ! -f "$godot_binary" ]; then + echo "Error: The specified Godot binary '$godot_binary' does not exist." + exit 1 +fi + +if [ ! -x "$godot_binary" ]; then + echo "Error: The specified Godot binary '$godot_binary' is not executable." exit 1 fi -"$GODOT_BIN" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd $* +# Get Godot version and check if it's a .NET build +GODOT_VERSION=$("$godot_binary" --version) +if echo "$GODOT_VERSION" | grep -i "mono" > /dev/null; then + echo "Godot .NET detected" + echo "Compiling c# classes ... Please Wait" + dotnet build --debug + echo "done $?" +fi + +# Run the tests with the filtered arguments +"$godot_binary" --path . -s -d res://addons/gdUnit4/bin/GdUnitCmdTool.gd $filtered_args exit_code=$? echo "Run tests ends with $exit_code" -"$GODOT_BIN" --headless --path . --quiet -s -d res://addons/gdUnit4/bin/GdUnitCopyLog.gd $* > /dev/null +# Run the copy log command +"$godot_binary" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd $filtered_args > /dev/null exit_code2=$? exit $exit_code diff --git a/addons/gdUnit4/src/Comparator.gd.uid b/addons/gdUnit4/src/Comparator.gd.uid index ea72449d..addeb4fb 100644 --- a/addons/gdUnit4/src/Comparator.gd.uid +++ b/addons/gdUnit4/src/Comparator.gd.uid @@ -1 +1 @@ -uid://co4m2faghee1o +uid://ctu3xbpejb7xc diff --git a/addons/gdUnit4/src/Fuzzers.gd.uid b/addons/gdUnit4/src/Fuzzers.gd.uid index a4987b8b..2d09316e 100644 --- a/addons/gdUnit4/src/Fuzzers.gd.uid +++ b/addons/gdUnit4/src/Fuzzers.gd.uid @@ -1 +1 @@ -uid://dxv8rgaxywlsh +uid://yffwyn0x4je5 diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd b/addons/gdUnit4/src/GdUnitArrayAssert.gd index cc2226dd..eeb7ca6b 100644 --- a/addons/gdUnit4/src/GdUnitArrayAssert.gd +++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd @@ -1,111 +1,90 @@ ## An Assertion Tool to verify array values -class_name GdUnitArrayAssert +@abstract class_name GdUnitArrayAssert extends GdUnitAssert ## Verifies that the current value is null. -func is_null() -> GdUnitArrayAssert: - return self +@abstract func is_null() -> GdUnitArrayAssert ## Verifies that the current value is not null. -func is_not_null() -> GdUnitArrayAssert: - return self +@abstract func is_not_null() -> GdUnitArrayAssert ## Verifies that the current Array is equal to the given one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func is_equal(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array is equal to the given one, ignoring case considerations. -@warning_ignore("unused_parameter") -func is_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array is not equal to the given one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func is_not_equal(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array is not equal to the given one, ignoring case considerations. -@warning_ignore("unused_parameter") -func is_not_equal_ignoring_case(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitArrayAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitArrayAssert ## Verifies that the current Array is empty, it has a size of 0. -func is_empty() -> GdUnitArrayAssert: - return self +@abstract func is_empty() -> GdUnitArrayAssert ## Verifies that the current Array is not empty, it has a size of minimum 1. -func is_not_empty() -> GdUnitArrayAssert: - return self +@abstract func is_not_empty() -> GdUnitArrayAssert + ## Verifies that the current Array is the same. [br] ## Compares the current by object reference equals -@warning_ignore("unused_parameter", "shadowed_global_identifier") -func is_same(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func is_same(expected: Variant) -> GdUnitArrayAssert ## Verifies that the current Array is NOT the same. [br] ## Compares the current by object reference equals -@warning_ignore("unused_parameter", "shadowed_global_identifier") -func is_not_same(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func is_not_same(expected: Variant) -> GdUnitArrayAssert ## Verifies that the current Array has a size of given value. -@warning_ignore("unused_parameter") -func has_size(expectd: int) -> GdUnitArrayAssert: - return self +@abstract func has_size(expectd: int) -> GdUnitArrayAssert ## Verifies that the current Array contains the given values, in any order.[br] ## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same] -@warning_ignore("unused_parameter") -func contains(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func contains(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br] ## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly] -@warning_ignore("unused_parameter") -func contains_exactly(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func contains_exactly(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br] ## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly_in_any_order] -@warning_ignore("unused_parameter") -func contains_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array contains the given values, in any order.[br] ## The values are compared by object reference, for deep parameter comparision use [method contains] -@warning_ignore("unused_parameter") -func contains_same(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func contains_same(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br] ## The values are compared by object reference, for deep parameter comparision use [method contains_exactly] -@warning_ignore("unused_parameter") -func contains_same_exactly(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br] ## The values are compared by object reference, for deep parameter comparision use [method contains_exactly_in_any_order] -@warning_ignore("unused_parameter") -func contains_same_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array do NOT contains the given values, in any order.[br] @@ -113,13 +92,11 @@ func contains_same_exactly_in_any_order(expected :Variant) -> GdUnitArrayAssert: ## [b]Example:[/b] ## [codeblock] ## # will succeed -## assert_array([1, 2, 3, 4, 5]).not_contains([6]) +## assert_array([1, 2, 3, 4, 5]).not_contains(6) ## # will fail -## assert_array([1, 2, 3, 4, 5]).not_contains([2, 6]) +## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6) ## [/codeblock] -@warning_ignore("unused_parameter") -func not_contains(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func not_contains(...expected: Array) -> GdUnitArrayAssert ## Verifies that the current Array do NOT contains the given values, in any order.[br] @@ -127,45 +104,19 @@ func not_contains(expected :Variant) -> GdUnitArrayAssert: ## [b]Example:[/b] ## [codeblock] ## # will succeed -## assert_array([1, 2, 3, 4, 5]).not_contains([6]) +## assert_array([1, 2, 3, 4, 5]).not_contains(6) ## # will fail -## assert_array([1, 2, 3, 4, 5]).not_contains([2, 6]) +## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6) ## [/codeblock] -@warning_ignore("unused_parameter") -func not_contains_same(expected :Variant) -> GdUnitArrayAssert: - return self +@abstract func not_contains_same(...expected: Array) -> GdUnitArrayAssert ## Extracts all values by given function name and optional arguments into a new ArrayAssert. ## If the elements not accessible by `func_name` the value is converted to `"n.a"`, expecting null values -@warning_ignore("unused_parameter") -func extract(func_name: String, args := Array()) -> GdUnitArrayAssert: - return self +@abstract func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert ## Extracts all values by given extractor's into a new ArrayAssert. ## If the elements not extractable than the value is converted to `"n.a"`, expecting null values -@warning_ignore("unused_parameter") -func extractv( - extractor0 :GdUnitValueExtractor, - extractor1 :GdUnitValueExtractor = null, - extractor2 :GdUnitValueExtractor = null, - extractor3 :GdUnitValueExtractor = null, - extractor4 :GdUnitValueExtractor = null, - extractor5 :GdUnitValueExtractor = null, - extractor6 :GdUnitValueExtractor = null, - extractor7 :GdUnitValueExtractor = null, - extractor8 :GdUnitValueExtractor = null, - extractor9 :GdUnitValueExtractor = null) -> GdUnitArrayAssert: - return self - - - -@warning_ignore("unused_parameter") -func override_failure_message(message :String) -> GdUnitArrayAssert: - return self - - -@warning_ignore("unused_parameter") -func append_failure_message(message :String) -> GdUnitArrayAssert: - return self +## -- The argument type is Array[GdUnitValueExtractor] +@abstract func extractv(...extractors: Array) -> GdUnitArrayAssert diff --git a/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid index 638f7e55..0027b493 100644 --- a/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitArrayAssert.gd.uid @@ -1 +1 @@ -uid://cg7e8mo4ts63x +uid://bs4k5s8f0w08j diff --git a/addons/gdUnit4/src/GdUnitAssert.gd b/addons/gdUnit4/src/GdUnitAssert.gd index 6b354750..41382d9f 100644 --- a/addons/gdUnit4/src/GdUnitAssert.gd +++ b/addons/gdUnit4/src/GdUnitAssert.gd @@ -1,49 +1,47 @@ ## Base interface of all GdUnit asserts -class_name GdUnitAssert +@abstract class_name GdUnitAssert extends RefCounted ## Verifies that the current value is null. -@warning_ignore("untyped_declaration") -func is_null(): - return self +@abstract func is_null() -> GdUnitAssert ## Verifies that the current value is not null. -@warning_ignore("untyped_declaration") -func is_not_null(): - return self +@abstract func is_not_null() -> GdUnitAssert ## Verifies that the current value is equal to expected one. -@warning_ignore("unused_parameter") -@warning_ignore("untyped_declaration") -func is_equal(expected: Variant): - return self +@abstract func is_equal(expected: Variant) -> GdUnitAssert ## Verifies that the current value is not equal to expected one. -@warning_ignore("unused_parameter") -@warning_ignore("untyped_declaration") -func is_not_equal(expected: Variant): - return self - - -@warning_ignore("untyped_declaration") -func do_fail(): - return self - - -## Overrides the default failure message by given custom message. -@warning_ignore("unused_parameter") -@warning_ignore("untyped_declaration") -func override_failure_message(message :String): - return self - - -## Appends a custom message to the failure message. -## This can be used to add additional infromations to the generated failure message. -@warning_ignore("unused_parameter") -@warning_ignore("untyped_declaration") -func append_failure_message(message :String): - return self +@abstract func is_not_equal(expected: Variant) -> GdUnitAssert + + +## Overrides the default failure message by given custom message.[br] +## This function allows you to replace the automatically generated failure message with a more specific +## or user-friendly message that better describes the test failure context.[br] +## Usage: +## [codeblock] +## # Override with custom context-specific message +## func test_player_inventory(): +## assert_that(player.get_item_count("sword"))\ +## .override_failure_message("Player should have exactly one sword")\ +## .is_equal(1) +## [/codeblock] +@abstract func override_failure_message(message: String) -> GdUnitAssert + + +## Appends a custom message to the failure message.[br] +## This can be used to add additional information to the generated failure message +## while keeping the original assertion details for better debugging context.[br] +## Usage: +## [codeblock] +## # Add context to existing failure message +## func test_player_health(): +## assert_that(player.health)\ +## .append_failure_message("Player was damaged by: %s" % last_damage_source)\ +## .is_greater(0) +## [/codeblock] +@abstract func append_failure_message(message: String) -> GdUnitAssert diff --git a/addons/gdUnit4/src/GdUnitAssert.gd.uid b/addons/gdUnit4/src/GdUnitAssert.gd.uid index 5c824fda..7e93fbaa 100644 --- a/addons/gdUnit4/src/GdUnitAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitAssert.gd.uid @@ -1 +1 @@ -uid://chj3ccr30pm1d +uid://byo1s7ubv1f76 diff --git a/addons/gdUnit4/src/GdUnitAwaiter.gd.uid b/addons/gdUnit4/src/GdUnitAwaiter.gd.uid index 69fc95b8..a63ef1c5 100644 --- a/addons/gdUnit4/src/GdUnitAwaiter.gd.uid +++ b/addons/gdUnit4/src/GdUnitAwaiter.gd.uid @@ -1 +1 @@ -uid://bhodqbw5yg8rt +uid://b1kmmghjf1bg1 diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd b/addons/gdUnit4/src/GdUnitBoolAssert.gd index de67a941..714f8fc5 100644 --- a/addons/gdUnit4/src/GdUnitBoolAssert.gd +++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd @@ -1,41 +1,35 @@ ## An Assertion Tool to verify boolean values -class_name GdUnitBoolAssert +@abstract class_name GdUnitBoolAssert extends GdUnitAssert ## Verifies that the current value is null. -func is_null() -> GdUnitBoolAssert: - return self +@abstract func is_null() -> GdUnitBoolAssert ## Verifies that the current value is not null. -func is_not_null() -> GdUnitBoolAssert: - return self +@abstract func is_not_null() -> GdUnitBoolAssert ## Verifies that the current value is equal to the given one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitBoolAssert: - return self +@abstract func is_equal(expected: Variant) -> GdUnitBoolAssert ## Verifies that the current value is not equal to the given one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitBoolAssert: - return self +@abstract func is_not_equal(expected: Variant) -> GdUnitBoolAssert -## Verifies that the current value is true. -func is_true() -> GdUnitBoolAssert: - return self +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitBoolAssert -## Verifies that the current value is false. -func is_false() -> GdUnitBoolAssert: - return self +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitBoolAssert -## Overrides the default failure message by given custom message. -@warning_ignore("unused_parameter") -func override_failure_message(message :String) -> GdUnitBoolAssert: - return self +## Verifies that the current value is true. +@abstract func is_true() -> GdUnitBoolAssert + + +## Verifies that the current value is false. +@abstract func is_false() -> GdUnitBoolAssert diff --git a/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid b/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid index 6608ab19..db6e9314 100644 --- a/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitBoolAssert.gd.uid @@ -1 +1 @@ -uid://bxo6mkky8xf27 +uid://cx23l4urr7nli diff --git a/addons/gdUnit4/src/GdUnitConstants.gd b/addons/gdUnit4/src/GdUnitConstants.gd index 04458947..e43c75ab 100644 --- a/addons/gdUnit4/src/GdUnitConstants.gd +++ b/addons/gdUnit4/src/GdUnitConstants.gd @@ -4,3 +4,7 @@ extends RefCounted const NO_ARG :Variant = "<--null-->" const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures" + +## The maximum number of report history files to store +const DEFAULT_REPORT_HISTORY_COUNT = 20 +const REPORT_DIR_PREFIX = "report_" diff --git a/addons/gdUnit4/src/GdUnitConstants.gd.uid b/addons/gdUnit4/src/GdUnitConstants.gd.uid index cc8824a9..cb5bf9e8 100644 --- a/addons/gdUnit4/src/GdUnitConstants.gd.uid +++ b/addons/gdUnit4/src/GdUnitConstants.gd.uid @@ -1 +1 @@ -uid://chyqbtvl6c5pp +uid://blatoco5nffpr diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd index 49dd3053..45cc62a4 100644 --- a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd +++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd @@ -1,105 +1,79 @@ ## An Assertion Tool to verify dictionary -class_name GdUnitDictionaryAssert +@abstract class_name GdUnitDictionaryAssert extends GdUnitAssert ## Verifies that the current value is null. -func is_null() -> GdUnitDictionaryAssert: - return self +@abstract func is_null() -> GdUnitDictionaryAssert ## Verifies that the current value is not null. -func is_not_null() -> GdUnitDictionaryAssert: - return self +@abstract func is_not_null() -> GdUnitDictionaryAssert ## Verifies that the current dictionary is equal to the given one, ignoring order. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitDictionaryAssert: - return self +@abstract func is_equal(expected: Variant) -> GdUnitDictionaryAssert ## Verifies that the current dictionary is not equal to the given one, ignoring order. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitDictionaryAssert: - return self +@abstract func is_not_equal(expected: Variant) -> GdUnitDictionaryAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitDictionaryAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitDictionaryAssert ## Verifies that the current dictionary is empty, it has a size of 0. -func is_empty() -> GdUnitDictionaryAssert: - return self +@abstract func is_empty() -> GdUnitDictionaryAssert ## Verifies that the current dictionary is not empty, it has a size of minimum 1. -func is_not_empty() -> GdUnitDictionaryAssert: - return self +@abstract func is_not_empty() -> GdUnitDictionaryAssert ## Verifies that the current dictionary is the same. [br] ## Compares the current by object reference equals -@warning_ignore("unused_parameter", "shadowed_global_identifier") -func is_same(expected :Variant) -> GdUnitDictionaryAssert: - return self +@abstract func is_same(expected: Variant) -> GdUnitDictionaryAssert ## Verifies that the current dictionary is NOT the same. [br] ## Compares the current by object reference equals -@warning_ignore("unused_parameter") -func is_not_same(expected :Variant) -> GdUnitDictionaryAssert: - return self +@abstract func is_not_same(expected: Variant) -> GdUnitDictionaryAssert ## Verifies that the current dictionary has a size of given value. -@warning_ignore("unused_parameter") -func has_size(expected: int) -> GdUnitDictionaryAssert: - return self +@abstract func has_size(expected: int) -> GdUnitDictionaryAssert ## Verifies that the current dictionary contains the given key(s).[br] ## The keys are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_keys] -@warning_ignore("unused_parameter") -func contains_keys(expected :Array) -> GdUnitDictionaryAssert: - return self +@abstract func contains_keys(...expected: Array) -> GdUnitDictionaryAssert ## Verifies that the current dictionary contains the given key and value.[br] ## The key and value are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_key_value] -@warning_ignore("unused_parameter") -func contains_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert: - return self - - -## Verifies that the current dictionary not contains the given key(s).[br] -## This function is [b]deprecated[/b] you have to use [method not_contains_keys] instead -@warning_ignore("unused_parameter") -func contains_not_keys(expected :Array) -> GdUnitDictionaryAssert: - push_warning("Deprecated: 'contains_not_keys' is deprectated and will be removed soon, use `not_contains_keys` instead!") - return not_contains_keys(expected) +@abstract func contains_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert ## Verifies that the current dictionary not contains the given key(s).[br] ## The keys are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same_keys] -@warning_ignore("unused_parameter") -func not_contains_keys(expected :Array) -> GdUnitDictionaryAssert: - return self +@abstract func not_contains_keys(...expected: Array) -> GdUnitDictionaryAssert ## Verifies that the current dictionary contains the given key(s).[br] ## The keys are compared by object reference, for deep parameter comparision use [method contains_keys] -@warning_ignore("unused_parameter") -func contains_same_keys(expected :Array) -> GdUnitDictionaryAssert: - return self +@abstract func contains_same_keys(expected: Array) -> GdUnitDictionaryAssert ## Verifies that the current dictionary contains the given key and value.[br] ## The key and value are compared by object reference, for deep parameter comparision use [method contains_key_value] -@warning_ignore("unused_parameter") -func contains_same_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert: - return self +@abstract func contains_same_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert ## Verifies that the current dictionary not contains the given key(s). ## The keys are compared by object reference, for deep parameter comparision use [method not_contains_keys] -@warning_ignore("unused_parameter") -func not_contains_same_keys(expected :Array) -> GdUnitDictionaryAssert: - return self +@abstract func not_contains_same_keys(...expected: Array) -> GdUnitDictionaryAssert diff --git a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid index 29d24b93..4fa9f6af 100644 --- a/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitDictionaryAssert.gd.uid @@ -1 +1 @@ -uid://bmsvoaraxsuh6 +uid://215u3ktvnwp6 diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd b/addons/gdUnit4/src/GdUnitFailureAssert.gd index 1ee987ff..6fec1910 100644 --- a/addons/gdUnit4/src/GdUnitFailureAssert.gd +++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd @@ -1,37 +1,52 @@ ## An assertion tool to verify GDUnit asserts. ## This assert is for internal use only, to verify that failed asserts work as expected. -class_name GdUnitFailureAssert +@abstract class_name GdUnitFailureAssert extends GdUnitAssert +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitFailureAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitFailureAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitFailureAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFailureAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFailureAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFailureAssert + + ## Verifies if the executed assert was successful -func is_success() -> GdUnitFailureAssert: - return self +@abstract func is_success() -> GdUnitFailureAssert + ## Verifies if the executed assert has failed -func is_failed() -> GdUnitFailureAssert: - return self +@abstract func is_failed() -> GdUnitFailureAssert ## Verifies the failure line is equal to expected one. -@warning_ignore("unused_parameter") -func has_line(expected :int) -> GdUnitFailureAssert: - return self +@abstract func has_line(expected: int) -> GdUnitFailureAssert ## Verifies the failure message is equal to expected one. -@warning_ignore("unused_parameter") -func has_message(expected: String) -> GdUnitFailureAssert: - return self +@abstract func has_message(expected: String) -> GdUnitFailureAssert ## Verifies that the failure message starts with the expected message. -@warning_ignore("unused_parameter") -func starts_with_message(expected: String) -> GdUnitFailureAssert: - return self +@abstract func starts_with_message(expected: String) -> GdUnitFailureAssert ## Verifies that the failure message contains the expected message. -@warning_ignore("unused_parameter") -func contains_message(expected: String) -> GdUnitFailureAssert: - return self +@abstract func contains_message(expected: String) -> GdUnitFailureAssert diff --git a/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid b/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid index 3c51d812..f2de9b3a 100644 --- a/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFailureAssert.gd.uid @@ -1 +1 @@ -uid://cq3a3stpl2wyf +uid://bamtimy2noquj diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd b/addons/gdUnit4/src/GdUnitFileAssert.gd index 21bf21a6..771da906 100644 --- a/addons/gdUnit4/src/GdUnitFileAssert.gd +++ b/addons/gdUnit4/src/GdUnitFileAssert.gd @@ -1,19 +1,38 @@ -class_name GdUnitFileAssert +@abstract class_name GdUnitFileAssert extends GdUnitAssert -func is_file() -> GdUnitFileAssert: - return self +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitFileAssert -func exists() -> GdUnitFileAssert: - return self +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitFileAssert -func is_script() -> GdUnitFileAssert: - return self +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitFileAssert -@warning_ignore("unused_parameter") -func contains_exactly(expected_rows :Array) -> GdUnitFileAssert: - return self +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFileAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFileAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFileAssert + + +@abstract func is_file() -> GdUnitFileAssert + + +@abstract func exists() -> GdUnitFileAssert + + +@abstract func is_script() -> GdUnitFileAssert + + +@abstract func contains_exactly(expected_rows :Array) -> GdUnitFileAssert diff --git a/addons/gdUnit4/src/GdUnitFileAssert.gd.uid b/addons/gdUnit4/src/GdUnitFileAssert.gd.uid index 6740f86d..1bc3f0c2 100644 --- a/addons/gdUnit4/src/GdUnitFileAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFileAssert.gd.uid @@ -1 +1 @@ -uid://muxdl41o1880 +uid://cr6fmdftrdhh3 diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd b/addons/gdUnit4/src/GdUnitFloatAssert.gd index 6ce5f1e8..2695ab0e 100644 --- a/addons/gdUnit4/src/GdUnitFloatAssert.gd +++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd @@ -1,83 +1,75 @@ ## An Assertion Tool to verify float values -class_name GdUnitFloatAssert +@abstract class_name GdUnitFloatAssert extends GdUnitAssert -## Verifies that the current String is equal to the given one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitFloatAssert: - return self +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitFloatAssert -## Verifies that the current String is not equal to the given one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitFloatAssert: - return self +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitFloatAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitFloatAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFloatAssert ## Verifies that the current and expected value are approximately equal. -@warning_ignore("unused_parameter", "shadowed_global_identifier") -func is_equal_approx(expected :float, approx :float) -> GdUnitFloatAssert: - return self +@abstract func is_equal_approx(expected: float, approx: float) -> GdUnitFloatAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFloatAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFloatAssert ## Verifies that the current value is less than the given one. -@warning_ignore("unused_parameter") -func is_less(expected :float) -> GdUnitFloatAssert: - return self +@abstract func is_less(expected: float) -> GdUnitFloatAssert ## Verifies that the current value is less than or equal the given one. -@warning_ignore("unused_parameter") -func is_less_equal(expected :float) -> GdUnitFloatAssert: - return self +@abstract func is_less_equal(expected: float) -> GdUnitFloatAssert ## Verifies that the current value is greater than the given one. -@warning_ignore("unused_parameter") -func is_greater(expected :float) -> GdUnitFloatAssert: - return self +@abstract func is_greater(expected: float) -> GdUnitFloatAssert ## Verifies that the current value is greater than or equal the given one. -@warning_ignore("unused_parameter") -func is_greater_equal(expected :float) -> GdUnitFloatAssert: - return self +@abstract func is_greater_equal(expected: float) -> GdUnitFloatAssert ## Verifies that the current value is negative. -func is_negative() -> GdUnitFloatAssert: - return self +@abstract func is_negative() -> GdUnitFloatAssert ## Verifies that the current value is not negative. -func is_not_negative() -> GdUnitFloatAssert: - return self +@abstract func is_not_negative() -> GdUnitFloatAssert ## Verifies that the current value is equal to zero. -func is_zero() -> GdUnitFloatAssert: - return self +@abstract func is_zero() -> GdUnitFloatAssert ## Verifies that the current value is not equal to zero. -func is_not_zero() -> GdUnitFloatAssert: - return self +@abstract func is_not_zero() -> GdUnitFloatAssert ## Verifies that the current value is in the given set of values. -@warning_ignore("unused_parameter") -func is_in(expected :Array) -> GdUnitFloatAssert: - return self +@abstract func is_in(expected: Array) -> GdUnitFloatAssert ## Verifies that the current value is not in the given set of values. -@warning_ignore("unused_parameter") -func is_not_in(expected :Array) -> GdUnitFloatAssert: - return self +@abstract func is_not_in(expected: Array) -> GdUnitFloatAssert ## Verifies that the current value is between the given boundaries (inclusive). -@warning_ignore("unused_parameter") -func is_between(from :float, to :float) -> GdUnitFloatAssert: - return self +@abstract func is_between(from: float, to: float) -> GdUnitFloatAssert diff --git a/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid b/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid index 64e2147b..607cc1bf 100644 --- a/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFloatAssert.gd.uid @@ -1 +1 @@ -uid://c0jvmr0v8kffl +uid://b3jnp7abh7f4b diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd b/addons/gdUnit4/src/GdUnitFuncAssert.gd index 75e8ebd6..e8a49c51 100644 --- a/addons/gdUnit4/src/GdUnitFuncAssert.gd +++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd @@ -1,56 +1,42 @@ ## An Assertion Tool to verify function callback values -class_name GdUnitFuncAssert +@abstract class_name GdUnitFuncAssert extends GdUnitAssert ## Verifies that the current value is null. -func is_null() -> GdUnitFuncAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func is_null() -> GdUnitFuncAssert ## Verifies that the current value is not null. -func is_not_null() -> GdUnitFuncAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func is_not_null() -> GdUnitFuncAssert ## Verifies that the current value is equal to the given one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitFuncAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func is_equal(expected: Variant) -> GdUnitFuncAssert -## Verifies that the current value is not equal to the given one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitFuncAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitFuncAssert -## Verifies that the current value is true. -func is_true() -> GdUnitFuncAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitFuncAssert -## Verifies that the current value is false. -func is_false() -> GdUnitFuncAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitFuncAssert -## Overrides the default failure message by given custom message. -@warning_ignore("unused_parameter") -func override_failure_message(message :String) -> GdUnitFuncAssert: - return self +## Verifies that the current value is true. +@abstract func is_true() -> GdUnitFuncAssert + + +## Verifies that the current value is false. +@abstract func is_false() -> GdUnitFuncAssert ## Sets the timeout in ms to wait the function returnd the expected value, if the time over a failure is emitted.[br] ## e.g.[br] ## do wait until 5s the function `is_state` is returns 10 [br] ## [code]assert_func(instance, "is_state").wait_until(5000).is_equal(10)[/code] -@warning_ignore("unused_parameter") -func wait_until(timeout :int) -> GdUnitFuncAssert: - return self +@abstract func wait_until(timeout: int) -> GdUnitFuncAssert diff --git a/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid b/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid index 42b652e9..a1629489 100644 --- a/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitFuncAssert.gd.uid @@ -1 +1 @@ -uid://c2anhvhblruw1 +uid://daosqxaehunjs diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd index c5e9f863..01711f9a 100644 --- a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd +++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd @@ -1,16 +1,38 @@ ## An assertion tool to verify for Godot runtime errors like assert() and push notifications like push_error(). -class_name GdUnitGodotErrorAssert +@abstract class_name GdUnitGodotErrorAssert extends GdUnitAssert +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitGodotErrorAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitGodotErrorAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitGodotErrorAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitGodotErrorAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitGodotErrorAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitGodotErrorAssert + + ## Verifies if the executed code runs without any runtime errors ## Usage: ## [codeblock] ## await assert_error().is_success() ## [/codeblock] -func is_success() -> GdUnitGodotErrorAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func is_success() -> GdUnitGodotErrorAssert ## Verifies if the executed code runs into a runtime error @@ -18,10 +40,7 @@ func is_success() -> GdUnitGodotErrorAssert: ## [codeblock] ## await assert_error().is_runtime_error() ## [/codeblock] -@warning_ignore("unused_parameter") -func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert ## Verifies if the executed code has a push_warning() used @@ -29,10 +48,7 @@ func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: ## [codeblock] ## await assert_error().is_push_warning() ## [/codeblock] -@warning_ignore("unused_parameter") -func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert ## Verifies if the executed code has a push_error() used @@ -40,7 +56,4 @@ func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: ## [codeblock] ## await assert_error().is_push_error() ## [/codeblock] -@warning_ignore("unused_parameter") -func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert diff --git a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid index b0fd3f4d..9b94f5a8 100644 --- a/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitGodotErrorAssert.gd.uid @@ -1 +1 @@ -uid://bkfk8lbtcmnvi +uid://h36vxwpenfe7 diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd b/addons/gdUnit4/src/GdUnitIntAssert.gd index d593edf9..05eb9223 100644 --- a/addons/gdUnit4/src/GdUnitIntAssert.gd +++ b/addons/gdUnit4/src/GdUnitIntAssert.gd @@ -1,86 +1,79 @@ ## An Assertion Tool to verify integer values -class_name GdUnitIntAssert +@abstract class_name GdUnitIntAssert extends GdUnitAssert -## Verifies that the current String is equal to the given one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitIntAssert: - return self +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitIntAssert -## Verifies that the current String is not equal to the given one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitIntAssert: - return self + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitIntAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitIntAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitIntAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitIntAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitIntAssert ## Verifies that the current value is less than the given one. -@warning_ignore("unused_parameter") -func is_less(expected :int) -> GdUnitIntAssert: - return self +@abstract func is_less(expected: int) -> GdUnitIntAssert ## Verifies that the current value is less than or equal the given one. -@warning_ignore("unused_parameter") -func is_less_equal(expected :int) -> GdUnitIntAssert: - return self +@abstract func is_less_equal(expected: int) -> GdUnitIntAssert ## Verifies that the current value is greater than the given one. -@warning_ignore("unused_parameter") -func is_greater(expected :int) -> GdUnitIntAssert: - return self +@abstract func is_greater(expected: int) -> GdUnitIntAssert ## Verifies that the current value is greater than or equal the given one. -@warning_ignore("unused_parameter") -func is_greater_equal(expected :int) -> GdUnitIntAssert: - return self +@abstract func is_greater_equal(expected: int) -> GdUnitIntAssert ## Verifies that the current value is even. -func is_even() -> GdUnitIntAssert: - return self +@abstract func is_even() -> GdUnitIntAssert ## Verifies that the current value is odd. -func is_odd() -> GdUnitIntAssert: - return self +@abstract func is_odd() -> GdUnitIntAssert ## Verifies that the current value is negative. -func is_negative() -> GdUnitIntAssert: - return self +@abstract func is_negative() -> GdUnitIntAssert ## Verifies that the current value is not negative. -func is_not_negative() -> GdUnitIntAssert: - return self +@abstract func is_not_negative() -> GdUnitIntAssert ## Verifies that the current value is equal to zero. -func is_zero() -> GdUnitIntAssert: - return self +@abstract func is_zero() -> GdUnitIntAssert ## Verifies that the current value is not equal to zero. -func is_not_zero() -> GdUnitIntAssert: - return self +@abstract func is_not_zero() -> GdUnitIntAssert ## Verifies that the current value is in the given set of values. -@warning_ignore("unused_parameter") -func is_in(expected :Array) -> GdUnitIntAssert: - return self +@abstract func is_in(expected: Array) -> GdUnitIntAssert ## Verifies that the current value is not in the given set of values. -@warning_ignore("unused_parameter") -func is_not_in(expected :Array) -> GdUnitIntAssert: - return self +@abstract func is_not_in(expected: Array) -> GdUnitIntAssert ## Verifies that the current value is between the given boundaries (inclusive). -@warning_ignore("unused_parameter") -func is_between(from :int, to :int) -> GdUnitIntAssert: - return self +@abstract func is_between(from: int, to: int) -> GdUnitIntAssert diff --git a/addons/gdUnit4/src/GdUnitIntAssert.gd.uid b/addons/gdUnit4/src/GdUnitIntAssert.gd.uid index 3307a53c..6176c6b6 100644 --- a/addons/gdUnit4/src/GdUnitIntAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitIntAssert.gd.uid @@ -1 +1 @@ -uid://8duxwqybyfgd +uid://owftkm7kpycc diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd b/addons/gdUnit4/src/GdUnitObjectAssert.gd index f2068e5a..9d7e76ea 100644 --- a/addons/gdUnit4/src/GdUnitObjectAssert.gd +++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd @@ -1,49 +1,51 @@ ## An Assertion Tool to verify Object values -class_name GdUnitObjectAssert +@abstract class_name GdUnitObjectAssert extends GdUnitAssert -## Verifies that the current value is equal to expected one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitObjectAssert: - return self +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitObjectAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitObjectAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitObjectAssert ## Verifies that the current value is not equal to expected one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitObjectAssert: - return self +@abstract func is_not_equal(expected: Variant) -> GdUnitObjectAssert -## Verifies that the current value is null. -func is_null() -> GdUnitObjectAssert: - return self +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitObjectAssert -## Verifies that the current value is not null. -func is_not_null() -> GdUnitObjectAssert: - return self +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitObjectAssert + + +## Verifies that the current object is the same as the given one. +@abstract func is_same(expected: Variant) -> GdUnitObjectAssert + + +## Verifies that the current object is not the same as the given one. +@abstract func is_not_same(expected: Variant) -> GdUnitObjectAssert -## Verifies that the current value is the same as the given one. -@warning_ignore("unused_parameter", "shadowed_global_identifier") -func is_same(expected :Variant) -> GdUnitObjectAssert: - return self +## Verifies that the current object is an instance of the given type. +@abstract func is_instanceof(type: Variant) -> GdUnitObjectAssert -## Verifies that the current value is not the same as the given one. -@warning_ignore("unused_parameter") -func is_not_same(expected :Variant) -> GdUnitObjectAssert: - return self +## Verifies that the current object is not an instance of the given type. +@abstract func is_not_instanceof(type: Variant) -> GdUnitObjectAssert -## Verifies that the current value is an instance of the given type. -@warning_ignore("unused_parameter") -func is_instanceof(expected :Object) -> GdUnitObjectAssert: - return self +## Checks whether the current object inherits from the specified type. +@abstract func is_inheriting(type: Variant) -> GdUnitObjectAssert -## Verifies that the current value is not an instance of the given type. -@warning_ignore("unused_parameter") -func is_not_instanceof(expected :Variant) -> GdUnitObjectAssert: - return self +## Checks whether the current object does NOT inherit from the specified type. +@abstract func is_not_inheriting(type: Variant) -> GdUnitObjectAssert diff --git a/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid b/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid index 005c5aca..e04a9e28 100644 --- a/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitObjectAssert.gd.uid @@ -1 +1 @@ -uid://cxqjq7qkfh0qi +uid://d3fy67cwrju16 diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd b/addons/gdUnit4/src/GdUnitResultAssert.gd index 347e6374..01eb8800 100644 --- a/addons/gdUnit4/src/GdUnitResultAssert.gd +++ b/addons/gdUnit4/src/GdUnitResultAssert.gd @@ -1,45 +1,51 @@ ## An Assertion Tool to verify Results -class_name GdUnitResultAssert +@abstract class_name GdUnitResultAssert extends GdUnitAssert ## Verifies that the current value is null. -func is_null() -> GdUnitResultAssert: - return self +@abstract func is_null() -> GdUnitResultAssert ## Verifies that the current value is not null. -func is_not_null() -> GdUnitResultAssert: - return self +@abstract func is_not_null() -> GdUnitResultAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitResultAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitResultAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitResultAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitResultAssert ## Verifies that the result is ends up with empty -func is_empty() -> GdUnitResultAssert: - return self +@abstract func is_empty() -> GdUnitResultAssert ## Verifies that the result is ends up with success -func is_success() -> GdUnitResultAssert: - return self +@abstract func is_success() -> GdUnitResultAssert ## Verifies that the result is ends up with warning -func is_warning() -> GdUnitResultAssert: - return self +@abstract func is_warning() -> GdUnitResultAssert ## Verifies that the result is ends up with error -func is_error() -> GdUnitResultAssert: - return self +@abstract func is_error() -> GdUnitResultAssert ## Verifies that the result contains the given message -@warning_ignore("unused_parameter") -func contains_message(expected :String) -> GdUnitResultAssert: - return self +@abstract func contains_message(expected: String) -> GdUnitResultAssert ## Verifies that the result contains the given value -@warning_ignore("unused_parameter") -func is_value(expected :Variant) -> GdUnitResultAssert: - return self +@abstract func is_value(expected: Variant) -> GdUnitResultAssert diff --git a/addons/gdUnit4/src/GdUnitResultAssert.gd.uid b/addons/gdUnit4/src/GdUnitResultAssert.gd.uid index 5efa3c4b..83842b0c 100644 --- a/addons/gdUnit4/src/GdUnitResultAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitResultAssert.gd.uid @@ -1 +1 @@ -uid://dhoui8wa3f5c5 +uid://cb6xwld1v312k diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd b/addons/gdUnit4/src/GdUnitSceneRunner.gd index 184be50a..6b11918d 100644 --- a/addons/gdUnit4/src/GdUnitSceneRunner.gd +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd @@ -1,31 +1,26 @@ ## The Scene Runner is a tool used for simulating interactions on a scene. ## With this tool, you can simulate input events such as keyboard or mouse input and/or simulate scene processing over a certain number of frames. ## This tool is typically used for integration testing a scene. -class_name GdUnitSceneRunner +@abstract class_name GdUnitSceneRunner extends RefCounted -const NO_ARG = GdUnitConstants.NO_ARG - ## Simulates that an action has been pressed.[br] ## [member action] : the action e.g. [code]"ui_up"[/code][br] -@warning_ignore("unused_parameter") -func simulate_action_pressed(action: String) -> GdUnitSceneRunner: - return self +## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br] +@abstract func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner ## Simulates that an action is pressed.[br] ## [member action] : the action e.g. [code]"ui_up"[/code][br] -@warning_ignore("unused_parameter") -func simulate_action_press(action: String) -> GdUnitSceneRunner: - return self +## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br] +@abstract func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner ## Simulates that an action has been released.[br] ## [member action] : the action e.g. [code]"ui_up"[/code][br] -@warning_ignore("unused_parameter") -func simulate_action_release(action: String) -> GdUnitSceneRunner: - return self +## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br] +@abstract func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner ## Simulates that a key has been pressed.[br] @@ -37,59 +32,39 @@ func simulate_action_release(action: String) -> GdUnitSceneRunner: ## var runner = scene_runner("res://scenes/simple_scene.tscn") ## await runner.simulate_key_pressed(KEY_SPACE) ## [/codeblock] -@warning_ignore("unused_parameter") -func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner ## Simulates that a key is pressed.[br] ## [member key_code] : the key code e.g. [constant KEY_ENTER][br] ## [member shift_pressed] : false by default set to true if simmulate shift is press[br] ## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] -@warning_ignore("unused_parameter") -func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: - return self +@abstract func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner ## Simulates that a key has been released.[br] ## [member key_code] : the key code e.g. [constant KEY_ENTER][br] ## [member shift_pressed] : false by default set to true if simmulate shift is press[br] ## [member ctrl_pressed] : false by default set to true if simmulate control is press[br] -@warning_ignore("unused_parameter") -func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner: - return self - - -## Sets the mouse cursor to given position relative to the viewport. -## @deprecated: Use [set_mouse_position] instead. -@warning_ignore("unused_parameter") -func set_mouse_pos(position: Vector2) -> GdUnitSceneRunner: - return self +@abstract func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner ## Sets the mouse position to the specified vector, provided in pixels and relative to an origin at the upper left corner of the currently focused Window Manager game window.[br] ## [member position] : The absolute position in pixels as Vector2 -@warning_ignore("unused_parameter") -func set_mouse_position(position: Vector2) -> GdUnitSceneRunner: - return self +@abstract func set_mouse_position(position: Vector2) -> GdUnitSceneRunner ## Returns the mouse's position in this Viewport using the coordinate system of this Viewport. -func get_mouse_position() -> Vector2: - return Vector2.ZERO +@abstract func get_mouse_position() -> Vector2 ## Gets the current global mouse position of the current window -func get_global_mouse_position() -> Vector2: - return Vector2.ZERO +@abstract func get_global_mouse_position() -> Vector2 ## Simulates a mouse moved to final position.[br] ## [member position] : The final mouse position -@warning_ignore("unused_parameter") -func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner: - return self +@abstract func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner ## Simulates a mouse move to the relative coordinates (offset).[br] @@ -103,10 +78,7 @@ func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner: ## var runner = scene_runner("res://scenes/simple_scene.tscn") ## await runner.simulate_mouse_move_relative(Vector2(100,100)) ## [/codeblock] -@warning_ignore("unused_parameter") -func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner ## Simulates a mouse move to the absolute coordinates.[br] @@ -120,59 +92,44 @@ func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_ty ## var runner = scene_runner("res://scenes/simple_scene.tscn") ## await runner.simulate_mouse_move_absolute(Vector2(100,100)) ## [/codeblock] -@warning_ignore("unused_parameter") -func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner ## Simulates a mouse button pressed.[br] ## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. ## [member double_click] : Set to true to simulate a double-click -@warning_ignore("unused_parameter") -func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: - return self +@abstract func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner ## Simulates a mouse button press (holding)[br] ## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. ## [member double_click] : Set to true to simulate a double-click -@warning_ignore("unused_parameter") -func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner: - return self +@abstract func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner ## Simulates a mouse button released.[br] ## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants. -@warning_ignore("unused_parameter") -func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner: - return self +@abstract func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner ## Simulates a screen touch is pressed.[br] ## [member index] : The touch index in the case of a multi-touch event.[br] ## [member position] : The position to touch the screen.[br] ## [member double_tap] : If true, the touch's state is a double tab. -@warning_ignore("unused_parameter") -func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: - return self +@abstract func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner ## Simulates a screen touch press without releasing it immediately, effectively simulating a "hold" action.[br] ## [member index] : The touch index in the case of a multi-touch event.[br] ## [member position] : The position to touch the screen.[br] ## [member double_tap] : If true, the touch's state is a double tab. -@warning_ignore("unused_parameter") -func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: - return self +@abstract func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner ## Simulates a screen touch is released.[br] ## [member index] : The touch index in the case of a multi-touch event.[br] ## [member double_tap] : If true, the touch's state is a double tab. -@warning_ignore("unused_parameter") -func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner: - return self +@abstract func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner ## Simulates a touch drag and drop event to a relative position.[br] @@ -190,10 +147,7 @@ func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSce ## # and drop it at final at 150,50 relative (50,50 + 100,0) ## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0)) ## [/codeblock] -@warning_ignore("unused_parameter") -func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner ## Simulates a touch screen drop to the absolute coordinates (offset).[br] @@ -211,10 +165,7 @@ func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: fl ## # and drop it at 100,50 ## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50)) ## [/codeblock] -@warning_ignore("unused_parameter") -func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner ## Simulates a complete drag and drop event from one position to another.[br] @@ -232,91 +183,47 @@ func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: fl ## # start drag at position 50,50 and drop it at 100,50 ## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50)) ## [/codeblock] -@warning_ignore("unused_parameter") -func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner ## Simulates a touch screen drag event to given position.[br] ## [member index] : The touch index in the case of a multi-touch event.[br] ## [member position] : The drag start position, indicating the drag position.[br] -@warning_ignore("unused_parameter") -func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner: - return self +@abstract func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner ## Returns the actual position of the touchscreen drag position by given index. ## [member index] : The touch index in the case of a multi-touch event.[br] -@warning_ignore("unused_parameter") -func get_screen_touch_drag_position(index: int) -> Vector2: - return Vector2.ZERO +@abstract func get_screen_touch_drag_position(index: int) -> Vector2 ## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br] ## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life, ## whilst a value of 0.5 means the game moves at half the regular speed. - - -## Sets the time factor for the scene simulation. ## [member time_factor] : A float representing the simulation speed.[br] ## - Default is 1.0, meaning the simulation runs at normal speed.[br] ## - A value of 2.0 means the simulation runs twice as fast as real time.[br] ## - A value of 0.5 means the simulation runs at half the regular speed.[br] -@warning_ignore("unused_parameter") -func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner: - return self +@abstract func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner ## Simulates scene processing for a certain number of frames.[br] ## [member frames] : amount of frames to process[br] ## [member delta_milli] : the time delta between a frame in milliseconds -@warning_ignore("unused_parameter") -func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner ## Simulates scene processing until the given signal is emitted by the scene.[br] ## [member signal_name] : the signal to stop the simulation[br] ## [member args] : optional signal arguments to be matched for stop[br] -@warning_ignore("unused_parameter") -func simulate_until_signal( - signal_name: String, - arg0: Variant = NO_ARG, - arg1: Variant = NO_ARG, - arg2: Variant = NO_ARG, - arg3: Variant = NO_ARG, - arg4: Variant = NO_ARG, - arg5: Variant = NO_ARG, - arg6: Variant = NO_ARG, - arg7: Variant = NO_ARG, - arg8: Variant = NO_ARG, - arg9: Variant = NO_ARG) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner ## Simulates scene processing until the given signal is emitted by the given object.[br] ## [member source] : the object that should emit the signal[br] ## [member signal_name] : the signal to stop the simulation[br] ## [member args] : optional signal arguments to be matched for stop -@warning_ignore("unused_parameter") -func simulate_until_object_signal( - source: Object, - signal_name: String, - arg0: Variant = NO_ARG, - arg1: Variant = NO_ARG, - arg2: Variant = NO_ARG, - arg3: Variant = NO_ARG, - arg4: Variant = NO_ARG, - arg5: Variant = NO_ARG, - arg6: Variant = NO_ARG, - arg7: Variant = NO_ARG, - arg8: Variant = NO_ARG, - arg9: Variant = NO_ARG) -> GdUnitSceneRunner: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +@abstract func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner ## Waits for all input events to be processed by flushing any buffered input events @@ -331,11 +238,7 @@ func simulate_until_object_signal( ## [codeblock] ## await await_input_processed() # Ensure all inputs are processed before continuing ## [/codeblock] -func await_input_processed() -> void: - if scene() != null and scene().process_mode != Node.PROCESS_MODE_DISABLED: - Input.flush_buffered_events() - await (Engine.get_main_loop() as SceneTree).process_frame - await (Engine.get_main_loop() as SceneTree).physics_frame +@abstract func await_input_processed() -> void ## The await_func function pauses execution until a specified function in the scene returns a value.[br] @@ -348,10 +251,7 @@ func await_input_processed() -> void: ## # Waits for 'calculate_score' function and verifies the result is equal to 100. ## await_func("calculate_score").is_equal(100) ## [/codeblock] -@warning_ignore("unused_parameter") -func await_func(func_name: String, args := []) -> GdUnitFuncAssert: - return null - +@abstract func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert ## The await_func_on function extends the functionality of await_func by allowing you to specify a source node within the scene.[br] @@ -366,19 +266,14 @@ func await_func(func_name: String, args := []) -> GdUnitFuncAssert: ## var my_instance := ScoreCalculator.new() ## await_func(my_instance, "calculate_score").is_equal(100) ## [/codeblock] -@warning_ignore("unused_parameter") -func await_func_on(source: Object, func_name: String, args := []) -> GdUnitFuncAssert: - return null +@abstract func await_func_on(source: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert ## Waits for the specified signal to be emitted by the scene. If the signal is not emitted within the given timeout, the operation fails.[br] ## [member signal_name] : The name of the signal to wait for[br] ## [member args] : The signal arguments as an array[br] ## [member timeout] : The maximum duration (in milliseconds) to wait for the signal to be emitted before failing -@warning_ignore("unused_parameter") -func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void: - await (Engine.get_main_loop() as SceneTree).process_frame - pass +@abstract func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void ## Waits for the specified signal to be emitted by a particular source node. If the signal is not emitted within the given timeout, the operation fails.[br] @@ -386,69 +281,45 @@ func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void: ## [member signal_name] : The name of the signal to wait for[br] ## [member args] : The signal arguments as an array[br] ## [member timeout] : tThe maximum duration (in milliseconds) to wait for the signal to be emitted before failing -@warning_ignore("unused_parameter") -func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void: - pass +@abstract func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void ## Restores the scene window to a windowed mode and brings it to the foreground.[br] ## This ensures that the scene is visible and active during testing, making it easier to observe and interact with. -func move_window_to_foreground() -> GdUnitSceneRunner: - return self +@abstract func move_window_to_foreground() -> GdUnitSceneRunner -## Restores the scene window to a windowed mode and brings it to the foreground.[br] -## This ensures that the scene is visible and active during testing, making it easier to observe and interact with. -## @deprecated: Use [move_window_to_foreground] instead. -func maximize_view() -> GdUnitSceneRunner: - return self +## Minimizes the scene window to a windowed mode and brings it to the background.[br] +## This ensures that the scene is hidden during testing. +@abstract func move_window_to_background() -> GdUnitSceneRunner ## Return the current value of the property with the name .[br] ## [member name] : name of property[br] ## [member return] : the value of the property -@warning_ignore("unused_parameter") -func get_property(name: String) -> Variant: - return null +@abstract func get_property(name: String) -> Variant + ## Set the value of the property with the name .[br] ## [member name] : name of property[br] ## [member value] : value of property[br] ## [member return] : true|false depending on valid property name. -@warning_ignore("unused_parameter") -func set_property(name: String, value: Variant) -> bool: - return false +@abstract func set_property(name: String, value: Variant) -> bool ## executes the function specified by in the scene and returns the result.[br] ## [member name] : the name of the function to execute[br] ## [member args] : optional function arguments[br] ## [member return] : the function result -@warning_ignore("unused_parameter") -func invoke( - name: String, - arg0: Variant = NO_ARG, - arg1: Variant = NO_ARG, - arg2: Variant = NO_ARG, - arg3: Variant = NO_ARG, - arg4: Variant = NO_ARG, - arg5: Variant = NO_ARG, - arg6: Variant = NO_ARG, - arg7: Variant = NO_ARG, - arg8: Variant = NO_ARG, - arg9: Variant = NO_ARG) -> Variant: - return null +@abstract func invoke(name: String, ...args: Array) -> Variant ## Searches for the specified node with the name in the current scene and returns it, otherwise null.[br] ## [member name] : the name of the node to find[br] ## [member recursive] : enables/disables seraching recursive[br] ## [member return] : the node if find otherwise null -@warning_ignore("unused_parameter") -func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node: - return null +@abstract func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node ## Access to current running scene -func scene() -> Node: - return null +@abstract func scene() -> Node diff --git a/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid b/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid index db32d3a5..e85cc753 100644 --- a/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid +++ b/addons/gdUnit4/src/GdUnitSceneRunner.gd.uid @@ -1 +1 @@ -uid://c4yegvmgs3p3d +uid://bgc5iuggvw4fs diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd b/addons/gdUnit4/src/GdUnitSignalAssert.gd index 9dbc76d3..bb975e89 100644 --- a/addons/gdUnit4/src/GdUnitSignalAssert.gd +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd @@ -1,38 +1,46 @@ ## An Assertion Tool to verify for emitted signals until a waiting time -class_name GdUnitSignalAssert +@abstract class_name GdUnitSignalAssert extends GdUnitAssert -## Verifies that given signal is emitted until waiting time -@warning_ignore("unused_parameter") -func is_emitted(name :String, args := []) -> GdUnitSignalAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitSignalAssert -## Verifies that given signal is NOT emitted until waiting time -@warning_ignore("unused_parameter") -func is_not_emitted(name :String, args := []) -> GdUnitSignalAssert: - await (Engine.get_main_loop() as SceneTree).process_frame - return self +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitSignalAssert -## Verifies the signal exists checked the emitter -@warning_ignore("unused_parameter") -func is_signal_exists(name :String) -> GdUnitSignalAssert: - return self +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitSignalAssert + + +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitSignalAssert ## Overrides the default failure message by given custom message. -@warning_ignore("unused_parameter") -func override_failure_message(message :String) -> GdUnitSignalAssert: - return self +@abstract func override_failure_message(message: String) -> GdUnitSignalAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitSignalAssert + + +## Verifies that given signal is emitted until waiting time +@abstract func is_emitted(name: String, args := []) -> GdUnitSignalAssert + + +## Verifies that given signal is NOT emitted until waiting time +@abstract func is_not_emitted(name: String, args := []) -> GdUnitSignalAssert + + +## Verifies the signal exists checked the emitter +@abstract func is_signal_exists(name: String) -> GdUnitSignalAssert ## Sets the assert signal timeout in ms, if the time over a failure is reported.[br] ## e.g.[br] ## do wait until 5s the instance has emitted the signal `signal_a`[br] ## [code]assert_signal(instance).wait_until(5000).is_emitted("signal_a")[/code] -@warning_ignore("unused_parameter") -func wait_until(timeout :int) -> GdUnitSignalAssert: - return self +@abstract func wait_until(timeout: int) -> GdUnitSignalAssert diff --git a/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid b/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid index 5c2b6585..00751308 100644 --- a/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitSignalAssert.gd.uid @@ -1 +1 @@ -uid://7bekapf057l0 +uid://vye3abjnxxca diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd b/addons/gdUnit4/src/GdUnitStringAssert.gd index 5b4a6a1e..2de698bd 100644 --- a/addons/gdUnit4/src/GdUnitStringAssert.gd +++ b/addons/gdUnit4/src/GdUnitStringAssert.gd @@ -1,79 +1,71 @@ ## An Assertion Tool to verify String values -class_name GdUnitStringAssert +@abstract class_name GdUnitStringAssert extends GdUnitAssert -## Verifies that the current String is equal to the given one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitStringAssert: - return self +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitStringAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitStringAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitStringAssert ## Verifies that the current String is equal to the given one, ignoring case considerations. -@warning_ignore("unused_parameter") -func is_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert: - return self +@abstract func is_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert -## Verifies that the current String is not equal to the given one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitStringAssert: - return self +## Verifies that the current value is not equal to expected one. +@abstract func is_not_equal(expected: Variant) -> GdUnitStringAssert ## Verifies that the current String is not equal to the given one, ignoring case considerations. -@warning_ignore("unused_parameter") -func is_not_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert: - return self +@abstract func is_not_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitStringAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitStringAssert ## Verifies that the current String is empty, it has a length of 0. -func is_empty() -> GdUnitStringAssert: - return self +@abstract func is_empty() -> GdUnitStringAssert ## Verifies that the current String is not empty, it has a length of minimum 1. -func is_not_empty() -> GdUnitStringAssert: - return self +@abstract func is_not_empty() -> GdUnitStringAssert ## Verifies that the current String contains the given String. -@warning_ignore("unused_parameter") -func contains(expected: String) -> GdUnitStringAssert: - return self +@abstract func contains(expected: String) -> GdUnitStringAssert ## Verifies that the current String does not contain the given String. -@warning_ignore("unused_parameter") -func not_contains(expected: String) -> GdUnitStringAssert: - return self +@abstract func not_contains(expected: String) -> GdUnitStringAssert ## Verifies that the current String does not contain the given String, ignoring case considerations. -@warning_ignore("unused_parameter") -func contains_ignoring_case(expected: String) -> GdUnitStringAssert: - return self +@abstract func contains_ignoring_case(expected: String) -> GdUnitStringAssert ## Verifies that the current String does not contain the given String, ignoring case considerations. -@warning_ignore("unused_parameter") -func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert: - return self +@abstract func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert ## Verifies that the current String starts with the given prefix. -@warning_ignore("unused_parameter") -func starts_with(expected: String) -> GdUnitStringAssert: - return self +@abstract func starts_with(expected: String) -> GdUnitStringAssert ## Verifies that the current String ends with the given suffix. -@warning_ignore("unused_parameter") -func ends_with(expected: String) -> GdUnitStringAssert: - return self +@abstract func ends_with(expected: String) -> GdUnitStringAssert ## Verifies that the current String has the expected length by used comparator. -@warning_ignore("unused_parameter") -func has_length(length: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert: - return self +@abstract func has_length(length: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert diff --git a/addons/gdUnit4/src/GdUnitStringAssert.gd.uid b/addons/gdUnit4/src/GdUnitStringAssert.gd.uid index fa76e347..14cc8e5b 100644 --- a/addons/gdUnit4/src/GdUnitStringAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitStringAssert.gd.uid @@ -1 +1 @@ -uid://brnymrcxd2spw +uid://8ees6wtyjco8 diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd b/addons/gdUnit4/src/GdUnitTestSuite.gd index 13241f4a..e69c7a8d 100644 --- a/addons/gdUnit4/src/GdUnitTestSuite.gd +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd @@ -54,7 +54,7 @@ func __gdunit_argument_matchers() -> GDScript: func __gdunit_object_interactions() -> GDScript: - return __lazy_load("res://addons/gdUnit4/src/core/GdUnitObjectInteractions.gd") + return __lazy_load("res://addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd") ## This function is called before a test suite starts[br] @@ -161,7 +161,7 @@ func resource_as_var(resource_path :String) -> Variant: return str_to_var(__gdunit_file_access().resource_as_string(resource_path) as String) -## Waits for given signal is emited by the until a specified timeout to fail[br] +## Waits for given signal to be emitted by until a specified timeout to fail[br] ## source: the object from which the signal is emitted[br] ## signal_name: signal name[br] ## args: the expected signal arguments as an array[br] @@ -177,7 +177,7 @@ func await_idle_frame() -> void: await __awaiter.await_idle_frame() -## Waits for for a given amount of milliseconds[br] +## Waits for a given amount of milliseconds[br] ## example:[br] ## [codeblock] ## # waits for 100ms @@ -307,7 +307,7 @@ func any_float() -> GdUnitArgumentMatcher: return __gdunit_argument_matchers().by_type(TYPE_FLOAT) -## Argument matcher to match any string value +## Argument matcher to match any String value func any_string() -> GdUnitArgumentMatcher: @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_STRING) @@ -362,7 +362,7 @@ func any_vector4() -> GdUnitArgumentMatcher: return __gdunit_argument_matchers().by_type(TYPE_VECTOR4) -## Argument matcher to match any Vector3i value +## Argument matcher to match any Vector4i value func any_vector4i() -> GdUnitArgumentMatcher: @warning_ignore("unsafe_method_access") return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I) @@ -607,7 +607,7 @@ func assert_func(instance :Object, func_name :String, args := Array()) -> GdUnit return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args) -## An Assertion Tool to verify for emitted signals until a certain time. +## An assertion tool to verify for emitted signals until a certain time. func assert_signal(instance :Object) -> GdUnitSignalAssert: return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance) @@ -636,15 +636,15 @@ func assert_failure_await(assertion :Callable) -> GdUnitFailureAssert: return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion) -## An assertion tool to verify for Godot errors.[br] -## You can use to verify for certain Godot erros like failing assertions, push_error, push_warn.[br] +## An assertion tool to verify Godot errors.[br] +## You can use to verify certain Godot errors like failing assertions, push_error, push_warn.[br] ## Usage: ## [codeblock] -## # tests no error was occured during execution the code +## # tests no error occurred during execution of the code ## await assert_error(func (): return 0 )\ ## .is_success() ## -## # tests an push_error('test error') was occured during execution the code +## # tests a push_error('test error') occured during execution of the code ## await assert_error(func (): push_error('test error') )\ ## .is_push_error('test error') ## [/codeblock] @@ -652,12 +652,36 @@ func assert_error(current :Callable) -> GdUnitGodotErrorAssert: return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current) +## Explicitly fails the current test indicating that the feature is not yet implemented.[br] +## This function is useful during development when you want to write test cases before implementing the actual functionality.[br] +## It provides a clear indication that the test failure is expected because the feature is still under development.[br] +## Usage: +## [codeblock] +## # Test for a feature that will be implemented later +## func test_advanced_ai_behavior(): +## assert_not_yet_implemented() +## +## [/codeblock] func assert_not_yet_implemented() -> void: @warning_ignore("unsafe_method_access") __gdunit_assert().new(null).do_fail() -func fail(message :String) -> void: +## Explicitly fails the current test with a custom error message.[br] +## This function reports an error but does not terminate test execution automatically.[br] +## You must use 'return' after calling fail() to stop the test since GDScript has no exception support.[br] +## Useful for complex conditional testing scenarios where standard assertions are insufficient.[br] +## Usage: +## [codeblock] +## # Fail test when conditions are not met +## if !custom_check(player): +## fail("Player should be alive but has %d health" % player.health) +## return +## +## # Continue with test if conditions pass +## assert_that(player.health).is_greater(0) +## [/codeblock] +func fail(message: String) -> void: @warning_ignore("unsafe_method_access") __gdunit_assert().new(null).report_error(message) diff --git a/addons/gdUnit4/src/GdUnitTestSuite.gd.uid b/addons/gdUnit4/src/GdUnitTestSuite.gd.uid index 2c1baa1c..606f8262 100644 --- a/addons/gdUnit4/src/GdUnitTestSuite.gd.uid +++ b/addons/gdUnit4/src/GdUnitTestSuite.gd.uid @@ -1 +1 @@ -uid://cexcjumasxyv6 +uid://dj5hr5hq7neaj diff --git a/addons/gdUnit4/src/GdUnitTuple.gd.uid b/addons/gdUnit4/src/GdUnitTuple.gd.uid index 937ff62a..dd552f51 100644 --- a/addons/gdUnit4/src/GdUnitTuple.gd.uid +++ b/addons/gdUnit4/src/GdUnitTuple.gd.uid @@ -1 +1 @@ -uid://egwstyxfnubi +uid://lb4ker4h17gp diff --git a/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid b/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid index 50e737ce..b99be20f 100644 --- a/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid +++ b/addons/gdUnit4/src/GdUnitValueExtractor.gd.uid @@ -1 +1 @@ -uid://dc7o8eekqecc3 +uid://ggmsvu80wb74 diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd b/addons/gdUnit4/src/GdUnitVectorAssert.gd index 915fd3b6..c186cba2 100644 --- a/addons/gdUnit4/src/GdUnitVectorAssert.gd +++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd @@ -1,57 +1,55 @@ ## An Assertion Tool to verify Vector values -class_name GdUnitVectorAssert +@abstract class_name GdUnitVectorAssert extends GdUnitAssert -## Verifies that the current value is equal to expected one. -@warning_ignore("unused_parameter") -func is_equal(expected :Variant) -> GdUnitVectorAssert: - return self +## Verifies that the current value is null. +@abstract func is_null() -> GdUnitVectorAssert + + +## Verifies that the current value is not null. +@abstract func is_not_null() -> GdUnitVectorAssert + + +## Verifies that the current value is equal to the given one. +@abstract func is_equal(expected: Variant) -> GdUnitVectorAssert ## Verifies that the current value is not equal to expected one. -@warning_ignore("unused_parameter") -func is_not_equal(expected :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_not_equal(expected: Variant) -> GdUnitVectorAssert ## Verifies that the current and expected value are approximately equal. -@warning_ignore("unused_parameter", "shadowed_global_identifier") -func is_equal_approx(expected :Variant, approx :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_equal_approx(expected: Variant, approx: Variant) -> GdUnitVectorAssert + + +## Overrides the default failure message by given custom message. +@abstract func override_failure_message(message: String) -> GdUnitVectorAssert + + +## Appends a custom message to the failure message. +@abstract func append_failure_message(message: String) -> GdUnitVectorAssert ## Verifies that the current value is less than the given one. -@warning_ignore("unused_parameter") -func is_less(expected :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_less(expected: Variant) -> GdUnitVectorAssert ## Verifies that the current value is less than or equal the given one. -@warning_ignore("unused_parameter") -func is_less_equal(expected :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_less_equal(expected: Variant) -> GdUnitVectorAssert ## Verifies that the current value is greater than the given one. -@warning_ignore("unused_parameter") -func is_greater(expected :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_greater(expected: Variant) -> GdUnitVectorAssert ## Verifies that the current value is greater than or equal the given one. -@warning_ignore("unused_parameter") -func is_greater_equal(expected :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_greater_equal(expected: Variant) -> GdUnitVectorAssert ## Verifies that the current value is between the given boundaries (inclusive). -@warning_ignore("unused_parameter") -func is_between(from :Variant, to :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_between(from: Variant, to: Variant) -> GdUnitVectorAssert ## Verifies that the current value is not between the given boundaries (inclusive). -@warning_ignore("unused_parameter") -func is_not_between(from :Variant, to :Variant) -> GdUnitVectorAssert: - return self +@abstract func is_not_between(from: Variant, to: Variant) -> GdUnitVectorAssert diff --git a/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid b/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid index c5352407..a8d4bbd9 100644 --- a/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid +++ b/addons/gdUnit4/src/GdUnitVectorAssert.gd.uid @@ -1 +1 @@ -uid://bi7whv2uffxqp +uid://duucfgdngx3ww diff --git a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid index 99ad9094..1112c1ad 100644 --- a/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid +++ b/addons/gdUnit4/src/asserts/CallBackValueProvider.gd.uid @@ -1 +1 @@ -uid://dhr2doj0xcctg +uid://dmtjlpl4kdwoh diff --git a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid index 8408aadb..16aa6887 100644 --- a/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid +++ b/addons/gdUnit4/src/asserts/DefaultValueProvider.gd.uid @@ -1 +1 @@ -uid://dvl2mbijb28tv +uid://c44jw5aieq7fj diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd b/addons/gdUnit4/src/asserts/GdAssertMessages.gd index 2be8c117..abc135a1 100644 --- a/addons/gdUnit4/src/asserts/GdAssertMessages.gd +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd @@ -8,6 +8,19 @@ const SUB_COLOR := Color(1, 0, 0, .3) const ADD_COLOR := Color(0, 1, 0, .3) +# Dictionary of control characters and their readable representations +const CONTROL_CHARS = { + "\n": "", # Line Feed + "\r": "", # Carriage Return + "\t": "", # Tab + "\b": "", # Backspace + "\f": "", # Form Feed + "\v": "", # Vertical Tab + "\a": "", # Bell + "": "" # Escape +} + + static func format_dict(value :Variant) -> String: if not value is Dictionary: return str(value) @@ -45,17 +58,17 @@ static func input_event_as_text(event :InputEvent) -> String: return text -static func _colored_string_div(characters :String) -> String: - return colored_array_div(characters.to_utf8_buffer()) +static func _colored_string_div(characters: String) -> String: + return colored_array_div(characters.to_utf32_buffer().to_int32_array()) -static func colored_array_div(characters :PackedByteArray) -> String: +static func colored_array_div(characters: PackedInt32Array) -> String: if characters.is_empty(): return "" - var result := PackedByteArray() + var result := PackedInt32Array() var index := 0 - var missing_chars := PackedByteArray() - var additional_chars := PackedByteArray() + var missing_chars := PackedInt32Array() + var additional_chars := PackedInt32Array() while index < characters.size(): var character := characters[index] @@ -71,17 +84,17 @@ static func colored_array_div(characters :PackedByteArray) -> String: _: if not missing_chars.is_empty(): result.append_array(format_chars(missing_chars, SUB_COLOR)) - missing_chars = PackedByteArray() + missing_chars = PackedInt32Array() if not additional_chars.is_empty(): result.append_array(format_chars(additional_chars, ADD_COLOR)) - additional_chars = PackedByteArray() + additional_chars = PackedInt32Array() @warning_ignore("return_value_discarded") result.append(character) index += 1 result.append_array(format_chars(missing_chars, SUB_COLOR)) result.append_array(format_chars(additional_chars, ADD_COLOR)) - return result.get_string_from_utf8() + return result.to_byte_array().get_string_from_utf32() static func _typed_value(value :Variant) -> String: @@ -624,13 +637,35 @@ static func error_contains_exactly(current: Array, expected: Array) -> String: return "%s\n %s\n but was\n %s" % [_error("Expecting exactly equal:"), _colored_value(expected), _colored_value(current)] -static func format_chars(characters :PackedByteArray, type :Color) -> PackedByteArray: +static func format_chars(characters: PackedInt32Array, type: Color) -> PackedInt32Array: if characters.size() == 0:# or characters[0] == 10: return characters - var result := PackedByteArray() - var message := "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [ - type.to_html(), characters.get_string_from_utf8().replace("\n", "")] - result.append_array(message.to_utf8_buffer()) + + # Replace each control character with its readable form + var formatted_text := characters.to_byte_array().get_string_from_utf32() + for control_char: String in CONTROL_CHARS: + var replace_text: String = CONTROL_CHARS[control_char] + formatted_text = formatted_text.replace(control_char, replace_text) + + # Handle special ASCII control characters (0x00-0x1F, 0x7F) + var ascii_text := "" + for i in formatted_text.length(): + var character := formatted_text[i] + var code := character.unicode_at(0) + if code < 0x20 and not CONTROL_CHARS.has(character): # Control characters not handled above + ascii_text += "<0x%02X>" % code + elif code == 0x7F: # DEL character + ascii_text += "" + else: + ascii_text += character + + var message := "[bgcolor=#%s][color=white]%s[/color][/bgcolor]" % [ + type.to_html(), + ascii_text + ] + + var result := PackedInt32Array() + result.append_array(message.to_utf32_buffer().to_int32_array()) return result diff --git a/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid b/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid index d2d5cefd..6f42d4e0 100644 --- a/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid +++ b/addons/gdUnit4/src/asserts/GdAssertMessages.gd.uid @@ -1 +1 @@ -uid://sajh2u2n6n6f +uid://cx3stsdp081tn diff --git a/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid b/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid index 85d7b85f..785fa9cb 100644 --- a/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid +++ b/addons/gdUnit4/src/asserts/GdAssertReports.gd.uid @@ -1 +1 @@ -uid://gn113v5mri8f +uid://bwpe1kwgw4v4r diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd index 748e20e6..730b5da9 100644 --- a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd @@ -26,11 +26,13 @@ func _notification(event: int) -> void: func report_success() -> GdUnitArrayAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error: String) -> GdUnitArrayAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -40,11 +42,13 @@ func failure_message() -> String: func override_failure_message(message: String) -> GdUnitArrayAssert: + @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self func append_failure_message(message: String) -> GdUnitArrayAssert: + @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self @@ -109,76 +113,86 @@ func _array_div(compare_mode: GdObjects.COMPARE_MODE, left: Array[Variant], righ return [not_expect, not_found] -func _contains(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: - if not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func _contains(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: var by_reference := compare_mode == GdObjects.COMPARE_MODE.OBJECT_REFERENCE var current_value: Variant = get_current_value() + var expected_value: Variant = _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) + if current_value == null: - return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], expected, by_reference)) + return report_error(GdAssertMessages.error_arr_contains(current_value, expected_value, [], expected_value, by_reference)) @warning_ignore("unsafe_cast") - var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant]) + var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant]) #var not_expect := diffs[0] as Array var not_found: Array = diffs[1] if not not_found.is_empty(): - return report_error(GdAssertMessages.error_arr_contains(current_value, expected, [], not_found, by_reference)) + return report_error(GdAssertMessages.error_arr_contains(current_value, expected_value, [], not_found, by_reference)) return report_success() -func _contains_exactly(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: - if not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func _contains_exactly(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: var current_value: Variant = get_current_value() + var expected_value: Variant = _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) + if current_value == null: - return report_error(GdAssertMessages.error_arr_contains_exactly(null, expected, [], expected, compare_mode)) + return report_error(GdAssertMessages.error_arr_contains_exactly(null, expected_value, [], expected_value, compare_mode)) # has same content in same order - if _is_equal(current_value, expected, false, compare_mode): + if _is_equal(current_value, expected_value, false, compare_mode): return report_success() # check has same elements but in different order - if _is_equals_sorted(current_value, expected, false, compare_mode): - return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, [], [], compare_mode)) + if _is_equals_sorted(current_value, expected_value, false, compare_mode): + return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected_value, [], [], compare_mode)) # find the difference @warning_ignore("unsafe_cast") var diffs := _array_div(compare_mode, current_value as Array[Variant], - expected as Array[Variant], + expected_value as Array[Variant], GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) var not_expect: Array[Variant] = diffs[0] var not_found: Array[Variant] = diffs[1] - return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected, not_expect, not_found, compare_mode)) + return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected_value, not_expect, not_found, compare_mode)) -func _contains_exactly_in_any_order(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: - if not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func _contains_exactly_in_any_order(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: var current_value: Variant = get_current_value() + var expected_value: Variant = _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) + if current_value == null: - return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode)) + return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, [], + expected_value, compare_mode)) # find the difference @warning_ignore("unsafe_cast") - var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant], false) + var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant], false) var not_expect: Array[Variant] = diffs[0] var not_found: Array[Variant] = diffs[1] if not_expect.is_empty() and not_found.is_empty(): return report_success() - return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, not_expect, not_found, compare_mode)) + return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, not_expect, + not_found, compare_mode)) -func _not_contains(expected: Variant, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: - if not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func _not_contains(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert: var current_value: Variant = get_current_value() + var expected_value: Variant = _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) if current_value == null: - return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected, [], expected, compare_mode)) + return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, [], + expected_value, compare_mode)) @warning_ignore("unsafe_cast") - var diffs := _array_div(compare_mode, current_value as Array[Variant], expected as Array[Variant]) + var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant]) var found: Array[Variant] = diffs[0] @warning_ignore("unsafe_cast") if found.size() == (current_value as Array).size(): return report_success() @warning_ignore("unsafe_cast") - var diffs2 := _array_div(compare_mode, expected as Array[Variant], diffs[1] as Array[Variant]) - return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected, diffs2[0], compare_mode)) + var diffs2 := _array_div(compare_mode, expected_value as Array[Variant], diffs[1] as Array[Variant]) + return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected_value, diffs2[0], compare_mode)) func is_null() -> GdUnitArrayAssert: @@ -193,15 +207,16 @@ func is_not_null() -> GdUnitArrayAssert: return self -# Verifies that the current String is equal to the given one. -func is_equal(expected: Variant) -> GdUnitArrayAssert: - if _type_check and not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func is_equal(...expected: Array) -> GdUnitArrayAssert: var current_value: Variant = get_current_value() - if current_value == null and expected != null: - return report_error(GdAssertMessages.error_equal(null, expected)) - if not _is_equal(current_value, expected): - var diff := _array_equals_div(current_value, expected) + var expected_value: Variant= _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) + if current_value == null and expected_value != null: + return report_error(GdAssertMessages.error_equal(null, expected_value)) + + if not _is_equal(current_value, expected_value): + var diff := _array_equals_div(current_value, expected_value) var expected_as_list := GdArrayTools.as_string(diff[0], false) var current_as_list := GdArrayTools.as_string(diff[1], false) var index_report: Array = diff[2] @@ -210,16 +225,18 @@ func is_equal(expected: Variant) -> GdUnitArrayAssert: # Verifies that the current Array is equal to the given one, ignoring case considerations. -func is_equal_ignoring_case(expected: Variant) -> GdUnitArrayAssert: - if _type_check and not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert: var current_value: Variant = get_current_value() - if current_value == null and expected != null: + var expected_value: Variant = _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) + if current_value == null and expected_value != null: @warning_ignore("unsafe_cast") - return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected as Array))) - if not _is_equal(current_value, expected, true): + return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected_value))) + + if not _is_equal(current_value, expected_value, true): @warning_ignore("unsafe_cast") - var diff := _array_equals_div(current_value as Array[Variant], expected as Array[Variant], true) + var diff := _array_equals_div(current_value, expected_value, true) var expected_as_list := GdArrayTools.as_string(diff[0]) var current_as_list := GdArrayTools.as_string(diff[1]) var index_report: Array = diff[2] @@ -227,24 +244,28 @@ func is_equal_ignoring_case(expected: Variant) -> GdUnitArrayAssert: return report_success() -func is_not_equal(expected: Variant) -> GdUnitArrayAssert: - if not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func is_not_equal(...expected: Array) -> GdUnitArrayAssert: var current_value: Variant = get_current_value() - if _is_equal(current_value, expected): - return report_error(GdAssertMessages.error_not_equal(current_value, expected)) + var expected_value: Variant = _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) + + if _is_equal(current_value, expected_value): + return report_error(GdAssertMessages.error_not_equal(current_value, expected_value)) return report_success() -func is_not_equal_ignoring_case(expected: Variant) -> GdUnitArrayAssert: - if not _validate_value_type(expected): - return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected)) +func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert: var current_value: Variant = get_current_value() - if _is_equal(current_value, expected, true): + var expected_value: Variant = _extract_variadic_value(expected) + if not _validate_value_type(expected_value): + return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value)) + + if _is_equal(current_value, expected_value, true): @warning_ignore("unsafe_cast") var c := GdArrayTools.as_string(current_value as Array) @warning_ignore("unsafe_cast") - var e := GdArrayTools.as_string(expected as Array) + var e := GdArrayTools.as_string(expected_value) return report_error(GdAssertMessages.error_not_equal_case_insensetiv(c, e)) return report_success() @@ -294,35 +315,35 @@ func has_size(expected: int) -> GdUnitArrayAssert: return report_success() -func contains(expected: Variant) -> GdUnitArrayAssert: +func contains(...expected: Array) -> GdUnitArrayAssert: return _contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func contains_exactly(expected: Variant) -> GdUnitArrayAssert: +func contains_exactly(...expected: Array) -> GdUnitArrayAssert: return _contains_exactly(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func contains_exactly_in_any_order(expected: Variant) -> GdUnitArrayAssert: +func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert: return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func contains_same(expected: Variant) -> GdUnitArrayAssert: +func contains_same(...expected: Array) -> GdUnitArrayAssert: return _contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func contains_same_exactly(expected: Variant) -> GdUnitArrayAssert: +func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert: return _contains_exactly(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func contains_same_exactly_in_any_order(expected: Variant) -> GdUnitArrayAssert: +func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert: return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func not_contains(expected: Variant) -> GdUnitArrayAssert: +func not_contains(...expected: Array) -> GdUnitArrayAssert: return _not_contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func not_contains_same(expected: Variant) -> GdUnitArrayAssert: +func not_contains_same(...expected: Array) -> GdUnitArrayAssert: return _not_contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) @@ -332,9 +353,9 @@ func is_instanceof(expected: Variant) -> GdUnitAssert: return self -func extract(func_name: String, args := Array()) -> GdUnitArrayAssert: +func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert: var extracted_elements := Array() - + var args: Array = _extract_variadic_value(func_args) var extractor := GdUnitFuncValueExtractor.new(func_name, args) var current: Variant = get_current_value() if current == null: @@ -346,18 +367,7 @@ func extract(func_name: String, args := Array()) -> GdUnitArrayAssert: return self -func extractv( - extr0: GdUnitValueExtractor, - extr1: GdUnitValueExtractor = null, - extr2: GdUnitValueExtractor = null, - extr3: GdUnitValueExtractor = null, - extr4: GdUnitValueExtractor = null, - extr5: GdUnitValueExtractor = null, - extr6: GdUnitValueExtractor = null, - extr7: GdUnitValueExtractor = null, - extr8: GdUnitValueExtractor = null, - extr9: GdUnitValueExtractor = null) -> GdUnitArrayAssert: - var extractors: Variant = GdArrayTools.filter_value([extr0, extr1, extr2, extr3, extr4, extr5, extr6, extr7, extr8, extr9], null) +func extractv(...extractors: Array) -> GdUnitArrayAssert: var extracted_elements := Array() var current: Variant = get_current_value() if current == null: @@ -376,12 +386,11 @@ func extractv( GdUnitTuple.NO_ARG, GdUnitTuple.NO_ARG ] - @warning_ignore("unsafe_cast") - for index: int in (extractors as Array).size(): + + for index: int in extractors.size(): var extractor: GdUnitValueExtractor = extractors[index] ev[index] = extractor.extract_value(element) - @warning_ignore("unsafe_cast") - if (extractors as Array).size() > 1: + if extractors.size() > 1: extracted_elements.append(GdUnitTuple.new(ev[0], ev[1], ev[2], ev[3], ev[4], ev[5], ev[6], ev[7], ev[8], ev[9])) else: extracted_elements.append(ev[0]) @@ -389,6 +398,14 @@ func extractv( return self +## Small helper to support the old expected arguments as single array and variadic arguments +func _extract_variadic_value(values: Variant) -> Variant: + @warning_ignore("unsafe_method_access") + if values != null and values.size() == 1 and GdArrayTools.is_array_type(values[0]): + return values[0] + return values + + @warning_ignore("incompatible_ternary") func _is_equal( left: Variant, diff --git a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid index bb3534ed..7288be4b 100644 --- a/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd.uid @@ -1 +1 @@ -uid://b14kl614bjt7a +uid://74iuv3grl0lq diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd index c08bc132..9f578c4d 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd @@ -42,30 +42,16 @@ func do_fail() -> GdUnitAssert: return report_error(GdAssertMessages.error_not_implemented()) -func override_failure_message(message :String) -> GdUnitAssert: +func override_failure_message(message: String) -> GdUnitAssert: _custom_failure_message = message return self -func append_failure_message(message :String) -> GdUnitAssert: +func append_failure_message(message: String) -> GdUnitAssert: _additional_failure_message = message return self -func is_equal(expected :Variant) -> GdUnitAssert: - var current :Variant = current_value() - if not GdObjects.equals(current, expected): - return report_error(GdAssertMessages.error_equal(current, expected)) - return report_success() - - -func is_not_equal(expected :Variant) -> GdUnitAssert: - var current :Variant = current_value() - if GdObjects.equals(current, expected): - return report_error(GdAssertMessages.error_not_equal(current, expected)) - return report_success() - - func is_null() -> GdUnitAssert: var current :Variant = current_value() if current != null: @@ -78,3 +64,17 @@ func is_not_null() -> GdUnitAssert: if current == null: return report_error(GdAssertMessages.error_is_not_null()) return report_success() + + +func is_equal(expected: Variant) -> GdUnitAssert: + var current: Variant = current_value() + if not GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_equal(current, expected)) + return report_success() + + +func is_not_equal(expected: Variant) -> GdUnitAssert: + var current: Variant = current_value() + if GdObjects.equals(current, expected): + return report_error(GdAssertMessages.error_not_equal(current, expected)) + return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid index 2660822c..22ea58e1 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd.uid @@ -1 +1 @@ -uid://bsbji4rq6p3m5 +uid://cg3rnqubdphf diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd index 897d63f4..a5b53c17 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd @@ -7,6 +7,7 @@ extends RefCounted func _init() -> void: # preload all gdunit assertions to speedup testsuite loading time # gdlint:disable=private-method-call + @warning_ignore_start("return_value_discarded") GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd") GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd") GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd") @@ -22,6 +23,7 @@ func _init() -> void: GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd") GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd") GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd") + @warning_ignore_restore("return_value_discarded") ### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading" @@ -59,6 +61,7 @@ static func get_line_number() -> int: or source.ends_with("GdUnitTestSuite.gd") \ or source.ends_with("GdUnitSceneRunnerImpl.gd") \ or source.ends_with("GdUnitObjectInteractions.gd") \ + or source.ends_with("GdUnitObjectInteractionsVerifier.gd") \ or source.ends_with("GdUnitAwaiter.gd"): continue return stack_info.get("line") diff --git a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid index e7200d24..a09bd0e9 100644 --- a/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitAssertions.gd.uid @@ -1 +1 @@ -uid://dpy52fonltgls +uid://nr8ifpqtdi3t diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd index 5daebcec..2fc0ce43 100644 --- a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd @@ -24,11 +24,13 @@ func current_value() -> Variant: func report_success() -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitBoolAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -37,26 +39,24 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitBoolAssert: +func override_failure_message(message: String) -> GdUnitBoolAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitBoolAssert: +func append_failure_message(message: String) -> GdUnitBoolAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self -# Verifies that the current value is null. func is_null() -> GdUnitBoolAssert: @warning_ignore("return_value_discarded") _base.is_null() return self -# Verifies that the current value is not null. func is_not_null() -> GdUnitBoolAssert: @warning_ignore("return_value_discarded") _base.is_not_null() diff --git a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid index 7b6c8afb..f7ca0bd9 100644 --- a/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd.uid @@ -1 +1 @@ -uid://dedbtnuvvd78c +uid://gq2h57pfenn4 diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd index 17eba6ab..c57bbc23 100644 --- a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd @@ -20,11 +20,13 @@ func _notification(event :int) -> void: func report_success() -> GdUnitDictionaryAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitDictionaryAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -33,13 +35,13 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitDictionaryAssert: +func override_failure_message(message: String) -> GdUnitDictionaryAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitDictionaryAssert: +func append_failure_message(message: String) -> GdUnitDictionaryAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self @@ -61,21 +63,19 @@ func is_not_null() -> GdUnitDictionaryAssert: return self -func is_equal(expected :Variant) -> GdUnitDictionaryAssert: +func is_equal(expected: Variant) -> GdUnitDictionaryAssert: var current :Variant = current_value() if current == null: return report_error(GdAssertMessages.error_equal(null, GdAssertMessages.format_dict(expected))) if not GdObjects.equals(current, expected): var c := GdAssertMessages.format_dict(current) var e := GdAssertMessages.format_dict(expected) - var diff := GdDiffTool.string_diff(c, e) - var curent_diff := GdAssertMessages.colored_array_div(diff[1]) - return report_error(GdAssertMessages.error_equal(curent_diff, e)) + return report_error(GdAssertMessages.error_equal(c, e)) return report_success() -func is_not_equal(expected :Variant) -> GdUnitDictionaryAssert: - var current :Variant = current_value() +func is_not_equal(expected: Variant) -> GdUnitDictionaryAssert: + var current: Variant = current_value() if GdObjects.equals(current, expected): return report_error(GdAssertMessages.error_not_equal(current, expected)) return report_success() @@ -89,9 +89,7 @@ func is_same(expected :Variant) -> GdUnitDictionaryAssert: if not is_same(current, expected): var c := GdAssertMessages.format_dict(current) var e := GdAssertMessages.format_dict(expected) - var diff := GdDiffTool.string_diff(c, e) - var curent_diff := GdAssertMessages.colored_array_div(diff[1]) - return report_error(GdAssertMessages.error_is_same(curent_diff, e)) + return report_error(GdAssertMessages.error_is_same(c, e)) return report_success() @@ -129,16 +127,18 @@ func has_size(expected: int) -> GdUnitDictionaryAssert: return report_success() -func _contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert: +func _contains_keys(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert: var current :Variant = current_value() + var expected_value: Array = _extract_variadic_value(expected) if current == null: return report_error(GdAssertMessages.error_is_not_null()) # find expected keys @warning_ignore("unsafe_cast") - var keys_not_found :Array = expected.filter(_filter_by_key.bind((current as Dictionary).keys(), compare_mode)) + var keys_not_found :Array = expected_value.filter(_filter_by_key.bind((current as Dictionary).keys(), compare_mode)) if not keys_not_found.is_empty(): @warning_ignore("unsafe_cast") - return report_error(GdAssertMessages.error_contains_keys((current as Dictionary).keys() as Array, expected, keys_not_found, compare_mode)) + return report_error(GdAssertMessages.error_contains_keys((current as Dictionary).keys() as Array, expected_value, + keys_not_found, compare_mode)) return report_success() @@ -156,18 +156,19 @@ func _contains_key_value(key :Variant, value :Variant, compare_mode :GdObjects.C return report_success() -func _not_contains_keys(expected :Array, compare_mode :GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert: +func _not_contains_keys(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitDictionaryAssert: var current :Variant = current_value() + var expected_value: Array = _extract_variadic_value(expected) if current == null: return report_error(GdAssertMessages.error_is_not_null()) var dict_current: Dictionary = current - var keys_found :Array = dict_current.keys().filter(_filter_by_key.bind(expected, compare_mode, true)) + var keys_found :Array = dict_current.keys().filter(_filter_by_key.bind(expected_value, compare_mode, true)) if not keys_found.is_empty(): - return report_error(GdAssertMessages.error_not_contains_keys(dict_current.keys() as Array, expected, keys_found, compare_mode)) + return report_error(GdAssertMessages.error_not_contains_keys(dict_current.keys() as Array, expected_value, keys_found, compare_mode)) return report_success() -func contains_keys(expected :Array) -> GdUnitDictionaryAssert: +func contains_keys(...expected: Array) -> GdUnitDictionaryAssert: return _contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) @@ -175,7 +176,7 @@ func contains_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAssert: return _contains_key_value(key, value, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -func not_contains_keys(expected :Array) -> GdUnitDictionaryAssert: +func not_contains_keys(...expected: Array) -> GdUnitDictionaryAssert: return _not_contains_keys(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) @@ -187,7 +188,7 @@ func contains_same_key_value(key :Variant, value :Variant) -> GdUnitDictionaryAs return _contains_key_value(key, value, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) -func not_contains_same_keys(expected :Array) -> GdUnitDictionaryAssert: +func not_contains_same_keys(...expected: Array) -> GdUnitDictionaryAssert: return _not_contains_keys(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE) @@ -196,3 +197,11 @@ func _filter_by_key(element :Variant, values :Array, compare_mode :GdObjects.COM if GdObjects.equals(key, element, false, compare_mode): return is_not return !is_not + + +## Small helper to support the old expected arguments as single array and variadic arguments +func _extract_variadic_value(values: Variant) -> Variant: + @warning_ignore("unsafe_method_access") + if values != null and values.size() == 1 and GdArrayTools.is_array_type(values[0]): + return values[0] + return values diff --git a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid index 4cc67ddc..63c481ff 100644 --- a/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd.uid @@ -1 +1 @@ -uid://c752602u6l26t +uid://cuolaq6omu4fn diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd index 845d5fa9..198624c6 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd @@ -3,7 +3,10 @@ extends GdUnitFailureAssert const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") var _is_failed := false -var _failure_message :String +var _failure_message: String +var _current_failure_message := "" +var _custom_failure_message := "" +var _additional_failure_message := "" func _set_do_expect_fail(enabled :bool = true) -> void: @@ -44,12 +47,10 @@ func _on_test_failed(value :bool) -> void: _is_failed = value -@warning_ignore("unused_parameter") func is_equal(_expected: Variant) -> GdUnitFailureAssert: return _report_error("Not implemented") -@warning_ignore("unused_parameter") func is_not_equal(_expected: Variant) -> GdUnitFailureAssert: return _report_error("Not implemented") @@ -62,6 +63,16 @@ func is_not_null() -> GdUnitFailureAssert: return _report_error("Not implemented") +func override_failure_message(message: String) -> GdUnitFailureAssert: + _custom_failure_message = message + return self + + +func append_failure_message(message: String) -> GdUnitFailureAssert: + _additional_failure_message = message + return self + + func is_success() -> GdUnitFailureAssert: if _is_failed: return _report_error("Expect: assertion ends successfully.") @@ -115,7 +126,8 @@ func starts_with_message(expected :String) -> GdUnitFailureAssert: func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() - GdAssertReports.report_error(error_message, line_number) + _current_failure_message = GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(_current_failure_message, line_number) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid index 1b6c37d8..a138a2eb 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd.uid @@ -1 +1 @@ -uid://rvcc4827i7s4 +uid://0kcx8nknrvef diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd index f98bc933..c4f9570e 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd @@ -26,11 +26,13 @@ func current_value() -> String: func report_success() -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -39,25 +41,37 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitFileAssert: +func override_failure_message(message: String) -> GdUnitFileAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitFileAssert: +func append_failure_message(message: String) -> GdUnitFileAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self -func is_equal(expected :Variant) -> GdUnitFileAssert: +func is_null() -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.is_null() + return self + + +func is_not_null() -> GdUnitFileAssert: + @warning_ignore("return_value_discarded") + _base.is_not_null() + return self + + +func is_equal(expected: Variant) -> GdUnitFileAssert: @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :Variant) -> GdUnitFileAssert: +func is_not_equal(expected: Variant) -> GdUnitFileAssert: @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self @@ -97,5 +111,6 @@ func contains_exactly(expected_rows: Array) -> GdUnitFileAssert: if script is GDScript: var source_code := GdScriptParser.to_unix_format(script.source_code) var rows := Array(source_code.split("\n")) + @warning_ignore("return_value_discarded") GdUnitArrayAssertImpl.new(rows).contains_exactly(expected_rows) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid index c526e369..57a1a111 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd.uid @@ -1 +1 @@ -uid://blhgdgip64kgd +uid://bxqm5ruxgq6bo diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd index 05d05b84..83d7e05e 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd @@ -24,11 +24,13 @@ func current_value() -> Variant: func report_success() -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitFloatAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -37,13 +39,13 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitFloatAssert: +func override_failure_message(message: String) -> GdUnitFloatAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitFloatAssert: +func append_failure_message(message: String) -> GdUnitFloatAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self @@ -61,13 +63,13 @@ func is_not_null() -> GdUnitFloatAssert: return self -func is_equal(expected :Variant) -> GdUnitFloatAssert: +func is_equal(expected: Variant) -> GdUnitFloatAssert: @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :Variant) -> GdUnitFloatAssert: +func is_not_equal(expected: Variant) -> GdUnitFloatAssert: @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid index dce38311..b08c0317 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd.uid @@ -1 +1 @@ -uid://dgrojrsc8iqlc +uid://brpstspqw280k diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd index 270119a6..c38acf08 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd @@ -59,12 +59,12 @@ func failure_message() -> String: return _current_failure_message -func override_failure_message(message :String) -> GdUnitFuncAssert: +func override_failure_message(message: String) -> GdUnitFuncAssert: _custom_failure_message = message return self -func append_failure_message(message :String) -> GdUnitFuncAssert: +func append_failure_message(message: String) -> GdUnitFuncAssert: _additional_failure_message = message return self @@ -98,12 +98,12 @@ func is_true() -> GdUnitFuncAssert: return self -func is_equal(expected :Variant) -> GdUnitFuncAssert: +func is_equal(expected: Variant) -> GdUnitFuncAssert: await _validate_callback(cb_is_equal, expected) return self -func is_not_equal(expected :Variant) -> GdUnitFuncAssert: +func is_not_equal(expected: Variant) -> GdUnitFuncAssert: await _validate_callback(cb_is_not_equal, expected) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid index c93b9bbd..7897dd13 100644 --- a/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd.uid @@ -1 +1 @@ -uid://df4nvydivcokq +uid://3w1gnd5w3qgf diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd index adf2a2eb..fc010db3 100644 --- a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd @@ -1,13 +1,12 @@ extends GdUnitGodotErrorAssert -var _current_error_message :String -var _callable :Callable +var _current_failure_message := "" +var _custom_failure_message := "" +var _additional_failure_message := "" +var _callable: Callable -func _init(callable :Callable) -> void: - # we only support Godot 4.1.x+ because of await issue https://github.com/godotengine/godot/issues/80292 - assert(Engine.get_version_info().hex >= 0x40100, - "This assertion is not supported for Godot 4.0.x. Please upgrade to the minimum version Godot 4.1.0!") +func _init(callable: Callable) -> void: # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) GdAssertReports.reset_last_error_line_number() @@ -29,7 +28,7 @@ func _error_monitor() -> GodotGdErrorMonitor: func failure_message() -> String: - return _current_error_message + return _current_failure_message func _report_success() -> GdUnitAssert: @@ -37,23 +36,23 @@ func _report_success() -> GdUnitAssert: return self -func _report_error(error_message :String, failure_line_number: int = -1) -> GdUnitAssert: +func _report_error(error_message: String, failure_line_number: int = -1) -> GdUnitAssert: var line_number := failure_line_number if failure_line_number != -1 else GdUnitAssertions.get_line_number() - _current_error_message = error_message - GdAssertReports.report_error(error_message, line_number) + _current_failure_message = GdAssertMessages.build_failure_message(error_message, _additional_failure_message, _custom_failure_message) + GdAssertReports.report_error(_current_failure_message, line_number) return self -func _has_log_entry(log_entries :Array[ErrorLogEntry], type :ErrorLogEntry.TYPE, error :String) -> bool: +func _has_log_entry(log_entries: Array[ErrorLogEntry], type: ErrorLogEntry.TYPE, error: Variant) -> bool: for entry in log_entries: - if entry._type == type and entry._message == error: + if entry._type == type and GdObjects.equals(entry._message, error): # Erase the log entry we already handled it by this assertion, otherwise it will report at twice _error_monitor().erase_log_entry(entry) return true return false -func _to_list(log_entries :Array[ErrorLogEntry]) -> String: +func _to_list(log_entries: Array[ErrorLogEntry]) -> String: if log_entries.is_empty(): return "no errors" if log_entries.size() == 1: @@ -64,6 +63,32 @@ func _to_list(log_entries :Array[ErrorLogEntry]) -> String: return value +func is_null() -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func is_not_null() -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func is_equal(_expected: Variant) -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func is_not_equal(_expected: Variant) -> GdUnitGodotErrorAssert: + return _report_error("Not implemented") + + +func override_failure_message(message: String) -> GdUnitGodotErrorAssert: + _custom_failure_message = message + return self + + +func append_failure_message(message: String) -> GdUnitGodotErrorAssert: + _additional_failure_message = message + return self + + func is_success() -> GdUnitGodotErrorAssert: var log_entries := await _execute() if log_entries.is_empty(): @@ -74,7 +99,10 @@ func is_success() -> GdUnitGodotErrorAssert: """.dedent().trim_prefix("\n") % _to_list(log_entries)) -func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: +func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert: + var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_error) + if result.is_error(): + return _report_error(result.error_message()) var log_entries := await _execute() if _has_log_entry(log_entries, ErrorLogEntry.TYPE.SCRIPT_ERROR, expected_error): return _report_success() @@ -85,7 +113,10 @@ func is_runtime_error(expected_error :String) -> GdUnitGodotErrorAssert: """.dedent().trim_prefix("\n") % [expected_error, _to_list(log_entries)]) -func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: +func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert: + var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_warning) + if result.is_error(): + return _report_error(result.error_message()) var log_entries := await _execute() if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_WARNING, expected_warning): return _report_success() @@ -96,7 +127,10 @@ func is_push_warning(expected_warning :String) -> GdUnitGodotErrorAssert: """.dedent().trim_prefix("\n") % [expected_warning, _to_list(log_entries)]) -func is_push_error(expected_error :String) -> GdUnitGodotErrorAssert: +func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert: + var result := GdUnitArgumentMatchers.is_variant_string_matching(expected_error) + if result.is_error(): + return _report_error(result.error_message()) var log_entries := await _execute() if _has_log_entry(log_entries, ErrorLogEntry.TYPE.PUSH_ERROR, expected_error): return _report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid index ab3ddc1c..5e564f36 100644 --- a/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd.uid @@ -1 +1 @@ -uid://boa4g1suoabx4 +uid://cxftq2y1pj36g diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd index 1527cac5..bdee249e 100644 --- a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd @@ -24,11 +24,13 @@ func current_value() -> Variant: func report_success() -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitIntAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -37,13 +39,13 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitIntAssert: +func override_failure_message(message: String) -> GdUnitIntAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitIntAssert: +func append_failure_message(message: String) -> GdUnitIntAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self @@ -61,13 +63,13 @@ func is_not_null() -> GdUnitIntAssert: return self -func is_equal(expected :Variant) -> GdUnitIntAssert: +func is_equal(expected: Variant) -> GdUnitIntAssert: @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :Variant) -> GdUnitIntAssert: +func is_not_equal(expected: Variant) -> GdUnitIntAssert: @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid index 13e49a36..16b1cda3 100644 --- a/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd.uid @@ -1 +1 @@ -uid://dcxltyi6r7k33 +uid://bb3ltpyyxpu5c diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd index ce78a186..955e1ef3 100644 --- a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd @@ -3,7 +3,7 @@ extends GdUnitObjectAssert var _base: GdUnitAssertImpl -func _init(current :Variant) -> void: +func _init(current: Variant) -> void: _base = GdUnitAssertImpl.new(current) # save the actual assert instance on the current thread context GdUnitThreadManager.get_current_context().set_assert(self) @@ -16,7 +16,7 @@ func _init(current :Variant) -> void: report_error("GdUnitObjectAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current)) -func _notification(event :int) -> void: +func _notification(event: int) -> void: if event == NOTIFICATION_PREDELETE: if _base != null: _base.notification(event) @@ -28,11 +28,13 @@ func current_value() -> Variant: func report_success() -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self -func report_error(error :String) -> GdUnitObjectAssert: +func report_error(error: String) -> GdUnitObjectAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -41,25 +43,25 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitObjectAssert: +func override_failure_message(message: String) -> GdUnitObjectAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitObjectAssert: +func append_failure_message(message: String) -> GdUnitObjectAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self -func is_equal(expected :Variant) -> GdUnitObjectAssert: +func is_equal(expected: Variant) -> GdUnitObjectAssert: @warning_ignore("return_value_discarded") _base.is_equal(expected) return self -func is_not_equal(expected :Variant) -> GdUnitObjectAssert: +func is_not_equal(expected: Variant) -> GdUnitObjectAssert: @warning_ignore("return_value_discarded") _base.is_not_equal(expected) return self @@ -78,36 +80,87 @@ func is_not_null() -> GdUnitObjectAssert: @warning_ignore("shadowed_global_identifier") -func is_same(expected :Variant) -> GdUnitObjectAssert: - var current :Variant = current_value() +func is_same(expected: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() if not is_same(current, expected): return report_error(GdAssertMessages.error_is_same(current, expected)) return report_success() -func is_not_same(expected :Variant) -> GdUnitObjectAssert: - var current :Variant = current_value() +func is_not_same(expected: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() if is_same(current, expected): return report_error(GdAssertMessages.error_not_same(current, expected)) return report_success() -func is_instanceof(type :Object) -> GdUnitObjectAssert: - var current :Variant = current_value() +func is_instanceof(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() if current == null or not is_instance_of(current, type): - var result_expected: = GdObjects.extract_class_name(type) - var result_current: = GdObjects.extract_class_name(current) + var result_expected := GdObjects.extract_class_name(type) + var result_current := GdObjects.extract_class_name(current) return report_error(GdAssertMessages.error_is_instanceof(result_current, result_expected)) return report_success() -func is_not_instanceof(type :Variant) -> GdUnitObjectAssert: - var current :Variant = current_value() +func is_not_instanceof(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() if is_instance_of(current, type): - var result: = GdObjects.extract_class_name(type) + var result := GdObjects.extract_class_name(type) if result.is_success(): return report_error("Expected not be a instance of <%s>" % str(result.value())) push_error("Internal ERROR: %s" % result.error_message()) return self return report_success() + + +## Checks whether the current object inherits from the specified type. +func is_inheriting(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if not is_instance_of(current, TYPE_OBJECT): + return report_error("Expected '%s' to inherit from at least Object." % str(current)) + var result := _inherits(current, type) + if result.is_success(): + return report_success() + return report_error(result.error_message()) + + +## Checks whether the current object does NOT inherit from the specified type. +func is_not_inheriting(type: Variant) -> GdUnitObjectAssert: + var current: Variant = current_value() + if not is_instance_of(current, TYPE_OBJECT): + return report_error("Expected '%s' to inherit from at least Object." % str(current)) + var result := _inherits(current, type) + if result.is_success(): + return report_error("Expected type to not inherit from <%s>" % _extract_class_type(type)) + return report_success() + + +func _inherits(current: Variant, type: Variant) -> GdUnitResult: + var type_as_string := _extract_class_type(type) + if type_as_string == "Object": + return GdUnitResult.success("") + + var obj: Object = current + for p in obj.get_property_list(): + var clazz_name :String = p["name"] + if p["usage"] == PROPERTY_USAGE_CATEGORY and clazz_name == p["hint_string"] and clazz_name == type_as_string: + return GdUnitResult.success("") + var script: Script = obj.get_script() + if script != null: + while script != null: + var result := GdObjects.extract_class_name(script) + if result.is_success() and result.value() == type_as_string: + return GdUnitResult.success("") + script = script.get_base_script() + return GdUnitResult.error("Expected type to inherit from <%s>" % type_as_string) + + +func _extract_class_type(type: Variant) -> String: + if type is String: + return type + var result := GdObjects.extract_class_name(type) + if result.is_error(): + return "" + return result.value() diff --git a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid index 4234026e..28922d61 100644 --- a/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd.uid @@ -1 +1 @@ -uid://smsoqmfkns0j +uid://bio2eeyfg1kbf diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd index 8b6c7f26..98a6768f 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd @@ -28,11 +28,13 @@ func current_value() -> GdUnitResult: func report_success() -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitResultAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -41,13 +43,13 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitResultAssert: +func override_failure_message(message: String) -> GdUnitResultAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitResultAssert: +func append_failure_message(message: String) -> GdUnitResultAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self @@ -65,6 +67,18 @@ func is_not_null() -> GdUnitResultAssert: return self +func is_equal(expected: Variant) -> GdUnitResultAssert: + return is_value(expected) + + +func is_not_equal(expected: Variant) -> GdUnitResultAssert: + var result := current_value() + var value :Variant = null if result == null else result.value() + if GdObjects.equals(value, expected): + return report_error(GdAssertMessages.error_not_equal(value, expected)) + return report_success() + + func is_empty() -> GdUnitResultAssert: var result := current_value() if result == null or not result.is_empty(): @@ -106,13 +120,9 @@ func contains_message(expected :String) -> GdUnitResultAssert: return report_success() -func is_value(expected :Variant) -> GdUnitResultAssert: +func is_value(expected: Variant) -> GdUnitResultAssert: var result := current_value() var value :Variant = null if result == null else result.value() if not GdObjects.equals(value, expected): return report_error(GdAssertMessages.error_result_is_value(value, expected)) return report_success() - - -func is_equal(expected :Variant) -> GdUnitResultAssert: - return is_value(expected) diff --git a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid index a4bcaebe..ae3cfeb9 100644 --- a/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd.uid @@ -1 +1 @@ -uid://wkkx6a5226vd +uid://bvyreybo4m2n1 diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd index 7d4f57e4..6f5878c8 100644 --- a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd @@ -50,12 +50,12 @@ func failure_message() -> String: return _current_failure_message -func override_failure_message(message :String) -> GdUnitSignalAssert: +func override_failure_message(message: String) -> GdUnitSignalAssert: _custom_failure_message = message return self -func append_failure_message(message :String) -> GdUnitSignalAssert: +func append_failure_message(message: String) -> GdUnitSignalAssert: _additional_failure_message = message return self @@ -70,6 +70,26 @@ func wait_until(timeout := 2000) -> GdUnitSignalAssert: return self +func is_null() -> GdUnitSignalAssert: + if _emitter != null: + return report_error(GdAssertMessages.error_is_null(_emitter)) + return report_success() + + +func is_not_null() -> GdUnitSignalAssert: + if _emitter == null: + return report_error(GdAssertMessages.error_is_not_null()) + return report_success() + + +func is_equal(_expected: Variant) -> GdUnitSignalAssert: + return report_error("Not implemented") + + +func is_not_equal(_expected: Variant) -> GdUnitSignalAssert: + return report_error("Not implemented") + + # Verifies the signal exists checked the emitter func is_signal_exists(signal_name :String) -> GdUnitSignalAssert: if not _emitter.has_signal(signal_name): diff --git a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid index 61c14165..1278f73a 100644 --- a/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd.uid @@ -1 +1 @@ -uid://latvyovyfj4u +uid://brd46i1fx41k8 diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd index 0f15956c..cb49c9ce 100644 --- a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd @@ -28,22 +28,24 @@ func current_value() -> Variant: func report_success() -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitStringAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self -func override_failure_message(message :String) -> GdUnitStringAssert: +func override_failure_message(message: String) -> GdUnitStringAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self -func append_failure_message(message :String) -> GdUnitStringAssert: +func append_failure_message(message: String) -> GdUnitStringAssert: @warning_ignore("return_value_discarded") _base.append_failure_message(message) return self @@ -61,8 +63,8 @@ func is_not_null() -> GdUnitStringAssert: return self -func is_equal(expected :Variant) -> GdUnitStringAssert: - var current :Variant = current_value() +func is_equal(expected: Variant) -> GdUnitStringAssert: + var current: Variant = current_value() if current == null: return report_error(GdAssertMessages.error_equal(current, expected)) if not GdObjects.equals(current, expected): @@ -83,8 +85,8 @@ func is_equal_ignoring_case(expected :Variant) -> GdUnitStringAssert: return report_success() -func is_not_equal(expected :Variant) -> GdUnitStringAssert: - var current :Variant = current_value() +func is_not_equal(expected: Variant) -> GdUnitStringAssert: + var current: Variant = current_value() if GdObjects.equals(current, expected): return report_error(GdAssertMessages.error_not_equal(current, expected)) return report_success() diff --git a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid index 26ee7021..655803b2 100644 --- a/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd.uid @@ -1 +1 @@ -uid://ck80o6kb6xji5 +uid://cggae0ath5sfg diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd index 7b10d6bb..fbc031a4 100644 --- a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd @@ -50,11 +50,13 @@ func current_value() -> Variant: func report_success() -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") _base.report_success() return self func report_error(error :String) -> GdUnitVectorAssert: + @warning_ignore("return_value_discarded") _base.report_error(error) return self @@ -63,7 +65,7 @@ func failure_message() -> String: return _base.failure_message() -func override_failure_message(message :String) -> GdUnitVectorAssert: +func override_failure_message(message: String) -> GdUnitVectorAssert: @warning_ignore("return_value_discarded") _base.override_failure_message(message) return self diff --git a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid index 517e3de1..64d204e2 100644 --- a/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid +++ b/addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd.uid @@ -1 +1 @@ -uid://dc6idwfgn555 +uid://dxt2spula6ex6 diff --git a/addons/gdUnit4/src/asserts/ValueProvider.gd.uid b/addons/gdUnit4/src/asserts/ValueProvider.gd.uid index f566c29d..7afb5b4f 100644 --- a/addons/gdUnit4/src/asserts/ValueProvider.gd.uid +++ b/addons/gdUnit4/src/asserts/ValueProvider.gd.uid @@ -1 +1 @@ -uid://0np0my0dchjg +uid://gn3hsk23b2je diff --git a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid index d9bb6055..3d0949d8 100644 --- a/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdArgumentParser.gd.uid @@ -1 +1 @@ -uid://tvrj3s6ytpwr +uid://bvgpt6ubyrn5l diff --git a/addons/gdUnit4/src/cmd/CmdCommand.gd.uid b/addons/gdUnit4/src/cmd/CmdCommand.gd.uid index b6d08bc3..20769616 100644 --- a/addons/gdUnit4/src/cmd/CmdCommand.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdCommand.gd.uid @@ -1 +1 @@ -uid://blj1mjterk4kp +uid://cywq2cxy4lo1k diff --git a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid index a1ba0c76..b27ec21f 100644 --- a/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdCommandHandler.gd.uid @@ -1 +1 @@ -uid://2xibp622j8bv +uid://dijlao2grvq6i diff --git a/addons/gdUnit4/src/cmd/CmdConsole.gd b/addons/gdUnit4/src/cmd/CmdConsole.gd deleted file mode 100644 index a10c73e7..00000000 --- a/addons/gdUnit4/src/cmd/CmdConsole.gd +++ /dev/null @@ -1,147 +0,0 @@ -# prototype of console with CSI support -# https://notes.burke.libbey.me/ansi-escape-codes/ -class_name CmdConsole -extends RefCounted - -enum { - COLOR_TABLE, - COLOR_RGB -} - -const BOLD = 0x1 -const ITALIC = 0x2 -const UNDERLINE = 0x4 - -const CSI_BOLD = "" -const CSI_ITALIC = "" -const CSI_UNDERLINE = "" - -# Control Sequence Introducer -var _debug_show_color_codes := false -var _color_mode := COLOR_TABLE - - -func color(p_color :Color) -> CmdConsole: - # using color table 16 - 231 a 6 x 6 x 6 RGB color cube (16 + R * 36 + G * 6 + B) - #if _color_mode == COLOR_TABLE: - # @warning_ignore("integer_division") - # var c2 := 16 + (int(p_color.r8/42) * 36) + (int(p_color.g8/42) * 6) + int(p_color.b8/42) - # if _debug_show_color_codes: - # printraw("%6d" % [c2]) - # printraw("[38;5;%dm" % c2 ) - #else: - printraw("[38;2;%d;%d;%dm" % [p_color.r8, p_color.g8, p_color.b8] ) - return self - - -func save_cursor() -> CmdConsole: - printraw("") - return self - - -func restore_cursor() -> CmdConsole: - printraw("") - return self - - -func end_color() -> CmdConsole: - printraw("") - return self - - -func row_pos(row :int) -> CmdConsole: - printraw("[%d;0H" % row ) - return self - - -func scroll_area(from :int, to :int) -> CmdConsole: - printraw("[%d;%dr" % [from ,to]) - return self - - -@warning_ignore("return_value_discarded") -func progress_bar(p_progress :int, p_color :Color = Color.POWDER_BLUE) -> CmdConsole: - if p_progress < 0: - p_progress = 0 - if p_progress > 100: - p_progress = 100 - color(p_color) - printraw("[%-50s] %-3d%%\r" % ["".lpad(int(p_progress/2.0), "■").rpad(50, "-"), p_progress]) - end_color() - return self - - -func printl(value :String) -> CmdConsole: - printraw(value) - return self - - -func new_line() -> CmdConsole: - prints() - return self - - -func reset() -> CmdConsole: - return self - - -func bold(enable :bool) -> CmdConsole: - if enable: - printraw(CSI_BOLD) - return self - - -func italic(enable :bool) -> CmdConsole: - if enable: - printraw(CSI_ITALIC) - return self - - -func underline(enable :bool) -> CmdConsole: - if enable: - printraw(CSI_UNDERLINE) - return self - - -func prints_error(message :String) -> CmdConsole: - return color(Color.CRIMSON).printl(message).end_color().new_line() - - -func prints_warning(message :String) -> CmdConsole: - return color(Color.GOLDENROD).printl(message).end_color().new_line() - - -func prints_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole: - return print_color(p_message, p_color, p_flags).new_line() - - -func print_color(p_message :String, p_color :Color, p_flags := 0) -> CmdConsole: - return color(p_color)\ - .bold(p_flags&BOLD == BOLD)\ - .italic(p_flags&ITALIC == ITALIC)\ - .underline(p_flags&UNDERLINE == UNDERLINE)\ - .printl(p_message)\ - .end_color() - - -@warning_ignore("return_value_discarded") -func print_color_table() -> void: - prints_color("Color Table 6x6x6", Color.ANTIQUE_WHITE) - _debug_show_color_codes = true - for green in range(0, 6): - for red in range(0, 6): - for blue in range(0, 6): - print_color("████████ ", Color8(red*42, green*42, blue*42)) - new_line() - new_line() - - prints_color("Color Table RGB", Color.ANTIQUE_WHITE) - _color_mode = COLOR_RGB - for green in range(0, 6): - for red in range(0, 6): - for blue in range(0, 6): - print_color("████████ ", Color8(red*42, green*42, blue*42)) - new_line() - new_line() - _color_mode = COLOR_TABLE - _debug_show_color_codes = false diff --git a/addons/gdUnit4/src/cmd/CmdConsole.gd.uid b/addons/gdUnit4/src/cmd/CmdConsole.gd.uid deleted file mode 100644 index 04de1518..00000000 --- a/addons/gdUnit4/src/cmd/CmdConsole.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://de7ibpr0q5obs diff --git a/addons/gdUnit4/src/cmd/CmdOption.gd.uid b/addons/gdUnit4/src/cmd/CmdOption.gd.uid index 17759e2c..e56ea52f 100644 --- a/addons/gdUnit4/src/cmd/CmdOption.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdOption.gd.uid @@ -1 +1 @@ -uid://wetsko8deqmc +uid://b5ks5cvvfiq1y diff --git a/addons/gdUnit4/src/cmd/CmdOptions.gd.uid b/addons/gdUnit4/src/cmd/CmdOptions.gd.uid index f1543c76..c59a8ed7 100644 --- a/addons/gdUnit4/src/cmd/CmdOptions.gd.uid +++ b/addons/gdUnit4/src/cmd/CmdOptions.gd.uid @@ -1 +1 @@ -uid://sdvb0h18mgo +uid://dfjkvc6bhnvts diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd b/addons/gdUnit4/src/core/GdArrayTools.gd index d4c7e8f2..74f0e175 100644 --- a/addons/gdUnit4/src/core/GdArrayTools.gd +++ b/addons/gdUnit4/src/core/GdArrayTools.gd @@ -14,11 +14,12 @@ const ARRAY_TYPES := [ TYPE_PACKED_STRING_ARRAY, TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, + TYPE_PACKED_VECTOR4_ARRAY, TYPE_PACKED_COLOR_ARRAY ] -static func is_array_type(value :Variant) -> bool: +static func is_array_type(value: Variant) -> bool: return is_type_array(typeof(value)) @@ -28,18 +29,35 @@ static func is_type_array(type :int) -> bool: ## Filters an array by given value[br] ## If the given value not an array it returns null, will remove all occurence of given value. -@warning_ignore("unsafe_method_access") static func filter_value(array: Variant, value: Variant) -> Variant: if not is_array_type(array): return null + + @warning_ignore("unsafe_method_access") var filtered_array: Variant = array.duplicate() - var index :int = filtered_array.find(value) + @warning_ignore("unsafe_method_access") + var index: int = filtered_array.find(value) while index != -1: + @warning_ignore("unsafe_method_access") filtered_array.remove_at(index) + @warning_ignore("unsafe_method_access") index = filtered_array.find(value) return filtered_array +## Groups an array by a custom key selector +## The function should take an item and return the group key +static func group_by(array: Array, key_selector: Callable) -> Dictionary: + var result := {} + + for item: Variant in array: + var group_key: Variant = key_selector.call(item) + var values: Array = result.get_or_add(group_key, []) + values.append(item) + + return result + + ## Erases a value from given array by using equals(l,r) to find the element to erase static func erase_value(array :Array, value :Variant) -> void: for element :Variant in array: diff --git a/addons/gdUnit4/src/core/GdArrayTools.gd.uid b/addons/gdUnit4/src/core/GdArrayTools.gd.uid index 35186abc..2d35f9ef 100644 --- a/addons/gdUnit4/src/core/GdArrayTools.gd.uid +++ b/addons/gdUnit4/src/core/GdArrayTools.gd.uid @@ -1 +1 @@ -uid://bvw165txfy1qp +uid://coqvut1bjsit5 diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd b/addons/gdUnit4/src/core/GdDiffTool.gd index 16e567b0..5131df71 100644 --- a/addons/gdUnit4/src/core/GdDiffTool.gd +++ b/addons/gdUnit4/src/core/GdDiffTool.gd @@ -1,4 +1,5 @@ -# A tool to find differences between two objects +# Myers' Diff Algorithm implementation +# Based on "An O(ND) Difference Algorithm and Its Variations" by Eugene W. Myers class_name GdDiffTool extends RefCounted @@ -7,79 +8,144 @@ const DIV_ADD :int = 214 const DIV_SUB :int = 215 -static func _diff(lb: PackedByteArray, rb: PackedByteArray, lookup: Array[Array], ldiff: Array, rdiff: Array) -> void: - var loffset := lb.size() - var roffset := rb.size() - - while true: - #if last character of X and Y matches - if loffset > 0 && roffset > 0 && lb[loffset - 1] == rb[roffset - 1]: - loffset -= 1 - roffset -= 1 - ldiff.push_front(lb[loffset]) - rdiff.push_front(rb[roffset]) - continue - #current character of Y is not present in X - else: if (roffset > 0 && (loffset == 0 || lookup[loffset][roffset - 1] >= lookup[loffset - 1][roffset])): - roffset -= 1 - ldiff.push_front(rb[roffset]) - ldiff.push_front(DIV_ADD) - rdiff.push_front(rb[roffset]) - rdiff.push_front(DIV_SUB) - continue - #current character of X is not present in Y - else: if (loffset > 0 && (roffset == 0 || lookup[loffset][roffset - 1] < lookup[loffset - 1][roffset])): - loffset -= 1 - ldiff.push_front(lb[loffset]) - ldiff.push_front(DIV_SUB) - rdiff.push_front(lb[loffset]) - rdiff.push_front(DIV_ADD) - continue - break - - -# lookup[i][j] stores the length of LCS of substring X[0..i-1], Y[0..j-1] -static func _createLookUp(lb: PackedByteArray, rb: PackedByteArray) -> Array[Array]: - var lookup: Array[Array] = [] - @warning_ignore("return_value_discarded") - lookup.resize(lb.size() + 1) - for i in lookup.size(): - var x := [] - @warning_ignore("return_value_discarded") - x.resize(rb.size() + 1) - lookup[i] = x - return lookup - - -static func _buildLookup(lb: PackedByteArray, rb: PackedByteArray) -> Array[Array]: - var lookup := _createLookUp(lb, rb) - # first column of the lookup table will be all 0 - for i in lookup.size(): - lookup[i][0] = 0 - # first row of the lookup table will be all 0 - for j :int in lookup[0].size(): - lookup[0][j] = 0 - - # fill the lookup table in bottom-up manner - for i in range(1, lookup.size()): - for j in range(1, lookup[0].size()): - # if current character of left and right matches - if lb[i - 1] == rb[j - 1]: - lookup[i][j] = lookup[i - 1][j - 1] + 1; - # else if current character of left and right don't match +class Edit: + enum Type { EQUAL, INSERT, DELETE } + var type: Type + var character: int + + func _init(t: Type, chr: int) -> void: + type = t + character = chr + + +# Main entry point - returns [ldiff, rdiff] +static func string_diff(left: Variant, right: Variant) -> Array[PackedInt32Array]: + var lb := PackedInt32Array() if left == null else str(left).to_utf32_buffer().to_int32_array() + var rb := PackedInt32Array() if right == null else str(right).to_utf32_buffer().to_int32_array() + + # Early exit for identical strings + if lb == rb: + return [lb.duplicate(), rb.duplicate()] + + var edits := _myers_diff(lb, rb) + return _edits_to_diff_format(edits) + + +# Core Myers' algorithm +static func _myers_diff(a: PackedInt32Array, b: PackedInt32Array) -> Array[Edit]: + var n := a.size() + var m := b.size() + var max_d := n + m + + # V array stores the furthest reaching x coordinate for each k-line + # We need indices from -max_d to max_d, so we offset by max_d + var v := PackedInt32Array() + v.resize(2 * max_d + 1) + v.fill(-1) + v[max_d + 1] = 0 # k=1 starts at x=0 + + var trace := [] # Store V arrays for each d to backtrack later + + # Find the edit distance + for d in range(0, max_d + 1): + # Store current V for backtracking + trace.append(v.duplicate()) + + for k in range(-d, d + 1, 2): + var k_offset := k + max_d + + # Decide whether to move down or right + var x: int + if k == -d or (k != d and v[k_offset - 1] < v[k_offset + 1]): + x = v[k_offset + 1] # Move down (insert from b) + else: + x = v[k_offset - 1] + 1 # Move right (delete from a) + + var y := x - k + + # Follow diagonal as far as possible (matching characters) + while x < n and y < m and a[x] == b[y]: + x += 1 + y += 1 + + v[k_offset] = x + + # Check if we've reached the end + if x >= n and y >= m: + return _backtrack(a, b, trace, d, max_d) + + # Should never reach here for valid inputs + return [] + + +# Backtrack through the edit graph to build the edit script +static func _backtrack(a: PackedInt32Array, b: PackedInt32Array, trace: Array, d: int, max_d: int) -> Array[Edit]: + var edits: Array[Edit] = [] + var x := a.size() + var y := b.size() + + # Walk backwards through each d value + for depth in range(d, -1, -1): + var v: PackedInt32Array = trace[depth] + var k := x - y + var k_offset := k + max_d + + # Determine previous k + var prev_k: int + if k == -depth or (k != depth and v[k_offset - 1] < v[k_offset + 1]): + prev_k = k + 1 + else: + prev_k = k - 1 + + var prev_k_offset := prev_k + max_d + var prev_x := v[prev_k_offset] + var prev_y := prev_x - prev_k + + # Extract diagonal (equal) characters + while x > prev_x and y > prev_y: + x -= 1 + y -= 1 + #var char_array := PackedInt32Array([a[x]]) + edits.insert(0, Edit.new(Edit.Type.EQUAL, a[x])) + + # Record the edit operation + if depth > 0: + if x == prev_x: + # Insert from b + y -= 1 + #var char_array := PackedInt32Array([b[y]]) + edits.insert(0, Edit.new(Edit.Type.INSERT, b[y])) else: - lookup[i][j] = max(lookup[i - 1][j], lookup[i][j - 1]); - return lookup - - -static func string_diff(left :Variant, right :Variant) -> Array[PackedByteArray]: - var lb := PackedByteArray() if left == null else str(left).to_utf8_buffer() - var rb := PackedByteArray() if right == null else str(right).to_utf8_buffer() - var ldiff := Array() - var rdiff := Array() - var lookup := _buildLookup(lb, rb); - _diff(lb, rb, lookup, ldiff, rdiff) - return [PackedByteArray(ldiff), PackedByteArray(rdiff)] + # Delete from a + x -= 1 + #var char_array := PackedInt32Array([a[x]]) + edits.insert(0, Edit.new(Edit.Type.DELETE, a[x])) + + return edits + + +# Convert edit script to the DIV_ADD/DIV_SUB format +static func _edits_to_diff_format(edits: Array[Edit]) -> Array[PackedInt32Array]: + var ldiff := PackedInt32Array() + var rdiff := PackedInt32Array() + + for edit in edits: + match edit.type: + Edit.Type.EQUAL: + ldiff.append(edit.character) + rdiff.append(edit.character) + Edit.Type.INSERT: + ldiff.append(DIV_ADD) + ldiff.append(edit.character) + rdiff.append(DIV_SUB) + rdiff.append(edit.character) + Edit.Type.DELETE: + ldiff.append(DIV_SUB) + ldiff.append(edit.character) + rdiff.append(DIV_ADD) + rdiff.append(edit.character) + + return [ldiff, rdiff] # prototype diff --git a/addons/gdUnit4/src/core/GdDiffTool.gd.uid b/addons/gdUnit4/src/core/GdDiffTool.gd.uid index 75356854..ff316b72 100644 --- a/addons/gdUnit4/src/core/GdDiffTool.gd.uid +++ b/addons/gdUnit4/src/core/GdDiffTool.gd.uid @@ -1 +1 @@ -uid://4l3wbqoppfxx +uid://rnet4m70ynnu diff --git a/addons/gdUnit4/src/core/GdFunctionDoubler.gd b/addons/gdUnit4/src/core/GdFunctionDoubler.gd deleted file mode 100644 index 99a522f2..00000000 --- a/addons/gdUnit4/src/core/GdFunctionDoubler.gd +++ /dev/null @@ -1,219 +0,0 @@ -class_name GdFunctionDoubler -extends RefCounted - -const DEFAULT_TYPED_RETURN_VALUES := { - TYPE_NIL: "null", - TYPE_BOOL: "false", - TYPE_INT: "0", - TYPE_FLOAT: "0.0", - TYPE_STRING: "\"\"", - TYPE_STRING_NAME: "&\"\"", - TYPE_VECTOR2: "Vector2.ZERO", - TYPE_VECTOR2I: "Vector2i.ZERO", - TYPE_RECT2: "Rect2()", - TYPE_RECT2I: "Rect2i()", - TYPE_VECTOR3: "Vector3.ZERO", - TYPE_VECTOR3I: "Vector3i.ZERO", - TYPE_VECTOR4: "Vector4.ZERO", - TYPE_VECTOR4I: "Vector4i.ZERO", - TYPE_TRANSFORM2D: "Transform2D()", - TYPE_PLANE: "Plane()", - TYPE_QUATERNION: "Quaternion()", - TYPE_AABB: "AABB()", - TYPE_BASIS: "Basis()", - TYPE_TRANSFORM3D: "Transform3D()", - TYPE_PROJECTION: "Projection()", - TYPE_COLOR: "Color()", - TYPE_NODE_PATH: "NodePath()", - TYPE_RID: "RID()", - TYPE_OBJECT: "null", - TYPE_CALLABLE: "Callable()", - TYPE_SIGNAL: "Signal()", - TYPE_DICTIONARY: "Dictionary()", - TYPE_ARRAY: "Array()", - TYPE_PACKED_BYTE_ARRAY: "PackedByteArray()", - TYPE_PACKED_INT32_ARRAY: "PackedInt32Array()", - TYPE_PACKED_INT64_ARRAY: "PackedInt64Array()", - TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array()", - TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array()", - TYPE_PACKED_STRING_ARRAY: "PackedStringArray()", - TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()", - TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()", - # since Godot 4.3.beta1 TYPE_PACKED_VECTOR4_ARRAY = 38 - GdObjects.TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array()", - TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()", - GdObjects.TYPE_VARIANT: "null", - GdObjects.TYPE_ENUM: "0" -} - -# @GlobalScript enums -# needs to manually map because of https://github.com/godotengine/godot/issues/73835 -const DEFAULT_ENUM_RETURN_VALUES = { - "Side" : "SIDE_LEFT", - "Corner" : "CORNER_TOP_LEFT", - "Orientation" : "HORIZONTAL", - "ClockDirection" : "CLOCKWISE", - "HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT", - "VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP", - "InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO", - "EulerOrder" : "EULER_ORDER_XYZ", - "Key" : "KEY_NONE", - "KeyModifierMask" : "KEY_CODE_MASK", - "MouseButton" : "MOUSE_BUTTON_NONE", - "MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT", - "JoyButton" : "JOY_BUTTON_INVALID", - "JoyAxis" : "JOY_AXIS_INVALID", - "MIDIMessage" : "MIDI_MESSAGE_NONE", - "Error" : "OK", - "PropertyHint" : "PROPERTY_HINT_NONE", - "Variant.Type" : "TYPE_NIL", -} - -var _push_errors :String - - -# Determine the enum default by reflection -static func get_enum_default(value :String) -> Variant: - var script := GDScript.new() - script.source_code = """ - extends Resource - - static func get_enum_default() -> Variant: - return %s.values()[0] - - """.dedent() % value - @warning_ignore("return_value_discarded") - script.reload() - @warning_ignore("unsafe_method_access") - return script.new().call("get_enum_default") - - -static func default_return_value(func_descriptor :GdFunctionDescriptor) -> String: - var return_type :Variant = func_descriptor.return_type() - if return_type == GdObjects.TYPE_ENUM: - var enum_class := func_descriptor._return_class - var enum_path := enum_class.split(".") - if enum_path.size() >= 2: - var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1]) - if not keys.is_empty(): - return "%s.%s" % [enum_path[0], keys[0]] - var enum_value :Variant = get_enum_default(enum_class) - if enum_value != null: - return str(enum_value) - # we need fallback for @GlobalScript enums, - return DEFAULT_ENUM_RETURN_VALUES.get(func_descriptor._return_class, "0") - return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid") - - -func _init(push_errors :bool = false) -> void: - _push_errors = "true" if push_errors else "false" - for type_key in TYPE_MAX: - if not DEFAULT_TYPED_RETURN_VALUES.has(type_key): - push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX]) - prints("missing default definition for type", type_key) - assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!") - - -@warning_ignore("unused_parameter") -func get_template(return_type: GdFunctionDescriptor, is_callable: bool) -> String: - assert(false, "'get_template' must be implemented!") - return "" - - -func double(func_descriptor: GdFunctionDescriptor, is_callable: bool = false) -> PackedStringArray: - var is_static := func_descriptor.is_static() - var is_coroutine := func_descriptor.is_coroutine() - var func_name := func_descriptor.name() - var args := func_descriptor.args() - var varargs := func_descriptor.varargs() - var return_value := GdFunctionDoubler.default_return_value(func_descriptor) - var arg_names := extract_arg_names(args, true) - var vararg_names := extract_arg_names(varargs) - - # save original constructor arguments - if func_name == "_init": - var constructor_args := ",".join(GdFunctionDoubler.extract_constructor_args(args)) - var constructor := "func _init(%s) -> void:\n super(%s)\n pass\n" % [constructor_args, ", ".join(arg_names)] - return constructor.split("\n") - - var double_src := "@warning_ignore('shadowed_variable', 'untyped_declaration', 'unsafe_call_argument', 'unsafe_method_access')\n" - if func_descriptor.is_engine(): - double_src += '@warning_ignore("native_method_override")\n' - if func_descriptor.return_type() == GdObjects.TYPE_ENUM: - double_src += '@warning_ignore("int_as_enum_without_match")\n' - double_src += '@warning_ignore("int_as_enum_without_cast")\n' - double_src += GdFunctionDoubler.extract_func_signature(func_descriptor) - # fix to unix format, this is need when the template is edited under windows than the template is stored with \r\n - var func_template := get_template(func_descriptor, is_callable).replace("\r\n", "\n") - double_src += func_template\ - .replace("$(arguments)", ", ".join(arg_names))\ - .replace("$(varargs)", ", ".join(vararg_names))\ - .replace("$(await)", GdFunctionDoubler.await_is_coroutine(is_coroutine)) \ - .replace("$(func_name)", func_name )\ - .replace("${default_return_value}", return_value)\ - .replace("$(push_errors)", _push_errors) - - if is_static: - double_src = double_src.replace("$(instance)", "__instance().") - else: - double_src = double_src.replace("$(instance)", "") - return double_src.split("\n") - - -func extract_arg_names(argument_signatures: Array[GdFunctionArgument], add_suffix := false) -> PackedStringArray: - var arg_names := PackedStringArray() - for arg in argument_signatures: - @warning_ignore("return_value_discarded") - arg_names.append(arg._name + ("_" if add_suffix else "")) - return arg_names - - -static func extract_constructor_args(args :Array[GdFunctionArgument]) -> PackedStringArray: - var constructor_args := PackedStringArray() - for arg in args: - var arg_name := arg._name + "_" - var default_value := get_default(arg) - if default_value == "null": - @warning_ignore("return_value_discarded") - constructor_args.append(arg_name + ":Variant=" + default_value) - else: - @warning_ignore("return_value_discarded") - constructor_args.append(arg_name + ":=" + default_value) - return constructor_args - - -static func extract_func_signature(descriptor: GdFunctionDescriptor) -> String: - var func_signature := "" - if descriptor._return_type == TYPE_NIL: - func_signature = "func %s(%s) -> void:" % [descriptor.name(), typeless_args(descriptor)] - elif descriptor._return_type == GdObjects.TYPE_VARIANT: - func_signature = "func %s(%s):" % [descriptor.name(), typeless_args(descriptor)] - else: - func_signature = "func %s(%s) -> %s:" % [descriptor.name(), typeless_args(descriptor), descriptor.return_type_as_string()] - return "static " + func_signature if descriptor.is_static() else func_signature - - -static func typeless_args(descriptor: GdFunctionDescriptor) -> String: - var collect := PackedStringArray() - for arg in descriptor.args(): - if arg.has_default(): - @warning_ignore("return_value_discarded") - collect.push_back(arg.name() + "_" + "=" + arg.value_as_string()) - else: - @warning_ignore("return_value_discarded") - collect.push_back(arg.name() + "_") - for arg in descriptor.varargs(): - @warning_ignore("return_value_discarded") - collect.push_back(arg.name() + "=" + arg.value_as_string()) - return ", ".join(collect) - - -static func get_default(arg :GdFunctionArgument) -> String: - if arg.has_default(): - return arg.value_as_string() - else: - return DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null") - - -static func await_is_coroutine(is_coroutine :bool) -> String: - return "await " if is_coroutine else "" diff --git a/addons/gdUnit4/src/core/GdFunctionDoubler.gd.uid b/addons/gdUnit4/src/core/GdFunctionDoubler.gd.uid deleted file mode 100644 index a5ed75fd..00000000 --- a/addons/gdUnit4/src/core/GdFunctionDoubler.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cdnegm0bxiklb diff --git a/addons/gdUnit4/src/core/GdObjects.gd b/addons/gdUnit4/src/core/GdObjects.gd index 2a780ce8..279e5188 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd +++ b/addons/gdUnit4/src/core/GdObjects.gd @@ -6,8 +6,6 @@ const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") # introduced with Godot 4.3.beta1 -const TYPE_PACKED_VECTOR4_ARRAY = 38 #TYPE_PACKED_VECTOR4_ARRAY - const TYPE_VOID = 1000 const TYPE_VARARG = 1001 const TYPE_VARIANT = 1002 @@ -20,10 +18,6 @@ const TYPE_CANVAS = 2003 const TYPE_ENUM = 2004 -# used as default value for varargs -const TYPE_VARARG_PLACEHOLDER_VALUE = "__null__" - - const TYPE_AS_STRING_MAPPINGS := { TYPE_NIL: "null", TYPE_BOOL: "bool", @@ -72,11 +66,18 @@ const TYPE_AS_STRING_MAPPINGS := { } +class EditorNotifications: + # NOTE: Hardcoding to avoid runtime errors in exported projects when editor + # classes are not available. These values are unlikely to change. + # See: EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED + const NOTIFICATION_EDITOR_SETTINGS_CHANGED := 10000 + + const NOTIFICATION_AS_STRING_MAPPINGS := { TYPE_OBJECT: { Object.NOTIFICATION_POSTINITIALIZE : "POSTINITIALIZE", Object.NOTIFICATION_PREDELETE: "PREDELETE", - EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED", + EditorNotifications.NOTIFICATION_EDITOR_SETTINGS_CHANGED: "EDITOR_SETTINGS_CHANGED", }, TYPE_NODE: { Node.NOTIFICATION_ENTER_TREE : "ENTER_TREE", @@ -145,7 +146,6 @@ enum COMPARE_MODE { # prototype of better object to dictionary -@warning_ignore("unsafe_cast") static func obj2dict(obj: Object, hashed_objects := Dictionary()) -> Dictionary: if obj == null: return {} @@ -185,6 +185,7 @@ static func obj2dict(obj: Object, hashed_objects := Dictionary()) -> Dictionary: dict[property_name] = str(property_value) continue hashed_objects[obj] = true + @warning_ignore("unsafe_cast") dict[property_name] = obj2dict(property_value as Object, hashed_objects) else: dict[property_name] = property_value @@ -210,7 +211,6 @@ static func equals_sorted(obj_a: Array[Variant], obj_b: Array[Variant], case_sen return equals(a, b, case_sensitive, compare_mode) -@warning_ignore("unsafe_method_access", "unsafe_cast") static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compare_mode :COMPARE_MODE, deep_stack :Array, stack_depth :int ) -> bool: var type_a := typeof(obj_a) var type_b := typeof(obj_b) @@ -221,8 +221,10 @@ static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compar # use argument matcher if requested if is_instance_valid(obj_a) and obj_a is GdUnitArgumentMatcher: + @warning_ignore("unsafe_cast") return (obj_a as GdUnitArgumentMatcher).is_match(obj_b) if is_instance_valid(obj_b) and obj_b is GdUnitArgumentMatcher: + @warning_ignore("unsafe_cast") return (obj_b as GdUnitArgumentMatcher).is_match(obj_a) stack_depth += 1 @@ -248,26 +250,35 @@ static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compar # fail fast if not is_instance_valid(obj_a) or not is_instance_valid(obj_b): return false + @warning_ignore("unsafe_method_access") if obj_a.get_class() != obj_b.get_class(): return false + @warning_ignore("unsafe_cast") var a := obj2dict(obj_a as Object) + @warning_ignore("unsafe_cast") var b := obj2dict(obj_b as Object) return _equals(a, b, case_sensitive, compare_mode, deep_stack, stack_depth) return obj_a == obj_b TYPE_ARRAY: + @warning_ignore("unsafe_method_access") if obj_a.size() != obj_b.size(): return false + @warning_ignore("unsafe_method_access") for index :int in obj_a.size(): if not _equals(obj_a[index], obj_b[index], case_sensitive, compare_mode, deep_stack, stack_depth): return false return true TYPE_DICTIONARY: + @warning_ignore("unsafe_method_access") if obj_a.size() != obj_b.size(): return false + @warning_ignore("unsafe_method_access") for key :Variant in obj_a.keys(): + @warning_ignore("unsafe_method_access") var value_a :Variant = obj_a[key] if obj_a.has(key) else null + @warning_ignore("unsafe_method_access") var value_b :Variant = obj_b[key] if obj_b.has(key) else null if not _equals(value_a, value_b, case_sensitive, compare_mode, deep_stack, stack_depth): return false @@ -275,6 +286,7 @@ static func _equals(obj_a :Variant, obj_b :Variant, case_sensitive :bool, compar TYPE_STRING: if case_sensitive: + @warning_ignore("unsafe_method_access") return obj_a.to_lower() == obj_b.to_lower() else: return obj_a == obj_b @@ -408,10 +420,6 @@ static func is_script(value :Variant) -> bool: return is_object(value) and value is Script -static func is_test_suite(script :Script) -> bool: - return is_gd_testsuite(script) or GdUnit4CSharpApiLoader.is_test_suite(script.resource_path) - - static func is_native_class(value :Variant) -> bool: return is_object(value) and is_engine_type(value) @@ -425,28 +433,6 @@ static func is_scene_resource_path(value :Variant) -> bool: return value is String and (value as String).ends_with(".tscn") -static func is_gd_script(script :Script) -> bool: - return script is GDScript - - -static func is_cs_script(script :Script) -> bool: - # we need to check by stringify name because checked non mono Godot the class CSharpScript is not available - return str(script).find("CSharpScript") != -1 - - -static func is_gd_testsuite(script :Script) -> bool: - if is_gd_script(script): - var stack := [script] - while not stack.is_empty(): - var current: Script = stack.pop_front() - var base: Script = current.get_base_script() - if base != null: - if base.resource_path.find("GdUnitTestSuite") != -1: - return true - stack.push_back(base) - return false - - static func is_singleton(value: Variant) -> bool: if not is_instance_valid(value) or is_native_class(value): return false @@ -512,6 +498,13 @@ static func create_instance(clazz :Variant) -> GdUnitResult: return GdUnitResult.error("Can't create instance for class '%s'." % str(clazz)) +## We do dispose 'GDScriptFunctionState' in a kacky style because the class is not visible anymore +static func dispose_function_state(func_state: Variant) -> void: + if func_state != null and str(func_state).contains("GDScriptFunctionState"): + @warning_ignore("unsafe_method_access") + func_state.completed.emit() + + @warning_ignore("return_value_discarded") static func extract_class_path(clazz :Variant) -> PackedStringArray: var clazz_path := PackedStringArray() diff --git a/addons/gdUnit4/src/core/GdObjects.gd.uid b/addons/gdUnit4/src/core/GdObjects.gd.uid index e23f630b..b5faf110 100644 --- a/addons/gdUnit4/src/core/GdObjects.gd.uid +++ b/addons/gdUnit4/src/core/GdObjects.gd.uid @@ -1 +1 @@ -uid://b4rl842ud3y3u +uid://bcap2udrnxox3 diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd b/addons/gdUnit4/src/core/GdUnit4Version.gd index 777eb92c..3e6a334e 100644 --- a/addons/gdUnit4/src/core/GdUnit4Version.gd +++ b/addons/gdUnit4/src/core/GdUnit4Version.gd @@ -59,3 +59,7 @@ static func init_version_label(label :Control) -> void: func _to_string() -> String: return "v%d.%d.%d" % [_major, _minor, _patch] + + +func documentation_version() -> String: + return "v%d.%d.x" % [_major, _minor] diff --git a/addons/gdUnit4/src/core/GdUnit4Version.gd.uid b/addons/gdUnit4/src/core/GdUnit4Version.gd.uid index 5c331737..6f9078f7 100644 --- a/addons/gdUnit4/src/core/GdUnit4Version.gd.uid +++ b/addons/gdUnit4/src/core/GdUnit4Version.gd.uid @@ -1 +1 @@ -uid://bonlnbcr2m6g6 +uid://ba72tcnf0s2ld diff --git a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd.uid b/addons/gdUnit4/src/core/GdUnitClassDoubler.gd.uid deleted file mode 100644 index dd77b063..00000000 --- a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://d2s2tfjy04e1j diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd b/addons/gdUnit4/src/core/GdUnitFileAccess.gd index cf8663c0..f6db5b4a 100644 --- a/addons/gdUnit4/src/core/GdUnitFileAccess.gd +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd @@ -92,6 +92,7 @@ static func copy_directory(from_dir :String, to_dir :String, recursive :bool = f static func delete_directory(path :String, only_content := false) -> void: var dir := DirAccess.open(path) if dir != null: + dir.include_hidden = true @warning_ignore("return_value_discarded") dir.list_dir_begin() var file_name := "." @@ -154,11 +155,18 @@ static func find_last_path_index(path :String, prefix :String) -> int: return last_iteration +static func as_resource_path(value: String) -> String: + if value.begins_with("res://"): + return value + return "res://" + value.trim_prefix("//").trim_prefix("/").trim_suffix("/") + + static func scan_dir(path :String) -> PackedStringArray: var dir := DirAccess.open(path) if dir == null or not dir.dir_exists(path): return PackedStringArray() var content := PackedStringArray() + dir.include_hidden = true @warning_ignore("return_value_discarded") dir.list_dir_begin() var next := "." diff --git a/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid b/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid index 43db6fd4..07beb492 100644 --- a/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitFileAccess.gd.uid @@ -1 +1 @@ -uid://ckxradlneutqd +uid://ctxy5tcx42o0j diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd deleted file mode 100644 index c3ea021a..00000000 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd +++ /dev/null @@ -1,47 +0,0 @@ -class_name GdUnitObjectInteractions -extends RefCounted - - -static func verify(interaction_object :Object, interactions_times :int) -> Variant: - if not _is_mock_or_spy(interaction_object, "__verify"): - return interaction_object - @warning_ignore("unsafe_method_access") - return interaction_object.__do_verify_interactions(interactions_times) - - -static func verify_no_interactions(interaction_object :Object) -> GdUnitAssert: - var __gd_assert := GdUnitAssertImpl.new("") - if not _is_mock_or_spy(interaction_object, "__verify"): - return __gd_assert.report_success() - @warning_ignore("unsafe_method_access") - var __summary :Dictionary = interaction_object.__verify_no_interactions() - if __summary.is_empty(): - return __gd_assert.report_success() - return __gd_assert.report_error(GdAssertMessages.error_no_more_interactions(__summary)) - - -static func verify_no_more_interactions(interaction_object :Object) -> GdUnitAssert: - var __gd_assert := GdUnitAssertImpl.new("") - if not _is_mock_or_spy(interaction_object, "__verify_no_more_interactions"): - return __gd_assert - @warning_ignore("unsafe_method_access") - var __summary :Dictionary = interaction_object.__verify_no_more_interactions() - if __summary.is_empty(): - return __gd_assert - return __gd_assert.report_error(GdAssertMessages.error_no_more_interactions(__summary)) - - -static func reset(interaction_object :Object) -> Object: - if not _is_mock_or_spy(interaction_object, "__reset"): - return interaction_object - @warning_ignore("unsafe_method_access") - interaction_object.__reset_interactions() - return interaction_object - - -static func _is_mock_or_spy(interaction_object :Object, mock_function_signature :String) -> bool: - @warning_ignore("unsafe_cast") - if interaction_object is GDScript and not (interaction_object.get_script() as GDScript).has_method(mock_function_signature): - push_error("Error: You try to use a non mock or spy!") - return false - return true diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd.uid b/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd.uid deleted file mode 100644 index b8802939..00000000 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractions.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://x5r2uxb850o diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd deleted file mode 100644 index 94c435f4..00000000 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd +++ /dev/null @@ -1,92 +0,0 @@ - -var __expected_interactions :int = -1 -var __saved_interactions := Dictionary() -var __verified_interactions := Array() - - -func __save_function_interaction(function_args :Array[Variant]) -> void: - var __matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) - for __index in __saved_interactions.keys().size(): - var __key :Variant = __saved_interactions.keys()[__index] - if __matcher.is_match(__key): - __saved_interactions[__key] += 1 - return - __saved_interactions[function_args] = 1 - - -func __is_verify_interactions() -> bool: - return __expected_interactions != -1 - - -func __do_verify_interactions(interactions_times :int = 1) -> Object: - __expected_interactions = interactions_times - return self - - -func __verify_interactions(function_args :Array[Variant]) -> void: - var __summary := Dictionary() - var __total_interactions := 0 - var __matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) - for __index in __saved_interactions.keys().size(): - var __key :Variant = __saved_interactions.keys()[__index] - if __matcher.is_match(__key): - var __interactions :int = __saved_interactions.get(__key, 0) - __total_interactions += __interactions - __summary[__key] = __interactions - # add as verified - __verified_interactions.append(__key) - - var __gd_assert := GdUnitAssertImpl.new("") - if __total_interactions != __expected_interactions: - var __expected_summary := {function_args : __expected_interactions} - var __error_message :String - # if no __interactions macht collect not verified __interactions for failure report - if __summary.is_empty(): - var __current_summary := __verify_no_more_interactions() - __error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary) - else: - __error_message = GdAssertMessages.error_validate_interactions(__summary, __expected_summary) - @warning_ignore("return_value_discarded") - __gd_assert.report_error(__error_message) - else: - @warning_ignore("return_value_discarded") - __gd_assert.report_success() - __expected_interactions = -1 - - -func __verify_no_interactions() -> Dictionary: - var __summary := Dictionary() - if not __saved_interactions.is_empty(): - for __index in __saved_interactions.keys().size(): - var func_call :Variant = __saved_interactions.keys()[__index] - __summary[func_call] = __saved_interactions[func_call] - return __summary - - -func __verify_no_more_interactions() -> Dictionary: - var __summary := Dictionary() - var called_functions :Array[Variant] = __saved_interactions.keys() - if called_functions != __verified_interactions: - # collect the not verified functions - var called_but_not_verified := called_functions.duplicate() - for __index in __verified_interactions.size(): - called_but_not_verified.erase(__verified_interactions[__index]) - - for __index in called_but_not_verified.size(): - var not_verified :Variant = called_but_not_verified[__index] - __summary[not_verified] = __saved_interactions[not_verified] - return __summary - - -func __reset_interactions() -> void: - __saved_interactions.clear() - - -func __filter_vargs(arg_values :Array[Variant]) -> Array[Variant]: - var filtered :Array[Variant] = [] - for __index in arg_values.size(): - var arg :Variant = arg_values[__index] - if typeof(arg) == TYPE_STRING and arg == GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE: - continue - filtered.append(arg) - return filtered diff --git a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd.uid b/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd.uid deleted file mode 100644 index 5e61073f..00000000 --- a/addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dxwdwomb3gg81 diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd b/addons/gdUnit4/src/core/GdUnitProperty.gd index 138dd9f7..3d050b19 100644 --- a/addons/gdUnit4/src/core/GdUnitProperty.gd +++ b/addons/gdUnit4/src/core/GdUnitProperty.gd @@ -31,6 +31,9 @@ func value() -> Variant: return _value +func int_value() -> int: + return _value + func value_as_string() -> String: return _value @@ -43,16 +46,18 @@ func is_selectable_value() -> bool: return not _value_set.is_empty() -func set_value(p_value :Variant) -> void: +func set_value(p_value: Variant) -> void: match _type: TYPE_STRING: _value = str(p_value) TYPE_BOOL: - _value = convert(p_value, TYPE_BOOL) + _value = type_convert(p_value, TYPE_BOOL) TYPE_INT: - _value = convert(p_value, TYPE_INT) + _value = type_convert(p_value, TYPE_INT) TYPE_FLOAT: - _value = convert(p_value, TYPE_FLOAT) + _value = type_convert(p_value, TYPE_FLOAT) + TYPE_DICTIONARY: + _value = type_convert(p_value, TYPE_DICTIONARY) _: _value = p_value diff --git a/addons/gdUnit4/src/core/GdUnitProperty.gd.uid b/addons/gdUnit4/src/core/GdUnitProperty.gd.uid index 3cff4807..5a2dafe5 100644 --- a/addons/gdUnit4/src/core/GdUnitProperty.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitProperty.gd.uid @@ -1 +1 @@ -uid://badil4qrfp0pb +uid://jb5pgif12g8s diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd b/addons/gdUnit4/src/core/GdUnitResult.gd index c7187d48..42392a5e 100644 --- a/addons/gdUnit4/src/core/GdUnitResult.gd +++ b/addons/gdUnit4/src/core/GdUnitResult.gd @@ -20,7 +20,7 @@ static func empty() -> GdUnitResult: return result -static func success(p_value :Variant) -> GdUnitResult: +static func success(p_value: Variant = "") -> GdUnitResult: assert(p_value != null, "The value must not be NULL") var result := GdUnitResult.new() result._value = p_value @@ -28,7 +28,7 @@ static func success(p_value :Variant) -> GdUnitResult: return result -static func warn(p_warn_message :String, p_value :Variant = null) -> GdUnitResult: +static func warn(p_warn_message: String, p_value: Variant = null) -> GdUnitResult: assert(not p_warn_message.is_empty()) #,"The message must not be empty") var result := GdUnitResult.new() result._value = p_value @@ -37,7 +37,7 @@ static func warn(p_warn_message :String, p_value :Variant = null) -> GdUnitResul return result -static func error(p_error_message :String) -> GdUnitResult: +static func error(p_error_message: String) -> GdUnitResult: assert(not p_error_message.is_empty(), "The message must not be empty") var result := GdUnitResult.new() result._value = null @@ -70,7 +70,7 @@ func value_as_string() -> String: return _value -func or_else(p_value :Variant) -> Variant: +func or_else(p_value: Variant) -> Variant: if not is_success(): return p_value return value() @@ -88,7 +88,7 @@ func _to_string() -> String: return str(GdUnitResult.serialize(self)) -static func serialize(result :GdUnitResult) -> Dictionary: +static func serialize(result: GdUnitResult) -> Dictionary: if result == null: push_error("Can't serialize a Null object from type GdUnitResult") return { @@ -99,7 +99,7 @@ static func serialize(result :GdUnitResult) -> Dictionary: } -static func deserialize(config :Dictionary) -> GdUnitResult: +static func deserialize(config: Dictionary) -> GdUnitResult: var result := GdUnitResult.new() var cfg_value: String = config.get("value", "") result._value = str_to_var(cfg_value) diff --git a/addons/gdUnit4/src/core/GdUnitResult.gd.uid b/addons/gdUnit4/src/core/GdUnitResult.gd.uid index 27128ca7..4d8d3742 100644 --- a/addons/gdUnit4/src/core/GdUnitResult.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitResult.gd.uid @@ -1 +1 @@ -uid://boxxqwin14hsi +uid://cxreyen5wvoui diff --git a/addons/gdUnit4/src/core/GdUnitRunner.gd b/addons/gdUnit4/src/core/GdUnitRunner.gd deleted file mode 100644 index 7f3d7702..00000000 --- a/addons/gdUnit4/src/core/GdUnitRunner.gd +++ /dev/null @@ -1,172 +0,0 @@ -extends Node - -@onready var _client :GdUnitTcpClient = $GdUnitTcpClient -@onready var _executor :GdUnitTestSuiteExecutor = GdUnitTestSuiteExecutor.new() - -enum { - INIT, - RUN, - STOP, - EXIT -} - -const GDUNIT_RUNNER = "GdUnitRunner" - -var _config := GdUnitRunnerConfig.new() -var _test_suites_to_process :Array[Node] -var _state :int = INIT -var _cs_executor :RefCounted - - -func _init() -> void: - # minimize scene window checked debug mode - if OS.get_cmdline_args().size() == 1: - DisplayServer.window_set_title("GdUnit4 Runner (Debug Mode)") - else: - DisplayServer.window_set_title("GdUnit4 Runner (Release Mode)") - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) - # store current runner instance to engine meta data to can be access in as a singleton - Engine.set_meta(GDUNIT_RUNNER, self) - _cs_executor = GdUnit4CSharpApiLoader.create_executor(self) - - -func _ready() -> void: - var config_result := _config.load_config() - if config_result.is_error(): - push_error(config_result.error_message()) - _state = EXIT - return - @warning_ignore("return_value_discarded") - _client.connect("connection_failed", _on_connection_failed) - @warning_ignore("return_value_discarded") - GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) - var result := _client.start("127.0.0.1", _config.server_port()) - if result.is_error(): - push_error(result.error_message()) - return - _state = INIT - - -func _on_connection_failed(message :String) -> void: - prints("_on_connection_failed", message, _test_suites_to_process) - _state = STOP - - -func _notification(what :int) -> void: - #prints("GdUnitRunner", self, GdObjects.notification_as_string(what)) - if what == NOTIFICATION_PREDELETE: - Engine.remove_meta(GDUNIT_RUNNER) - - -func _process(_delta :float) -> void: - match _state: - INIT: - # wait until client is connected to the GdUnitServer - if _client.is_client_connected(): - var time := LocalTime.now() - prints("Scan for test suites.") - _test_suites_to_process = load_test_suites() - prints("Scanning of %d test suites took" % _test_suites_to_process.size(), time.elapsed_since()) - gdUnitInit() - _state = RUN - RUN: - # all test suites executed - if _test_suites_to_process.is_empty(): - _state = STOP - else: - # process next test suite - set_process(false) - var test_suite :Node = _test_suites_to_process.pop_front() - @warning_ignore("unsafe_method_access") - if _cs_executor != null and _cs_executor.IsExecutable(test_suite): - @warning_ignore("unsafe_method_access") - _cs_executor.Execute(test_suite) - @warning_ignore("unsafe_property_access") - await _cs_executor.ExecutionCompleted - else: - await _executor.execute(test_suite as GdUnitTestSuite) - set_process(true) - STOP: - _state = EXIT - # give the engine small amount time to finish the rpc - _on_gdunit_event(GdUnitStop.new()) - await get_tree().create_timer(0.1).timeout - await get_tree().process_frame - get_tree().quit(0) - - -func load_test_suites() -> Array[Node]: - var to_execute := _config.to_execute() - if to_execute.is_empty(): - prints("No tests selected to execute!") - _state = EXIT - return [] - # scan for the requested test suites - var test_suites :Array[Node] = [] - var _scanner := GdUnitTestSuiteScanner.new() - for resource_path :String in to_execute.keys(): - var selected_tests :PackedStringArray = to_execute.get(resource_path) - var scanned_suites := _scanner.scan(resource_path) - _filter_test_case(scanned_suites, selected_tests) - test_suites += scanned_suites - return test_suites - - -func gdUnitInit() -> void: - #enable_manuall_polling() - send_message("Scanned %d test suites" % _test_suites_to_process.size()) - var total_count := _collect_test_case_count(_test_suites_to_process) - _on_gdunit_event(GdUnitInit.new(_test_suites_to_process.size(), total_count)) - if not GdUnitSettings.is_test_discover_enabled(): - for test_suite in _test_suites_to_process: - send_test_suite(test_suite) - - -func _filter_test_case(test_suites :Array[Node], included_tests :PackedStringArray) -> void: - if included_tests.is_empty(): - return - for test_suite in test_suites: - for test_case in test_suite.get_children(): - _do_filter_test_case(test_suite, test_case, included_tests) - - -func _do_filter_test_case(test_suite :Node, test_case :Node, included_tests :PackedStringArray) -> void: - for included_test in included_tests: - var test_meta :PackedStringArray = included_test.split(":") - var test_name := test_meta[0] - if test_case.get_name() == test_name: - # we have a paremeterized test selection - if test_meta.size() > 1: - var test_param_index := test_meta[1] - @warning_ignore("unsafe_method_access") - test_case.set_test_parameter_index(test_param_index.to_int()) - return - # the test is filtered out - test_suite.remove_child(test_case) - test_case.free() - - -func _collect_test_case_count(testSuites :Array[Node]) -> int: - var total :int = 0 - for test_suite in testSuites: - total += test_suite.get_child_count() - return total - - -# RPC send functions -func send_message(message :String) -> void: - _client.rpc_send(RPCMessage.of(message)) - - -func send_test_suite(test_suite :Node) -> void: - _client.rpc_send(RPCGdUnitTestSuite.of(test_suite)) - - -func _on_gdunit_event(event :GdUnitEvent) -> void: - _client.rpc_send(RPCGdUnitEvent.of(event)) - - -# Event bridge from C# GdUnit4.ITestEventListener.cs -func PublishEvent(data :Dictionary) -> void: - var event := GdUnitEvent.new().deserialize(data) - _client.rpc_send(RPCGdUnitEvent.of(event)) diff --git a/addons/gdUnit4/src/core/GdUnitRunner.gd.uid b/addons/gdUnit4/src/core/GdUnitRunner.gd.uid deleted file mode 100644 index 1ef58e9c..00000000 --- a/addons/gdUnit4/src/core/GdUnitRunner.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dq3umcnybe20l diff --git a/addons/gdUnit4/src/core/GdUnitRunner.tscn b/addons/gdUnit4/src/core/GdUnitRunner.tscn deleted file mode 100644 index 632f9cc2..00000000 --- a/addons/gdUnit4/src/core/GdUnitRunner.tscn +++ /dev/null @@ -1,10 +0,0 @@ -[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"] - -[ext_resource type="Script" uid="uid://dq3umcnybe20l" path="res://addons/gdUnit4/src/core/GdUnitRunner.gd" id="1"] -[ext_resource type="Script" uid="uid://cra3gsh4gftcw" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"] - -[node name="Control" type="Node"] -script = ExtResource("1") - -[node name="GdUnitTcpClient" type="Node" parent="."] -script = ExtResource("2") diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd index 205d4e5b..9f36354d 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd @@ -3,10 +3,9 @@ extends Resource const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") -const CONFIG_VERSION = "1.0" +const CONFIG_VERSION = "5.0" const VERSION = "version" -const INCLUDED = "included" -const SKIPPED = "skipped" +const TESTS = "tests" const SERVER_PORT = "server_port" const EXIT_FAIL_FAST = "exit_on_first_fail" @@ -15,17 +14,20 @@ const CONFIG_FILE = "res://addons/gdUnit4/GdUnitRunner.cfg" var _config := { VERSION : CONFIG_VERSION, # a set of directories or testsuite paths as key and a optional set of testcases as values - INCLUDED : Dictionary(), - # a set of skipped directories or testsuite paths - SKIPPED : Dictionary(), + + TESTS : Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase), + # the port of running test server for this session SERVER_PORT : -1 } +func version() -> String: + return _config[VERSION] + + func clear() -> GdUnitRunnerConfig: - _config[INCLUDED] = Dictionary() - _config[SKIPPED] = Dictionary() + _config[TESTS] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) return self @@ -38,80 +40,13 @@ func server_port() -> int: return _config.get(SERVER_PORT, -1) -@warning_ignore("return_value_discarded") -func self_test() -> GdUnitRunnerConfig: - add_test_suite("res://addons/gdUnit4/test/") - add_test_suite("res://addons/gdUnit4/mono/test/") +func add_test_cases(tests: Array[GdUnitTestCase]) -> GdUnitRunnerConfig: + test_cases().append_array(tests) return self -func add_test_suite(p_resource_path: String) -> GdUnitRunnerConfig: - var to_execute_ := to_execute() - to_execute_[p_resource_path] = to_execute_.get(p_resource_path, PackedStringArray()) - return self - - -func add_test_suites(resource_paths: PackedStringArray) -> GdUnitRunnerConfig: - for resource_path_ in resource_paths: - @warning_ignore("return_value_discarded") - add_test_suite(resource_path_) - return self - - -func add_test_case(p_resource_path: String, test_name: String, test_param_index: int = -1) -> GdUnitRunnerConfig: - var to_execute_ := to_execute() - var test_cases: PackedStringArray = to_execute_.get(p_resource_path, PackedStringArray()) - if test_param_index != -1: - @warning_ignore("return_value_discarded") - test_cases.append("%s:%d" % [test_name, test_param_index]) - else: - @warning_ignore("return_value_discarded") - test_cases.append(test_name) - to_execute_[p_resource_path] = test_cases - return self - - -# supports full path or suite name with optional test case name -# [:] -# '/path/path', res://path/path', 'res://path/path/testsuite.gd' or 'testsuite' -# 'res://path/path/testsuite.gd:test_case' or 'testsuite:test_case' -func skip_test_suite(value: String) -> GdUnitRunnerConfig: - var parts: PackedStringArray = GdUnitFileAccess.make_qualified_path(value).rsplit(":") - if parts[0] == "res": - parts.remove_at(0) - parts[0] = GdUnitFileAccess.make_qualified_path(parts[0]) - match parts.size(): - 1: - skipped()[parts[0]] = PackedStringArray() - 2: - @warning_ignore("return_value_discarded") - _skip_test_case(parts[0], parts[1]) - return self - - -func skip_test_suites(resource_paths: PackedStringArray) -> GdUnitRunnerConfig: - for resource_path_ in resource_paths: - @warning_ignore("return_value_discarded") - skip_test_suite(resource_path_) - return self - - -func _skip_test_case(p_resource_path: String, test_name: String) -> GdUnitRunnerConfig: - var to_ignore := skipped() - var test_cases: PackedStringArray = to_ignore.get(p_resource_path, PackedStringArray()) - @warning_ignore("return_value_discarded") - test_cases.append(test_name) - to_ignore[p_resource_path] = test_cases - return self - - -# Dictionary[String, Dictionary[String, PackedStringArray]] -func to_execute() -> Dictionary: - return _config.get(INCLUDED, {"res://" : PackedStringArray()}) - - -func skipped() -> Dictionary: - return _config.get(SKIPPED, {}) +func test_cases() -> Array[GdUnitTestCase]: + return _config.get(TESTS, []) func save_config(path: String = CONFIG_FILE) -> GdUnitResult: @@ -119,14 +54,23 @@ func save_config(path: String = CONFIG_FILE) -> GdUnitResult: if file == null: var error := FileAccess.get_open_error() return GdUnitResult.error("Can't write test runner configuration '%s'! %s" % [path, error_string(error)]) - _config[VERSION] = CONFIG_VERSION - file.store_string(JSON.stringify(_config)) + + var to_save := { + VERSION : CONFIG_VERSION, + SERVER_PORT : _config.get(SERVER_PORT), + TESTS : Array() + } + + var tests: Array = to_save.get(TESTS) + for test in test_cases(): + tests.append(inst_to_dict(test)) + file.store_string(JSON.stringify(to_save, "\t")) return GdUnitResult.success(path) func load_config(path: String = CONFIG_FILE) -> GdUnitResult: if not FileAccess.file_exists(path): - return GdUnitResult.error("Can't find test runner configuration '%s'! Please select a test to run." % path) + return GdUnitResult.warn("Can't find test runner configuration '%s'! Please select a test to run." % path) var file := FileAccess.open(path, FileAccess.READ) if file == null: var error := FileAccess.get_open_error() @@ -138,20 +82,38 @@ func load_config(path: String = CONFIG_FILE) -> GdUnitResult: var error := test_json_conv.parse(content) if error != OK: return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) - _config = test_json_conv.get_data() - if not _config.has(VERSION): + var config: Dictionary = test_json_conv.get_data() + if not config.has(VERSION): return GdUnitResult.error("The runner configuration '%s' is invalid! The format is changed please delete it manually and start a new test run." % path) + + var default: Array[Dictionary] = Array([], TYPE_DICTIONARY, "", null) + var tests_as_json: Array = config.get(TESTS, default) + _config = config + _config[TESTS] = convert_test_json_to_test_cases(tests_as_json) + + fix_value_types() return GdUnitResult.success(path) -@warning_ignore("unsafe_cast") +func convert_test_json_to_test_cases(jsons: Array) -> Array[GdUnitTestCase]: + if jsons.is_empty(): + return [] + var tests := jsons.map(func(d: Dictionary) -> GdUnitTestCase: + var test: GdUnitTestCase = dict_to_inst(d) + # we need o covert manually to the corect type becaus JSON do not handle typed values + test.guid = GdUnitGUID.new(str(d["guid"])) + test.attribute_index = test.attribute_index as int + test.line_number = test.line_number as int + return test + ) + return Array(tests, TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + func fix_value_types() -> void: # fix float value to int json stores all numbers as float var server_port_: int = _config.get(SERVER_PORT, -1) _config[SERVER_PORT] = server_port_ - convert_Array_to_PackedStringArray(_config[INCLUDED] as Dictionary) - convert_Array_to_PackedStringArray(_config[SKIPPED] as Dictionary) func convert_Array_to_PackedStringArray(data: Dictionary) -> void: diff --git a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid index af545ef8..28a243d3 100644 --- a/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitRunnerConfig.gd.uid @@ -1 +1 @@ -uid://i2bpxosay4tj +uid://gyi4wwcni60n diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd index c70be022..f2718537 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd @@ -82,7 +82,8 @@ func _init(p_scene: Variant, p_verbose: bool, p_hide_push_errors := false) -> vo ) _simulate_start_time = LocalTime.now() # we need to set inital a valid window otherwise the warp_mouse() is not handled - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + move_window_to_foreground() + # set inital mouse pos to 0,0 var max_iteration_to_wait := 0 while get_global_mouse_position() != Vector2.ZERO and max_iteration_to_wait < 100: @@ -95,6 +96,7 @@ func _notification(what: int) -> void: # reset time factor to normal __deactivate_time_factor() if is_instance_valid(_current_scene): + move_window_to_background() _scene_tree().root.remove_child(_current_scene) # do only free scenes instanciated by this runner if _scene_auto_free: @@ -107,33 +109,36 @@ func _scene_tree() -> SceneTree: return Engine.get_main_loop() as SceneTree +func await_input_processed() -> void: + if scene() != null and scene().process_mode != Node.PROCESS_MODE_DISABLED: + Input.flush_buffered_events() + await (Engine.get_main_loop() as SceneTree).process_frame + await (Engine.get_main_loop() as SceneTree).physics_frame + + @warning_ignore("return_value_discarded") -func simulate_action_pressed(action: String) -> GdUnitSceneRunner: - simulate_action_press(action) - simulate_action_release(action) +func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner: + simulate_action_press(action, event_index) + simulate_action_release(action, event_index) return self -func simulate_action_press(action: String) -> GdUnitSceneRunner: +func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner: __print_current_focus() var event := InputEventAction.new() event.pressed = true event.action = action - if Engine.get_version_info().hex >= 0x40300: - @warning_ignore("unsafe_property_access") - event.event_index = InputMap.get_actions().find(action) + event.event_index = event_index _action_on_press.append(action) return _handle_input_event(event) -func simulate_action_release(action: String) -> GdUnitSceneRunner: +func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner: __print_current_focus() var event := InputEventAction.new() event.pressed = false event.action = action - if Engine.get_version_info().hex >= 0x40300: - @warning_ignore("unsafe_property_access") - event.event_index = InputMap.get_actions().find(action) + event.event_index = event_index _action_on_press.erase(action) return _handle_input_event(event) @@ -152,6 +157,7 @@ func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := f event.pressed = true event.keycode = key_code as Key event.physical_keycode = key_code as Key + event.unicode = key_code event.alt_pressed = key_code == KEY_ALT event.shift_pressed = shift_pressed or key_code == KEY_SHIFT event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL @@ -166,6 +172,7 @@ func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := event.pressed = false event.keycode = key_code as Key event.physical_keycode = key_code as Key + event.unicode = key_code event.alt_pressed = key_code == KEY_ALT event.shift_pressed = shift_pressed or key_code == KEY_SHIFT event.ctrl_pressed = ctrl_pressed or key_code == KEY_CTRL @@ -174,10 +181,6 @@ func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := return _handle_input_event(event) -func set_mouse_pos(pos: Vector2) -> GdUnitSceneRunner: - return set_mouse_position(pos) - - func set_mouse_position(pos: Vector2) -> GdUnitSceneRunner: var event := InputEventMouseMotion.new() event.position = pos @@ -277,7 +280,7 @@ func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner: if is_emulate_mouse_from_touch(): # we need to simulate in addition to the touch the mouse events - set_mouse_pos(position) + set_mouse_position(position) simulate_mouse_button_press(MOUSE_BUTTON_LEFT) # push touch press event at position var event := InputEventScreenTouch.new() @@ -413,46 +416,21 @@ func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner: return self -func simulate_until_signal( - signal_name: String, - arg0: Variant = NO_ARG, - arg1: Variant = NO_ARG, - arg2: Variant = NO_ARG, - arg3: Variant = NO_ARG, - arg4: Variant = NO_ARG, - arg5: Variant = NO_ARG, - arg6: Variant = NO_ARG, - arg7: Variant = NO_ARG, - arg8: Variant = NO_ARG, - arg9: Variant = NO_ARG) -> GdUnitSceneRunner: - var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) +func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner: await _awaiter.await_signal_idle_frames(scene(), signal_name, args, 10000) return self -func simulate_until_object_signal( - source: Object, - signal_name: String, - arg0: Variant = NO_ARG, - arg1: Variant = NO_ARG, - arg2: Variant = NO_ARG, - arg3: Variant = NO_ARG, - arg4: Variant = NO_ARG, - arg5: Variant = NO_ARG, - arg6: Variant = NO_ARG, - arg7: Variant = NO_ARG, - arg8: Variant = NO_ARG, - arg9: Variant = NO_ARG) -> GdUnitSceneRunner: - var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) +func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner: await _awaiter.await_signal_idle_frames(source, signal_name, args, 10000) return self -func await_func(func_name: String, args := []) -> GdUnitFuncAssert: +func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert: return GdUnitFuncAssertImpl.new(scene(), func_name, args) -func await_func_on(instance: Object, func_name: String, args := []) -> GdUnitFuncAssert: +func await_func_on(instance: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert: return GdUnitFuncAssertImpl.new(instance, func_name, args) @@ -464,9 +442,17 @@ func await_signal_on(source: Object, signal_name: String, args := [], timeout := await _awaiter.await_signal_on(source, signal_name, args, timeout) -func maximize_view() -> GdUnitSceneRunner: - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) - DisplayServer.window_move_to_foreground() +func move_window_to_foreground() -> GdUnitSceneRunner: + if not Engine.is_embedded_in_editor(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_move_to_foreground() + return self + + +func move_window_to_background() -> GdUnitSceneRunner: + if not Engine.is_embedded_in_editor(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) return self @@ -488,21 +474,9 @@ func set_property(name: String, value: Variant) -> bool: return true -func invoke( - name: String, - arg0: Variant = NO_ARG, - arg1: Variant = NO_ARG, - arg2: Variant = NO_ARG, - arg3: Variant = NO_ARG, - arg4: Variant = NO_ARG, - arg5: Variant = NO_ARG, - arg6: Variant = NO_ARG, - arg7: Variant = NO_ARG, - arg8: Variant = NO_ARG, - arg9: Variant = NO_ARG) -> Variant: - var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], NO_ARG) +func invoke(name: String, ...args: Array) -> Variant: if scene().has_method(name): - return scene().callv(name, args) + return await scene().callv(name, args) return "The method '%s' not exist checked loaded scene." % name @@ -569,7 +543,7 @@ func _handle_actions(event: InputEventAction) -> bool: return false __print(" process action %s (%s) <- %s" % [scene(), _scene_name(), event.as_text()]) if event.is_pressed(): - Input.action_press(event.action, InputMap.action_get_deadzone(event.action)) + Input.action_press(event.action, event.get_strength()) else: Input.action_release(event.action) return true diff --git a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid index 7207f9d1..6bebd0b8 100644 --- a/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd.uid @@ -1 +1 @@ -uid://b7he5w27rw805 +uid://chwwu1byj24a4 diff --git a/addons/gdUnit4/src/core/GdUnitScriptType.gd b/addons/gdUnit4/src/core/GdUnitScriptType.gd deleted file mode 100644 index 7e1be519..00000000 --- a/addons/gdUnit4/src/core/GdUnitScriptType.gd +++ /dev/null @@ -1,16 +0,0 @@ -class_name GdUnitScriptType -extends RefCounted - -const UNKNOWN := "" -const CS := "cs" -const GD := "gd" - - -static func type_of(script :Script) -> String: - if script == null: - return UNKNOWN - if GdObjects.is_gd_script(script): - return GD - if GdObjects.is_cs_script(script): - return CS - return UNKNOWN diff --git a/addons/gdUnit4/src/core/GdUnitScriptType.gd.uid b/addons/gdUnit4/src/core/GdUnitScriptType.gd.uid deleted file mode 100644 index a6012ae6..00000000 --- a/addons/gdUnit4/src/core/GdUnitScriptType.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://culd5andxen5v diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd b/addons/gdUnit4/src/core/GdUnitSettings.gd index 2fc1cff2..f6bff127 100644 --- a/addons/gdUnit4/src/core/GdUnitSettings.gd +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd @@ -11,6 +11,9 @@ const GROUP_COMMON = COMMON_SETTINGS + "/common" const UPDATE_NOTIFICATION_ENABLED = GROUP_COMMON + "/update_notification_enabled" const SERVER_TIMEOUT = GROUP_COMMON + "/server_connection_timeout_minutes" +const GROUP_HOOKS = MAIN_CATEGORY + "/hooks" +const SESSION_HOOKS = GROUP_HOOKS + "/session_hooks" + const GROUP_TEST = COMMON_SETTINGS + "/test" const TEST_TIMEOUT = GROUP_TEST + "/test_timeout_seconds" const TEST_LOOKUP_FOLDER = GROUP_TEST + "/test_lookup_folder" @@ -74,6 +77,10 @@ const SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG = GROUP_SHORTCUT_FILESYSTEM + "/run_tes const GROUP_UI_TOOLBAR = UI_SETTINGS + "/toolbar" const INSPECTOR_TOOLBAR_BUTTON_RUN_OVERALL = GROUP_UI_TOOLBAR + "/run_overall" +# Feature flags +const GROUP_FEATURE = MAIN_CATEGORY + "/feature" + + # defaults # server connection timeout in minutes const DEFAULT_SERVER_TIMEOUT :int = 30 @@ -123,6 +130,7 @@ static func setup() -> void: "Show 'Run overall Tests' button in the inspector toolbar") create_property_if_need(TEMPLATE_TS_GD, GdUnitTestSuiteTemplate.default_GD_template(), "Test suite template to use") create_shortcut_properties_if_need() + create_property_if_need(SESSION_HOOKS, {} as Dictionary[String,bool]) migrate_properties() @@ -199,6 +207,20 @@ static func set_log_path(path :String) -> void: ProjectSettings.save() +static func get_session_hooks() -> Dictionary[String, bool]: + var property := get_property(SESSION_HOOKS) + if property == null: + return {} + var hooks: Dictionary[String, bool] = property.value() + return hooks + + +static func set_session_hooks(hooks: Dictionary[String, bool]) -> void: + var property := get_property(SESSION_HOOKS) + property.set_value(hooks) + update_property(property) + + static func set_inspector_tree_sort_mode(sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void: var property := get_property(INSPECTOR_TREE_SORT_MODE) property.set_value(sort_mode) @@ -276,6 +298,10 @@ static func is_test_flaky_check_enabled() -> bool: return get_setting(TEST_FLAKY_CHECK, false) +static func is_feature_enabled(feature: String) -> bool: + return get_setting(feature, false) + + static func get_flaky_max_retries() -> int: return get_setting(TEST_FLAKY_MAX_RETRIES, 3) diff --git a/addons/gdUnit4/src/core/GdUnitSettings.gd.uid b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid index 7719107a..eeb14da9 100644 --- a/addons/gdUnit4/src/core/GdUnitSettings.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSettings.gd.uid @@ -1 +1 @@ -uid://x1rivb4trvj4 +uid://f7k6qlm8xs2 diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd index ccc013fb..528e133f 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd +++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd @@ -68,6 +68,7 @@ func on_signal(source :Object, signal_name :String, expected_signal_args :Array) source.disconnect(signal_name, _on_signal_emmited) _time_left = timer.time_left + timer.queue_free() await scene_tree.process_frame @warning_ignore("unsafe_cast") if value is Array and (value as Array).size() == 1: diff --git a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid index 10588873..c686cafb 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSignalAwaiter.gd.uid @@ -1 +1 @@ -uid://xo5jt7cpsoy8 +uid://cnodv5gyv8qsk diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd index 10bacc4a..d03cc8ea 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd @@ -82,17 +82,22 @@ func _on_signal_emmited( (_collected_signals[emitter][signal_name] as Array).append(signal_args) -func reset_received_signals(emitter :Object, signal_name: String, signal_args :Array) -> void: +func reset_received_signals(emitter: Object, signal_name: String, signal_args: Array) -> void: #_debug_signal_list("before claer"); if _collected_signals.has(emitter): var signals_by_emitter :Dictionary = _collected_signals[emitter] if signals_by_emitter.has(signal_name): - @warning_ignore("unsafe_cast") - (_collected_signals[emitter][signal_name] as Array).erase(signal_args) + var received_args: Array = _collected_signals[emitter][signal_name] + # We iterate backwarts over to received_args to remove matching args. + # This will avoid array corruption see comment on `erase` otherwise we need a timeconsuming duplicate before + for arg_pos: int in range(received_args.size()-1, -1, -1): + var arg: Variant = received_args[arg_pos] + if GdObjects.equals(arg, signal_args): + received_args.remove_at(arg_pos) #_debug_signal_list("after claer"); -func is_signal_collecting(emitter :Object, signal_name :String) -> bool: +func is_signal_collecting(emitter: Object, signal_name: String) -> bool: @warning_ignore("unsafe_cast") return _collected_signals.has(emitter) and (_collected_signals[emitter] as Dictionary).has(signal_name) diff --git a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid index e43baeb1..0c16d947 100644 --- a/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSignalCollector.gd.uid @@ -1 +1 @@ -uid://cb7q7t47rby1e +uid://bxtxhv2qd0nj4 diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd b/addons/gdUnit4/src/core/GdUnitSignals.gd index b67d7a19..53aafe99 100644 --- a/addons/gdUnit4/src/core/GdUnitSignals.gd +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd @@ -1,29 +1,98 @@ class_name GdUnitSignals extends RefCounted +## Singleton class that handles GdUnit's signal communication.[br] +## [br] +## This class manages all signals used to communicate test events, discovery, and status changes.[br] +## It uses a singleton pattern stored in Engine metadata to ensure a single instance.[br] +## [br] +## Signals are grouped by purpose:[br] +## - Client connection handling[br] +## - Test execution events[br] +## - Test discovery events[br] +## - Settings and status updates[br] +## [br] +## Example usage:[br] +## [codeblock] +## # Connect to test discovery +## GdUnitSignals.instance().gdunit_test_discovered.connect(self._on_test_discovered) +## +## # Emit test event +## GdUnitSignals.instance().gdunit_event.emit(test_event) +## [/codeblock] + +## Emitted when a client connects to the GdUnit server.[br] +## [param client_id] The ID of the connected client. @warning_ignore("unused_signal") -signal gdunit_client_connected(client_id :int) +signal gdunit_client_connected(client_id: int) + + +## Emitted when a client disconnects from the GdUnit server.[br] +## [param client_id] The ID of the disconnected client. @warning_ignore("unused_signal") -signal gdunit_client_disconnected(client_id :int) +signal gdunit_client_disconnected(client_id: int) + + +## Emitted when a client terminates unexpectedly. @warning_ignore("unused_signal") signal gdunit_client_terminated() + +## Emitted when a test execution event occurs.[br] +## [param event] The test event containing details about test execution. +@warning_ignore("unused_signal") +signal gdunit_event(event: GdUnitEvent) + + +## Emitted for test debug events during execution.[br] +## [param event] The debug event containing test execution details. +@warning_ignore("unused_signal") +signal gdunit_event_debug(event: GdUnitEvent) + + +## Emitted to broadcast a general message.[br] +## [param message] The message to broadcast. @warning_ignore("unused_signal") -signal gdunit_event(event :GdUnitEvent) +signal gdunit_message(message: String) + + +## Emitted to update test failure status.[br] +## [param is_failed] Whether the test has failed. @warning_ignore("unused_signal") -signal gdunit_event_debug(event :GdUnitEvent) +signal gdunit_set_test_failed(is_failed: bool) + + +## Emitted when a GdUnit setting changes.[br] +## [param property] The property that was changed. @warning_ignore("unused_signal") -signal gdunit_add_test_suite(test_suite :GdUnitTestSuiteDto) +signal gdunit_settings_changed(property: GdUnitProperty) + +## Called when a new test case is discovered during the discovery process. +## Custom implementations should connect to this signal and store the discovered test case as needed.[br] +## [param test_case] The discovered test case instance to be processed. @warning_ignore("unused_signal") -signal gdunit_message(message :String) +signal gdunit_test_discover_added(test_case: GdUnitTestCase) + + +## Emitted when a test case is deleted.[br] +## [param test_case] The test case that was deleted. @warning_ignore("unused_signal") -signal gdunit_set_test_failed(is_failed :bool) +signal gdunit_test_discover_deleted(test_case: GdUnitTestCase) + + +## Emitted when a test case is modified.[br] +## [param test_case] The test case that was modified. @warning_ignore("unused_signal") -signal gdunit_settings_changed(property :GdUnitProperty) +signal gdunit_test_discover_modified(test_case: GdUnitTestCase) + const META_KEY := "GdUnitSignals" +## Returns the singleton instance of GdUnitSignals.[br] +## Creates a new instance if none exists.[br] +## [br] +## Returns: The GdUnitSignals singleton instance. static func instance() -> GdUnitSignals: if Engine.has_meta(META_KEY): return Engine.get_meta(META_KEY) @@ -32,6 +101,10 @@ static func instance() -> GdUnitSignals: return instance_ +## Cleans up the singleton instance and disconnects all signals.[br] +## [br] +## Should be called when GdUnit is shutting down or needs to reset.[br] +## Ensures proper cleanup of signal connections and resources. static func dispose() -> void: var signals := instance() # cleanup connected signals diff --git a/addons/gdUnit4/src/core/GdUnitSignals.gd.uid b/addons/gdUnit4/src/core/GdUnitSignals.gd.uid index c3a621fd..21f5ea10 100644 --- a/addons/gdUnit4/src/core/GdUnitSignals.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSignals.gd.uid @@ -1 +1 @@ -uid://c5pyaxs31guij +uid://dg5wqm36etlw2 diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd b/addons/gdUnit4/src/core/GdUnitSingleton.gd index daa221c0..b8e08cc3 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd @@ -12,10 +12,10 @@ const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const MEATA_KEY := "GdUnitSingletons" -static func instance(name :String, clazz :Callable) -> Variant: +static func instance(name: String, clazz: Callable) -> Variant: if Engine.has_meta(name): return Engine.get_meta(name) - var singleton :Variant = clazz.call() + var singleton: Variant = clazz.call() if is_instance_of(singleton, RefCounted): @warning_ignore("unsafe_cast") push_error("Invalid singleton implementation detected for '%s' is `%s`!" % [name, (singleton as RefCounted).get_class()]) @@ -23,20 +23,20 @@ static func instance(name :String, clazz :Callable) -> Variant: Engine.set_meta(name, singleton) GdUnitTools.prints_verbose("Register singleton '%s:%s'" % [name, singleton]) - var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) @warning_ignore("return_value_discarded") singletons.append(name) Engine.set_meta(MEATA_KEY, singletons) return singleton -static func unregister(p_singleton :String, use_call_deferred :bool = false) -> void: - var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) +static func unregister(p_singleton: String, use_call_deferred: bool = false) -> void: + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) if singletons.has(p_singleton): GdUnitTools.prints_verbose("\n Unregister singleton '%s'" % p_singleton); var index := singletons.find(p_singleton) singletons.remove_at(index) - var instance_ :Object = Engine.get_meta(p_singleton) + var instance_: Object = Engine.get_meta(p_singleton) GdUnitTools.prints_verbose(" Free singleton instance '%s:%s'" % [p_singleton, instance_]) @warning_ignore("return_value_discarded") GdUnitTools.free_instance(instance_, use_call_deferred) @@ -45,9 +45,9 @@ static func unregister(p_singleton :String, use_call_deferred :bool = false) -> Engine.set_meta(MEATA_KEY, singletons) -static func dispose(use_call_deferred :bool = false) -> void: +static func dispose(use_call_deferred: bool = false) -> void: # use a copy because unregister is modify the singletons array - var singletons :PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) + var singletons: PackedStringArray = Engine.get_meta(MEATA_KEY, PackedStringArray()) GdUnitTools.prints_verbose("----------------------------------------------------------------") GdUnitTools.prints_verbose("Cleanup singletons %s" % singletons) for singleton in PackedStringArray(singletons): diff --git a/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid b/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid index 0b3a4d29..6fbb4288 100644 --- a/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitSingleton.gd.uid @@ -1 +1 @@ -uid://bnvrs3b8fx7e0 +uid://vo88blqpntx3 diff --git a/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd new file mode 100644 index 00000000..33b80e28 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd @@ -0,0 +1,97 @@ +class_name GdUnitTestResourceLoader +extends RefCounted + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +enum { + GD_SUITE, + CS_SUITE +} + + +static func load_test_suite(resource_path: String, script_type := GD_SUITE) -> Node: + match script_type: + GD_SUITE: + return load_test_suite_gd(resource_path) + CS_SUITE: + return load_test_suite_cs(resource_path) + assert("type '%s' is not implemented" % script_type) + return null + + +static func load_tests(resource_path: String) -> Dictionary: + var script := load_gd_script(resource_path) + var discovered_tests := {} + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + discovered_tests[test.display_name] = test + ) + + return discovered_tests + + +static func load_test_suite_gd(resource_path: String) -> GdUnitTestSuite: + var script := load_gd_script(resource_path) + var discovered_tests: Array[GdUnitTestCase] = [] + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + discovered_tests.append(test) + ) + # complete test suite wiht parsed test cases + return GdUnitTestSuiteScanner.new().load_suite(script, discovered_tests) + + +static func load_test_suite_cs(resource_path: String) -> Node: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return null + var script :Script = ClassDB.instantiate("CSharpScript") + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + script.resource_path = resource_path + script.reload() + return null + + +static func load_cs_script(resource_path: String, debug_write := false) -> Script: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return null + var script :Script = ClassDB.instantiate("CSharpScript") + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + var script_resource_path := resource_path.replace(resource_path.get_extension(), "cs") + if debug_write: + script_resource_path = GdUnitFileAccess.create_temp_dir("test") + "/%s" % script_resource_path.get_file() + print_debug("save resource:", script_resource_path) + DirAccess.remove_absolute(script_resource_path) + var err := ResourceSaver.save(script, script_resource_path) + if err != OK: + print_debug("Can't save debug resource",script_resource_path, "Error:", error_string(err)) + script.take_over_path(script_resource_path) + else: + script.take_over_path(resource_path) + script.reload() + return script + + +static func load_gd_script(resource_path: String, debug_write := false) -> GDScript: + # grap current level + var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access") + # disable and load the script + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0) + + var script := GDScript.new() + script.source_code = GdUnitFileAccess.resource_as_string(resource_path) + var script_resource_path := resource_path.replace(resource_path.get_extension(), "gd") + if debug_write: + script_resource_path = script_resource_path.replace("res://", GdUnitFileAccess.temp_dir() + "/") + #print_debug("save resource: ", script_resource_path) + DirAccess.remove_absolute(script_resource_path) + DirAccess.make_dir_recursive_absolute(script_resource_path.get_base_dir()) + var err := ResourceSaver.save(script, script_resource_path, ResourceSaver.FLAG_REPLACE_SUBRESOURCE_PATHS) + if err != OK: + print_debug("Can't save debug resource", script_resource_path, "Error:", error_string(err)) + script.take_over_path(script_resource_path) + else: + script.take_over_path(resource_path) + var error := script.reload() + if error != OK: + push_error("Errors on loading script %s. Error: %s" % [resource_path, error_string(error)]) + ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access) + return script + #@warning_ignore("unsafe_cast") diff --git a/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid new file mode 100644 index 00000000..445d4c94 --- /dev/null +++ b/addons/gdUnit4/src/core/GdUnitTestResourceLoader.gd.uid @@ -0,0 +1 @@ +uid://slwvv8m1opts diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd index aabbac75..97a49b00 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd @@ -9,7 +9,7 @@ static func create(source :Script, line_number :int) -> GdUnitResult: ScriptEditorControls.save_an_open_script(source.resource_path) @warning_ignore("return_value_discarded") ScriptEditorControls.save_an_open_script(test_suite_path, true) - if GdObjects.is_cs_script(source): + if source.get_class() == "CSharpScript": return GdUnit4CSharpApiLoader.create_test_suite(source.resource_path, line_number+1, test_suite_path) var parser := GdScriptParser.new() var lines := source.source_code.split("\n") diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid index 931d79ca..1685cfbb 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteBuilder.gd.uid @@ -1 +1 @@ -uid://da2hhor4v6f37 +uid://05m8b2cn6hte diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd index 395958c3..63028970 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd @@ -16,19 +16,25 @@ const exclude_scan_directories = [ "res://reports"] +const ARGUMENT_TIMEOUT := "timeout" +const ARGUMENT_SKIP := "do_skip" +const ARGUMENT_SKIP_REASON := "skip_reason" +const ARGUMENT_PARAMETER_SET := "test_parameters" + + var _script_parser := GdScriptParser.new() -var _included_resources :PackedStringArray = [] -var _excluded_resources :PackedStringArray = [] +var _included_resources: PackedStringArray = [] +var _excluded_resources: PackedStringArray = [] var _expression_runner := GdUnitExpressionRunner.new() var _regex_extends_clazz_name := RegEx.create_from_string("extends[\\s]+([\\S]+)") func prescan_testsuite_classes() -> void: # scan and cache extends GdUnitTestSuite by class name an resource paths - var script_classes :Array[Dictionary] = ProjectSettings.get_global_class_list() + var script_classes: Array[Dictionary] = ProjectSettings.get_global_class_list() for script_meta in script_classes: - var base_class :String = script_meta["base"] - var resource_path :String = script_meta["path"] + var base_class: String = script_meta["base"] + var resource_path: String = script_meta["path"] if base_class == "GdUnitTestSuite": @warning_ignore("return_value_discarded") _included_resources.append(resource_path) @@ -37,27 +43,39 @@ func prescan_testsuite_classes() -> void: _excluded_resources.append(resource_path) -func scan(resource_path :String) -> Array[Node]: +func scan(resource_path: String) -> Array[Script]: prescan_testsuite_classes() # if single testsuite requested if FileAccess.file_exists(resource_path): - var test_suite := _parse_is_test_suite(resource_path) + var test_suite := _load_is_test_suite(resource_path) if test_suite != null: return [test_suite] - return [] as Array[Node] + return [] + return scan_directory(resource_path) + + +func scan_directory(resource_path: String) -> Array[Script]: + prescan_testsuite_classes() + # We use the global cache to fast scan for test suites. + if _excluded_resources.has(resource_path): + return [] + var base_dir := DirAccess.open(resource_path) if base_dir == null: prints("Given directory or file does not exists:", resource_path) return [] - return _scan_test_suites(base_dir, []) + prints("Scanning for test suites in:", resource_path) + return _scan_test_suites_scripts(base_dir, []) -func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[Node]: + +func _scan_test_suites_scripts(dir: DirAccess, collected_suites: Array[Script]) -> Array[Script]: if exclude_scan_directories.has(dir.get_current_dir()): return collected_suites - prints("Scanning for test suites in:", dir.get_current_dir()) - @warning_ignore("return_value_discarded") - dir.list_dir_begin() # TODOGODOT4 fill missing arguments https://github.com/godotengine/godot/pull/40547 + var err := dir.list_dir_begin() + if err != OK: + push_error("Error on scanning directory %s" % dir.get_current_dir(), error_string(err)) + return collected_suites var file_name := dir.get_next() while file_name != "": var resource_path := GdUnitTestSuiteScanner._file(dir, file_name) @@ -65,10 +83,10 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N var sub_dir := DirAccess.open(resource_path) if sub_dir != null: @warning_ignore("return_value_discarded") - _scan_test_suites(sub_dir, collected_suites) + _scan_test_suites_scripts(sub_dir, collected_suites) else: var time := LocalTime.now() - var test_suite := _parse_is_test_suite(resource_path) + var test_suite := _load_is_test_suite(resource_path) if test_suite: collected_suites.append(test_suite) if OS.is_stdout_verbose() and time.elapsed_since_ms() > 300: @@ -77,169 +95,167 @@ func _scan_test_suites(dir :DirAccess, collected_suites :Array[Node]) -> Array[N return collected_suites -static func _file(dir :DirAccess, file_name :String) -> String: +static func _file(dir: DirAccess, file_name: String) -> String: var current_dir := dir.get_current_dir() if current_dir.ends_with("/"): return current_dir + file_name return current_dir + "/" + file_name -func _parse_is_test_suite(resource_path :String) -> Node: +func _load_is_test_suite(resource_path: String) -> Script: if not GdUnitTestSuiteScanner._is_script_format_supported(resource_path): return null - if GdUnit4CSharpApiLoader.is_test_suite(resource_path): - return GdUnit4CSharpApiLoader.parse_test_suite(resource_path) # We use the global cache to fast scan for test suites. if _excluded_resources.has(resource_path): return null # Check in the global class cache whether the GdUnitTestSuite class has been extended. if _included_resources.has(resource_path): - return _parse_test_suite(GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path)) + return GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path) # Otherwise we need to scan manual, we need to exclude classes where direct extends form Godot classes # the resource loader can fail to load e.g. plugin classes with do preload other scripts - var extends_from := get_extends_classname(resource_path) + #var extends_from := get_extends_classname(resource_path) # If not extends is defined or extends from a Godot class - if extends_from.is_empty() or ClassDB.class_exists(extends_from): - return null + #if extends_from.is_empty() or ClassDB.class_exists(extends_from): + # return null # Finally, we need to load the class to determine it is a test suite var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(resource_path) - if not GdObjects.is_test_suite(script): + if not is_test_suite(script): return null - return _parse_test_suite(script) + return script + + +func load_suite(script: GDScript, tests: Array[GdUnitTestCase]) -> GdUnitTestSuite: + var test_suite: GdUnitTestSuite = script.new() + var first_test: GdUnitTestCase = tests.front() + test_suite.set_name(first_test.suite_name) + + # We need to group first all parameterized tests together to load the parameter set once + var grouped_by_test := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.test_name + ) + # Extract function descriptors + var test_names: PackedStringArray = grouped_by_test.keys() + test_names.append("before") + var function_descriptors := _script_parser.get_function_descriptors(script, test_names) + + # Convert to test + for fd in function_descriptors: + if fd.name() == "before": + _handle_test_suite_arguments(test_suite, script, fd) + continue + + # Build test attributes from test method + var test_attribute := _build_test_attribute(script, fd) + # Create test from descriptor and given attributes + var test_group: Array = grouped_by_test[fd.name()] + for test: GdUnitTestCase in test_group: + # We need a copy, because of mutable state + var attribute: TestCaseAttribute = test_attribute.clone() + test_suite.add_child(_TestCase.new(test, attribute, fd)) + return test_suite + + +func _build_test_attribute(script: GDScript, fd: GdFunctionDescriptor) -> TestCaseAttribute: + var collected_unknown_aruments := PackedStringArray() + var attribute := TestCaseAttribute.new() + + # Collect test attributes + for arg: GdFunctionArgument in fd.args(): + if arg.type() == GdObjects.TYPE_FUZZER: + attribute.fuzzers.append(arg) + else: + # We allow underscore as prefix to prevent unused argument warnings + match arg.name().trim_prefix("_"): + ARGUMENT_TIMEOUT: + attribute.timeout = type_convert(arg.default(), TYPE_INT) + ARGUMENT_SKIP: + var result: Variant = _expression_runner.execute(script, arg.plain_value()) + if result is bool: + attribute.is_skipped = result + else: + push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value()) + ARGUMENT_SKIP_REASON: + attribute.skip_reason = arg.plain_value() + Fuzzer.ARGUMENT_ITERATIONS: + attribute.fuzzer_iterations = type_convert(arg.default(), TYPE_INT) + Fuzzer.ARGUMENT_SEED: + attribute.test_seed = type_convert(arg.default(), TYPE_INT) + ARGUMENT_PARAMETER_SET: + collected_unknown_aruments.clear() + pass + _: + collected_unknown_aruments.append(arg.name()) + + # Verify for unknown arguments + if not collected_unknown_aruments.is_empty(): + attribute.is_skipped = true + attribute.skip_reason = "Unknown test case argument's %s found." % collected_unknown_aruments + + return attribute # We load the test suites with disabled unsafe_method_access to avoid spamming loading errors # `unsafe_method_access` will happen when using `assert_that` -static func load_with_disabled_warnings(resource_path: String) -> GDScript: +static func load_with_disabled_warnings(resource_path: String) -> Script: # grap current level var unsafe_method_access: Variant = ProjectSettings.get_setting("debug/gdscript/warnings/unsafe_method_access") # disable and load the script ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", 0) - var script: GDScript = ResourceLoader.load(resource_path) + + var script: Script = ( + GdUnitTestResourceLoader.load_gd_script(resource_path) if resource_path.ends_with("resource") + else ResourceLoader.load(resource_path)) # restore ProjectSettings.set_setting("debug/gdscript/warnings/unsafe_method_access", unsafe_method_access) return script -static func _is_script_format_supported(resource_path :String) -> bool: - var ext := resource_path.get_extension() - if ext == "gd": +static func is_test_suite(script: Script) -> bool: + if script is GDScript: + var stack := [script] + while not stack.is_empty(): + var current: Script = stack.pop_front() + var base: Script = current.get_base_script() + if base != null: + if base.resource_path.find("GdUnitTestSuite") != -1: + return true + stack.push_back(base) + elif script != null and script.get_class() == "CSharpScript": return true - return GdUnit4CSharpApiLoader.is_csharp_file(resource_path) - - -func _parse_test_suite(script: Script) -> GdUnitTestSuite: - if not GdObjects.is_test_suite(script): - return null - - # If test suite a C# script - if GdUnit4CSharpApiLoader.is_test_suite(script.resource_path): - return GdUnit4CSharpApiLoader.parse_test_suite(script.resource_path) - - # Do pares as GDScript - var test_suite: GdUnitTestSuite = (script as GDScript).new() - test_suite.set_name(GdUnitTestSuiteScanner.parse_test_suite_name(script)) - # add test cases to test suite and parse test case line nummber - var test_case_names := _extract_test_case_names(script as GDScript) - _parse_and_add_test_cases(test_suite, script as GDScript, test_case_names) - return test_suite + return false -func _extract_test_case_names(script :GDScript) -> PackedStringArray: - return script.get_script_method_list()\ - .map(func(descriptor: Dictionary) -> String: return descriptor["name"])\ - .filter(func(func_name: String) -> bool: return func_name.begins_with("test")) +static func _is_script_format_supported(resource_path: String) -> bool: + var ext := resource_path.get_extension() + return ext == "gd" or ext == "cs" -static func parse_test_suite_name(script :Script) -> String: +static func parse_test_suite_name(script: Script) -> String: return script.resource_path.get_file().replace(".gd", "") func _handle_test_suite_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void: for arg in fd.args(): - match arg.name(): - _TestCase.ARGUMENT_SKIP: + # We allow underscore as prefix to prevent unused argument warnings + match arg.name().trim_prefix("_"): + ARGUMENT_SKIP: var result: Variant = _expression_runner.execute(script, arg.plain_value()) if result is bool: test_suite.__is_skipped = result else: push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value()) - _TestCase.ARGUMENT_SKIP_REASON: + ARGUMENT_SKIP_REASON: test_suite.__skip_reason = arg.plain_value() _: push_error("Unsuported argument `%s` found on before() at '%s'!" % [arg.name(), script.resource_path]) -func _handle_test_case_arguments(test_suite: GdUnitTestSuite, script: GDScript, fd: GdFunctionDescriptor) -> void: - var timeout := _TestCase.DEFAULT_TIMEOUT - var iterations := Fuzzer.ITERATION_DEFAULT_COUNT - var seed_value := -1 - var is_skipped := false - var skip_reason := "Unknown." - var fuzzers: Array[GdFunctionArgument] = [] - var test := _TestCase.new() - - for arg: GdFunctionArgument in fd.args(): - # verify argument is allowed - # is test using fuzzers? - if arg.type() == GdObjects.TYPE_FUZZER: - fuzzers.append(arg) - elif arg.has_default(): - match arg.name(): - _TestCase.ARGUMENT_TIMEOUT: - timeout = arg.default() - _TestCase.ARGUMENT_SKIP: - var result :Variant = _expression_runner.execute(script, arg.plain_value()) - if result is bool: - is_skipped = result - else: - push_error("Test expression '%s' cannot be evaluated because it is not of type bool!" % arg.plain_value()) - _TestCase.ARGUMENT_SKIP_REASON: - skip_reason = arg.plain_value() - Fuzzer.ARGUMENT_ITERATIONS: - iterations = arg.default() - Fuzzer.ARGUMENT_SEED: - seed_value = arg.default() - # create new test - @warning_ignore("return_value_discarded") - test.configure(fd.name(), fd.line_number(), fd.source_path(), timeout, fuzzers, iterations, seed_value) - test.set_function_descriptor(fd) - test.skip(is_skipped, skip_reason) - _validate_argument(fd, test) - test_suite.add_child(test) - - -func _parse_and_add_test_cases(test_suite: GdUnitTestSuite, script: GDScript, test_case_names: PackedStringArray) -> void: - var test_cases_to_find := Array(test_case_names) - var functions_to_scan := test_case_names.duplicate() - @warning_ignore("return_value_discarded") - functions_to_scan.append("before") - - var function_descriptors := _script_parser.get_function_descriptors(script, functions_to_scan) - for fd in function_descriptors: - if fd.name() == "before": - _handle_test_suite_arguments(test_suite, script, fd) - if test_cases_to_find.has(fd.name()): - _handle_test_case_arguments(test_suite, script, fd) - - -const TEST_CASE_ARGUMENTS = [_TestCase.ARGUMENT_TIMEOUT, _TestCase.ARGUMENT_SKIP, _TestCase.ARGUMENT_SKIP_REASON, Fuzzer.ARGUMENT_ITERATIONS, Fuzzer.ARGUMENT_SEED] - -func _validate_argument(fd :GdFunctionDescriptor, test_case :_TestCase) -> void: - if fd.is_parameterized(): - return - for argument in fd.args(): - if argument.type() == GdObjects.TYPE_FUZZER or argument.name() in TEST_CASE_ARGUMENTS: - continue - test_case.skip(true, "Unknown test case argument '%s' found." % argument.name()) - - # converts given file name by configured naming convention -static func _to_naming_convention(file_name :String) -> String: +static func _to_naming_convention(file_name: String) -> String: var nc :int = GdUnitSettings.get_setting(GdUnitSettings.TEST_SUITE_NAMING_CONVENTION, 0) match nc: GdUnitSettings.NAMING_CONVENTIONS.AUTO_DETECT: @@ -254,7 +270,7 @@ static func _to_naming_convention(file_name :String) -> String: return "--" -static func resolve_test_suite_path(source_script_path :String, test_root_folder :String = "test") -> String: +static func resolve_test_suite_path(source_script_path: String, test_root_folder: String = "test") -> String: var file_name := source_script_path.get_basename().get_file() var suite_name := _to_naming_convention(file_name) if test_root_folder.is_empty() or test_root_folder == "/": @@ -265,7 +281,7 @@ static func resolve_test_suite_path(source_script_path :String, test_root_folder return normalize_path(source_script_path.replace("user://tmp", "user://tmp/" + test_root_folder)).replace(file_name, suite_name) # at first look up is the script under a "src" folder located - var test_suite_path :String + var test_suite_path: String var src_folder := source_script_path.find("/src/") if src_folder != -1: test_suite_path = source_script_path.replace("/src/", "/"+test_root_folder+"/") @@ -284,11 +300,11 @@ static func resolve_test_suite_path(source_script_path :String, test_root_folder return normalize_path(test_suite_path).replace(file_name, suite_name) -static func normalize_path(path :String) -> String: +static func normalize_path(path: String) -> String: return path.replace("///", "/") -static func create_test_suite(test_suite_path :String, source_path :String) -> GdUnitResult: +static func create_test_suite(test_suite_path: String, source_path: String) -> GdUnitResult: # create directory if not exists if not DirAccess.dir_exists_absolute(test_suite_path.get_base_dir()): var error_ := DirAccess.make_dir_recursive_absolute(test_suite_path.get_base_dir()) @@ -302,7 +318,7 @@ static func create_test_suite(test_suite_path :String, source_path :String) -> G return GdUnitResult.success(test_suite_path) -static func get_test_case_line_number(resource_path :String, func_name :String) -> int: +static func get_test_case_line_number(resource_path: String, func_name: String) -> int: var file := FileAccess.open(resource_path, FileAccess.READ) if file != null: var line_number := 0 @@ -318,7 +334,7 @@ static func get_test_case_line_number(resource_path :String, func_name :String) return -1 -func get_extends_classname(resource_path :String) -> String: +func get_extends_classname(resource_path: String) -> String: var file := FileAccess.open(resource_path, FileAccess.READ) if file != null: while not file.eof_reached(): @@ -335,15 +351,20 @@ func get_extends_classname(resource_path :String) -> String: return "" -static func add_test_case(resource_path :String, func_name :String) -> GdUnitResult: +static func add_test_case(resource_path: String, func_name: String) -> GdUnitResult: var script := load_with_disabled_warnings(resource_path) # count all exiting lines and add two as space to add new test case var line_number := count_lines(script) + 2 var func_body := TEST_FUNC_TEMPLATE.replace("${func_name}", func_name) if Engine.is_editor_hint(): - var settings := EditorInterface.get_editor_settings() - var ident_type :int = settings.get_setting("text_editor/behavior/indent/type") - var ident_size :int = settings.get_setting("text_editor/behavior/indent/size") + # NOTE: Avoid using EditorInterface and EditorSettings directly, + # as it causes compilation errors in exported projects. + @warning_ignore_start("unsafe_method_access") + var editor_interface: Object = Engine.get_singleton("EditorInterface") + var settings: Object = editor_interface.get_editor_settings() + var ident_type: int = settings.get_setting("text_editor/behavior/indent/type") + var ident_size: int = settings.get_setting("text_editor/behavior/indent/size") + @warning_ignore_restore("unsafe_method_access") if ident_type == 1: func_body = func_body.replace(" ", "".lpad(ident_size, " ")) script.source_code += func_body @@ -353,13 +374,14 @@ static func add_test_case(resource_path :String, func_name :String) -> GdUnitRe return GdUnitResult.success({ "path" : resource_path, "line" : line_number}) -static func count_lines(script : GDScript) -> int: +static func count_lines(script: Script) -> int: return script.source_code.split("\n").size() -static func test_suite_exists(test_suite_path :String) -> bool: +static func test_suite_exists(test_suite_path: String) -> bool: return FileAccess.file_exists(test_suite_path) + static func test_case_exists(test_suite_path :String, func_name :String) -> bool: if not test_suite_exists(test_suite_path): return false @@ -369,7 +391,8 @@ static func test_case_exists(test_suite_path :String, func_name :String) -> bool return true return false -static func create_test_case(test_suite_path :String, func_name :String, source_script_path :String) -> GdUnitResult: + +static func create_test_case(test_suite_path: String, func_name: String, source_script_path: String) -> GdUnitResult: if test_case_exists(test_suite_path, func_name): var line_number := get_test_case_line_number(test_suite_path, func_name) return GdUnitResult.success({ "path" : test_suite_path, "line" : line_number}) diff --git a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid index 40e703e0..f886a792 100644 --- a/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitTestSuiteScanner.gd.uid @@ -1 +1 @@ -uid://dl8h2rio363u4 +uid://clah81rbv4svl diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd b/addons/gdUnit4/src/core/GdUnitTools.gd index 2015fb8c..0742896a 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd +++ b/addons/gdUnit4/src/core/GdUnitTools.gd @@ -27,32 +27,36 @@ static func prints_verbose(message :String) -> void: prints(message) -@warning_ignore("unsafe_cast") static func free_instance(instance :Variant, use_call_deferred :bool = false, is_stdout_verbose := false) -> bool: if instance is Array: - for element :Variant in instance: + var as_array: Array = instance + for element: Variant in as_array: @warning_ignore("return_value_discarded") free_instance(element) - (instance as Array).clear() + as_array.clear() return true # do not free an already freed instance if not is_instance_valid(instance): return false # do not free a class refernece + @warning_ignore("unsafe_cast") if typeof(instance) == TYPE_OBJECT and (instance as Object).is_class("GDScriptNativeClass"): return false if is_stdout_verbose: print_verbose("GdUnit4:gc():free instance ", instance) + @warning_ignore("unsafe_cast") release_double(instance as Object) if instance is RefCounted: + @warning_ignore("unsafe_cast") (instance as RefCounted).notification(Object.NOTIFICATION_PREDELETE) # If scene runner freed we explicit await all inputs are processed if instance is GdUnitSceneRunnerImpl: + @warning_ignore("unsafe_cast") await (instance as GdUnitSceneRunnerImpl).await_input_processed() return true else: if instance is Timer: - var timer := instance as Timer + var timer: Timer = instance timer.stop() if use_call_deferred: timer.call_deferred("free") @@ -61,8 +65,9 @@ static func free_instance(instance :Variant, use_call_deferred :bool = false, is await (Engine.get_main_loop() as SceneTree).process_frame return true + @warning_ignore("unsafe_cast") if instance is Node and (instance as Node).get_parent() != null: - var node := instance as Node + var node: Node = instance if is_stdout_verbose: print_verbose("GdUnit4:gc():remove node from parent ", node.get_parent(), node) if use_call_deferred: @@ -73,8 +78,10 @@ static func free_instance(instance :Variant, use_call_deferred :bool = false, is if is_stdout_verbose: print_verbose("GdUnit4:gc():freeing `free()` the instance ", instance) if use_call_deferred: + @warning_ignore("unsafe_cast") (instance as Object).call_deferred("free") else: + @warning_ignore("unsafe_cast") (instance as Object).free() return !is_instance_valid(instance) @@ -120,6 +127,18 @@ static func release_double(instance :Object) -> void: instance.call("__release_double") -static func register_expect_interupted_by_timeout(test_suite :Node, test_case_name :String) -> void: - var test_case: _TestCase = test_suite.find_child(test_case_name, false, false) - test_case.expect_to_interupt() + +static func find_test_case(test_suite: Node, test_case_name: String, index := -1) -> _TestCase: + for test_case: _TestCase in test_suite.get_children(): + if test_case.test_name() == test_case_name: + if index != -1: + if test_case._test_case.attribute_index != index: + continue + return test_case + return null + + +static func register_expect_interupted_by_timeout(test_suite: Node, test_case_name: String) -> void: + var test_case := find_test_case(test_suite, test_case_name) + if test_case: + test_case.expect_to_interupt() diff --git a/addons/gdUnit4/src/core/GdUnitTools.gd.uid b/addons/gdUnit4/src/core/GdUnitTools.gd.uid index c760850d..b2c18b7e 100644 --- a/addons/gdUnit4/src/core/GdUnitTools.gd.uid +++ b/addons/gdUnit4/src/core/GdUnitTools.gd.uid @@ -1 +1 @@ -uid://eenopg7kvdj1 +uid://b8vfekq2yqc3g diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd b/addons/gdUnit4/src/core/GodotVersionFixures.gd index 9d5b6bb3..52993251 100644 --- a/addons/gdUnit4/src/core/GodotVersionFixures.gd +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd @@ -3,16 +3,6 @@ class_name GodotVersionFixures extends RefCounted -@warning_ignore("shadowed_global_identifier") -static func type_convert(value: Variant, type: int) -> Variant: - return convert(value, type) - - -@warning_ignore("shadowed_global_identifier") -static func convert(value: Variant, type: int) -> Variant: - return type_convert(value, type) - - # handle global_position fixed by https://github.com/godotengine/godot/pull/88473 static func set_event_global_position(event: InputEventMouseMotion, global_position: Vector2) -> void: if Engine.get_version_info().hex >= 0x40202 or Engine.get_version_info().hex == 0x40104: diff --git a/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid b/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid index 375664ca..7889a7a2 100644 --- a/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid +++ b/addons/gdUnit4/src/core/GodotVersionFixures.gd.uid @@ -1 +1 @@ -uid://cu2rm4jv1h1hf +uid://dwudifytnnnrc diff --git a/addons/gdUnit4/src/core/LocalTime.gd b/addons/gdUnit4/src/core/LocalTime.gd index cf6c22c8..fabaaf6f 100644 --- a/addons/gdUnit4/src/core/LocalTime.gd +++ b/addons/gdUnit4/src/core/LocalTime.gd @@ -77,12 +77,14 @@ static func elapsed(p_time_ms :int) -> String: return "%dms" % local_time_._millisecond -@warning_ignore("integer_division") # create from epoch timestamp in ms -func _init(time :int) -> void: +func _init(time: int) -> void: _time = time + @warning_ignore("integer_division") _hour = (time / MILLIS_PER_HOUR) % 24 + @warning_ignore("integer_division") _minute = (time / MILLIS_PER_MINUTE) % 60 + @warning_ignore("integer_division") _second = (time / MILLIS_PER_SECOND) % 60 _millisecond = time % 1000 diff --git a/addons/gdUnit4/src/core/LocalTime.gd.uid b/addons/gdUnit4/src/core/LocalTime.gd.uid index a016cad1..fac34966 100644 --- a/addons/gdUnit4/src/core/LocalTime.gd.uid +++ b/addons/gdUnit4/src/core/LocalTime.gd.uid @@ -1 +1 @@ -uid://b4d3jitra7e13 +uid://t0ssl1sp7gio diff --git a/addons/gdUnit4/src/core/_TestCase.gd b/addons/gdUnit4/src/core/_TestCase.gd index b8d9273d..58409e79 100644 --- a/addons/gdUnit4/src/core/_TestCase.gd +++ b/addons/gdUnit4/src/core/_TestCase.gd @@ -3,47 +3,23 @@ extends Node signal completed() -# default timeout 5min -const DEFAULT_TIMEOUT := -1 -const ARGUMENT_TIMEOUT := "timeout" -const ARGUMENT_SKIP := "do_skip" -const ARGUMENT_SKIP_REASON := "skip_reason" -var _iterations: int = 1 +var _test_case: GdUnitTestCase +var _attribute: TestCaseAttribute var _current_iteration: int = -1 -var _seed: int -var _fuzzers: Array[GdFunctionArgument] = [] -var _test_param_index := -1 -var _line_number: int = -1 -var _script_path: String -var _skipped := false -var _skip_reason := "" var _expect_to_interupt := false var _timer: Timer var _interupted: bool = false var _failed := false var _parameter_set_resolver: GdUnitTestParameterSetResolver var _is_disposed := false +var _func_state: Variant -var timeout: int = DEFAULT_TIMEOUT: - set(value): - timeout = value - get: - if timeout == DEFAULT_TIMEOUT: - timeout = GdUnitSettings.test_timeout() - return timeout - -@warning_ignore("shadowed_variable_base_class") -func configure(p_name: String, p_line_number: int, p_script_path: String, p_timeout: int=DEFAULT_TIMEOUT, p_fuzzers: Array[GdFunctionArgument]=[], p_iterations: int=1, p_seed: int=-1) -> _TestCase: - set_name(p_name) - _line_number = p_line_number - _fuzzers = p_fuzzers - _iterations = p_iterations - _seed = p_seed - _script_path = p_script_path - timeout = p_timeout - return self +func _init(test_case: GdUnitTestCase, attribute: TestCaseAttribute, fd: GdFunctionDescriptor) -> void: + _test_case = test_case + _attribute = attribute + set_function_descriptor(fd) func execute(p_test_parameter := Array(), p_iteration := 0) -> void: @@ -52,23 +28,53 @@ func execute(p_test_parameter := Array(), p_iteration := 0) -> void: if _current_iteration == - 1: _set_failure_handler() set_timeout() - if not p_test_parameter.is_empty(): + + if is_parameterized(): + execute_parameterized() + elif not p_test_parameter.is_empty(): update_fuzzers(p_test_parameter, p_iteration) - _execute_test_case(name, p_test_parameter) + _execute_test_case(test_name(), p_test_parameter) else: - _execute_test_case(name, []) + _execute_test_case(test_name(), []) await completed -func execute_paramaterized(p_test_parameter: Array) -> void: +func execute_parameterized() -> void: _failure_received(false) set_timeout() + + # Resolve parameter set at runtime to include runtime variables + var test_parameters := await _resolve_test_parameters(_test_case.attribute_index) + if test_parameters.is_empty(): + return + + await _execute_test_case(test_name(), test_parameters) + + +func _resolve_test_parameters(attribute_index: int) -> Array: + var result := _parameter_set_resolver.load_parameter_sets(get_parent()) + if result.is_error(): + do_skip(true, result.error_message()) + await (Engine.get_main_loop() as SceneTree).process_frame + completed.emit() + return [] + + # validate the parameter set + var parameter_sets: Array = result.value() + result = _parameter_set_resolver.validate(parameter_sets, attribute_index) + if result.is_error(): + do_skip(true, result.error_message()) + await (Engine.get_main_loop() as SceneTree).process_frame + completed.emit() + return [] + + @warning_ignore("unsafe_method_access") + var test_parameters: Array = parameter_sets[attribute_index].duplicate() # We need here to add a empty array to override the `test_parameters` to prevent initial "default" parameters from being used. # This prevents objects in the argument list from being unnecessarily re-instantiated. - var test_parameters := p_test_parameter.duplicate() # is strictly need to duplicate the paramters before extend test_parameters.append([]) - _execute_test_case(name, test_parameters) - await completed + + return test_parameters func dispose() -> void: @@ -78,13 +84,15 @@ func dispose() -> void: Engine.remove_meta("GD_TEST_FAILURE") stop_timer() _remove_failure_handler() - _fuzzers.clear() + _attribute.fuzzers.clear() @warning_ignore("shadowed_variable_base_class", "redundant_await") func _execute_test_case(name: String, test_parameter: Array) -> void: + # save the function state like GDScriptFunctionState to dispose at test timeout to prevent orphan state + _func_state = get_parent().callv(name, test_parameter) + await _func_state # needs at least on await otherwise it breaks the awaiting chain - await get_parent().callv(name, test_parameter) await (Engine.get_main_loop() as SceneTree).process_frame completed.emit() @@ -98,7 +106,7 @@ func update_fuzzers(input_values: Array, iteration: int) -> void: func set_timeout() -> void: if is_instance_valid(_timer): return - var time: float = timeout / 1000.0 + var time: float = _attribute.timeout / 1000.0 _timer = Timer.new() add_child(_timer) _timer.set_name("gdunit_test_case_timer_%d" % _timer.get_instance_id()) @@ -112,6 +120,8 @@ func set_timeout() -> void: func do_interrupt() -> void: _interupted = true + # We need to dispose manually the function state here + GdObjects.dispose_function_state(_func_state) if not is_expect_interupted(): var execution_context:= GdUnitThreadManager.get_current_context().get_execution_context() if is_fuzzed(): @@ -119,7 +129,7 @@ func do_interrupt() -> void: .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.fuzzer_interuped(_current_iteration, "timedout"))) else: execution_context.add_report(GdUnitReport.new()\ - .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(timeout))) + .create(GdUnitReport.INTERUPTED, line_number(), GdAssertMessages.test_timeout(_attribute.timeout))) completed.emit() @@ -167,74 +177,67 @@ func is_parameterized() -> bool: func is_skipped() -> bool: - return _skipped + return _attribute.is_skipped func skip_info() -> String: - return _skip_reason + return _attribute.skip_reason + + +func id() -> GdUnitGUID: + return _test_case.guid + + +func test_name() -> String: + return _test_case.test_name + + +@warning_ignore("native_method_override") +func get_name() -> StringName: + return _test_case.test_name func line_number() -> int: - return _line_number + return _test_case.line_number func iterations() -> int: - return _iterations + return _attribute.fuzzer_iterations func seed_value() -> int: - return _seed + return _attribute.test_seed func is_fuzzed() -> bool: - return not _fuzzers.is_empty() + return not _attribute.fuzzers.is_empty() func fuzzer_arguments() -> Array[GdFunctionArgument]: - return _fuzzers + return _attribute.fuzzers func script_path() -> String: - return _script_path + return _test_case.source_file func ResourcePath() -> String: - return _script_path + return _test_case.source_file func generate_seed() -> void: - if _seed != -1: - seed(_seed) + if _attribute.test_seed != -1: + seed(_attribute.test_seed) -func skip(skipped: bool, reason: String="") -> void: - _skipped = skipped - _skip_reason = reason +func do_skip(skipped: bool, reason: String="") -> void: + _attribute.is_skipped = skipped + _attribute.skip_reason = reason func set_function_descriptor(fd: GdFunctionDescriptor) -> void: _parameter_set_resolver = GdUnitTestParameterSetResolver.new(fd) -func set_test_parameter_index(index: int) -> void: - _test_param_index = index - - -func test_parameter_index() -> int: - return _test_param_index - - -func test_case_names() -> PackedStringArray: - return _parameter_set_resolver.build_test_case_names(self) - - -func load_parameter_sets() -> Array: - return _parameter_set_resolver.load_parameter_sets(self, true) - - -func parameter_set_resolver() -> GdUnitTestParameterSetResolver: - return _parameter_set_resolver - - func _to_string() -> String: - return "%s :%d (%dms)" % [get_name(), _line_number, timeout] + return "%s :%d (%dms)" % [get_name(), _test_case.line_number, _attribute.timeout] diff --git a/addons/gdUnit4/src/core/_TestCase.gd.uid b/addons/gdUnit4/src/core/_TestCase.gd.uid index 46826b29..be8def6a 100644 --- a/addons/gdUnit4/src/core/_TestCase.gd.uid +++ b/addons/gdUnit4/src/core/_TestCase.gd.uid @@ -1 +1 @@ -uid://dj7xab5frlre5 +uid://cyqyfl0hcfhug diff --git a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd new file mode 100644 index 00000000..cf7a2b97 --- /dev/null +++ b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd @@ -0,0 +1,76 @@ +class_name TestCaseAttribute +extends Resource +## Holds configuration and metadata for individual test cases.[br] +## [br] +## This class defines test behaviors and properties such as:[br] +## - Test timeouts[br] +## - Skip conditions[br] +## - Fuzzing parameters[br] +## - Random seed values[br] + + +## When set, no specific timeout value is configured and test will use the [code]test_timeout[/code][br] +## value from [GdUnitSettings]. +const DEFAULT_TIMEOUT := -1 + + +## The maximum time in milliseconds for test completion.[br] +## The test fails if execution exceeds this duration.[br] +## [br] +## When set to [constant DEFAULT_TIMEOUT], uses the value from [method GdUnitSettings.test_timeout]. +var timeout: int = DEFAULT_TIMEOUT: + set(value): + timeout = value + get: + if timeout == DEFAULT_TIMEOUT: + # get the default timeout from the settings + timeout = GdUnitSettings.test_timeout() + return timeout + + +## The seed used for random number generation in the test.[br] +## Ensures reproducible results for randomized test scenarios.[br] +## A value of -1 indicates no specific seed is set. +var test_seed: int = -1 + + +## Controls whether this test should be skipped during execution.[br] +## Useful for temporarily disabling tests without removing them. +var is_skipped := false + + +## Documents why the test is being skipped.[br] +## [br] +## Should explain the reason for skipping and ideally include:[br] +## - Why the test was disabled[br] +## - Under what conditions it should be re-enabled[br] +## - Any related issues or tickets +var skip_reason := "Unknown" + + +## Number of iterations to run when using fuzzers.[br] +## [br] +## Fuzzers generate random test data to help find edge cases.[br] +## Higher values provide better coverage but increase test duration. +var fuzzer_iterations: int = Fuzzer.ITERATION_DEFAULT_COUNT + + +## Array of fuzzer configurations for test parameters.[br] +## [br] +## Each [GdFunctionArgument] defines how random test data[br] +## should be generated for a particular parameter. +var fuzzers: Array[GdFunctionArgument] = [] + + +# There is a bug in `duplicate` see https://github.com/godotengine/godot/issues/98644 +# we need in addition to overwrite default values with the source values +@warning_ignore("native_method_override") +func clone() -> Resource: + var copy: TestCaseAttribute = TestCaseAttribute.new() + copy.timeout = timeout + copy.test_seed = test_seed + copy.is_skipped = is_skipped + copy.skip_reason = skip_reason + copy.fuzzer_iterations = fuzzer_iterations + copy.fuzzers = fuzzers.duplicate() + return copy diff --git a/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid new file mode 100644 index 00000000..2f6f116b --- /dev/null +++ b/addons/gdUnit4/src/core/attributes/TestCaseAttribute.gd.uid @@ -0,0 +1 @@ +uid://x4lrqpt63ld0 diff --git a/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid index c3900ab4..05283d3b 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitCommand.gd.uid @@ -1 +1 @@ -uid://dh12oaaicg21s +uid://da20vybopa1xx diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd index 76a138af..1f64e2ad 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd @@ -26,8 +26,20 @@ const SETTINGS_SHORTCUT_MAPPING := { GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE, GdUnitSettings.SHORTCUT_EDITOR_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG, GdUnitSettings.SHORTCUT_EDITOR_CREATE_TEST : GdUnitShortcut.ShortCut.CREATE_TEST, - GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTCASE, - GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG + GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST : GdUnitShortcut.ShortCut.RUN_TESTSUITE, + GdUnitSettings.SHORTCUT_FILESYSTEM_RUN_TEST_DEBUG : GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG +} + +const CommandMapping := { + GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL: GdUnitCommandHandler.CMD_RUN_OVERALL, + GdUnitShortcut.ShortCut.RUN_TESTCASE: GdUnitCommandHandler.CMD_RUN_TESTCASE, + GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG, + GdUnitShortcut.ShortCut.RUN_TESTSUITE: GdUnitCommandHandler.CMD_RUN_TESTSUITE, + GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG, + GdUnitShortcut.ShortCut.RERUN_TESTS: GdUnitCommandHandler.CMD_RERUN_TESTS, + GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG: GdUnitCommandHandler.CMD_RERUN_TESTS_DEBUG, + GdUnitShortcut.ShortCut.STOP_TEST_RUN: GdUnitCommandHandler.CMD_STOP_TEST_RUN, + GdUnitShortcut.ShortCut.CREATE_TEST: GdUnitCommandHandler.CMD_CREATE_TESTCASE, } # the current test runner config @@ -68,8 +80,8 @@ func _init() -> void: register_command(GdUnitCommand.new(CMD_RUN_OVERALL, is_not_running, cmd_run_overall.bind(true), GdUnitShortcut.ShortCut.RUN_TESTS_OVERALL)) register_command(GdUnitCommand.new(CMD_RUN_TESTCASE, is_not_running, cmd_editor_run_test.bind(false), GdUnitShortcut.ShortCut.RUN_TESTCASE)) register_command(GdUnitCommand.new(CMD_RUN_TESTCASE_DEBUG, is_not_running, cmd_editor_run_test.bind(true), GdUnitShortcut.ShortCut.RUN_TESTCASE_DEBUG)) - register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE, is_not_running, cmd_run_test_suites.bind(false))) - register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE_DEBUG, is_not_running, cmd_run_test_suites.bind(true))) + register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE, is_not_running, cmd_run_test_suites.bind(false), GdUnitShortcut.ShortCut.RUN_TESTSUITE)) + register_command(GdUnitCommand.new(CMD_RUN_TESTSUITE_DEBUG, is_not_running, cmd_run_test_suites.bind(true), GdUnitShortcut.ShortCut.RUN_TESTSUITE_DEBUG)) register_command(GdUnitCommand.new(CMD_RERUN_TESTS, is_not_running, cmd_run.bind(false), GdUnitShortcut.ShortCut.RERUN_TESTS)) register_command(GdUnitCommand.new(CMD_RERUN_TESTS_DEBUG, is_not_running, cmd_run.bind(true), GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG)) register_command(GdUnitCommand.new(CMD_CREATE_TESTCASE, is_not_running, cmd_create_test, GdUnitShortcut.ShortCut.CREATE_TEST)) @@ -156,7 +168,7 @@ func get_shortcut_action(shortcut_type: GdUnitShortcut.ShortCut) -> GdUnitShortc func get_shortcut_command(p_shortcut: GdUnitShortcut.ShortCut) -> String: - return GdUnitShortcut.CommandMapping.get(p_shortcut, "unknown command") + return CommandMapping.get(p_shortcut, "unknown command") func register_command(p_command: GdUnitCommand) -> void: @@ -167,11 +179,22 @@ func command(cmd_name: String) -> GdUnitCommand: return _commands.get(cmd_name) -func cmd_run_test_suites(test_suite_paths: PackedStringArray, debug: bool, rerun := false) -> void: +func cmd_run_test_suites(scripts: Array[Script], debug: bool, rerun := false) -> void: + # Update test discovery + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + var tests_to_execute: Array[GdUnitTestCase] = [] + for script in scripts: + GdUnitTestDiscoverer.discover_tests(script, func(test_case: GdUnitTestCase) -> void: + tests_to_execute.append(test_case) + GdUnitTestDiscoverSink.discover(test_case) + ) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute) + # create new runner runner_config for fresh run otherwise use saved one if not rerun: var result := _runner_config.clear()\ - .add_test_suites(test_suite_paths)\ + .add_test_cases(tests_to_execute)\ .save_config() if result.is_error(): push_error(result.error_message()) @@ -179,11 +202,27 @@ func cmd_run_test_suites(test_suite_paths: PackedStringArray, debug: bool, rerun cmd_run(debug) -func cmd_run_test_case(test_suite_resource_path: String, test_case: String, test_param_index: int, debug: bool, rerun := false) -> void: +func cmd_run_test_case(script: Script, test_case: String, test_param_index: int, debug: bool, rerun := false) -> void: + # Update test discovery + var tests_to_execute: Array[GdUnitTestCase] = [] + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + # We filter for a single test + if test.test_name == test_case: + # We only add selected parameterized test to the execution list + if test_param_index == -1: + tests_to_execute.append(test) + elif test.attribute_index == test_param_index: + tests_to_execute.append(test) + GdUnitTestDiscoverSink.discover(test) + ) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + GdUnitTestDiscoverer.console_log_discover_results(tests_to_execute) + # create new runner config for fresh run otherwise use saved one if not rerun: var result := _runner_config.clear()\ - .add_test_case(test_suite_resource_path, test_case, test_param_index)\ + .add_test_cases(tests_to_execute)\ .save_config() if result.is_error(): push_error(result.error_message()) @@ -191,10 +230,21 @@ func cmd_run_test_case(test_suite_resource_path: String, test_case: String, test cmd_run(debug) +func cmd_run_tests(tests_to_execute: Array[GdUnitTestCase], debug: bool) -> void: + # Save tests to runner config before execute + var result := _runner_config.clear()\ + .add_test_cases(tests_to_execute)\ + .save_config() + if result.is_error(): + push_error(result.error_message()) + return + cmd_run(debug) + + func cmd_run_overall(debug: bool) -> void: - var test_suite_paths: PackedStringArray = GdUnitCommandHandler.scan_all_test_directories(GdUnitSettings.test_root_folder()) + var tests_to_execute := await GdUnitTestDiscoverer.run() var result := _runner_config.clear()\ - .add_test_suites(test_suite_paths)\ + .add_test_cases(tests_to_execute)\ .save_config() if result.is_error(): push_error(result.error_message()) @@ -206,6 +256,7 @@ func cmd_run(debug: bool) -> void: # don't start is already running if _is_running: return + # save current selected excution config var server_port: int = Engine.get_meta("gdunit_server_port") var result := _runner_config.set_server_port(server_port).save_config() @@ -240,24 +291,26 @@ func cmd_stop(client_id: int) -> void: func cmd_editor_run_test(debug: bool) -> void: - var cursor_line := active_base_editor().get_caret_line() - #run test case? - var regex := RegEx.new() - @warning_ignore("return_value_discarded") - regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") - var result := regex.search(active_base_editor().get_line(cursor_line)) - if result: - var func_name := result.get_string(2).strip_edges() - prints("Run test:", func_name, "debug", debug) - if func_name.begins_with("test_"): - cmd_run_test_case(active_script().resource_path, func_name, -1, debug) - return + if is_active_script_editor(): + var cursor_line := active_base_editor().get_caret_line() + #run test case? + var regex := RegEx.new() + @warning_ignore("return_value_discarded") + regex.compile("(^func[ ,\t])(test_[a-zA-Z0-9_]*)") + var result := regex.search(active_base_editor().get_line(cursor_line)) + if result: + var func_name := result.get_string(2).strip_edges() + if func_name.begins_with("test_"): + cmd_run_test_case(active_script(), func_name, -1, debug) + return # otherwise run the full test suite - var selected_test_suites := [active_script().resource_path] + var selected_test_suites: Array[Script] = [active_script()] cmd_run_test_suites(selected_test_suites, debug) func cmd_create_test() -> void: + if not is_active_script_editor(): + return var cursor_line := active_base_editor().get_caret_line() var result := GdUnitTestSuiteBuilder.create(active_script(), cursor_line) if result.is_error(): @@ -273,40 +326,9 @@ func cmd_create_test() -> void: func cmd_discover_tests() -> void: await GdUnitTestDiscoverer.run() -static func scan_all_test_directories(root: String) -> PackedStringArray: - var base_directory := "res://" - # If the test root folder is configured as blank, "/", or "res://", use the root folder as described in the settings panel - if root.is_empty() or root == "/" or root == base_directory: - return [base_directory] - return scan_test_directories(base_directory, root, []) - -static func scan_test_directories(base_directory: String, test_directory: String, test_suite_paths: PackedStringArray) -> PackedStringArray: - print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory]) - for directory in DirAccess.get_directories_at(base_directory): - if directory.begins_with("."): - continue - var current_directory := normalize_path(base_directory + "/" + directory) - if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): - continue - if match_test_directory(directory, test_directory): - @warning_ignore("return_value_discarded") - test_suite_paths.append(current_directory) - else: - @warning_ignore("return_value_discarded") - scan_test_directories(current_directory, test_directory, test_suite_paths) - return test_suite_paths - - -static func normalize_path(path: String) -> String: - return path.replace("///", "//") - - -static func match_test_directory(directory: String, test_directory: String) -> bool: - return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://" - func run_debug_mode() -> void: - EditorInterface.play_custom_scene("res://addons/gdUnit4/src/core/GdUnitRunner.tscn") + EditorInterface.play_custom_scene("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn") _is_running = true @@ -317,11 +339,15 @@ func run_release_mode() -> void: arguments.append("--no-window") arguments.append("--path") arguments.append(ProjectSettings.globalize_path("res://")) - arguments.append("res://addons/gdUnit4/src/core/GdUnitRunner.tscn") + arguments.append("res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn") _current_runner_process_id = OS.create_process(OS.get_executable_path(), arguments, false); _is_running = true +func is_active_script_editor() -> bool: + return EditorInterface.get_script_editor().get_current_editor() != null + + func active_base_editor() -> TextEdit: return EditorInterface.get_script_editor().get_current_editor().get_base_editor() @@ -335,7 +361,7 @@ func active_script() -> Script: # signals handles ################################################################################ func _on_event(event: GdUnitEvent) -> void: - if event.type() == GdUnitEvent.STOP: + if event.type() == GdUnitEvent.SESSION_CLOSE: cmd_stop(_client_id) @@ -357,7 +383,11 @@ func _on_settings_changed(property: GdUnitProperty) -> void: var value: PackedInt32Array = property.value() var input_event := create_shortcut_input_even(value) prints("Shortcut changed: '%s' to '%s'" % [GdUnitShortcut.ShortCut.keys()[shortcut], input_event.as_text()]) - register_shortcut(shortcut, input_event) + var action := get_shortcut_action(shortcut) + if action != null: + action.update_shortcut(input_event) + else: + register_shortcut(shortcut, input_event) if property.name() == GdUnitSettings.TEST_DISCOVER_ENABLED: var timer :SceneTreeTimer = (Engine.get_main_loop() as SceneTree).create_timer(3) @warning_ignore("return_value_discarded") diff --git a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid index 0a484969..e5383285 100644 --- a/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitCommandHandler.gd.uid @@ -1 +1 @@ -uid://covwuntwthc0o +uid://cgo8wyef7556y diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd index bf8ac83a..0535869b 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd +++ b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd @@ -7,28 +7,20 @@ enum ShortCut { RUN_TESTS_OVERALL, RUN_TESTCASE, RUN_TESTCASE_DEBUG, + RUN_TESTSUITE, + RUN_TESTSUITE_DEBUG, RERUN_TESTS, RERUN_TESTS_DEBUG, STOP_TEST_RUN, CREATE_TEST, } - -const CommandMapping = { - ShortCut.RUN_TESTS_OVERALL: GdUnitCommandHandler.CMD_RUN_OVERALL, - ShortCut.RUN_TESTCASE: GdUnitCommandHandler.CMD_RUN_TESTCASE, - ShortCut.RUN_TESTCASE_DEBUG: GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG, - ShortCut.RERUN_TESTS: GdUnitCommandHandler.CMD_RERUN_TESTS, - ShortCut.RERUN_TESTS_DEBUG: GdUnitCommandHandler.CMD_RERUN_TESTS_DEBUG, - ShortCut.STOP_TEST_RUN: GdUnitCommandHandler.CMD_STOP_TEST_RUN, - ShortCut.CREATE_TEST: GdUnitCommandHandler.CMD_CREATE_TESTCASE, -} - - const DEFAULTS_MACOS := { ShortCut.NONE : [], ShortCut.RUN_TESTCASE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5], ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTSUITE : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_META, Key.KEY_ALT, Key.KEY_F6], ShortCut.RUN_TESTS_OVERALL : [Key.KEY_META, Key.KEY_F7], ShortCut.STOP_TEST_RUN : [Key.KEY_META, Key.KEY_F8], ShortCut.RERUN_TESTS : [Key.KEY_META, Key.KEY_F5], @@ -39,7 +31,9 @@ const DEFAULTS_MACOS := { const DEFAULTS_WINDOWS := { ShortCut.NONE : [], ShortCut.RUN_TESTCASE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5], - ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTCASE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6], + ShortCut.RUN_TESTSUITE : [Key.KEY_CTRL, Key.KEY_ALT, Key.KEY_F5], + ShortCut.RUN_TESTSUITE_DEBUG : [Key.KEY_CTRL,Key.KEY_ALT, Key.KEY_F6], ShortCut.RUN_TESTS_OVERALL : [Key.KEY_CTRL, Key.KEY_F7], ShortCut.STOP_TEST_RUN : [Key.KEY_CTRL, Key.KEY_F8], ShortCut.RERUN_TESTS : [Key.KEY_CTRL, Key.KEY_F5], diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid index 60aee3e6..152b6733 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitShortcut.gd.uid @@ -1 +1 @@ -uid://bwr4bshy5s87f +uid://4474hld76v4k diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd index 94b082e9..c49e83e5 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd +++ b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd @@ -32,5 +32,9 @@ var command: String: return command +func update_shortcut(input_event: InputEventKey) -> void: + shortcut.set_events([input_event]) + + func _to_string() -> String: return "GdUnitShortcutAction: %s (%s) -> %s" % [GdUnitShortcut.ShortCut.keys()[type], shortcut.get_as_text(), command] diff --git a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid index 415bf9d6..e469addf 100644 --- a/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid +++ b/addons/gdUnit4/src/core/command/GdUnitShortcutAction.gd.uid @@ -1 +1 @@ -uid://cub4g6jgmehwq +uid://duy84b0dubou0 diff --git a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd new file mode 100644 index 00000000..00332f90 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd @@ -0,0 +1,46 @@ +## A class representing a globally unique identifier for GdUnit test elements. +## Uses random values to generate unique identifiers that can be used +## to track and reference test cases and suites across the test framework. +class_name GdUnitGUID +extends RefCounted + + +## The internal string representation of the GUID. +## Generated using Godot's ResourceUID system when no existing GUID is provided. +var _guid: String + + +## Creates a new GUID instance. +## If no GUID is provided, generates a new one using Godot's ResourceUID system. +func _init(from_guid: String = "") -> void: + if from_guid.is_empty(): + _guid = _generate_guid() + else: + _guid = from_guid + + +## Compares this GUID with another for equality. +## Returns true if both GUIDs represent the same unique identifier. +func equals(other: GdUnitGUID) -> bool: + return other._guid == _guid + + +## Generates a custom GUID using random bytes.[br] +## The format uses 16 random bytes encoded to hex and formatted with hyphens. +static func _generate_guid() -> String: + # Pre-allocate array with exact size needed + var bytes := PackedByteArray() + bytes.resize(16) + + # Fill with random bytes + for i in range(16): + bytes[i] = randi() % 256 + + bytes[6] = (bytes[6] & 0x0f) | 0x40 + bytes[8] = (bytes[8] & 0x3f) | 0x80 + + return bytes.hex_encode().insert(8, "-").insert(16, "-").insert(24, "-") + + +func _to_string() -> String: + return _guid diff --git a/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid new file mode 100644 index 00000000..1d4bb3cb --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitGUID.gd.uid @@ -0,0 +1 @@ +uid://djg12w0iicfai diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd new file mode 100644 index 00000000..766f9eab --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd @@ -0,0 +1,125 @@ +## GdUnitTestCase +## A class representing a single test case in GdUnit4. +## This class is used as a data container to hold all relevant information about a test case, +## including its location, dependencies, and metadata for test discovery and execution. + +class_name GdUnitTestCase +extends RefCounted + +## A unique identifier for the test case. Used to track and reference specific test instances. +var guid := GdUnitGUID.new() + +## The resource path to the test suite +var suite_resource_path: String + +## The name of the test method/function. Should start with "test_" prefix. +var test_name: String + +## The class name of the test suite containing this test case. +var suite_name: String + +## The fully qualified name of the test case following C# namespace pattern: +## Constructed from the folder path (where folders are dot-separated), the test suite name, and the test case name. +## All parts are joined by dots: {folder1.folder2.folder3}.{suite_name}.{test_name} +var fully_qualified_name: String + +var display_name: String + +## Index tracking test attributes for ordered execution. Default is 0. +## Higher values indicate later execution in the test sequence. +var attribute_index: int + +## Flag indicating if this test requires the Godot runtime environment. +## Tests requiring runtime cannot be executed in isolation. +var require_godot_runtime: bool = true + +## The path to the source file containing this test case. +## Used for test discovery and execution. +var source_file: String + +## Optional holds the assembly location for C# tests +var assembly_location: String = "" + +## The line number where the test case is defined in the source file. +## Used for navigation and error reporting. +var line_number: int = -1 + +## Additional metadata about the test case, such as: +## - tags: Array[String] - Test categories/tags for filtering +## - timeout: int - Maximum execution time in milliseconds +## - skip: bool - Whether the test should be skipped +## - dependencies: Array[String] - Required test dependencies +var metadata: Dictionary = {} + + +static func from_dict(dict: Dictionary) -> GdUnitTestCase: + var test := GdUnitTestCase.new() + test.guid = GdUnitGUID.new(str(dict["guid"])) + test.suite_resource_path = dict["suite_resource_path"] if dict.has("suite_resource_path") else dict["source_file"] + test.suite_name = dict["managed_type"] + test.test_name = dict["test_name"] + test.display_name = dict["simple_name"] + test.fully_qualified_name = dict["fully_qualified_name"] + test.attribute_index = dict["attribute_index"] + test.source_file = dict["source_file"] + test.line_number = dict["line_number"] + test.require_godot_runtime = dict["require_godot_runtime"] + test.assembly_location = dict["assembly_location"] + return test + + +static func to_dict(test: GdUnitTestCase) -> Dictionary: + return { + "guid": test.guid._guid, + "suite_resource_path": test.suite_resource_path, + "managed_type": test.suite_name, + "test_name" : test.test_name, + "simple_name" : test.display_name, + "fully_qualified_name" : test.fully_qualified_name, + "attribute_index" : test.attribute_index, + "source_file" : test.source_file, + "line_number" : test.line_number, + "require_godot_runtime" : test.require_godot_runtime, + "assembly_location" : test.assembly_location + } + + +static func from(_suite_resource_path: String, _source_file: String, _line_number: int, _test_name: String, _attribute_index := -1, _test_parameters := "") -> GdUnitTestCase: + if(_source_file == null or _source_file.is_empty()): + prints(_test_name) + + assert(_test_name != null and not _test_name.is_empty(), "Precondition: The parameter 'test_name' is not set") + assert(_source_file != null and not _source_file.is_empty(), "Precondition: The parameter 'source_file' is not set") + + var test := GdUnitTestCase.new() + test.suite_resource_path = _suite_resource_path + test.test_name = _test_name + test.source_file = _source_file + test.line_number = _line_number + test.attribute_index = _attribute_index + test._build_suite_name() + test._build_display_name(_test_parameters) + test._build_fully_qualified_name(_suite_resource_path) + return test + + +func _build_suite_name() -> void: + suite_name = source_file.get_file().get_basename() + assert(suite_name != null and not suite_name.is_empty(), "Precondition: The parameter 'suite_name' can't be resolved") + + +func _build_display_name(_test_parameters: String) -> void: + if attribute_index == -1: + display_name = test_name + else: + display_name = "%s:%d (%s)" % [test_name, attribute_index, _test_parameters.trim_prefix("[").trim_suffix("]").replace('"', "'")] + + +func _build_fully_qualified_name(_resource_path: String) -> void: + var name_space := _resource_path.trim_prefix("res://").trim_suffix(".gd").trim_suffix(".cs").replace("/", ".") + + if attribute_index == -1: + fully_qualified_name = "%s.%s" % [name_space, test_name] + else: + fully_qualified_name = "%s.%s.%s" % [name_space, test_name, display_name] + assert(fully_qualified_name != null and not fully_qualified_name.is_empty(), "Precondition: The parameter 'fully_qualified_name' can't be resolved") diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid new file mode 100644 index 00000000..24a3b713 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestCase.gd.uid @@ -0,0 +1 @@ +uid://xpqoaxpt8ndk diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd index c3e4d2c0..6af8255a 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd @@ -1,132 +1,302 @@ -extends RefCounted - - -# Caches all test indices for parameterized tests -class TestCaseIndicesCache: - var _cache := {} +## Guards and tracks test case changes during test discovery and file modifications.[br] +## [br] +## This guard maintains a cache of discovered tests to track changes between test runs and during[br] +## file modifications. It is optimized for performance using simple but effective test identity checks.[br] +## [br] +## Test Change Detection:[br] +## - Moved tests: The test implementation remains at a different line number[br] +## - Renamed tests: The test line position remains but the test name changed[br] +## - Deleted tests: A previously discovered test was removed[br] +## - Added tests: A new test was discovered[br] +## [br] +## Cache Management:[br] +## - Maintains test identity through unique GdUnitTestCase GUIDs[br] +## - Maps source files to their discovered test cases[br] +## - Tracks only essential metadata (line numbers, names) to minimize memory use[br] +## [br] +## Change Detection Strategy:[br] +## The guard uses a lightweight approach by comparing only line numbers and test names.[br] +## This avoids expensive operations like test content parsing or similarity checks.[br] +## [br] +## Event Handling:[br] +## - Emits events on test changes through GdUnitSignals[br] +## - Synchronizes cache with test discovery events[br] +## - Notifies UI about test changes[br] +## [br] +## Example usage:[br] +## [codeblock] +## # Create guard for tracking test changes +## var guard := GdUnitTestDiscoverGuard.new() +## +## # Connect to test discovery events +## GdUnitSignals.instance().gdunit_test_discovered.connect(guard.sync_test_added) +## +## # Discover tests and track changes +## await guard.discover(test_script) +## [/codeblock] +class_name GdUnitTestDiscoverGuard +extends Object + + + +static func instance() -> GdUnitTestDiscoverGuard: + return GdUnitSingleton.instance("GdUnitTestDiscoverGuard", func() -> GdUnitTestDiscoverGuard: + return GdUnitTestDiscoverGuard.new() + ) + + +## Maps source files to their discovered test cases.[br] +## [br] +## Key: Test suite source file path[br] +## Value: Array of [class GdUnitTestCase] instances +var _discover_cache := {} - func _key(resource_path: String, test_name: String) -> StringName: - return &"%s_%s" % [resource_path, test_name] +## Tracks discovered test changes for debug purposes.[br] +## [br] +## Available in debug mode only. Contains dictionaries:[br] +## - changed_tests: Tests that were moved or renamed[br] +## - deleted_tests: Tests that were removed[br] +## - added_tests: New tests that were discovered +var _discovered_changes := {} - func contains_test_case(resource_path: String, test_name: String) -> bool: - return _cache.has(_key(resource_path, test_name)) +## Controls test change debug tracking.[br] +## [br] +## When true, maintains _discovered_changes for debugging.[br] +## Used primarily in tests to verify change detection. +var _is_debug := false - func validate(resource_path: String, test_name: String, indices: PackedStringArray) -> bool: - var cached_indicies: PackedStringArray = _cache[_key(resource_path, test_name)] - return GdArrayTools.has_same_content(cached_indicies, indices) +## Creates a new guard instance.[br] +## [br] +## [param is_debug] When true, enables change tracking for debugging. +func _init(is_debug := false) -> void: + _is_debug = is_debug + # Register for discovery events to sync the cache + @warning_ignore("return_value_discarded") + GdUnitSignals.instance().gdunit_test_discover_added.connect(sync_test_added) + GdUnitSignals.instance().gdunit_test_discover_deleted.connect(sync_test_deleted) + GdUnitSignals.instance().gdunit_test_discover_modified.connect(sync_test_modified) + GdUnitSignals.instance().gdunit_event.connect(handle_discover_events) + + +## Adds a discovered test to the cache.[br] +## [br] +## [param test_case] The test case to add to the cache. +func sync_test_added(test_case: GdUnitTestCase) -> void: + var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(test_case.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)) + test_cases.append(test_case) + + +## Removes a test from the cache.[br] +## [br] +## [param test_case] The test case to remove from the cache. +func sync_test_deleted(test_case: GdUnitTestCase) -> void: + var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(test_case.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)) + test_cases.erase(test_case) + + +## Updates a test from the cache.[br] +## [br] +## [param test_case] The test case to update from the cache. +func sync_test_modified(changed_test: GdUnitTestCase) -> void: + var test_cases: Array[GdUnitTestCase] = _discover_cache.get_or_add(changed_test.source_file, Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)) + for test in test_cases: + if test.guid == changed_test.guid: + test.test_name = changed_test.test_name + test.display_name = changed_test.display_name + test.line_number = changed_test.line_number + break + + +## Handles test discovery events.[br] +## [br] +## Resets the cache when a new discovery starts.[br] +## [param event] The discovery event to handle. +func handle_discover_events(event: GdUnitEvent) -> void: + # reset the cache on fresh discovery + if event.type() == GdUnitEvent.DISCOVER_START: + _discover_cache = {} + + +## Registers a callback for discovered tests.[br] +## [br] +## Default sink writes to [class GdUnitTestDiscoverSink]. +static func default_discover_sink(test_case: GdUnitTestCase) -> void: + GdUnitTestDiscoverSink.discover(test_case) + + +## Finds a test case by its unique identifier.[br] +## [br] +## Searches through all cached test cases across all test suites[br] +## to find a test with the matching GUID.[br] +## [br] +## [param id] The GUID of the test to find[br] +## Returns the matching test case or null if not found. +func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase: + for test_sets: Array[GdUnitTestCase] in _discover_cache.values(): + for test in test_sets: + if test.guid.equals(id): + return test + + return null + + +func get_discovered_tests() -> Array[GdUnitTestCase]: + var discovered_tests: Array[GdUnitTestCase] = [] + for test_sets: Array[GdUnitTestCase] in _discover_cache.values(): + discovered_tests.append_array(test_sets) + return discovered_tests + + +## Discovers tests in a script and tracks changes.[br] +## [br] +## Handles both GDScript and C# test suites.[br] +## The guard maintains test identity through changes.[br] +## [br] +## [param script] The test script to analyze[br] +## [param discover_sink] Optional callback for test discovery events +func discover(script: Script, discover_sink: Callable = default_discover_sink) -> void: + # Verify the script has no errors before run test discovery + var result := script.reload(true) + if result != OK: + return - func sync(resource_path: String, test_name: String, indices: PackedStringArray) -> void: - if indices.is_empty(): - _cache[_key(resource_path, test_name)] = [] - else: - _cache[_key(resource_path, test_name)] = indices + if _is_debug: + _discovered_changes["changed_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + _discovered_changes["deleted_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + _discovered_changes["added_tests"] = Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase) -# contains all tracked test suites where discovered since editor start -# key : test suite resource_path -# value: the list of discovered test case names -var _discover_cache := {} + if GdUnitTestSuiteScanner.is_test_suite(script): + # for cs scripts we need to recomplie before discover new tests + if script.get_class() == "CSharpScript": + await rebuild_project(script) -var discovered_test_case_indices_cache := TestCaseIndicesCache.new() + # rediscover all tests + var source_file := script.resource_path + var discovered_tests: Array[GdUnitTestCase] = [] + GdUnitTestDiscoverer.discover_tests(script, func(test_case: GdUnitTestCase) -> void: + discovered_tests.append(test_case) + ) -func _init() -> void: - # Register for discovery events to sync the cache - @warning_ignore("return_value_discarded") - GdUnitSignals.instance().gdunit_add_test_suite.connect(sync_cache) - - -func sync_cache(dto: GdUnitTestSuiteDto) -> void: - var resource_path := ProjectSettings.localize_path(dto.path()) - var discovered_test_cases: Array[String] = [] - for test_case in dto.test_cases(): - discovered_test_cases.append(test_case.name()) - discovered_test_case_indices_cache.sync(resource_path, test_case.name(), test_case.test_case_names()) - _discover_cache[resource_path] = discovered_test_cases - - -func discover(script: Script) -> void: - # for cs scripts we need to recomplie before discover new tests - if GdObjects.is_cs_script(script): - await rebuild_project(script) - - if GdObjects.is_test_suite(script): - # a new test suite is discovered - var script_path := ProjectSettings.localize_path(script.resource_path) - var scanner := GdUnitTestSuiteScanner.new() - var test_suite := scanner._parse_test_suite(script) - var suite_name := test_suite.get_name() - - if not _discover_cache.has(script_path): - var dto :GdUnitTestSuiteDto = GdUnitTestSuiteDto.of(test_suite) - GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestSuiteAdded.new(script_path, suite_name, dto)) - sync_cache(dto) - test_suite.queue_free() + # The suite is never discovered, we add all discovered tests + if not _discover_cache.has(source_file): + for test_case in discovered_tests: + discover_sink.call(test_case) return - var discovered_test_cases :Array[String] = _discover_cache.get(script_path, [] as Array[String]) - var script_test_cases := extract_test_functions(test_suite) - - # first detect removed/renamed tests - var tests_removed := PackedStringArray() - for test_case in discovered_test_cases: - if not script_test_cases.has(test_case): - @warning_ignore("return_value_discarded") - tests_removed.append(test_case) - # second detect new added tests - var tests_added :Array[String] = [] - for test_case in script_test_cases: - if not discovered_test_cases.has(test_case): - tests_added.append(test_case) - - # We need to scan for parameterized test because of possible test data changes - # For more details look at https://github.com/MikeSchulze/gdUnit4/issues/592 - for test_case_name in script_test_cases: - if discovered_test_case_indices_cache.contains_test_case(script_path, test_case_name): - var test_case: _TestCase = test_suite.find_child(test_case_name, false, false) - var test_indices := test_case.test_case_names() - if not discovered_test_case_indices_cache.validate(script_path, test_case_name, test_indices): - if !tests_removed.has(test_case_name): - tests_removed.append(test_case_name) - if !tests_added.has(test_case_name): - tests_added.append(test_case_name) - discovered_test_case_indices_cache.sync(script_path, test_case_name, test_indices) - - # finally notify changes to the inspector - if not tests_removed.is_empty() or not tests_added.is_empty(): - # emit deleted tests - for test_name in tests_removed: - discovered_test_cases.erase(test_name) - GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestRemoved.new(script_path, suite_name, test_name)) - - # emit new discovered tests - for test_name in tests_added: - discovered_test_cases.append(test_name) - var test_case := test_suite.find_child(test_name, false, false) - var dto := GdUnitTestCaseDto.new() - dto = dto.deserialize(dto.serialize(test_case)) - GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverTestAdded.new(script_path, suite_name, dto)) - # if the parameterized test fresh added we need to sync the cache - if not discovered_test_case_indices_cache.contains_test_case(script_path, test_name): - discovered_test_case_indices_cache.sync(script_path, test_name, dto.test_case_names()) - - # update the cache - _discover_cache[script_path] = discovered_test_cases - test_suite.queue_free() - - -func extract_test_functions(test_suite :Node) -> PackedStringArray: - return test_suite.get_children()\ - .filter(func(child: Node) -> bool: return is_instance_of(child, _TestCase))\ - .map(func (child: Node) -> String: return child.get_name()) - - -func is_paramaterized_test(test_suite :Node, test_case_name: String) -> bool: - return test_suite.get_children()\ - .filter(func(child: Node) -> bool: return child.name == test_case_name)\ - .any(func (test: _TestCase) -> bool: return test.is_parameterized()) + sync_moved_tests(source_file, discovered_tests) + sync_renamed_tests(source_file, discovered_tests) + sync_deleted_tests(source_file, discovered_tests) + sync_added_tests(source_file, discovered_tests, discover_sink) + + +## Synchronizes moved tests between discover cycles.[br] +## [br] +## A test is considered moved when:[br] +## - It has the same name[br] +## - But a different line number[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests +func sync_moved_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + for discovered_test in discovered_tests: + # lookup in cache + var original_tests: Array[GdUnitTestCase] = cache.filter(is_test_moved.bind(discovered_test)) + for test in original_tests: + # update the line_number + var line_number_before := test.line_number + test.line_number = discovered_test.line_number + GdUnitSignals.instance().gdunit_test_discover_modified.emit(test) + if _is_debug: + prints("-> moved test id:%s %s: line:(%d -> %d)" % [test.guid, test.display_name, line_number_before, test.line_number]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("changed_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +## Synchronizes renamed tests between discover cycles.[br] +## [br] +## A test is considered renamed when:[br] +## - It has the same line number[br] +## - But a different name[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests +func sync_renamed_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + for discovered_test in discovered_tests: + # lookup in cache + var original_tests: Array[GdUnitTestCase] = cache.filter(is_test_renamed.bind(discovered_test)) + for test in original_tests: + # update the renaming names + var original_display_name := test.display_name + test.test_name = discovered_test.test_name + test.display_name = discovered_test.display_name + GdUnitSignals.instance().gdunit_test_discover_modified.emit(test) + if _is_debug: + prints("-> renamed test id:%s %s -> %s" % [test.guid, original_display_name, test.display_name]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("changed_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +## Synchronizes deleted tests between discover cycles.[br] +## [br] +## A test is considered deleted when:[br] +## - It exists in the cache[br] +## - But is not found in the newly discovered tests[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests +func sync_deleted_tests(source_file: String, discovered_tests: Array[GdUnitTestCase]) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + # lookup in cache + for test in cache: + if not discovered_tests.any(test_equals.bind(test)): + GdUnitSignals.instance().gdunit_test_discover_deleted.emit(test) + if _is_debug: + prints("-> deleted test id:%s %s:%d" % [test.guid, test.display_name, test.line_number]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("deleted_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +## Synchronizes newly added tests between discover cycles.[br] +## [br] +## A test is considered added when:[br] +## - It exists in the newly discovered tests[br] +## - But is not found in the cache[br] +## [br] +## [param source_file] suite source path[br] +## [param discovered_tests] Newly discovered tests[br] +## [param discover_sink] Callback to handle newly discovered tests +func sync_added_tests(source_file: String, discovered_tests: Array[GdUnitTestCase], discover_sink: Callable) -> void: + @warning_ignore("unsafe_method_access") + var cache: Array[GdUnitTestCase] = _discover_cache.get(source_file).duplicate() + # lookup in cache + for test in discovered_tests: + if not cache.any(test_equals.bind(test)): + discover_sink.call(test) + if _is_debug: + prints("-> added test id:%s %s:%d" % [test.guid, test.display_name, test.line_number]) + @warning_ignore("unsafe_method_access") + _discovered_changes.get_or_add("added_tests", Array([], TYPE_OBJECT, "RefCounted", GdUnitTestCase)).append(test) + + +func is_test_renamed(left: GdUnitTestCase, right: GdUnitTestCase) -> bool: + return left.line_number == right.line_number and left.test_name != right.test_name + + +func is_test_moved(left: GdUnitTestCase, right: GdUnitTestCase) -> bool: + return left.line_number != right.line_number and left.test_name == right.test_name + + +func test_equals(left: GdUnitTestCase, right: GdUnitTestCase) -> bool: + return left.display_name == right.display_name # do rebuild the entire project, there is actual no way to enforce the Godot engine itself to do this @@ -142,11 +312,12 @@ func rebuild_project(script: Script) -> void: print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Rebuild the project failed.[/color]") print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=RED]Can't find installed `dotnet`! Please check your environment is setup correctly.[/color]") return - print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Found dotnet v%s[/color]" % output[0].strip_edges()) + + print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Found dotnet v%s[/color]" % str(output[0]).strip_edges()) output.clear() exit_code = OS.execute("dotnet", ["build"], output) print_rich("[color=CORNFLOWER_BLUE]GdUnitTestDiscoverGuard:[/color] [color=DEEP_SKY_BLUE]Rebuild the project ... [/color]") - for out:Variant in output: + for out: String in output: print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges()) await scene_tree.process_frame diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid index 04c9b294..938d3e7d 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverGuard.gd.uid @@ -1 +1 @@ -uid://b4od40e41aoh3 +uid://6p5153acn0jg diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd new file mode 100644 index 00000000..5d0e5b68 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd @@ -0,0 +1,13 @@ +## A static utility class that acts as a central sink for test case discovery events in GdUnit4. +## Instead of implementing custom sink classes, test discovery consumers should connect to +## the GdUnitSignals.gdunit_test_discovered signal to receive test case discoveries. +## This design allows for a more flexible and decoupled test discovery system. +class_name GdUnitTestDiscoverSink +extends RefCounted + + +## Emits a discovered test case through the GdUnitSignals system.[br] +## Sends the test case to all listeners connected to the gdunit_test_discovered signal.[br] +## [member test_case] The discovered test case to be broadcast to all connected listeners. +static func discover(test_case: GdUnitTestCase) -> void: + GdUnitSignals.instance().gdunit_test_discover_added.emit(test_case) diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid new file mode 100644 index 00000000..563f17d1 --- /dev/null +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverSink.gd.uid @@ -0,0 +1 @@ +uid://b56m02detescl diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd index febe0687..0d911fef 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd @@ -2,37 +2,168 @@ class_name GdUnitTestDiscoverer extends RefCounted -static func run() -> void: - prints("Running test discovery ..") +static func run() -> Array[GdUnitTestCase]: + console_log("Running test discovery ..") + await (Engine.get_main_loop() as SceneTree).process_frame GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) - await (Engine.get_main_loop() as SceneTree).create_timer(.5).timeout # We run the test discovery in an extra thread so that the main thread is not blocked var t:= Thread.new() @warning_ignore("return_value_discarded") - t.start(func () -> void: - var test_suite_directories :PackedStringArray = GdUnitCommandHandler.scan_all_test_directories(GdUnitSettings.test_root_folder()) + t.start(func () -> Array[GdUnitTestCase]: + # Loading previous test session + var runner_config := GdUnitRunnerConfig.new() + runner_config.load_config() + var recovered_tests := runner_config.test_cases() + var test_suite_directories := scan_all_test_directories(GdUnitSettings.test_root_folder()) var scanner := GdUnitTestSuiteScanner.new() - var _test_suites_to_process :Array[Node] = [] + var collected_tests: Array[GdUnitTestCase] = [] + var collected_test_suites: Array[Script] = [] + # collect test suites for test_suite_dir in test_suite_directories: - _test_suites_to_process.append_array(scanner.scan(test_suite_dir)) + collected_test_suites.append_array(scanner.scan_directory(test_suite_dir)) # Do sync the main thread before emit the discovered test suites to the inspector await (Engine.get_main_loop() as SceneTree).process_frame - var test_case_count :int = 0 - for test_suite in _test_suites_to_process: - test_case_count += test_suite.get_child_count() - var ts_dto := GdUnitTestSuiteDto.of(test_suite) - GdUnitSignals.instance().gdunit_add_test_suite.emit(ts_dto) - test_suite.free() - - prints("%d test suites discovered." % _test_suites_to_process.size()) - GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(_test_suites_to_process.size(), test_case_count)) - _test_suites_to_process.clear() + for test_suites_script in collected_test_suites: + discover_tests(test_suites_script, func(test_case: GdUnitTestCase) -> void: + # Sync test uid from last test session + recover_test_guid(test_case, recovered_tests) + collected_tests.append(test_case) + GdUnitTestDiscoverSink.discover(test_case) + ) + + console_log_discover_results(collected_tests) + if !recovered_tests.is_empty(): + console_log("Recovered last test session successfully, %d tests restored." % recovered_tests.size(), true) + return collected_tests ) # wait unblocked to the tread is finished while t.is_alive(): await (Engine.get_main_loop() as SceneTree).process_frame # needs finally to wait for finish - await t.wait_to_finish() + var test_to_execute: Array[GdUnitTestCase] = await t.wait_to_finish() + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + return test_to_execute + + +## Restores the last test run session by loading the test run config file and rediscover the tests +static func restore_last_session() -> void: + if GdUnitSettings.is_test_discover_enabled(): + return + + var runner_config := GdUnitRunnerConfig.new() + var result := runner_config.load_config() + # Report possible config loading errors + if result.is_error(): + console_log("Recovery of the last test session failed: %s" % result.error_message(), true) + # If no config file found, skip test recovery + if result.is_warn(): + return + + # If no tests recorded, skip test recovery + var test_cases := runner_config.test_cases() + if test_cases.size() == 0: + return + + # We run the test session restoring in an extra thread so that the main thread is not blocked + var t:= Thread.new() + t.start(func () -> void: + # Do sync the main thread before emit the discovered test suites to the inspector + await (Engine.get_main_loop() as SceneTree).process_frame + console_log("Recovering last test session ..", true) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverStart.new()) + for test_case in test_cases: + GdUnitTestDiscoverSink.discover(test_case) + GdUnitSignals.instance().gdunit_event.emit(GdUnitEventTestDiscoverEnd.new(0, 0)) + console_log("Recovered last test session successfully, %d tests restored." % test_cases.size(), true) + ) + t.wait_to_finish() + + +static func recover_test_guid(current: GdUnitTestCase, recovered_tests: Array[GdUnitTestCase]) -> void: + for recovered_test in recovered_tests: + if recovered_test.fully_qualified_name == current.fully_qualified_name: + current.guid = recovered_test.guid + + +static func console_log_discover_results(tests: Array[GdUnitTestCase]) -> void: + var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.source_file + ) + for suite_tests: Array in grouped_by_suites.values(): + var test_case: GdUnitTestCase = suite_tests[0] + console_log("Discover: TestSuite %s with %d tests found" % [test_case.source_file, suite_tests.size()]) + console_log("Discover tests done, %d TestSuites and total %d Tests found. " % [grouped_by_suites.size(), tests.size()]) + console_log("") + + +static func console_log(message: String, on_console := false) -> void: + prints(message) + if on_console: + GdUnitSignals.instance().gdunit_message.emit(message) + + +static func filter_tests(method: Dictionary) -> bool: + var method_name: String = method["name"] + return method_name.begins_with("test_") + + +static func default_discover_sink(test_case: GdUnitTestCase) -> void: + GdUnitTestDiscoverSink.discover(test_case) + + +static func discover_tests(source_script: Script, discover_sink := default_discover_sink) -> void: + if source_script is GDScript: + var test_names := source_script.get_script_method_list()\ + .filter(filter_tests)\ + .map(func(method: Dictionary) -> String: return method["name"]) + # no tests discovered? + if test_names.is_empty(): + return + + var parser := GdScriptParser.new() + var fds := parser.get_function_descriptors(source_script as GDScript, test_names) + for fd in fds: + var resolver := GdFunctionParameterSetResolver.new(fd) + for test_case in resolver.resolve_test_cases(source_script as GDScript): + discover_sink.call(test_case) + elif source_script.get_class() == "CSharpScript": + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return + for test_case in GdUnit4CSharpApiLoader.discover_tests(source_script): + discover_sink.call(test_case) + + +static func scan_all_test_directories(root: String) -> PackedStringArray: + var base_directory := "res://" + # If the test root folder is configured as blank, "/", or "res://", use the root folder as described in the settings panel + if root.is_empty() or root == "/" or root == base_directory: + return [base_directory] + return scan_test_directories(base_directory, root, []) + + +static func scan_test_directories(base_directory: String, test_directory: String, test_suite_paths: PackedStringArray) -> PackedStringArray: + print_verbose("Scannning for test directory '%s' at %s" % [test_directory, base_directory]) + for directory in DirAccess.get_directories_at(base_directory): + if directory.begins_with("."): + continue + var current_directory := normalize_path(base_directory + "/" + directory) + if GdUnitTestSuiteScanner.exclude_scan_directories.has(current_directory): + continue + if match_test_directory(directory, test_directory): + @warning_ignore("return_value_discarded") + test_suite_paths.append(current_directory) + else: + @warning_ignore("return_value_discarded") + scan_test_directories(current_directory, test_directory, test_suite_paths) + return test_suite_paths + + +static func normalize_path(path: String) -> String: + return path.replace("///", "//") + + +static func match_test_directory(directory: String, test_directory: String) -> bool: + return directory == test_directory or test_directory.is_empty() or test_directory == "/" or test_directory == "res://" diff --git a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid index 5067e0ab..31dcc7fb 100644 --- a/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid +++ b/addons/gdUnit4/src/core/discovery/GdUnitTestDiscoverer.gd.uid @@ -1 +1 @@ -uid://b16iydnuouq1q +uid://d4fcciiaycjg0 diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd b/addons/gdUnit4/src/core/event/GdUnitEvent.gd index 0fd2af2f..6bec1059 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEvent.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd @@ -20,24 +20,24 @@ enum { TESTSUITE_AFTER, TESTCASE_BEFORE, TESTCASE_AFTER, - TESTCASE_STATISTICS, DISCOVER_START, DISCOVER_END, - DISCOVER_SUITE_ADDED, - DISCOVER_TEST_ADDED, - DISCOVER_TEST_REMOVED, + SESSION_START, + SESSION_CLOSE } -var _event_type :int -var _resource_path :String -var _suite_name :String -var _test_name :String -var _total_count :int = 0 +var _event_type: int +var _guid: GdUnitGUID +var _resource_path: String +var _suite_name: String +var _test_name: String +var _total_count: int = 0 var _statistics := Dictionary() -var _reports :Array[GdUnitReport] = [] +var _reports: Array[GdUnitReport] = [] -func suite_before(p_resource_path :String, p_suite_name :String, p_total_count :int) -> GdUnitEvent: +func suite_before(p_resource_path: String, p_suite_name: String, p_total_count: int) -> GdUnitEvent: + _guid = GdUnitGUID.new() _event_type = TESTSUITE_BEFORE _resource_path = p_resource_path _suite_name = p_suite_name @@ -46,7 +46,8 @@ func suite_before(p_resource_path :String, p_suite_name :String, p_total_count : return self -func suite_after(p_resource_path :String, p_suite_name :String, p_statistics :Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent: +func suite_after(p_resource_path: String, p_suite_name: String, p_statistics: Dictionary = {}, p_reports: Array[GdUnitReport] = []) -> GdUnitEvent: + _guid = GdUnitGUID.new() _event_type = TESTSUITE_AFTER _resource_path = p_resource_path _suite_name = p_suite_name @@ -56,37 +57,28 @@ func suite_after(p_resource_path :String, p_suite_name :String, p_statistics :Di return self -func test_before(p_resource_path :String, p_suite_name :String, p_test_name :String) -> GdUnitEvent: +func test_before(p_guid: GdUnitGUID) -> GdUnitEvent: _event_type = TESTCASE_BEFORE - _resource_path = p_resource_path - _suite_name = p_suite_name - _test_name = p_test_name + _guid = p_guid return self -func test_after(p_resource_path :String, p_suite_name :String, p_test_name :String, p_statistics :Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent: +func test_after(p_guid: GdUnitGUID, p_statistics: Dictionary = {}, p_reports :Array[GdUnitReport] = []) -> GdUnitEvent: _event_type = TESTCASE_AFTER - _resource_path = p_resource_path - _suite_name = p_suite_name - _test_name = p_test_name + _guid = p_guid _statistics = p_statistics _reports = p_reports return self -func test_statistics(p_resource_path :String, p_suite_name :String, p_test_name :String, p_statistics :Dictionary = {}) -> GdUnitEvent: - _event_type = TESTCASE_STATISTICS - _resource_path = p_resource_path - _suite_name = p_suite_name - _test_name = p_test_name - _statistics = p_statistics - return self - - func type() -> int: return _event_type +func guid() -> GdUnitGUID: + return _guid + + func suite_name() -> String: return _suite_name @@ -127,12 +119,16 @@ func skipped_count() -> int: return _statistics.get(SKIPPED_COUNT, 0) +func retry_count() -> int: + return _statistics.get(RETRY_COUNT, 0) + + func resource_path() -> String: return _resource_path func is_success() -> bool: - return not is_warning() and not is_failed() and not is_error() and not is_skipped() + return not is_failed() and not is_error() func is_warning() -> bool: @@ -160,7 +156,7 @@ func reports() -> Array[GdUnitReport]: func _to_string() -> String: - return "Event: %s %s:%s, %s, %s" % [_event_type, _suite_name, _test_name, _statistics, _reports] + return "Event: %s id:%s %s:%s, %s, %s" % [_event_type, _guid, _suite_name, _test_name, _statistics, _reports] func serialize() -> Dictionary: @@ -172,12 +168,15 @@ func serialize() -> Dictionary: "total_count" : _total_count, "statistics" : _statistics } + if _guid != null: + serialized["guid"] = _guid._guid serialized["reports"] = _serialize_TestReports() return serialized -func deserialize(serialized :Dictionary) -> GdUnitEvent: +func deserialize(serialized: Dictionary) -> GdUnitEvent: _event_type = serialized.get("type", null) + _guid = GdUnitGUID.new(str(serialized.get("guid", ""))) _resource_path = serialized.get("resource_path", null) _suite_name = serialized.get("suite_name", null) _test_name = serialized.get("test_name", "unknown") @@ -199,7 +198,7 @@ func _serialize_TestReports() -> Array[Dictionary]: return serialized_reports -func _deserialize_reports(p_reports :Array[Dictionary]) -> Array[GdUnitReport]: +func _deserialize_reports(p_reports: Array[Dictionary]) -> Array[GdUnitReport]: var deserialized_reports :Array[GdUnitReport] = [] for report in p_reports: var test_report := GdUnitReport.new().deserialize(report) diff --git a/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid index f0aac79c..2ec6e8e6 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEvent.gd.uid @@ -1 +1 @@ -uid://cbjkp6x1c8c08 +uid://0vyuxbifi0mn diff --git a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd index 8bb1d496..774e7d4f 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd +++ b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd @@ -2,18 +2,5 @@ class_name GdUnitInit extends GdUnitEvent -var _total_testsuites :int - - -func _init(p_total_testsuites :int, p_total_count :int) -> void: +func _init() -> void: _event_type = INIT - _total_testsuites = p_total_testsuites - _total_count = p_total_count - - -func total_test_suites() -> int: - return _total_testsuites - - -func total_tests() -> int: - return _total_count diff --git a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid index 480b46c0..04fac6a6 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventInit.gd.uid @@ -1 +1 @@ -uid://ka2853lnb4dn +uid://dk6pvrtk6e1mj diff --git a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid index b6b0f9df..07e0e005 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventStop.gd.uid @@ -1 +1 @@ -uid://ca0155xx2mjpn +uid://cehbqsaxr3w47 diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid index 9eac8920..b0baa5a9 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverEnd.gd.uid @@ -1 +1 @@ -uid://d28i54x2d36ks +uid://bpkndiebfgl3f diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid index b550ce9d..d740982b 100644 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid +++ b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverStart.gd.uid @@ -1 +1 @@ -uid://cxtjqpa8pdmng +uid://muefohw1yx1y diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestAdded.gd b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestAdded.gd deleted file mode 100644 index c5f44599..00000000 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestAdded.gd +++ /dev/null @@ -1,17 +0,0 @@ -class_name GdUnitEventTestDiscoverTestAdded -extends GdUnitEvent - - -var _test_case_dto: GdUnitTestCaseDto - - -func _init(arg_resource_path: String, arg_suite_name: String, arg_test_case_dto: GdUnitTestCaseDto) -> void: - _event_type = DISCOVER_TEST_ADDED - _resource_path = arg_resource_path - _suite_name = arg_suite_name - _test_name = arg_test_case_dto.name() - _test_case_dto = arg_test_case_dto - - -func test_case_dto() -> GdUnitTestCaseDto: - return _test_case_dto diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestAdded.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestAdded.gd.uid deleted file mode 100644 index e0ca55cc..00000000 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestAdded.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://btr3wy50q3vmd diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestRemoved.gd b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestRemoved.gd deleted file mode 100644 index 77617d0e..00000000 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestRemoved.gd +++ /dev/null @@ -1,9 +0,0 @@ -class_name GdUnitEventTestDiscoverTestRemoved -extends GdUnitEvent - - -func _init(arg_resource_path: String, arg_suite_name: String, arg_test_name: String) -> void: - _event_type = DISCOVER_TEST_REMOVED - _resource_path = arg_resource_path - _suite_name = arg_suite_name - _test_name = arg_test_name diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestRemoved.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestRemoved.gd.uid deleted file mode 100644 index 4e01236f..00000000 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestRemoved.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cnt30aej5d2dt diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestSuiteAdded.gd b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestSuiteAdded.gd deleted file mode 100644 index b0e23f59..00000000 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestSuiteAdded.gd +++ /dev/null @@ -1,16 +0,0 @@ -class_name GdUnitEventTestDiscoverTestSuiteAdded -extends GdUnitEvent - - -var _dto: GdUnitTestSuiteDto - - -func _init(arg_resource_path: String, arg_suite_name: String, arg_dto: GdUnitTestSuiteDto) -> void: - _event_type = DISCOVER_SUITE_ADDED - _resource_path = arg_resource_path - _suite_name = arg_suite_name - _dto = arg_dto - - -func suite_dto() -> GdUnitTestSuiteDto: - return _dto diff --git a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestSuiteAdded.gd.uid b/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestSuiteAdded.gd.uid deleted file mode 100644 index 65885fd7..00000000 --- a/addons/gdUnit4/src/core/event/GdUnitEventTestDiscoverTestSuiteAdded.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dcqrcvtschqai diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd new file mode 100644 index 00000000..52dab3ff --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd @@ -0,0 +1,6 @@ +class_name GdUnitSessionClose +extends GdUnitEvent + + +func _init() -> void: + _event_type = SESSION_CLOSE diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid new file mode 100644 index 00000000..b40198bb --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionClose.gd.uid @@ -0,0 +1 @@ +uid://5s7oep23ihpf diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd new file mode 100644 index 00000000..420ad538 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd @@ -0,0 +1,6 @@ +class_name GdUnitSessionStart +extends GdUnitEvent + + +func _init() -> void: + _event_type = SESSION_START diff --git a/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid new file mode 100644 index 00000000..a06cbb41 --- /dev/null +++ b/addons/gdUnit4/src/core/event/GdUnitSessionStart.gd.uid @@ -0,0 +1 @@ +uid://cnfcailp3aaoh diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd index 0e62682e..457fd678 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd @@ -2,6 +2,14 @@ ## It contains all the necessary information about the executed stage, such as memory observers, reports, orphan monitor class_name GdUnitExecutionContext +enum GC_ORPHANS_CHECK { + NONE, + SUITE_HOOK_AFTER, + TEST_HOOK_AFTER, + TEST_CASE +} + + var _parent_context: GdUnitExecutionContext var _sub_context: Array[GdUnitExecutionContext] = [] var _orphan_monitor: GdUnitOrphanNodesMonitor @@ -14,20 +22,7 @@ var _name: String var _test_execution_iteration: int = 0 var _flaky_test_check := GdUnitSettings.is_test_flaky_check_enabled() var _flaky_test_retries := GdUnitSettings.get_flaky_max_retries() - - -# execution states -var _is_calculated := false -var _is_success: bool -var _is_flaky: bool -var _is_skipped: bool -var _has_warnings: bool -var _has_failures: bool -var _has_errors: bool -var _failure_count := 0 -var _orphan_count := 0 -var _error_count := 0 -var _skipped_count := 0 +var _orphans := -1 var error_monitor: GodotGdErrorMonitor = null: @@ -120,7 +115,7 @@ func get_test_suite_name() -> StringName: func get_test_case_name() -> StringName: if _test_case_name.is_empty(): - return test_case.get_name() + return test_case._test_case.display_name return _test_case_name @@ -143,8 +138,9 @@ func orphan_monitor_stop() -> void: _orphan_monitor.stop() -func add_report(report: GdUnitReport) -> void: +func add_report(report: GdUnitReport) -> GdUnitReport: _report_collector.push_back(report) + return report func reports() -> Array[GdUnitReport]: @@ -154,144 +150,61 @@ func reports() -> Array[GdUnitReport]: func collect_reports(recursive: bool) -> Array[GdUnitReport]: if not recursive: return reports() - var current_reports := reports() + # we combine the reports of test_before(), test_after() and test() to be reported by `fire_test_ended` + # we strictly need to copy the reports before adding sub context reports to avoid manipulation of the current context + var current_reports := reports().duplicate() for sub_context in _sub_context: - current_reports.append_array(sub_context.reports()) - # needs finally to clean the test reports to avoid counting twice - sub_context.reports().clear() + current_reports.append_array(sub_context.collect_reports(true)) + return current_reports -func collect_orphans(p_reports: Array[GdUnitReport]) -> int: - var orphans := 0 - if not _sub_context.is_empty(): - orphans += collect_testcase_orphan_reports(_sub_context[0], p_reports) - orphans += collect_teststage_orphan_reports(p_reports) - return orphans - - -func collect_testcase_orphan_reports(context: GdUnitExecutionContext, p_reports: Array[GdUnitReport]) -> int: - var orphans := context.count_orphans() - if orphans > 0: - p_reports.push_front(GdUnitReport.new()\ - .create(GdUnitReport.WARN, context.test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) - return orphans - - -func collect_teststage_orphan_reports(p_reports: Array[GdUnitReport]) -> int: - var orphans := count_orphans() - if orphans > 0: - p_reports.push_front(GdUnitReport.new()\ - .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test_setup(orphans))) - return orphans - - -func build_reports(recursive:= true) -> Array[GdUnitReport]: - var collected_reports: Array[GdUnitReport] = collect_reports(recursive) - if recursive: - _orphan_count = collect_orphans(collected_reports) - else: - _orphan_count = count_orphans() - if _orphan_count > 0: - collected_reports.push_front(GdUnitReport.new() \ - .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(_orphan_count))) - _is_skipped = is_skipped() - _skipped_count = count_skipped(recursive) - _is_success = is_success() - _is_flaky = is_flaky() - _has_warnings = has_warnings() - _has_errors = has_errors() - _error_count = count_errors(recursive) - if !_is_success: - _has_failures = has_failures() - _failure_count = count_failures(recursive) - _is_calculated = true - return collected_reports - - -# Evaluates the actual test case status by validate latest execution state (cold be more based on flaky max retry count) -func evaluate_test_retry_status() -> bool: - # get latest test execution status - var last_test_status :GdUnitExecutionContext = _sub_context.back() - _is_skipped = last_test_status.is_skipped() - _skipped_count = last_test_status.count_skipped(false) - _is_success = last_test_status.is_success() - # if success but it have more than one sub contexts the test was rerurn becouse of failures and will be marked as flaky - _is_flaky = _is_success and _sub_context.size() > 1 - _has_warnings = last_test_status.has_warnings() - _has_errors = last_test_status.has_errors() - _error_count = last_test_status.count_errors(false) - _has_failures = last_test_status.has_failures() - _failure_count = last_test_status.count_failures(false) - _orphan_count = last_test_status.collect_orphans(collect_reports(false)) - _is_calculated = true - # finally cleanup the retry execution contexts - dispose_sub_contexts() - return _is_success +func calculate_statistics(reports_: Array[GdUnitReport]) -> Dictionary: + var failed_count := GdUnitTestReportCollector.count_failures(reports_) + var error_count := GdUnitTestReportCollector.count_errors(reports_) + var warn_count := GdUnitTestReportCollector.count_warnings(reports_) + var skip_count := GdUnitTestReportCollector.count_skipped(reports_) + var is_failed := !is_success() + var orphan_count := _count_orphans() + var elapsed_time := _timer.elapsed_since_ms() + var retries := 1 if _parent_context == null else _sub_context.size() + # Mark as flaky if it is successful, but errors were counted + var is_flaky := retries > 1 and not is_failed + # In the case of a flakiness test, we do not report an error counter, as an unreliable test is considered successful + # after a certain number of repetitions. + if is_flaky: + failed_count = 0 - -func get_execution_statistics() -> Dictionary: return { - GdUnitEvent.RETRY_COUNT: _test_execution_iteration, - GdUnitEvent.ORPHAN_NODES: _orphan_count, - GdUnitEvent.ELAPSED_TIME: _timer.elapsed_since_ms(), - GdUnitEvent.FAILED: !_is_success, - GdUnitEvent.ERRORS: _has_errors, - GdUnitEvent.WARNINGS: _has_warnings, - GdUnitEvent.FLAKY: _is_flaky, - GdUnitEvent.SKIPPED: _is_skipped, - GdUnitEvent.FAILED_COUNT: _failure_count, - GdUnitEvent.ERROR_COUNT: _error_count, - GdUnitEvent.SKIPPED_COUNT: _skipped_count + GdUnitEvent.RETRY_COUNT: retries, + GdUnitEvent.ELAPSED_TIME: elapsed_time, + GdUnitEvent.FAILED: is_failed, + GdUnitEvent.ERRORS: error_count > 0, + GdUnitEvent.WARNINGS: warn_count > 0, + GdUnitEvent.FLAKY: is_flaky, + GdUnitEvent.SKIPPED: skip_count > 0, + GdUnitEvent.FAILED_COUNT: failed_count, + GdUnitEvent.ERROR_COUNT: error_count, + GdUnitEvent.SKIPPED_COUNT: skip_count, + GdUnitEvent.ORPHAN_NODES: orphan_count, } -func has_failures() -> bool: - return ( - _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c._has_failures if c._is_calculated else c.has_failures()) - or _report_collector.has_failures() - ) - - -func has_errors() -> bool: - return ( - _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c._has_errors if c._is_calculated else c.has_errors()) - or _report_collector.has_errors() - ) - - -func has_warnings() -> bool: - return ( - _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c._has_warnings if c._is_calculated else c.has_warnings()) - or _report_collector.has_warnings() - ) - - -func is_flaky() -> bool: - return ( - _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c._is_flaky if c._is_calculated else c.is_flaky()) - or _test_execution_iteration > 1 - ) - - func is_success() -> bool: if _sub_context.is_empty(): - return not has_failures() + return not _report_collector.has_failures() + # we on test suite level? + if _parent_context == null: + return not _report_collector.has_failures() - var failed_context := _sub_context.filter(func(c :GdUnitExecutionContext) -> bool: - return !(c._is_success if c._is_calculated else c.is_success())) - return failed_context.is_empty() and not has_failures() + return _sub_context[-1].is_success() and not _report_collector.has_failures() func is_skipped() -> bool: return ( _sub_context.any(func(c :GdUnitExecutionContext) -> bool: - return c._is_skipped if c._is_calculated else c.is_skipped()) + return c.is_skipped()) or test_case.is_skipped() if test_case != null else false ) @@ -300,35 +213,20 @@ func is_interupted() -> bool: return false if test_case == null else test_case.is_interupted() -func count_failures(recursive: bool) -> int: - if not recursive: - return _report_collector.count_failures() - return _sub_context\ - .map(func(c :GdUnitExecutionContext) -> int: - return c.count_failures(recursive)).reduce(sum, _report_collector.count_failures()) - - -func count_errors(recursive: bool) -> int: - if not recursive: - return _report_collector.count_errors() - return _sub_context\ - .map(func(c :GdUnitExecutionContext) -> int: - return c.count_errors(recursive)).reduce(sum, _report_collector.count_errors()) - - -func count_skipped(recursive: bool) -> int: - if not recursive: - return _report_collector.count_skipped() - return _sub_context\ - .map(func(c :GdUnitExecutionContext) -> int: - return c.count_skipped(recursive)).reduce(sum, _report_collector.count_skipped()) - +func _count_orphans() -> int: + if _orphans != -1: + return _orphans -func count_orphans() -> int: var orphans := 0 for c in _sub_context: - orphans += c._orphan_monitor.orphan_nodes() - return _orphan_monitor.orphan_nodes() - orphans + if _orphan_monitor.orphan_nodes() != c._orphan_monitor.orphan_nodes(): + orphans += c._count_orphans() + + _orphans = _orphan_monitor.orphan_nodes() + if _orphan_monitor.orphan_nodes() != orphans: + _orphans -= orphans + + return _orphans func sum(accum: int, number: int) -> int: @@ -336,7 +234,7 @@ func sum(accum: int, number: int) -> int: func retry_execution() -> bool: - var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries + var retry := _test_execution_iteration < 1 if not _flaky_test_check else _test_execution_iteration < _flaky_test_retries if retry: _test_execution_iteration += 1 return retry @@ -346,9 +244,26 @@ func register_auto_free(obj: Variant) -> Variant: return _memory_observer.register_auto_free(obj) -## Runs the gdunit garbage collector to free registered object -func gc() -> void: +## Runs the gdunit garbage collector to free registered object and handle orphan node reporting +func gc(gc_orphan_check: GC_ORPHANS_CHECK = GC_ORPHANS_CHECK.NONE) -> void: # unreference last used assert form the test to prevent memory leaks GdUnitThreadManager.get_current_context().clear_assert() await _memory_observer.gc() orphan_monitor_stop() + + var orphans := _count_orphans() + match(gc_orphan_check): + GC_ORPHANS_CHECK.SUITE_HOOK_AFTER: + if orphans > 0: + reports().push_front(GdUnitReport.new() \ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_suite_setup(orphans))) + + GC_ORPHANS_CHECK.TEST_HOOK_AFTER: + if orphans > 0: + reports().push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, 1, GdAssertMessages.orphan_detected_on_test_setup(orphans))) + + GC_ORPHANS_CHECK.TEST_CASE: + if orphans > 0: + reports().push_front(GdUnitReport.new()\ + .create(GdUnitReport.WARN, test_case.line_number(), GdAssertMessages.orphan_detected_on_test(orphans))) diff --git a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid index e0454702..4f44133e 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitExecutionContext.gd.uid @@ -1 +1 @@ -uid://dygyn28cdul6x +uid://233mffe3wskq diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd index 274b66f4..dd03a313 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd @@ -42,17 +42,18 @@ static func _is_instance_guard_enabled() -> bool: return false -@warning_ignore("unsafe_method_access") static func debug_observe(name :String, obj :Object, indent :int = 0) -> void: if not _show_debug: return var script :GDScript= obj if obj is GDScript else obj.get_script() if script: var base_script :GDScript = script.get_base_script() + @warning_ignore("unsafe_method_access") prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path) if base_script: debug_observe("+", base_script, indent+1) else: + @warning_ignore("unsafe_method_access") prints(name, obj, obj.get_class(), obj.get_name()) diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid index aaabbf0e..34c0dbd8 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd.uid @@ -1 +1 @@ -uid://crpiku88ap3y0 +uid://baqhilf43vgeg diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd index 62c4b02f..5f42d148 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd @@ -22,20 +22,20 @@ static func __filter_is_skipped(report :GdUnitReport) -> bool: return report.is_skipped() -func count_failures() -> int: - return _reports.filter(__filter_is_failure).size() +static func count_failures(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_failure).size() -func count_errors() -> int: - return _reports.filter(__filter_is_error).size() +static func count_errors(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_error).size() -func count_warnings() -> int: - return _reports.filter(__filter_is_warning).size() +static func count_warnings(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_warning).size() -func count_skipped() -> int: - return _reports.filter(__filter_is_skipped).size() +static func count_skipped(reports_: Array[GdUnitReport]) -> int: + return reports_.filter(__filter_is_skipped).size() func has_failures() -> bool: diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid index 260a23d7..0cd4ec82 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitTestReportCollector.gd.uid @@ -1 +1 @@ -uid://di2lh87di8lb5 +uid://b4r7exritngk4 diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd index 0dd0ba0f..e3fd510c 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd @@ -6,10 +6,11 @@ class_name GdUnitTestSuiteExecutor @warning_ignore("unused_private_class_variable") var _assertions := GdUnitAssertions.new() var _executeStage := GdUnitTestSuiteExecutionStage.new() - +var _debug_mode : bool func _init(debug_mode :bool = false) -> void: _executeStage.set_debug_mode(debug_mode) + _debug_mode = debug_mode func execute(test_suite :GdUnitTestSuite) -> void: @@ -22,5 +23,26 @@ func execute(test_suite :GdUnitTestSuite) -> void: await _executeStage.execute(GdUnitExecutionContext.of_test_suite(test_suite)) +func run_and_wait(tests: Array[GdUnitTestCase]) -> void: + if !_debug_mode: + GdUnitSignals.instance().gdunit_event.emit(GdUnitInit.new()) + # first we group all tests by resource path + var grouped_by_suites := GdArrayTools.group_by(tests, func(test: GdUnitTestCase) -> String: + return test.suite_resource_path + ) + var scanner := GdUnitTestSuiteScanner.new() + for suite_path: String in grouped_by_suites.keys(): + @warning_ignore("unsafe_call_argument") + var suite_tests: Array[GdUnitTestCase] = Array(grouped_by_suites[suite_path], TYPE_OBJECT, "RefCounted", GdUnitTestCase) + var script := GdUnitTestSuiteScanner.load_with_disabled_warnings(suite_path) + if script.get_class() == "GDScript": + var test_suite := scanner.load_suite(script as GDScript, suite_tests) + await execute(test_suite) + else: + await GdUnit4CSharpApiLoader.execute(suite_tests) + if !_debug_mode: + GdUnitSignals.instance().gdunit_event.emit(GdUnitStop.new()) + + func fail_fast(enabled :bool) -> void: _executeStage.fail_fast(enabled) diff --git a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid index 583db7a2..76b29c18 100644 --- a/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid +++ b/addons/gdUnit4/src/core/execution/GdUnitTestSuiteExecutor.gd.uid @@ -1 +1 @@ -uid://1wa5k041375j +uid://cun41u3qp3uh3 diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd index 461fb137..d3c62455 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd @@ -18,40 +18,5 @@ func _execute(context: GdUnitExecutionContext) -> void: @warning_ignore("redundant_await") await test_suite.after_test() - await context.gc() + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_HOOK_AFTER) await context.error_monitor_stop() - - var reports := context.build_reports() - - if context.is_skipped(): - fire_test_skipped(context) - else: - fire_event(GdUnitEvent.new() \ - .test_after(context.get_test_suite_path(), - context.get_test_suite_name(), - context.get_test_case_name(), - context.get_execution_statistics(), - reports)) - - -func fire_test_skipped(context: GdUnitExecutionContext) -> void: - var test_case := context.test_case - var statistics := { - GdUnitEvent.ORPHAN_NODES: 0, - GdUnitEvent.ELAPSED_TIME: 0, - GdUnitEvent.WARNINGS: false, - GdUnitEvent.ERRORS: false, - GdUnitEvent.ERROR_COUNT: 0, - GdUnitEvent.FAILED: false, - GdUnitEvent.FAILED_COUNT: 0, - GdUnitEvent.SKIPPED: true, - GdUnitEvent.SKIPPED_COUNT: 1, - } - var report := GdUnitReport.new() \ - .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) - fire_event(GdUnitEvent.new() \ - .test_after(context.get_test_suite_path(), - context.get_test_suite_name(), - context.get_test_case_name(), - statistics, - [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid index 66fb8d8a..a8670ef7 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseAfterStage.gd.uid @@ -1 +1 @@ -uid://bjvwkldg2eyw4 +uid://ctphbbips41gc diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd index 82e90d4a..4e04fad2 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd @@ -13,8 +13,6 @@ func _init(call_stage := true) -> void: func _execute(context :GdUnitExecutionContext) -> void: var test_suite := context.test_suite - fire_event(GdUnitEvent.new()\ - .test_before(context.get_test_suite_path(), context.get_test_suite_name(), context.get_test_case_name())) if _call_stage: @warning_ignore("redundant_await") await test_suite.before_test() diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid index 6e30cf8a..5efe8349 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseBeforeStage.gd.uid @@ -1 +1 @@ -uid://xd2w4aayiaga +uid://ccgmqswkrrdkn diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd index 853070d5..12cc6fde 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd @@ -3,9 +3,8 @@ class_name GdUnitTestCaseExecutionStage extends IGdUnitExecutionStage -var _stage_single_test :IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new() -var _stage_fuzzer_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new() -var _stage_parameterized_test :IGdUnitExecutionStage= GdUnitTestCaseParameterizedExecutionStage.new() +var _stage_single_test: IGdUnitExecutionStage = GdUnitTestCaseSingleExecutionStage.new() +var _stage_fuzzer_test: IGdUnitExecutionStage = GdUnitTestCaseFuzzedExecutionStage.new() ## Executes the test case 'test_()'.[br] @@ -19,9 +18,7 @@ func _execute(context :GdUnitExecutionContext) -> void: context.error_monitor_start() - if test_case.is_parameterized(): - await _stage_parameterized_test.execute(context) - elif test_case.is_fuzzed(): + if test_case.is_fuzzed(): await _stage_fuzzer_test.execute(context) else: await _stage_single_test.execute(context) @@ -29,13 +26,6 @@ func _execute(context :GdUnitExecutionContext) -> void: await context.gc() await context.error_monitor_stop() - # finally fire test statistics report - fire_event(GdUnitEvent.new()\ - .test_statistics(context.get_test_suite_path(), - context.get_test_suite_name(), - context.get_test_case_name(), - context.get_execution_statistics())) - # finally free the test instance if is_instance_valid(context.test_case): context.test_case.dispose() @@ -45,4 +35,3 @@ func set_debug_mode(debug_mode :bool = false) -> void: super.set_debug_mode(debug_mode) _stage_single_test.set_debug_mode(debug_mode) _stage_fuzzer_test.set_debug_mode(debug_mode) - _stage_parameterized_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid index 01aee2e2..496b38fe 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestCaseExecutionStage.gd.uid @@ -1 +1 @@ -uid://3g86oeg5416r +uid://cjm7hukt5g0dk diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd index a0f8ef19..03bbd0f7 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd @@ -12,16 +12,18 @@ func _execute(context :GdUnitExecutionContext) -> void: @warning_ignore("redundant_await") await test_suite.after() - await context.gc() - var reports := context.build_reports(false) + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.SUITE_HOOK_AFTER) + + var reports := context.collect_reports(false) + var statistics := context.calculate_statistics(reports) fire_event(GdUnitEvent.new()\ .suite_after(context.get_test_suite_path(),\ test_suite.get_name(), - context.get_execution_statistics(), + statistics, reports)) - GdUnitFileAccess.clear_tmp() # Guard that checks if all doubled (spy/mock) objects are released - GdUnitClassDoubler.check_leaked_instances() + await GdUnitClassDoubler.check_leaked_instances() # we hide the scene/main window after runner is finished - DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + if not Engine.is_embedded_in_editor(): + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid index 230b1813..2fe086f9 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteAfterStage.gd.uid @@ -1 +1 @@ -uid://3tjk62mmwatb +uid://cvdniahcsrvks diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid index e6f1ff54..5eb7403e 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteBeforeStage.gd.uid @@ -1 +1 @@ -uid://dbgnur4kr0vwb +uid://c4tq8tfwh8irb diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd index b62d27e0..ac921e07 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd @@ -27,7 +27,7 @@ func _execute(context :GdUnitExecutionContext) -> void: var test_case := context.test_suite.get_child(test_case_index) as _TestCase if not is_instance_valid(test_case): continue - context.test_suite.set_active_test_case(test_case.get_name()) + context.test_suite.set_active_test_case(test_case.test_name()) await _stage_test.execute(GdUnitExecutionContext.of_test_case(context, test_case)) # stop on first error or if fail fast is enabled if _fail_fast and not context.is_success(): @@ -97,9 +97,9 @@ func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: if not is_instance_valid(test_case): continue var test_case_context := GdUnitExecutionContext.of_test_case(context, test_case) - fire_event(GdUnitEvent.new()\ - .test_before(test_case_context.get_test_suite_path(), test_case_context.get_test_suite_name(), test_case_context.get_test_case_name())) - fire_test_skipped(test_case_context) + fire_event(GdUnitEvent.new().test_before(test_case.id())) + # use skip count 0 because we counted it over the complete test suite + fire_test_skipped(test_case_context, 0) var statistics := { @@ -118,7 +118,7 @@ func fire_test_suite_skipped(context :GdUnitExecutionContext) -> void: await (Engine.get_main_loop() as SceneTree).process_frame -func fire_test_skipped(context: GdUnitExecutionContext) -> void: +func fire_test_skipped(context: GdUnitExecutionContext, skip_count := 1) -> void: var test_case := context.test_case var statistics := { GdUnitEvent.ORPHAN_NODES: 0, @@ -129,22 +129,11 @@ func fire_test_skipped(context: GdUnitExecutionContext) -> void: GdUnitEvent.FAILED: false, GdUnitEvent.FAILED_COUNT: 0, GdUnitEvent.SKIPPED: true, - GdUnitEvent.SKIPPED_COUNT: 1, + GdUnitEvent.SKIPPED_COUNT: skip_count, } var report := GdUnitReport.new() \ .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped("Skipped from the entire test suite")) - fire_event(GdUnitEvent.new() \ - .test_after(context.get_test_suite_path(), - context.get_test_suite_name(), - context.get_test_case_name(), - statistics, - [report])) - # finally fire test statistics report - fire_event(GdUnitEvent.new()\ - .test_statistics(context.get_test_suite_path(), - context.get_test_suite_name(), - context.get_test_case_name(), - statistics)) + fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report])) func set_debug_mode(debug_mode :bool = false) -> void: diff --git a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid index 2f6c3e82..11b78802 100644 --- a/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/GdUnitTestSuiteExecutionStage.gd.uid @@ -1 +1 @@ -uid://c7pturv3ylkx8 +uid://mo6jkmhhcegj diff --git a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid index fb61e08e..aea3ad8c 100644 --- a/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/IGdUnitExecutionStage.gd.uid @@ -1 +1 @@ -uid://cr7udmpdfl2jk +uid://bnkyt48dv3qpg diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd index 243b889b..73a7a66e 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd @@ -8,6 +8,8 @@ var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseFuzzedTestStage.new() func _execute(context :GdUnitExecutionContext) -> void: + fire_event(GdUnitEvent.new().test_before(context.test_case.id())) + while context.retry_execution(): var test_context := GdUnitExecutionContext.of(context) await _stage_before.execute(test_context) @@ -16,12 +18,35 @@ func _execute(context :GdUnitExecutionContext) -> void: await _stage_after.execute(test_context) if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): break - @warning_ignore("return_value_discarded") - context.evaluate_test_retry_status() + context.gc() + if context.is_skipped(): + fire_test_skipped(context) + else: + var reports: = context.collect_reports(true) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new().test_after(context.test_case.id(), statistics, reports)) func set_debug_mode(debug_mode :bool = false) -> void: super.set_debug_mode(debug_mode) _stage_before.set_debug_mode(debug_mode) _stage_after.set_debug_mode(debug_mode) _stage_test.set_debug_mode(debug_mode) + + +func fire_test_skipped(context: GdUnitExecutionContext) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid index 638add7e..fd4dd62a 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedExecutionStage.gd.uid @@ -1 +1 @@ -uid://dqjc403bu61mr +uid://c217e5ynlwht4 diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd index d32c28ed..62dda37e 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd @@ -33,7 +33,7 @@ func _execute(context :GdUnitExecutionContext) -> void: reports.append(GdUnitReport.new() \ .create(GdUnitReport.FAILURE, report.line_number(), GdAssertMessages.fuzzer_interuped(iteration, report.message()))) break - await context.gc() + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) # unguard on fuzzers if not test_case.is_interupted(): diff --git a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid index 06e3695b..bab9094a 100644 --- a/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/fuzzed/GdUnitTestCaseFuzzedTestStage.gd.uid @@ -1 +1 @@ -uid://bon78ur6m2voo +uid://ba5cr0pv2m03r diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd deleted file mode 100644 index 5de35c07..00000000 --- a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd +++ /dev/null @@ -1,10 +0,0 @@ -class_name GdUnitTestCaseParameterSetTestStage -extends IGdUnitExecutionStage - - -## Executes a parameterized test case 'test_()' by given parameters.[br] -## It executes synchronized following stages[br] -## -> test_case() [br] -func _execute(context: GdUnitExecutionContext) -> void: - await context.test_case.execute_paramaterized(context._test_case_parameter_set) - await context.gc() diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd.uid deleted file mode 100644 index 81b94a38..00000000 --- a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterSetTestStage.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://n07b2oggk6fd diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd deleted file mode 100644 index eb0dc277..00000000 --- a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd +++ /dev/null @@ -1,22 +0,0 @@ -## The test case execution stage.[br] -class_name GdUnitTestCaseParameterizedExecutionStage -extends IGdUnitExecutionStage - - -var _stage_before :IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new(false) -var _stage_after :IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new(false) -var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseParamaterizedTestStage.new() - - -func _execute(context :GdUnitExecutionContext) -> void: - await _stage_before.execute(context) - if not context.test_case.is_skipped(): - await _stage_test.execute(GdUnitExecutionContext.of(context)) - await _stage_after.execute(context) - - -func set_debug_mode(debug_mode :bool = false) -> void: - super.set_debug_mode(debug_mode) - _stage_before.set_debug_mode(debug_mode) - _stage_after.set_debug_mode(debug_mode) - _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd.uid deleted file mode 100644 index 3eb6a4aa..00000000 --- a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedExecutionStage.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c10wkwo85mmc1 diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd deleted file mode 100644 index 820a9d9f..00000000 --- a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd +++ /dev/null @@ -1,86 +0,0 @@ -## The parameterized test case execution stage.[br] -class_name GdUnitTestCaseParamaterizedTestStage -extends IGdUnitExecutionStage - -var _stage_before: IGdUnitExecutionStage = GdUnitTestCaseBeforeStage.new() -var _stage_after: IGdUnitExecutionStage = GdUnitTestCaseAfterStage.new() -var _stage_test: IGdUnitExecutionStage = GdUnitTestCaseParameterSetTestStage.new() - - -## Executes a parameterized test case.[br] -## It executes synchronized following stages[br] -## -> test_case( ) [br] -func _execute(context: GdUnitExecutionContext) -> void: - var test_case := context.test_case - var test_parameter_index := test_case.test_parameter_index() - var parameter_set_resolver := test_case.parameter_set_resolver() - var test_names := parameter_set_resolver.build_test_case_names(test_case) - - # if all parameter sets has static values we can preload and reuse it for better performance - var parameter_sets :Array = [] - if parameter_set_resolver.is_parameter_sets_static(): - parameter_sets = parameter_set_resolver.load_parameter_sets(test_case, true) - - for parameter_set_index in test_names.size(): - # is test_parameter_index is set, we run this parameterized test only - if test_parameter_index != -1 and test_parameter_index != parameter_set_index: - continue - var current_test_case_name := test_names[parameter_set_index] - var test_case_parameter_set: Array - if parameter_set_resolver.is_parameter_set_static(parameter_set_index): - test_case_parameter_set = parameter_sets[parameter_set_index] - - var test_context := GdUnitExecutionContext.of(context) - test_context._test_case_name = current_test_case_name - var has_errors := false - while test_context.retry_execution(): - var retry_test_context := GdUnitExecutionContext.of(test_context) - - retry_test_context._test_case_name = current_test_case_name - await _stage_before.execute(retry_test_context) - if not test_case.is_interupted(): - # we need to load paramater set at execution level after the before stage to get the actual variables from the current test - if not parameter_set_resolver.is_parameter_set_static(parameter_set_index): - test_case_parameter_set = _load_parameter_set(context, parameter_set_index) - await _stage_test.execute(GdUnitExecutionContext.of_parameterized_test(retry_test_context, current_test_case_name, test_case_parameter_set)) - await _stage_after.execute(retry_test_context) - has_errors = retry_test_context.has_errors() - if retry_test_context.is_success() or retry_test_context.is_skipped() or retry_test_context.is_interupted(): - break - - var is_success := test_context.evaluate_test_retry_status() - report_test_failure(context, !is_success, has_errors, parameter_set_index) - - if test_case.is_interupted(): - break - await context.gc() - - -func report_test_failure(test_context: GdUnitExecutionContext, is_failed: bool, has_errors: bool, parameter_set_index: int) -> void: - var test_case := test_context.test_case - - if is_failed: - test_context.add_report(GdUnitReport.new().create(GdUnitReport.FAILURE, test_case.line_number(), "Test failed at parameterized index %d." % parameter_set_index)) - if has_errors: - test_context.add_report(GdUnitReport.new().create(GdUnitReport.ABORT, test_case.line_number(), "Test aborted at parameterized index %d." % parameter_set_index)) - - -func _load_parameter_set(context: GdUnitExecutionContext, parameter_set_index: int) -> Array: - var test_case := context.test_case - # we need to exchange temporary for parameter resolving the execution context - # this is necessary because of possible usage of `auto_free` and needs to run in the parent execution context - var thread_context := GdUnitThreadManager.get_current_context() - var save_execution_context := thread_context.get_execution_context() - thread_context.set_execution_context(context) - var parameters := test_case.load_parameter_sets() - # restore the original execution context and restart the orphan monitor to get new instances into account - thread_context.set_execution_context(save_execution_context) - save_execution_context.orphan_monitor_start() - return parameters[parameter_set_index] - - -func set_debug_mode(debug_mode: bool=false) -> void: - super.set_debug_mode(debug_mode) - _stage_before.set_debug_mode(debug_mode) - _stage_after.set_debug_mode(debug_mode) - _stage_test.set_debug_mode(debug_mode) diff --git a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd.uid deleted file mode 100644 index df1ab94b..00000000 --- a/addons/gdUnit4/src/core/execution/stages/parameterized/GdUnitTestCaseParameterizedTestStage.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://rljd6vuv3w2b diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd index 775b8dbc..70d687f4 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd @@ -9,6 +9,7 @@ var _stage_test :IGdUnitExecutionStage = GdUnitTestCaseSingleTestStage.new() func _execute(context :GdUnitExecutionContext) -> void: + fire_event(GdUnitEvent.new().test_before(context.test_case.id())) while context.retry_execution(): var test_context := GdUnitExecutionContext.of(context) await _stage_before.execute(test_context) @@ -17,8 +18,14 @@ func _execute(context :GdUnitExecutionContext) -> void: await _stage_after.execute(test_context) if test_context.is_success() or test_context.is_skipped() or test_context.is_interupted(): break - @warning_ignore("return_value_discarded") - context.evaluate_test_retry_status() + + context.gc() + if context.is_skipped(): + fire_test_skipped(context) + else: + var reports: = context.collect_reports(true) + var statistics := context.calculate_statistics(reports) + fire_event(GdUnitEvent.new().test_after(context.test_case.id(), statistics, reports)) func set_debug_mode(debug_mode :bool = false) -> void: @@ -26,3 +33,21 @@ func set_debug_mode(debug_mode :bool = false) -> void: _stage_before.set_debug_mode(debug_mode) _stage_after.set_debug_mode(debug_mode) _stage_test.set_debug_mode(debug_mode) + + +func fire_test_skipped(context: GdUnitExecutionContext) -> void: + var test_case := context.test_case + var statistics := { + GdUnitEvent.ORPHAN_NODES: 0, + GdUnitEvent.ELAPSED_TIME: 0, + GdUnitEvent.WARNINGS: false, + GdUnitEvent.ERRORS: false, + GdUnitEvent.ERROR_COUNT: 0, + GdUnitEvent.FAILED: false, + GdUnitEvent.FAILED_COUNT: 0, + GdUnitEvent.SKIPPED: true, + GdUnitEvent.SKIPPED_COUNT: 1, + } + var report := GdUnitReport.new() \ + .create(GdUnitReport.SKIPPED, test_case.line_number(), GdAssertMessages.test_skipped(test_case.skip_info())) + fire_event(GdUnitEvent.new().test_after(test_case.id(), statistics, [report])) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid index 74280951..9328ca9a 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleExecutionStage.gd.uid @@ -1 +1 @@ -uid://bje72let8q1wn +uid://de2bb4vcpcdw2 diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd index 6882fe29..9006b368 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd @@ -8,4 +8,4 @@ extends IGdUnitExecutionStage ## -> test_case() [br] func _execute(context :GdUnitExecutionContext) -> void: await context.test_case.execute() - await context.gc() + await context.gc(GdUnitExecutionContext.GC_ORPHANS_CHECK.TEST_CASE) diff --git a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid index d2a3932d..e7ac1174 100644 --- a/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid +++ b/addons/gdUnit4/src/core/execution/stages/single/GdUnitTestCaseSingleTestStage.gd.uid @@ -1 +1 @@ -uid://dtdadxrq0xyh7 +uid://bpfmb4aqyf2ow diff --git a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd new file mode 100644 index 00000000..0f87ad1b --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd @@ -0,0 +1,78 @@ +class_name GdUnitBaseReporterTestSessionHook +extends GdUnitTestSessionHook + + +var test_session: GdUnitTestSession: + get: + return test_session + set(value): + # disconnect first possible connected listener + if test_session != null: + test_session.test_event.disconnect(_on_test_event) + # add listening to current session + test_session = value + if test_session != null: + test_session.test_event.connect(_on_test_event) + + +var _report_summary: GdUnitReportSummary +var _reporter: GdUnitTestReporter +var _report_writer: GdUnitReportWriter +var _report_converter: Callable + +func _init(report_writer: GdUnitReportWriter, hook_name: String, hook_description: String, report_converter: Callable) -> void: + super(hook_name, hook_description) + _reporter = GdUnitTestReporter.new() + _report_writer = report_writer + _report_converter = report_converter + + +func startup(session: GdUnitTestSession) -> GdUnitResult: + test_session = session + _report_summary = GdUnitReportSummary.new(_report_converter) + _reporter.init_summary() + + return GdUnitResult.success() + + +func shutdown(session: GdUnitTestSession) -> GdUnitResult: + var report_path := _report_writer.write(session.report_path, _report_summary) + session.send_message("Open {0} Report at: file://{1}".format([_report_writer.output_format(), report_path])) + + return GdUnitResult.success() + + +func _on_test_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.TESTSUITE_BEFORE: + _reporter.init_statistics() + _report_summary.add_testsuite_report(event.resource_path(), event.suite_name(), event.total_count()) + GdUnitEvent.TESTSUITE_AFTER: + var statistics := _reporter.build_test_suite_statisitcs(event) + _report_summary.update_testsuite_counters( + event.resource_path(), + _reporter.error_count(statistics), + _reporter.failed_count(statistics), + _reporter.orphan_nodes(statistics), + _reporter.skipped_count(statistics), + _reporter.flaky_count(statistics), + event.elapsed_time()) + _report_summary.add_testsuite_reports( + event.resource_path(), + event.reports() + ) + GdUnitEvent.TESTCASE_BEFORE: + var test := test_session.find_test_by_id(event.guid()) + _report_summary.add_testcase(test.source_file, test.suite_name, test.display_name) + GdUnitEvent.TESTCASE_AFTER: + _reporter.add_test_statistics(event) + var test := test_session.find_test_by_id(event.guid()) + _report_summary.set_counters(test.source_file, + test.display_name, + event.error_count(), + event.failed_count(), + event.orphan_nodes(), + event.is_skipped(), + event.is_flaky(), + event.elapsed_time()) + _report_summary.add_reports(test.source_file, test.display_name, event.reports()) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid new file mode 100644 index 00000000..f734e4d7 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitBaseReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://bl4f3it63fcpf diff --git a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd new file mode 100644 index 00000000..4b8f390f --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd @@ -0,0 +1,9 @@ +class_name GdUnitHtmlReporterTestSessionHook +extends GdUnitBaseReporterTestSessionHook + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + + +func _init() -> void: + super(GdUnitHtmlReportWriter.new(), "GdUnitHtmlTestReporter", "The Html test reporting hook.", GdUnitTools.richtext_normalize) + set_meta("SYSTEM_HOOK", true) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid new file mode 100644 index 00000000..5ee5fcff --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://duosr8em0lcfx diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd new file mode 100644 index 00000000..23850e4a --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd @@ -0,0 +1,111 @@ +## @since GdUnit4 5.1.0 +## +## Base class for creating custom test session hooks in GdUnit4.[br] +## [br] +## [i]Test session hooks allow users to extend the GdUnit4 test framework by providing +## custom functionality that runs at specific points during the test execution lifecycle. +## This base class defines the interface that all test session hooks must implement.[/i] +## [br] +## [br] +## [b][u]Usage[/u][/b][br] +## 1. Create a new class that extends GdUnitTestSessionHook[br] +## 2. Override the required methods (startup, shutdown)[br] +## 3. Register your hook with the test engine (using the GdUnit4 settings dialog)[br] +## [br] +## [b][u]Example[/u][/b] +## [codeblock] +## class_name MyCustomTestHook +## extends GdUnitTestSessionHook +## +## func _init(): +## super("MyHook", "This is a description") +## +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Custom hook initialized") +## # Initialize resources, setup test environment, etc. +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Custom hook cleanup completed") +## # Cleanup resources, generate reports, etc. +## return GdUnitResult.success() +## [/codeblock] +## +## [b][u]Hook Lifecycle[/u][/b][br] +## 1. [i][b]Registration[/b][/i]: Hooks are registered with the test engine via settings dialog[br] +## 2. [i][b]Priority Sorting[/b][/i]: Hooks are sorted by priority[br] +## 3. [i][b]Startup[/b][/i]: startup() is called before test execution begins, if it returns an error is shown in the console[br] +## 4. [i][b]Test Execution[/b][/i]: Tests run normally (only if all hooks started successfully)[br] +## 5. [i][b]Shutdown[/b][/i]: shutdown() is called after all tests complete, regardless of startup success[br] +## [br] +## [b][u]Priority System[/u][/b][br] +## The priority system allows controlling the execution order of multiple hooks.[br] +## - The order can be changed in the GdUnit4 settings dialog.[br] +## - The priority of system hooks cannot be changed and they cannot be deleted.[br] +## [br] +## [b][u]Session Access[/u][/b][br] +## +## Both [i]startup()[/i] and [i]shutdown()[/i] methods receive a [GdUnitTestSession] parameter that provides:[br] +## - Access to test cases being executed[br] +## - Event emission capabilities for test progress tracking[br] +## - Message sending functionality for logging and communication[br] +class_name GdUnitTestSessionHook +extends RefCounted + + +## The display name of this hook. +var name: String: + get: + return name + + +## A detailed description of what this hook does. +var description: String: + get: + return description + + +## Initializes a new test session hook. +## +## [param _name] The display name for this hook +## [param _description] A detailed description of the hook's functionality +func _init(_name: String, _description: String) -> void: + self.name = _name + self.description = _description + + +## Called when the test session starts up, before any tests are executed.[br] +## [br] +## [color=yellow][i]This method should be overridden to implement custom initialization logic[/i][/color][br] +## [br] +## such as:[br] +## - Setting up test databases or external services[br] +## - Initializing mock objects or test fixtures[br] +## - Configuring logging or reporting systems[br] +## - Preparing the test environment[br] +## - Subscribing to test events via the session[br] +## [br] +## [param session] The test session instance providing access to test data and communication[br] +## [b]return:[/b] [code]GdUnitResult.success()[/code] if initialization succeeds, or [code]GdUnitResult.error("error")[/code] with +## an error message if initialization fails. +func startup(_session: GdUnitTestSession) -> GdUnitResult: + return GdUnitResult.error("%s:startup is not implemented" % get_script().resource_path) + + +## Called when the test session shuts down, after all tests have completed.[br] +## [br] +## [color=yellow][i]This method should be overridden to implement custom cleanup logic[/i][/color][br] +## [br] +## such as:[br] +## - Cleaning up test databases or external services[br] +## - Generating test reports or artifacts[br] +## - Releasing resources allocated during startup[br] +## - Performing final validation or assertions[br] +## - Processing collected test events and data[br] +## [br] +## [param session] The test session instance providing access to test results and communication[br] +## [b]return:[/b] [code]GdUnitResult.success()[/code] if cleanup succeeds, or [code]GdUnitResult.error("error")[/code] with +## an error message if cleanup fails. Cleanup errors are typically logged +## but don't prevent the test engine from shutting down. +func shutdown(_session: GdUnitTestSession) -> GdUnitResult: + return GdUnitResult.error("%s:shutdown is not implemented" % get_script().resource_path) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid new file mode 100644 index 00000000..52dac476 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://twyg7v88rnfo diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd new file mode 100644 index 00000000..5a9bdcb8 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd @@ -0,0 +1,191 @@ +class_name GdUnitTestSessionHookService +extends Object + + +var enigne_hooks: Array[GdUnitTestSessionHook] = []: + get: + return enigne_hooks + set(value): + enigne_hooks.append(value) + + +var _save_settings: bool = false + + +static func instance() -> GdUnitTestSessionHookService: + return GdUnitSingleton.instance("GdUnitTestSessionHookService", func()->GdUnitTestSessionHookService: + GdUnitSignals.instance().gdunit_message.emit("Installing GdUnit4 session system hooks.") + var service := GdUnitTestSessionHookService.new() + # Register default system hooks here + service._save_settings = false + service.register(GdUnitHtmlReporterTestSessionHook.new()) + service.register(GdUnitXMLReporterTestSessionHook.new()) + service.load_hook_settings() + service._save_settings = true + return service + ) + + +static func contains_hook(current: GdUnitTestSessionHook, other: GdUnitTestSessionHook) -> bool: + return current.get_script().resource_path == other.get_script().resource_path + + +func find_custom(hook: GdUnitTestSessionHook) -> int: + for index in enigne_hooks.size(): + if contains_hook.call(enigne_hooks[index], hook): + return index + return -1 + + +func load_hook(hook_resourc_path: String) -> GdUnitResult: + if !FileAccess.file_exists(hook_resourc_path): + return GdUnitResult.error("The hook '%s' not exists." % hook_resourc_path) + var script: GDScript = load(hook_resourc_path) + if script.get_base_script() != GdUnitTestSessionHook: + return GdUnitResult.error("The hook '%s' must inhertit from 'GdUnitTestSessionHook'." % hook_resourc_path) + + return GdUnitResult.success(script.new()) + + +func enable_hook(hook: GdUnitTestSessionHook, enabled: bool) -> void: + _enable_hook(hook, enabled) + GdUnitSignals.instance().gdunit_message.emit("Session hook '{name}' {enabled}.".format({ + "name": hook.name, + "enabled": "enabled" if enabled else "disabled"}) + ) + save_hock_setttings() + + +func register(hook: GdUnitTestSessionHook, enabled: bool = true) -> GdUnitResult: + if find_custom(hook) != -1: + return GdUnitResult.error("A hook instance of '%s' is already registered." % hook.get_script().resource_path) + + _enable_hook(hook, enabled) + enigne_hooks.append(hook) + save_hock_setttings() + GdUnitSignals.instance().gdunit_message.emit("Session hook '%s' installed." % hook.name) + + return GdUnitResult.success() + + +func unregister(hook: GdUnitTestSessionHook) -> GdUnitResult: + var hook_index := find_custom(hook) + if hook_index == -1: + return GdUnitResult.error("The hook instance of '%s' is NOT registered." % hook.get_script().resource_path) + + enigne_hooks.remove_at(hook_index) + save_hock_setttings() + return GdUnitResult.success() + + +func move_before(hook: GdUnitTestSessionHook, before: GdUnitTestSessionHook) -> void: + var before_index := find_custom(before) + var hook_index := find_custom(hook) + + # Verify the hook to move is behind the hook to be moved + if before_index >= hook_index: + return + + enigne_hooks.remove_at(hook_index) + enigne_hooks.insert(before_index, hook) + save_hock_setttings() + + +func move_after(hook: GdUnitTestSessionHook, after: GdUnitTestSessionHook) -> void: + var after_index := find_custom(after) + var hook_index := find_custom(hook) + + # Verify the hook to move is before the hook to be moved + if after_index <= hook_index: + return + + enigne_hooks.remove_at(hook_index) + enigne_hooks.insert(after_index, hook) + save_hock_setttings() + + +func execute_startup(session: GdUnitTestSession) -> GdUnitResult: + return await execute("startup", session) + + +func execute_shutdown(session: GdUnitTestSession) -> GdUnitResult: + return await execute("shutdown", session, true) + + +func execute(hook_func: String, session: GdUnitTestSession, reverse := false) -> GdUnitResult: + var failed_hook_calls: Array[GdUnitResult] = [] + + for hook_index in enigne_hooks.size(): + var index := enigne_hooks.size()-hook_index-1 if reverse else hook_index + var hook: = enigne_hooks[index] + if not is_enabled(hook): + continue + if OS.is_stdout_verbose(): + GdUnitSignals.instance().gdunit_message.emit("Session hook '%s' > %s()" % [hook.name, hook_func]) + var result: GdUnitResult = await hook.call(hook_func, session) + if result == null: + failed_hook_calls.push_back(GdUnitResult.error("Result is null! Check '%s'" % hook.get_script().resource_path)) + elif result.is_error(): + failed_hook_calls.push_back(result) + + if failed_hook_calls.is_empty(): + return GdUnitResult.success() + + var errors := failed_hook_calls.map(func(result: GdUnitResult) -> String: + return "Hook call '%s' failed with error: '%s'" % [hook_func, result.error_message()] + ) + return GdUnitResult.error( "\n".join(errors)) + + +func save_hock_setttings() -> void: + if not _save_settings: + return + + var hooks_to_save: Dictionary[String, bool] = {} + for hook in enigne_hooks: + var enabled: bool = hook.get_meta("enabled") + hooks_to_save[hook.get_script().resource_path] = enabled + + GdUnitSettings.set_session_hooks(hooks_to_save) + + +func load_hook_settings() -> void: + var hooks_resource_paths := GdUnitSettings.get_session_hooks() + if hooks_resource_paths.is_empty(): + return + + for hock_path: String in hooks_resource_paths.keys(): + var enabled := hooks_resource_paths[hock_path] + + # Do not reinstall already installed hooks + var existing_hook: GdUnitTestSessionHook = enigne_hooks.filter(func(element: GdUnitTestSessionHook) -> bool: + return element.get_script().resource_path == hock_path + ).front() + # Applay enabled settings + if existing_hook != null: + _enable_hook(existing_hook, enabled) + continue + + # Load additional hooks + var result := load_hook(hock_path) + if result.is_error(): + push_error(result.error_message()) + continue + + GdUnitSignals.instance().gdunit_message.emit("Installing GdUnit4 session hooks.") + var hook: GdUnitTestSessionHook = result.value() + + result = register(hook, enabled) + if result.is_error(): + push_error(result.error_message()) + continue + + +static func is_enabled(hook: GdUnitTestSessionHook) -> bool: + if hook.has_meta("enabled"): + return hook.get_meta("enabled") + return true + + +func _enable_hook(hook: GdUnitTestSessionHook, enabled: bool) -> void: + hook.set_meta("enabled", enabled) diff --git a/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid new file mode 100644 index 00000000..b9b56b89 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitTestSessionHookService.gd.uid @@ -0,0 +1 @@ +uid://dwndskvuv87il diff --git a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd new file mode 100644 index 00000000..94caef59 --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd @@ -0,0 +1,11 @@ +class_name GdUnitXMLReporterTestSessionHook +extends GdUnitBaseReporterTestSessionHook + + +func _init() -> void: + super(JUnitXmlReportWriter.new(), "GdUnitXMLTestReporter", "The JUnit XML test reporting hook.", convert_report_message) + set_meta("SYSTEM_HOOK", true) + + +func convert_report_message(value: String) -> String: + return value diff --git a/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid new file mode 100644 index 00000000..1aa819db --- /dev/null +++ b/addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd.uid @@ -0,0 +1 @@ +uid://bsxf0ve5hw76y diff --git a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid index 185a4a2e..c0853a49 100644 --- a/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdClassDescriptor.gd.uid @@ -1 +1 @@ -uid://bn8p0odai1uet +uid://do5rfsltevx33 diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd index 33022fd8..f1b22440 100644 --- a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd @@ -21,7 +21,7 @@ var _decoders := { TYPE_PACKED_COLOR_ARRAY: _on_type_Array.bind(TYPE_PACKED_COLOR_ARRAY), TYPE_PACKED_VECTOR2_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR2_ARRAY), TYPE_PACKED_VECTOR3_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR3_ARRAY), - GdObjects.TYPE_PACKED_VECTOR4_ARRAY: _on_type_Array.bind(GdObjects.TYPE_PACKED_VECTOR4_ARRAY), + TYPE_PACKED_VECTOR4_ARRAY: _on_type_Array.bind(TYPE_PACKED_VECTOR4_ARRAY), TYPE_DICTIONARY: _on_type_Dictionary, TYPE_RID: _on_type_RID, TYPE_NODE_PATH: _on_type_NodePath, @@ -45,7 +45,7 @@ var _decoders := { TYPE_OBJECT: _on_type_Object } -static func _regex(pattern :String) -> RegEx: +static func _regex(pattern: String) -> RegEx: var regex := RegEx.new() var err := regex.compile(pattern) if err != OK: @@ -54,11 +54,11 @@ static func _regex(pattern :String) -> RegEx: return regex -func get_decoder(type :int) -> Callable: +func get_decoder(type: int) -> Callable: return _decoders.get(type, func(value :Variant) -> String: return '%s' % value) -func _on_type_StringName(value :StringName) -> String: +func _on_type_StringName(value: StringName) -> String: if value.is_empty(): return 'StringName()' return 'StringName("%s")' % value @@ -74,27 +74,27 @@ func _on_type_Color(color: Color) -> String: return "Color%s" % color -func _on_type_NodePath(path :NodePath) -> String: +func _on_type_NodePath(path: NodePath) -> String: if path.is_empty(): return 'NodePath()' return 'NodePath("%s")' % path -func _on_type_Callable(_cb :Callable) -> String: +func _on_type_Callable(_cb: Callable) -> String: return 'Callable()' -func _on_type_Signal(_s :Signal) -> String: +func _on_type_Signal(_s: Signal) -> String: return 'Signal()' -func _on_type_Dictionary(dict :Dictionary) -> String: +func _on_type_Dictionary(dict: Dictionary) -> String: if dict.is_empty(): return '{}' return str(dict) -func _on_type_Array(value :Variant, type :int) -> String: +func _on_type_Array(value: Variant, type: int) -> String: match type: TYPE_ARRAY: return str(value) @@ -126,7 +126,7 @@ func _on_type_Array(value :Variant, type :int) -> String: return "PackedVector3Array()" return "PackedVector3Array([%s])" % ", ".join(vectors) - GdObjects.TYPE_PACKED_VECTOR4_ARRAY: + TYPE_PACKED_VECTOR4_ARRAY: var vectors := PackedStringArray() for vector: Vector4 in value: @warning_ignore("return_value_discarded") @@ -150,7 +150,7 @@ func _on_type_Array(value :Variant, type :int) -> String: TYPE_PACKED_INT32_ARRAY,\ TYPE_PACKED_INT64_ARRAY: var vectors := PackedStringArray() - for vector :Variant in value: + for vector: Variant in value: @warning_ignore("return_value_discarded") vectors.append(str(vector)) if vectors.is_empty(): @@ -159,7 +159,12 @@ func _on_type_Array(value :Variant, type :int) -> String: return "unknown array type %d" % type -func _on_type_Vector(value :Variant, type :int) -> String: +func _on_type_Vector(value: Variant, type: int) -> String: + + if typeof(value) != type: + push_error("Internal Error: type missmatch detected for value '%s', expects type %s" % [value, type_string(type)]) + return "" + match type: TYPE_VECTOR2: if value == Vector2(): @@ -188,73 +193,72 @@ func _on_type_Vector(value :Variant, type :int) -> String: return "unknown vector type %d" % type -func _on_type_Transform2D(transform :Transform2D) -> String: +func _on_type_Transform2D(transform: Transform2D) -> String: if transform == Transform2D(): return "Transform2D()" return "Transform2D(Vector2%s, Vector2%s, Vector2%s)" % [transform.x, transform.y, transform.origin] -func _on_type_Transform3D(transform :Transform3D) -> String: +func _on_type_Transform3D(transform: Transform3D) -> String: if transform == Transform3D(): return "Transform3D()" return "Transform3D(Vector3%s, Vector3%s, Vector3%s, Vector3%s)" % [transform.basis.x, transform.basis.y, transform.basis.z, transform.origin] -func _on_type_Projection(projection :Projection) -> String: +func _on_type_Projection(projection: Projection) -> String: return "Projection(Vector4%s, Vector4%s, Vector4%s, Vector4%s)" % [projection.x, projection.y, projection.z, projection.w] @warning_ignore("unused_parameter") -func _on_type_RID(value :RID) -> String: +func _on_type_RID(value: RID) -> String: return "RID()" -func _on_type_Rect2(rect :Rect2) -> String: +func _on_type_Rect2(rect: Rect2) -> String: if rect == Rect2(): return "Rect2()" return "Rect2(Vector2%s, Vector2%s)" % [rect.position, rect.size] -func _on_type_Rect2i(rect :Variant) -> String: +func _on_type_Rect2i(rect: Variant) -> String: if rect == Rect2i(): return "Rect2i()" return "Rect2i(Vector2i%s, Vector2i%s)" % [rect.position, rect.size] -func _on_type_Plane(plane :Plane) -> String: +func _on_type_Plane(plane: Plane) -> String: if plane == Plane(): return "Plane()" return "Plane(%d, %d, %d, %d)" % [plane.x, plane.y, plane.z, plane.d] -func _on_type_Quaternion(quaternion :Quaternion) -> String: +func _on_type_Quaternion(quaternion: Quaternion) -> String: if quaternion == Quaternion(): return "Quaternion()" return "Quaternion(%d, %d, %d, %d)" % [quaternion.x, quaternion.y, quaternion.z, quaternion.w] -func _on_type_AABB(aabb :AABB) -> String: +func _on_type_AABB(aabb: AABB) -> String: if aabb == AABB(): return "AABB()" return "AABB(Vector3%s, Vector3%s)" % [aabb.position, aabb.size] -func _on_type_Basis(basis :Basis) -> String: +func _on_type_Basis(basis: Basis) -> String: if basis == Basis(): return "Basis()" return "Basis(Vector3%s, Vector3%s, Vector3%s)" % [basis.x, basis.y, basis.z] -@warning_ignore("unsafe_cast") -static func decode(value :Variant) -> String: +static func decode(value: Variant) -> String: var type := typeof(value) + @warning_ignore("unsafe_cast") if GdArrayTools.is_type_array(type) and (value as Array).is_empty(): return "" - var decoder :Callable = ( - instance("GdUnitDefaultValueDecoders", - func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new() - ) as GdDefaultValueDecoder - ).get_decoder(type) + # For Variant types we need to determine the original type + if type == GdObjects.TYPE_VARIANT: + type = typeof(value) + var decoder := _get_value_decoder(type) if decoder == null: push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) return "null" @@ -263,18 +267,24 @@ static func decode(value :Variant) -> String: return decoder.call(value) -@warning_ignore("unsafe_cast") -static func decode_typed(type :int, value :Variant) -> String: +static func decode_typed(type: int, value: Variant) -> String: if value == null: return "null" - var decoder: Callable = ( - instance("GdUnitDefaultValueDecoders", - func() -> GdDefaultValueDecoder: return GdDefaultValueDecoder.new() - ) as GdDefaultValueDecoder - ).get_decoder(type) + # For Variant types we need to determine the original type + if type == GdObjects.TYPE_VARIANT: + type = typeof(value) + var decoder := _get_value_decoder(type) if decoder == null: push_error("No value decoder registered for type '%d'! Please open a Bug issue at 'https://github.com/MikeSchulze/gdUnit4/issues/new/choose'." % type) return "null" if type == TYPE_OBJECT: return decoder.call(value, type) return decoder.call(value) + + +static func _get_value_decoder(type: int) -> Callable: + var decoder: GdDefaultValueDecoder = instance( + "GdUnitDefaultValueDecoders", + func() -> GdDefaultValueDecoder: + return GdDefaultValueDecoder.new()) + return decoder.get_decoder(type) diff --git a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid index 2d320a58..1e8d0744 100644 --- a/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdDefaultValueDecoder.gd.uid @@ -1 +1 @@ -uid://djymig2e3wxpc +uid://c6ne3cfhftlbk diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd index 190d9461..bd38eb2a 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd @@ -4,7 +4,7 @@ extends RefCounted const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") const UNDEFINED: String = "<-NO_ARG->" -const ARG_PARAMETERIZED_TEST := "test_parameters" +const ARG_PARAMETERIZED_TEST := ["test_parameters", "_test_parameters"] static var _fuzzer_regex: RegEx static var _cleanup_leading_spaces: RegEx @@ -22,7 +22,7 @@ func _init(p_name: String, p_type: int, value: Variant = UNDEFINED, p_type_hint: _name = p_name _type = p_type _type_hint = p_type_hint - if value != null and p_name == ARG_PARAMETERIZED_TEST: + if value != null and p_name in ARG_PARAMETERIZED_TEST: _parameter_sets = _parse_parameter_set(str(value)) _default_value = value # is argument a fuzzer? @@ -42,7 +42,7 @@ func name() -> String: func default() -> Variant: - return GodotVersionFixures.convert(_default_value, _type) + return type_convert(_default_value, _type) func set_value(value: String) -> void: @@ -50,16 +50,29 @@ func set_value(value: String) -> void: if _type == GdObjects.TYPE_FUZZER: _default_value = value return - if _name == ARG_PARAMETERIZED_TEST: + if _name in ARG_PARAMETERIZED_TEST: _parameter_sets = _parse_parameter_set(value) _default_value = value return if _type == TYPE_NIL or _type == GdObjects.TYPE_VARIANT: _type = _extract_value_type(value) - _default_value = value + if _type == GdObjects.TYPE_VARIANT and _default_value == null: + _default_value = value if _default_value == null: - _default_value = value + match _type: + TYPE_DICTIONARY: + _default_value = as_dictionary(value) + TYPE_ARRAY: + _default_value = as_array(value) + GdObjects.TYPE_FUZZER: + _default_value = value + _: + _default_value = str_to_var(value) + # if converting fails assign the original value without converting + if _default_value == null and value != null: + _default_value = value + #prints("set default_value: ", _default_value, "with type %d" % _type, " from original: '%s'" % value) func _extract_value_type(value: String) -> int: @@ -98,7 +111,7 @@ func is_typed_array() -> bool: func is_parameter_set() -> bool: - return _name == ARG_PARAMETERIZED_TEST + return _name in ARG_PARAMETERIZED_TEST func parameter_sets() -> PackedStringArray: @@ -115,10 +128,10 @@ static func get_parameter_set(parameters :Array[GdFunctionArgument]) -> GdFuncti func _to_string() -> String: var s := _name if _type != TYPE_NIL: - s += ":" + GdObjects.type_as_string(_type) + s += ": " + GdObjects.type_as_string(_type) if _type_hint != TYPE_NIL: s += "[%s]" % GdObjects.type_as_string(_type_hint) - if typeof(_default_value) != TYPE_STRING: + if has_default(): s += "=" + value_as_string() return s @@ -170,3 +183,26 @@ func _parse_parameter_set(input :String) -> PackedStringArray: collected_characters.clear() matched = false return output + + +## value converters + +func as_array(value: String) -> Array: + if value == "Array()" or value == "[]": + return [] + + if value.begins_with("Array("): + value = value.lstrip("Array(").rstrip(")") + if value.begins_with("["): + return str_to_var(value) + return [] + + +func as_dictionary(value: String) -> Dictionary: + if value == "Dictionary()": + return {} + if value.begins_with("Dictionary("): + value = value.lstrip("Dictionary(").rstrip(")") + if value.begins_with("{"): + return str_to_var(value) + return {} diff --git a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid index f8be9f6d..96970ff2 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdFunctionArgument.gd.uid @@ -1 +1 @@ -uid://bcpjjsi01bw8s +uid://5d82r5ehvht4 diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd index 27a21b2f..7eae4b2f 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd @@ -87,7 +87,7 @@ func is_coroutine() -> bool: func is_parameterized() -> bool: for current in _args: var arg :GdFunctionArgument = current - if arg.name() == GdFunctionArgument.ARG_PARAMETERIZED_TEST: + if arg.name() in GdFunctionArgument.ARG_PARAMETERIZED_TEST: return true return false @@ -101,17 +101,28 @@ func return_type() -> int: func return_type_as_string() -> String: + if return_type() == TYPE_NIL: + return "void" if (return_type() == TYPE_OBJECT or return_type() == GdObjects.TYPE_ENUM) and not _return_class.is_empty(): return _return_class return GdObjects.type_as_string(return_type()) -@warning_ignore("unsafe_cast") func set_argument_value(arg_name: String, value: String) -> void: - ( - _args.filter(func(arg: GdFunctionArgument) -> bool: return arg.name() == arg_name)\ - .front() as GdFunctionArgument - ).set_value(value) + var argument: GdFunctionArgument = _args.filter(func(arg: GdFunctionArgument) -> bool: + return arg.name() == arg_name + ).front() + if argument != null: + argument.set_value(value) + + +func enrich_arguments(arguments: Array[Dictionary]) -> void: + for arg_index: int in arguments.size(): + var arg: Dictionary = arguments[arg_index] + if arg["type"] != GdObjects.TYPE_VARARG: + var arg_name: String = arg["name"] + var arg_value: String = arg["value"] + set_argument_value(arg_name, arg_value) func enrich_file_info(p_source_path: String, p_line_number: int) -> void: @@ -226,10 +237,7 @@ static func _build_varargs(p_is_vararg :bool) -> Array[GdFunctionArgument]: var varargs_ :Array[GdFunctionArgument] = [] if not p_is_vararg: return varargs_ - # if function has vararg we need to handle this manually by adding 10 default arguments - var type := GdObjects.TYPE_VARARG - for index in 10: - varargs_.push_back(GdFunctionArgument.new("vararg%d_" % index, type, '"%s"' % GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE)) + varargs_.push_back(GdFunctionArgument.new("varargs", GdObjects.TYPE_VARARG, '')) return varargs_ diff --git a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid index 27b7175a..ca0e8a09 100644 --- a/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdFunctionDescriptor.gd.uid @@ -1 +1 @@ -uid://dcw0cr5cv0qlj +uid://b2r5u0pn3l550 diff --git a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd new file mode 100644 index 00000000..9c45e625 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd @@ -0,0 +1,188 @@ +class_name GdFunctionParameterSetResolver +extends RefCounted + +const CLASS_TEMPLATE = """ +class_name _ParameterExtractor extends '${clazz_path}' + +func __extract_test_parameters() -> Array: + return ${test_params} + +""" + +const EXCLUDE_PROPERTIES_TO_COPY = [ + "script", + "type", + "Node", + "_import_path"] + + +var _fd: GdFunctionDescriptor +var _static_sets_by_index := {} +var _is_static := true + +func _init(fd: GdFunctionDescriptor) -> void: + _fd = fd + + +func resolve_test_cases(script: GDScript) -> Array[GdUnitTestCase]: + if not is_parameterized(): + return [GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name())] + return extract_test_cases_by_reflection(script) + + +func is_parameterized() -> bool: + return _fd.is_parameterized() + + +func is_parameter_sets_static() -> bool: + return _is_static + + +func is_parameter_set_static(index: int) -> bool: + return _is_static and _static_sets_by_index.get(index, false) + + +# validates the given arguments are complete and matches to required input fields of the test function +func validate(input_value_set: Array) -> String: + var input_arguments := _fd.args() + # check given parameter set with test case arguments + var expected_arg_count := input_arguments.size() - 1 + for input_values :Variant in input_value_set: + var parameter_set_index := input_value_set.find(input_values) + if input_values is Array: + var arr_values: Array = input_values + var current_arg_count := arr_values.size() + if current_arg_count != expected_arg_count: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count] + var error := validate_parameter_types(input_arguments, arr_values, parameter_set_index) + if not error.is_empty(): + return error + else: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n Expecting an array of input values." % parameter_set_index + return "" + + +static func validate_parameter_types(input_arguments: Array, input_values: Array, parameter_set_index: int) -> String: + for i in input_arguments.size(): + var input_param: GdFunctionArgument = input_arguments[i] + # only check the test input arguments + if input_param.is_parameter_set(): + continue + var input_param_type := input_param.type() + var input_value :Variant = input_values[i] + var input_value_type := typeof(input_value) + # input parameter is not typed or is Variant we skip the type test + if input_param_type == TYPE_NIL or input_param_type == GdObjects.TYPE_VARIANT: + continue + # is input type enum allow int values + if input_param_type == GdObjects.TYPE_VARIANT and input_value_type == TYPE_INT: + continue + # allow only equal types and object == null + if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL: + continue + if input_param_type != input_value_type: + return "\n The parameter set at index [%d] does not match the expected input parameters!\n The value '%s' does not match the required input parameter <%s>." % [parameter_set_index, input_value, input_param] + return "" + + +func extract_test_cases_by_reflection(script: GDScript) -> Array[GdUnitTestCase]: + var source: Node = script.new() + source.queue_free() + + var fa := GdFunctionArgument.get_parameter_set(_fd.args()) + var parameter_sets := fa.parameter_sets() + # if no parameter set detected we need to resolve it by using reflection + if parameter_sets.size() == 0: + _is_static = false + return _extract_test_cases_by_reflection(source, script) + else: + var test_cases: Array[GdUnitTestCase] = [] + var property_names := _extract_property_names(source) + for parameter_set_index in parameter_sets.size(): + var parameter_set := parameter_sets[parameter_set_index] + _static_sets_by_index[parameter_set_index] = _is_static_parameter_set(parameter_set, property_names) + @warning_ignore("return_value_discarded") + test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), parameter_set_index, parameter_set)) + parameter_set_index += 1 + return test_cases + + +func _extract_property_names(source: Node) -> PackedStringArray: + return source.get_property_list()\ + .map(func(property :Dictionary) -> String: return property["name"])\ + .filter(func(property :String) -> bool: return !EXCLUDE_PROPERTIES_TO_COPY.has(property)) + + +# tests if the test property set contains an property reference by name, if not the parameter set holds only static values +func _is_static_parameter_set(parameters :String, property_names :PackedStringArray) -> bool: + for property_name in property_names: + if parameters.contains(property_name): + _is_static = false + return false + return true + + +func _extract_test_cases_by_reflection(source: Node, script: GDScript) -> Array[GdUnitTestCase]: + var parameter_sets := load_parameter_sets(source) + var test_cases: Array[GdUnitTestCase] = [] + for index in parameter_sets.size(): + var parameter_set := str(parameter_sets[index]) + @warning_ignore("return_value_discarded") + test_cases.append(GdUnitTestCase.from(script.resource_path, _fd.source_path(), _fd.line_number(), _fd.name(), index, parameter_set)) + return test_cases + + +# extracts the arguments from the given test case, using kind of reflection solution +# to restore the parameters from a string representation to real instance type +func load_parameter_sets(source: Node) -> Array: + var source_script: GDScript = source.get_script() + var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args()) + var source_code := CLASS_TEMPLATE \ + .replace("${clazz_path}", source_script.resource_path) \ + .replace("${test_params}", parameter_arg.value_as_string()) + var script := GDScript.new() + script.source_code = source_code + # enable this lines only for debuging + #script.resource_path = GdUnitFileAccess.create_temp_dir("parameter_extract") + "/%s__.gd" % test_case.get_name() + #DirAccess.remove_absolute(script.resource_path) + #ResourceSaver.save(script, script.resource_path) + var result := script.reload() + if result != OK: + push_error("Extracting test parameters failed! Script loading error: %s" % result) + return [] + var instance: Node = script.new() + GdFunctionParameterSetResolver.copy_properties(source, instance) + instance.queue_free() + var parameter_sets: Array = instance.call("__extract_test_parameters") + return fixure_typed_parameters(parameter_sets, _fd.args()) + + +func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array: + for parameter_set_index in parameter_sets.size(): + var parameter_set: Array = parameter_sets[parameter_set_index] + # run over all function arguments + for parameter_index in parameter_set.size(): + var parameter :Variant = parameter_set[parameter_index] + var arg_descriptor: GdFunctionArgument = arg_descriptors[parameter_index] + if parameter is Array: + var as_array: Array = parameter + # we need to convert the untyped array to the expected typed version + if arg_descriptor.is_typed_array(): + parameter_set[parameter_index] = Array(as_array, arg_descriptor.type_hint(), "", null) + return parameter_sets + + +static func copy_properties(source: Object, dest: Object) -> void: + for property in source.get_property_list(): + var property_name :String = property["name"] + var property_value :Variant = source.get(property_name) + if EXCLUDE_PROPERTIES_TO_COPY.has(property_name): + continue + #if dest.get(property_name) == null: + # prints("|%s|" % property_name, source.get(property_name)) + + # check for invalid name property + if property_name == "name" and property_value == "": + dest.set(property_name, ""); + continue + dest.set(property_name, property_value) diff --git a/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid new file mode 100644 index 00000000..de18aca8 --- /dev/null +++ b/addons/gdUnit4/src/core/parse/GdFunctionParameterSetResolver.gd.uid @@ -0,0 +1 @@ +uid://dyskimjijmyhk diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd b/addons/gdUnit4/src/core/parse/GdScriptParser.gd index d0a6e8ef..a1025082 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd @@ -3,19 +3,25 @@ extends RefCounted const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") -const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"" +const TYPE_VOID = GdObjects.TYPE_VOID +const TYPE_VARIANT = GdObjects.TYPE_VARIANT +const TYPE_VARARG = GdObjects.TYPE_VARARG +const TYPE_FUNC = GdObjects.TYPE_FUNC +const TYPE_FUZZER = GdObjects.TYPE_FUZZER +const TYPE_ENUM = GdObjects.TYPE_ENUM + var TOKEN_NOT_MATCH := Token.new("") var TOKEN_SPACE := SkippableToken.new(" ") var TOKEN_TABULATOR := SkippableToken.new("\t") var TOKEN_NEW_LINE := SkippableToken.new("\n") var TOKEN_COMMENT := SkippableToken.new("#") -var TOKEN_CLASS_NAME := Token.new("class_name") -var TOKEN_INNER_CLASS := Token.new("class") -var TOKEN_EXTENDS := Token.new("extends") -var TOKEN_ENUM := Token.new("enum") -var TOKEN_FUNCTION_STATIC_DECLARATION := Token.new("static func") -var TOKEN_FUNCTION_DECLARATION := Token.new("func") +var TOKEN_CLASS_NAME := RegExToken.new("class_name", GdUnitTools.to_regex("(class_name)\\s+([\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class_name)\\s+([\\w\\p{L}\\p{N}_]+)"), 5) +var TOKEN_INNER_CLASS := TokenInnerClass.new("class", GdUnitTools.to_regex("(class)\\s+(\\w\\p{L}\\p{N}_]+) (extends[a-zA-Z]+:)|(class)\\s+([\\w\\p{L}\\p{N}_]+)"), 5) +var TOKEN_EXTENDS := RegExToken.new("extends", GdUnitTools.to_regex("extends\\s+")) +var TOKEN_ENUM := RegExToken.new("enum", GdUnitTools.to_regex("enum\\s+")) +var TOKEN_FUNCTION_STATIC_DECLARATION := RegExToken.new("static func", GdUnitTools.to_regex("^static\\s+func\\s+([\\w\\p{L}\\p{N}_]+)"), 1) +var TOKEN_FUNCTION_DECLARATION := RegExToken.new("func", GdUnitTools.to_regex("^func\\s+([\\w\\p{L}\\p{N}_]+)"), 1) var TOKEN_FUNCTION := Token.new(".") var TOKEN_FUNCTION_RETURN_TYPE := Token.new("->") var TOKEN_FUNCTION_END := Token.new("):") @@ -23,11 +29,15 @@ var TOKEN_ARGUMENT_ASIGNMENT := Token.new("=") var TOKEN_ARGUMENT_TYPE_ASIGNMENT := Token.new(":=") var TOKEN_ARGUMENT_FUZZER := FuzzerToken.new(GdUnitTools.to_regex("((?!(fuzzer_(seed|iterations)))fuzzer?\\w+)( ?+= ?+| ?+:= ?+| ?+:Fuzzer ?+= ?+|)")) var TOKEN_ARGUMENT_TYPE := Token.new(":") +var TOKEN_ARGUMENT_VARIADIC := Token.new("...") var TOKEN_ARGUMENT_SEPARATOR := Token.new(",") -var TOKEN_BRACKET_OPEN := Token.new("(") -var TOKEN_BRACKET_CLOSE := Token.new(")") -var TOKEN_ARRAY_OPEN := Token.new("[") -var TOKEN_ARRAY_CLOSE := Token.new("]") +var TOKEN_BRACKET_ROUND_OPEN := Token.new("(") +var TOKEN_BRACKET_ROUND_CLOSE := Token.new(")") +var TOKEN_BRACKET_SQUARE_OPEN := Token.new("[") +var TOKEN_BRACKET_SQUARE_CLOSE := Token.new("]") +var TOKEN_BRACKET_CURLY_OPEN := Token.new("{") +var TOKEN_BRACKET_CURLY_CLOSE := Token.new("}") + var OPERATOR_ADD := Operator.new("+") var OPERATOR_SUB := Operator.new("-") @@ -40,10 +50,12 @@ var TOKENS :Array[Token] = [ TOKEN_TABULATOR, TOKEN_NEW_LINE, TOKEN_COMMENT, - TOKEN_BRACKET_OPEN, - TOKEN_BRACKET_CLOSE, - TOKEN_ARRAY_OPEN, - TOKEN_ARRAY_CLOSE, + TOKEN_BRACKET_ROUND_OPEN, + TOKEN_BRACKET_ROUND_CLOSE, + TOKEN_BRACKET_SQUARE_OPEN, + TOKEN_BRACKET_SQUARE_CLOSE, + TOKEN_BRACKET_CURLY_OPEN, + TOKEN_BRACKET_CURLY_CLOSE, TOKEN_CLASS_NAME, TOKEN_INNER_CLASS, TOKEN_EXTENDS, @@ -54,6 +66,7 @@ var TOKENS :Array[Token] = [ TOKEN_ARGUMENT_TYPE_ASIGNMENT, TOKEN_ARGUMENT_ASIGNMENT, TOKEN_ARGUMENT_TYPE, + TOKEN_ARGUMENT_VARIADIC, TOKEN_FUNCTION, TOKEN_ARGUMENT_SEPARATOR, TOKEN_FUNCTION_RETURN_TYPE, @@ -64,10 +77,10 @@ var TOKENS :Array[Token] = [ OPERATOR_REMAINDER, ] -var _regex_clazz_name := GdUnitTools.to_regex("(class) ([a-zA-Z0-9_]+) (extends[a-zA-Z]+:)|(class) ([a-zA-Z0-9_]+)") var _regex_strip_comments := GdUnitTools.to_regex("^([^#\"']|'[^']*'|\"[^\"]*\")*\\K#.*") var _scanned_inner_classes := PackedStringArray() var _script_constants := {} +var _is_awaiting := GdUnitTools.to_regex("\\bawait\\s+(?![^\"]*\"[^\"]*$)(?!.*#.*await)") static func to_unix_format(input :String) -> String: @@ -78,24 +91,18 @@ class Token extends RefCounted: var _token: String var _consumed: int var _is_operator: bool - var _regex :RegEx - - func _init(p_token: String, p_is_operator := false, p_regex :RegEx = null) -> void: + func _init(p_token: String, p_is_operator := false) -> void: _token = p_token _is_operator = p_is_operator _consumed = p_token.length() - _regex = p_regex func match(input: String, pos: int) -> bool: - if _regex: - var result := _regex.search(input, pos) - if result == null: - return false - _consumed = result.get_end() - result.get_start() - return pos == result.get_start() return input.findn(_token, pos) == pos + func value() -> Variant: + return _token + func is_operator() -> bool: return _is_operator @@ -116,8 +123,8 @@ class Token extends RefCounted: class Operator extends Token: - func _init(value: String) -> void: - super(value, true) + func _init(p_value: String) -> void: + super(p_value, true) func _to_string() -> String: return "OperatorToken{%s}" % [_token] @@ -133,38 +140,6 @@ class SkippableToken extends Token: return true -# Token to parse Fuzzers -class FuzzerToken extends Token: - var _name: String - - - func _init(regex: RegEx) -> void: - super("", false, regex) - - - func match(input: String, pos: int) -> bool: - if _regex: - var result := _regex.search(input, pos) - if result == null: - return false - _name = result.strings[1] - _consumed = result.get_end() - result.get_start() - return pos == result.get_start() - return input.findn(_token, pos) == pos - - - func name() -> String: - return _name - - - func type() -> int: - return GdObjects.TYPE_FUZZER - - - func _to_string() -> String: - return "FuzzerToken{%s: '%s'}" % [_name, _token] - - # Token to parse function arguments class Variable extends Token: var _plain_value :String @@ -225,12 +200,57 @@ class Variable extends Token: return "Variable{%s: %s : '%s'}" % [_plain_value, GdObjects.type_as_string(_type), _token] -class TokenInnerClass extends Token: - var _clazz_name :String +class RegExToken extends Token: + var _regex: RegEx + var _extract_group_index: int + var _value := "" + + + func _init(token: String, regex: RegEx, extract_group_index: int = -1) -> void: + super(token, false) + _regex = regex + _extract_group_index = extract_group_index + + + func match(input: String, pos: int) -> bool: + var matching := _regex.search(input, pos) + if matching == null or pos != matching.get_start(): + return false + if _extract_group_index != -1: + _value = matching.get_string(_extract_group_index) + _consumed = matching.get_end() - matching.get_start() + return true + + + func value() -> String: + return _value + + +# Token to parse Fuzzers +class FuzzerToken extends RegExToken: + + + func _init(regex: RegEx) -> void: + super("fuzzer", regex, 1) + + + func name() -> String: + return value() + + + func type() -> int: + return GdObjects.TYPE_FUZZER + + + func _to_string() -> String: + return "FuzzerToken{%s: '%s'}" % [value(), _token] + + +class TokenInnerClass extends RegExToken: var _content := PackedStringArray() - static func _strip_leading_spaces(input :String) -> String: + static func _strip_leading_spaces(input: String) -> String: var characters := input.to_utf8_buffer() while not characters.is_empty(): if characters[0] != 0x20: @@ -239,26 +259,26 @@ class TokenInnerClass extends Token: return characters.get_string_from_utf8() - static func _consumed_bytes(row :String) -> int: + static func _consumed_bytes(row: String) -> int: return row.replace(" ", "").replace(" ", "").length() - func _init(clazz_name :String) -> void: - super("class") - _clazz_name = clazz_name + func _init(token: String, p_regex: RegEx, extract_group_index: int = -1) -> void: + super(token, p_regex, extract_group_index) - func is_class_name(clazz_name :String) -> bool: - return _clazz_name == clazz_name + func is_class_name(clazz_name: String) -> bool: + return value() == clazz_name func content() -> PackedStringArray: return _content - func parse(source_rows :PackedStringArray, offset :int) -> void: + @warning_ignore_start("return_value_discarded") + func parse(source_rows: PackedStringArray, offset: int) -> void: # add class signature - @warning_ignore("return_value_discarded") + _content.clear() _content.append(source_rows[offset]) # parse class content for row_index in range(offset+1, source_rows.size()): @@ -271,22 +291,21 @@ class TokenInnerClass extends Token: source_row = source_row.trim_prefix("\t") # refomat invalid empty lines if source_row.dedent().is_empty(): - @warning_ignore("return_value_discarded") _content.append("") else: - @warning_ignore("return_value_discarded") _content.append(source_row) continue break _consumed += TokenInnerClass._consumed_bytes("".join(_content)) + @warning_ignore_restore("return_value_discarded") func _to_string() -> String: - return "TokenInnerClass{%s}" % [_clazz_name] + return "TokenInnerClass{%s}" % [value()] -func get_token(input :String, current_index :int) -> Token: +func get_token(input: String, current_index: int) -> Token: for t in TOKENS: if t.match(input, current_index): return t @@ -296,13 +315,12 @@ func get_token(input :String, current_index :int) -> Token: func next_token(input: String, current_index: int, ignore_tokens :Array[Token] = []) -> Token: var token := TOKEN_NOT_MATCH for t :Token in TOKENS.filter(func(t :Token) -> bool: return not ignore_tokens.has(t)): + if t.match(input, current_index): token = t break if token == OPERATOR_SUB: token = tokenize_value(input, current_index, token) - if token == TOKEN_INNER_CLASS: - token = tokenize_inner_class(input, current_index, token) if token == TOKEN_NOT_MATCH: return tokenize_value(input, current_index, token, ignore_tokens.has(TOKEN_FUNCTION)) return token @@ -319,7 +337,7 @@ func tokenize_value(input: String, current: int, token: Token, ignore_dots := fa # or allowend charset # or is a float value if (test_for_sign and next==0) \ - or character in ALLOWED_CHARACTERS \ + or is_allowed_character(character) \ or (character == "." and (ignore_dots or current_token.is_valid_int())): current_token += character next += 1 @@ -330,21 +348,29 @@ func tokenize_value(input: String, current: int, token: Token, ignore_dots := fa return TOKEN_NOT_MATCH -func extract_clazz_name(value :String) -> String: - var result := _regex_clazz_name.search(value) - if result == null: - push_error("Can't extract class name from '%s'" % value) - return "" - if result.get_string(2).is_empty(): - return result.get_string(5) - else: - return result.get_string(2) - - -@warning_ignore("unused_parameter") -func tokenize_inner_class(source_code: String, current: int, token: Token) -> Token: - var clazz_name := extract_clazz_name(source_code.substr(current, 64)) - return TokenInnerClass.new(clazz_name) +# const ALLOWED_CHARACTERS := "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"" +func is_allowed_character(input: String) -> bool: + var code_point := input.unicode_at(0) + # Unicode + if code_point > 127: + # This is a Unicode character (Chinese, Japanese, etc.) + return true + # ASCII digit 0-9 + if code_point >= 48 and code_point <= 57: + return true + # ASCII lowercase a-z + if code_point >= 97 and code_point <= 122: + return true + # ASCII uppercase A-Z + if code_point >= 65 and code_point <= 90: + return true + # underscore _ + if code_point == 95: + return true + # quotes '" + if code_point == 34 or code_point == 39: + return true + return false func parse_return_token(input: String) -> Variable: @@ -383,12 +409,14 @@ func is_getter_or_setter(func_name: String) -> bool: return func_name.begins_with("@") and (func_name.ends_with("getter") or func_name.ends_with("setter")) -func _parse_function_arguments(input: String) -> Dictionary: - var arguments := {} +func _parse_function_arguments(input: String) -> Array[Dictionary]: + var arguments: Array[Dictionary] = [] var current_index := 0 - var token :Token = null + var token: Token = null var bracket := 0 var in_function := false + + while current_index < len(input): token = next_token(input, current_index) # fallback to not end in a endless loop @@ -407,69 +435,104 @@ func _parse_function_arguments(input: String) -> Dictionary: current_index += token._consumed if token.is_skippable(): continue - if token == TOKEN_BRACKET_OPEN: + if token == TOKEN_BRACKET_ROUND_OPEN : in_function = true bracket += 1 - continue - if token == TOKEN_BRACKET_CLOSE: + if token == TOKEN_BRACKET_ROUND_CLOSE: bracket -= 1 # if function end? if in_function and bracket == 0: return arguments # is function if token == TOKEN_FUNCTION_DECLARATION: - token = next_token(input, current_index) - current_index += token._consumed - continue - # is fuzzer argument - if token is FuzzerToken: - var arg_value := _parse_end_function(input.substr(current_index), true) - current_index += arg_value.length() - var arg_name :String = (token as FuzzerToken).name() - arguments[arg_name] = arg_value.lstrip(" ") continue + # is value argument - if in_function and token.is_variable(): - var arg_name: String = (token as Variable).plain_value() - var arg_value: String = GdFunctionArgument.UNDEFINED + if in_function: + var arg_value := "" + var current_argument := { + "name" : "", + "value" : GdFunctionArgument.UNDEFINED, + "type" : TYPE_VARIANT + } + # parse type and default value while current_index < len(input): token = next_token(input, current_index) current_index += token._consumed if token.is_skippable(): continue + + if token.is_variable() && current_argument["name"] == "": + arguments.append(current_argument) + current_argument["name"] = (token as Variable).plain_value() + continue + match token: + # is fuzzer argument + TOKEN_ARGUMENT_FUZZER: + arg_value = _parse_end_function(input.substr(current_index), true) + current_index += arg_value.length() + current_argument["name"] = (token as FuzzerToken).name() + current_argument["value"] = arg_value.lstrip(" ") + current_argument["type"] = TYPE_FUZZER + arguments.append(current_argument) + continue + + TOKEN_ARGUMENT_VARIADIC: + current_argument["type"] = TYPE_VARARG + TOKEN_ARGUMENT_TYPE: token = next_token(input, current_index) if token == TOKEN_SPACE: current_index += token._consumed token = next_token(input, current_index) + current_index += token._consumed + if current_argument["type"] != TYPE_VARARG: + current_argument["type"] = GdObjects.string_to_type((token as Variable).plain_value()) + TOKEN_ARGUMENT_TYPE_ASIGNMENT: arg_value = _parse_end_function(input.substr(current_index), true) current_index += arg_value.length() + current_argument["value"] = arg_value.lstrip(" ") TOKEN_ARGUMENT_ASIGNMENT: token = next_token(input, current_index) arg_value = _parse_end_function(input.substr(current_index), true) current_index += arg_value.length() - TOKEN_BRACKET_OPEN: + current_argument["value"] = arg_value.lstrip(" ") + + TOKEN_BRACKET_SQUARE_OPEN: + bracket += 1 + TOKEN_BRACKET_CURLY_OPEN: + bracket += 1 + TOKEN_BRACKET_ROUND_OPEN : bracket += 1 # if value a function? if bracket > 1: # complete the argument value - var func_begin := input.substr(current_index-TOKEN_BRACKET_OPEN._consumed) + var func_begin := input.substr(current_index-TOKEN_BRACKET_ROUND_OPEN ._consumed) var func_body := _parse_end_function(func_begin) arg_value += func_body # fix parse index to end of value - current_index += func_body.length() - TOKEN_BRACKET_OPEN._consumed - TOKEN_BRACKET_CLOSE._consumed - TOKEN_BRACKET_CLOSE: + current_index += func_body.length() - TOKEN_BRACKET_ROUND_OPEN ._consumed - TOKEN_BRACKET_ROUND_CLOSE._consumed + TOKEN_BRACKET_SQUARE_CLOSE: + bracket -= 1 + TOKEN_BRACKET_CURLY_CLOSE: + bracket -= 1 + TOKEN_BRACKET_ROUND_CLOSE: bracket -= 1 # end of function if bracket == 0: break TOKEN_ARGUMENT_SEPARATOR: if bracket <= 1: - break - arguments[arg_name] = arg_value.lstrip(" ") + # next argument + current_argument = { + "name" : "", + "value" : GdFunctionArgument.UNDEFINED, + "type" : GdObjects.TYPE_VARIANT + } + continue return arguments @@ -478,6 +541,7 @@ func _parse_end_function(input: String, remove_trailing_char := false) -> String var current_index := 0 var bracket_count := 0 var in_array := 0 + var in_dict := 0 var end_of_func := false while current_index < len(input) and not end_of_func: @@ -504,14 +568,17 @@ func _parse_end_function(input: String, remove_trailing_char := false) -> String # count if inside an array "[": in_array += 1 "]": in_array -= 1 + # count if inside an dictionary + "{": in_dict += 1 + "}": in_dict -= 1 # count if inside a function "(": bracket_count += 1 ")": bracket_count -= 1 - if bracket_count < 0 and in_array <= 0: + if bracket_count < 0 and in_array <= 0 and in_dict <= 0: end_of_func = true ",": - if bracket_count == 0 and in_array == 0: + if bracket_count == 0 and in_array == 0 and in_dict <= 0: end_of_func = true current_index += 1 if remove_trailing_char: @@ -523,19 +590,21 @@ func _parse_end_function(input: String, remove_trailing_char := false) -> String return input.substr(0, current_index) -@warning_ignore("unsafe_method_access") func extract_inner_class(source_rows: PackedStringArray, clazz_name :String) -> PackedStringArray: for row_index in source_rows.size(): var input := source_rows[row_index] var token := next_token(input, 0) if token.is_inner_class(): + @warning_ignore("unsafe_method_access") if token.is_class_name(clazz_name): + @warning_ignore("unsafe_method_access") token.parse(source_rows, row_index) + @warning_ignore("unsafe_method_access") return token.content() return PackedStringArray() -func extract_func_signature(rows :PackedStringArray, index :int) -> String: +func extract_func_signature(rows: PackedStringArray, index: int) -> String: var signature := "" for rowIndex in range(index, rows.size()): @@ -558,31 +627,24 @@ func get_class_name(script :GDScript) -> String: var input := source_rows[index] var token := next_token(input, 0) if token == TOKEN_CLASS_NAME: - var current_index := token._consumed - token = next_token(input, current_index) - current_index += token._consumed - token = tokenize_value(input, current_index, token) - return (token as Variable).value() + return token.value() # if no class_name found extract from file name return GdObjects.to_pascal_case(script.resource_path.get_basename().get_file()) -func parse_func_name(input :String) -> String: - var current_index := 0 - var token := next_token(input, current_index) - current_index += token._consumed - if token != TOKEN_FUNCTION_STATIC_DECLARATION and token != TOKEN_FUNCTION_DECLARATION: - return "" - while not token is Variable: - token = next_token(input, current_index) - current_index += token._consumed - return token._token +func parse_func_name(input: String) -> String: + if TOKEN_FUNCTION_DECLARATION.match(input, 0): + return TOKEN_FUNCTION_DECLARATION.value() + if TOKEN_FUNCTION_STATIC_DECLARATION.match(input, 0): + return TOKEN_FUNCTION_STATIC_DECLARATION.value() + push_error("Can't extract function name from '%s'" % input) + return "" ## Enriches the function descriptor by line number and argument default values ## - enrich all function descriptors form current script up to all inherited scrips func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescriptor]) -> void: - var enriched_functions := PackedStringArray() + var enriched_functions := {} # Use Dictionary for O(1) lookup instead of PackedStringArray var script_to_scan := script while script_to_scan != null: # do not scan the test suite base class itself @@ -599,29 +661,35 @@ func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescript if input.begins_with("#") or input.length() == 0: continue var token := next_token(input, 0) - if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: - var function_name := parse_func_name(input) - var fd: GdFunctionDescriptor = fds.filter(func(element: GdFunctionDescriptor) -> bool: - # is same function name and not already enriched - return function_name == element.name() and not enriched_functions.has(element.name()) - ).pop_front() - if fd != null: - # add as enriched function to exclude from next iteration (could be inherited) - @warning_ignore("return_value_discarded") - enriched_functions.append(fd.name()) - var func_signature := extract_func_signature(rows, rowIndex) - var func_arguments := _parse_function_arguments(func_signature) - # enrich missing default values - for arg_name: String in func_arguments.keys(): - var func_argument: String = func_arguments[arg_name] - fd.set_argument_value(arg_name, func_argument) - fd.enrich_file_info(script_to_scan.resource_path, rowIndex + 1) - fd._is_coroutine = is_func_coroutine(rows, rowIndex) - # enrich return class name if not set - if fd.return_type() == TYPE_OBJECT and fd._return_class in ["", "Resource", "RefCounted"]: - var var_token := parse_return_token(func_signature) - if var_token != TOKEN_NOT_MATCH and var_token.type() == TYPE_OBJECT: - fd._return_class = _patch_inner_class_names(var_token.plain_value(), "") + if token != TOKEN_FUNCTION_STATIC_DECLARATION and token != TOKEN_FUNCTION_DECLARATION: + continue + + var function_name: String = token.value() + # Skip if already enriched (from parent class scan) + if enriched_functions.has(function_name): + continue + + # Find matching function descriptor + var fd: GdFunctionDescriptor = null + for candidate in fds: + if candidate.name() == function_name: + fd = candidate + break + if fd == null: + continue + # Mark as enriched + enriched_functions[function_name] = true + var func_signature := extract_func_signature(rows, rowIndex) + var func_arguments := _parse_function_arguments(func_signature) + # enrich missing default values + fd.enrich_arguments(func_arguments) + fd.enrich_file_info(script_to_scan.resource_path, rowIndex + 1) + fd._is_coroutine = is_func_coroutine(rows, rowIndex) + # enrich return class name if not set + if fd.return_type() == TYPE_OBJECT and fd._return_class in ["", "Resource", "RefCounted"]: + var var_token := parse_return_token(func_signature) + if var_token != TOKEN_NOT_MATCH and var_token.type() == TYPE_OBJECT: + fd._return_class = _patch_inner_class_names(var_token.plain_value(), "") # if the script ihnerits we need to scan this also script_to_scan = script_to_scan.get_base_script() @@ -629,14 +697,16 @@ func _enrich_function_descriptor(script: GDScript, fds: Array[GdFunctionDescript func is_func_coroutine(rows :PackedStringArray, index :int) -> bool: var is_coroutine := false for rowIndex in range(index+1, rows.size()): - var input := rows[rowIndex] - is_coroutine = input.contains("await") - if is_coroutine: - return true + var input := rows[rowIndex].strip_edges() + if input.begins_with("#") or input.is_empty(): + continue var token := next_token(input, 0) # scan until next function if token == TOKEN_FUNCTION_STATIC_DECLARATION or token == TOKEN_FUNCTION_DECLARATION: break + + if _is_awaiting.search(input): + return true return is_coroutine diff --git a/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid b/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid index 67af830d..adfb2239 100644 --- a/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdScriptParser.gd.uid @@ -1 +1 @@ -uid://5prj53vbk460 +uid://ds5dvjko4ds3c diff --git a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid index 295aead6..0eed0d87 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdUnitExpressionRunner.gd.uid @@ -1 +1 @@ -uid://dp7yxcm6p70fw +uid://dmuh4qf6rolkc diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd index 73db59e4..527661da 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd @@ -1,3 +1,4 @@ +## @deprecated see GdFunctionParameterSetResolver class_name GdUnitTestParameterSetResolver extends RefCounted @@ -17,7 +18,6 @@ const EXCLUDE_PROPERTIES_TO_COPY = [ var _fd: GdFunctionDescriptor -var _test_case_names_cache := PackedStringArray() var _static_sets_by_index := {} var _is_static := true @@ -38,26 +38,33 @@ func is_parameter_set_static(index: int) -> bool: # validates the given arguments are complete and matches to required input fields of the test function -func validate(input_value_set: Array) -> String: - var input_arguments := _fd.args() +func validate(parameter_sets: Array, parameter_set_index: int) -> GdUnitResult: + if parameter_sets.size() < parameter_set_index: + return GdUnitResult.error("Internal error: the resolved paremeterset has invalid size.") + + var input_values: Array = parameter_sets[parameter_set_index] + if input_values == null: + return GdUnitResult.error("The parameter set '%s' must be an Array!" % parameter_sets[parameter_set_index]) + # check given parameter set with test case arguments - var expected_arg_count := input_arguments.size() - 1 - for input_values :Variant in input_value_set: - var parameter_set_index := input_value_set.find(input_values) - if input_values is Array: - var arr_values: Array = input_values - var current_arg_count := arr_values.size() - if current_arg_count != expected_arg_count: - return "\n The parameter set at index [%d] does not match the expected input parameters!\n The test case requires [%d] input parameters, but the set contains [%d]" % [parameter_set_index, expected_arg_count, current_arg_count] - var error := GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, arr_values, parameter_set_index) - if not error.is_empty(): - return error - else: - return "\n The parameter set at index [%d] does not match the expected input parameters!\n Expecting an array of input values." % parameter_set_index - return "" - - -static func validate_parameter_types(input_arguments: Array, input_values: Array, parameter_set_index: int) -> String: + var input_arguments := _fd.args() + var expected_arg_count := input_arguments.size() - 1 #(-1 we exclude the parameter set itself) + var current_arg_count := input_values.size() + if current_arg_count != expected_arg_count: + var arg_names := input_arguments\ + .filter(func(arg: GdFunctionArgument) -> bool: return not arg.is_parameter_set())\ + .map(func(arg: GdFunctionArgument) -> String: return str(arg)) + + return GdUnitResult.error(""" + The test data set at index (%d) does not match the expected test arguments: + test function: [color=snow]func test...(%s)[/color] + test input values: [color=snow]%s[/color] + """ + .dedent() % [parameter_set_index, ",".join(arg_names), input_values]) + return GdUnitTestParameterSetResolver.validate_parameter_types(input_arguments, input_values) + + +static func validate_parameter_types(input_arguments: Array[GdFunctionArgument], input_values: Array) -> GdUnitResult: for i in input_arguments.size(): var input_param: GdFunctionArgument = input_arguments[i] # only check the test input arguments @@ -76,32 +83,13 @@ static func validate_parameter_types(input_arguments: Array, input_values: Array if input_param_type == TYPE_OBJECT and input_value_type == TYPE_NIL: continue if input_param_type != input_value_type: - return "\n The parameter set at index [%d] does not match the expected input parameters!\n The value '%s' does not match the required input parameter <%s>." % [parameter_set_index, input_value, input_param] - return "" - - -func build_test_case_names(test_case: _TestCase) -> PackedStringArray: - if not is_parameterized(): - return [] - # if test names already resolved? - if not _test_case_names_cache.is_empty(): - return _test_case_names_cache - - var fa := GdFunctionArgument.get_parameter_set(_fd.args()) - var parameter_sets := fa.parameter_sets() - # if no parameter set detected we need to resolve it by using reflection - if parameter_sets.size() == 0: - _test_case_names_cache = _extract_test_names_by_reflection(test_case) - _is_static = false - else: - var property_names := _extract_property_names(test_case.get_parent()) - for parameter_set_index in parameter_sets.size(): - var parameter_set := parameter_sets[parameter_set_index] - _static_sets_by_index[parameter_set_index] = _is_static_parameter_set(parameter_set, property_names) - @warning_ignore("return_value_discarded") - _test_case_names_cache.append(GdUnitTestParameterSetResolver._build_test_case_name(test_case, parameter_set, parameter_set_index)) - parameter_set_index += 1 - return _test_case_names_cache + return GdUnitResult.error(""" + The test data value does not match the expected input type! + input value: [color=snow]'%s', <%s>[/color] + expected argument: [color=snow]%s[/color] + """ + .dedent() % [input_value, type_string(input_value_type), str(input_param)]) + return GdUnitResult.success("No errors found.") func _extract_property_names(node :Node) -> PackedStringArray: @@ -119,25 +107,10 @@ func _is_static_parameter_set(parameters :String, property_names :PackedStringAr return true -func _extract_test_names_by_reflection(test_case: _TestCase) -> PackedStringArray: - var parameter_sets := load_parameter_sets(test_case) - var test_case_names: PackedStringArray = [] - for index in parameter_sets.size(): - @warning_ignore("return_value_discarded") - test_case_names.append(GdUnitTestParameterSetResolver._build_test_case_name(test_case, str(parameter_sets[index]), index)) - return test_case_names - - -static func _build_test_case_name(test_case: _TestCase, test_parameter: String, parameter_set_index: int) -> String: - if not test_parameter.begins_with("["): - test_parameter = "[" + test_parameter - return "%s:%d %s" % [test_case.get_name(), parameter_set_index, test_parameter.replace("\t", "").replace('"', "'").replace("&'", "'")] - - # extracts the arguments from the given test case, using kind of reflection solution # to restore the parameters from a string representation to real instance type -func load_parameter_sets(test_case: _TestCase, do_validate := false) -> Array: - var source_script :Script = test_case.get_parent().get_script() +func load_parameter_sets(test_suite: Node) -> GdUnitResult: + var source_script: Script = test_suite.get_script() var parameter_arg := GdFunctionArgument.get_parameter_set(_fd.args()) var source_code := CLASS_TEMPLATE \ .replace("${clazz_path}", source_script.resource_path) \ @@ -150,36 +123,13 @@ func load_parameter_sets(test_case: _TestCase, do_validate := false) -> Array: #ResourceSaver.save(script, script.resource_path) var result := script.reload() if result != OK: - push_error("Extracting test parameters failed! Script loading error: %s" % result) - return [] + return GdUnitResult.error("Extracting test parameters failed! Script loading error: %s" % error_string(result)) var instance :Object = script.new() - GdUnitTestParameterSetResolver.copy_properties(test_case.get_parent(), instance) + GdUnitTestParameterSetResolver.copy_properties(test_suite, instance) (instance as Node).queue_free() var parameter_sets: Array = instance.call("__extract_test_parameters") - if not do_validate: - return parameter_sets - # validate the parameter set - var error := validate(parameter_sets) - if not error.is_empty(): - test_case.skip(true, error) - test_case._interupted = true - if parameter_sets.size() != _test_case_names_cache.size(): - push_error("Internal Error: The resolved test_case names has invalid size!") - error = """ - %s: - The resolved test_case names has invalid size! - %s - """.dedent().trim_prefix("\n") % [ - GdAssertMessages._error("Internal Error"), - GdAssertMessages._error("Please report this issue as a bug!")] - GdUnitThreadManager.get_current_context()\ - .get_execution_context()\ - .add_report(GdUnitReport.new().create(GdUnitReport.INTERUPTED, test_case.line_number(), error)) - test_case.skip(true, error) - test_case._interupted = true - @warning_ignore("return_value_discarded") fixure_typed_parameters(parameter_sets, _fd.args()) - return parameter_sets + return GdUnitResult.success(parameter_sets) func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFunctionArgument]) -> Array: @@ -197,7 +147,6 @@ func fixure_typed_parameters(parameter_sets: Array, arg_descriptors: Array[GdFun return parameter_sets - static func copy_properties(source: Object, dest: Object) -> void: for property in source.get_property_list(): var property_name :String = property["name"] diff --git a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid index ef9052a7..bcd6981a 100644 --- a/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid +++ b/addons/gdUnit4/src/core/parse/GdUnitTestParameterSetResolver.gd.uid @@ -1 +1 @@ -uid://dddaundb5suyv +uid://c7xulu5ucwm8 diff --git a/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid b/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid index b2b97e3a..e7643688 100644 --- a/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid +++ b/addons/gdUnit4/src/core/report/GdUnitReport.gd.uid @@ -1 +1 @@ -uid://b6pe422lxrmk8 +uid://df752hdjfqiq8 diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd new file mode 100644 index 00000000..34dcfa38 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd @@ -0,0 +1,470 @@ +#warning-ignore-all:return_value_discarded +class_name GdUnitTestCIRunner +extends "res://addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd" +## Command line test runner implementation.[br] +## [br] +## This runner is designed for CI/CD pipelines and command line test execution.[br] +## Features:[br] +## - Command line options for test configuration[br] +## - HTML and JUnit report generation[br] +## - Console output with colored formatting[br] +## - Progress and error reporting[br] +## - Test history management[br] +## [br] +## Example usage:[br] +## [codeblock] +## # Run all tests in a directory +## runtest -a +## +## # Run specific test suite with ignored tests +## runtest -a -i +## [/codeblock] + +const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") + +var _console := GdUnitCSIMessageWriter.new() +var _console_reporter: GdUnitConsoleTestReporter +var _headless_mode_ignore := false +var _runner_config_file := "" +var _debug_cmd_args := PackedStringArray() +var _included_tests := PackedStringArray() +var _excluded_tests := PackedStringArray() + +## Command line options configuration +var _cmd_options := CmdOptions.new([ + CmdOption.new( + "-a, --add", + "-a ", + "Adds the given test suite or directory to the execution pipeline.", + TYPE_STRING + ), + CmdOption.new( + "-i, --ignore", + "-i ", + "Adds the given test suite or test case to the ignore list.", + TYPE_STRING + ), + CmdOption.new( + "-c, --continue", + "", + """By default GdUnit will abort checked first test failure to be fail fast, + instead of stop after first failure you can use this option to run the complete test set.""".dedent() + ), + CmdOption.new( + "-conf, --config", + "-conf [testconfiguration.cfg]", + "Run all tests by given test configuration. Default is 'GdUnitRunner.cfg'", + TYPE_STRING, + true + ), + CmdOption.new( + "-help", "", + "Shows this help message." + ), + CmdOption.new("--help-advanced", "", + "Shows advanced options." + ) + ], + [ + # advanced options + CmdOption.new( + "-rd, --report-directory", + "-rd ", + "Specifies the output directory in which the reports are to be written. The default is res://reports/.", + TYPE_STRING, + true + ), + CmdOption.new( + "-rc, --report-count", + "-rc ", + "Specifies how many reports are saved before they are deleted. The default is %s." % str(GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT), + TYPE_INT, + true + ), + #CmdOption.new("--list-suites", "--list-suites [directory]", "Lists all test suites located in the given directory.", TYPE_STRING), + #CmdOption.new("--describe-suite", "--describe-suite ", "Shows the description of selected test suite.", TYPE_STRING), + CmdOption.new( + "--info", "", + "Shows the GdUnit version info" + ), + CmdOption.new( + "--selftest", "", + "Runs the GdUnit self test" + ), + CmdOption.new( + "--ignoreHeadlessMode", + "--ignoreHeadlessMode", + "By default, running GdUnit4 in headless mode is not allowed. You can switch off the headless mode check by set this property." + ), + ]) + + +func _init() -> void: + super() + + +func _ready() -> void: + super() + # stop checked first test failure to fail fast + _executor.fail_fast(true) + _console_reporter = GdUnitConsoleTestReporter.new(_console, true) + GdUnitSignals.instance().gdunit_message.connect(_on_send_message) + + +func _notification(what: int) -> void: + super(what) + if what == NOTIFICATION_PREDELETE: + prints("Finallize .. done") + + +func init_runner() -> void: + init_gd_unit() + + +## Returns the exit code based on test results.[br] +## Maps test report status to process exit codes. +func get_exit_code() -> int: + return report_exit_code() + + +## Cleanup and quit the runner.[br] +## [br] +## [param code] The exit code to return. +func quit(code: int) -> void: + _state = EXIT + GdUnitTools.dispose_all() + await GdUnitMemoryObserver.gc_on_guarded_instances() + await super(code) + + +## Prints info message to console.[br] +## [br] +## [param message] The message to print.[br] +## [param color] Optional color for the message. +func console_info(message: String, color: Color = Color.WHITE) -> void: + _console.color(color).println_message(message) + + +## Prints error message to console.[br] +## [br] +## [param message] The error message to print. +func console_error(message: String) -> void: + _console.prints_error(message) + + +## Prints warning message to console.[br] +## [br] +## [param message] The warning message to print. +func console_warning(message: String) -> void: + _console.prints_warning(message) + + +## Sets the directory for test reports.[br] +## [br] +## [param path] The path where reports should be written. +func set_report_dir(path: String) -> void: + report_base_path = ProjectSettings.globalize_path(GdUnitFileAccess.make_qualified_path(path)) + console_info( + "Set write reports to %s" % report_base_path, + Color.DEEP_SKY_BLUE + ) + + +## Sets how many report files to keep.[br] +## [br] +## [param count] The number of reports to keep. +func set_report_count(count: String) -> void: + var report_count := count.to_int() + if report_count < 1: + console_error( + "Invalid report history count '%s' set back to default %d" + % [count, GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT] + ) + max_report_history = GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT + else: + console_info( + "Set report history count to %s" % count, + Color.DEEP_SKY_BLUE + ) + max_report_history = report_count + + +## Disables fail-fast mode to run all tests.[br] +## By default tests stop on first failure. +func disable_fail_fast() -> void: + console_info( + "Disabled fail fast!", + Color.DEEP_SKY_BLUE + ) + @warning_ignore("unsafe_method_access") + _executor.fail_fast(false) + + +func run_self_test() -> void: + console_info( + "Run GdUnit4 self tests.", + Color.DEEP_SKY_BLUE + ) + disable_fail_fast() + + + +## Shows GdUnit and Godot version information. +func show_version() -> void: + console_info( + "Godot %s" % Engine.get_version_info().get("string") as String, + Color.DARK_SALMON + ) + var config := ConfigFile.new() + config.load("addons/gdUnit4/plugin.cfg") + console_info( + "GdUnit4 %s" % config.get_value("plugin", "version") as String, + Color.DARK_SALMON + ) + quit(RETURN_SUCCESS) + + +## Ignores headless mode restrictions.[br] +## Allows tests to run in headless mode despite limitations. +func check_headless_mode() -> void: + _headless_mode_ignore = true + + +## Shows available command line options.[br] +## [br] +## [param show_advanced] Whether to show advanced options. +func show_options(show_advanced: bool = false) -> void: + console_info( + """ + Usage: + runtest -a + runtest -a -i + """.dedent(), + Color.DARK_SALMON + ) + console_info( + "-- Options ---------------------------------------------------------------------------------------", + Color.DARK_SALMON + ) + for option in _cmd_options.default_options(): + descripe_option(option) + if show_advanced: + console_info( + "-- Advanced options --------------------------------------------------------------------------", + Color.DARK_SALMON + ) + for option in _cmd_options.advanced_options(): + descripe_option(option) + + +## Describes a single command line option.[br] +## [br] +## [param cmd_option] The option to describe. +func descripe_option(cmd_option: CmdOption) -> void: + console_info( + " %-40s" % str(cmd_option.commands()), + Color.CORNFLOWER_BLUE + ) + console_info( + cmd_option.description(), + Color.LIGHT_GREEN + ) + if not cmd_option.help().is_empty(): + console_info( + "%-4s %s" % ["", cmd_option.help()], + Color.DARK_TURQUOISE + ) + console_info("") + + +## Loads test configuration from file.[br] +## [br] +## [param path] Path to the configuration file. +func load_test_config(path := GdUnitRunnerConfig.CONFIG_FILE) -> void: + console_info( + "Loading test configuration %s\n" % path, + Color.CORNFLOWER_BLUE + ) + _runner_config_file = path + _runner_config.load_config(path) + + +## Shows basic help and exits. +func show_help() -> void: + show_options() + quit(RETURN_SUCCESS) + + +## Shows advanced help and exits. +func show_advanced_help() -> void: + show_options(true) + quit(RETURN_SUCCESS) + + +## Gets command line arguments.[br] +## Returns debug args if set, otherwise actual command line args. +func get_cmdline_args() -> PackedStringArray: + if _debug_cmd_args.is_empty(): + return OS.get_cmdline_args() + return _debug_cmd_args + + +## Initializes the test runner and processes command line arguments. +func init_gd_unit() -> void: + console_info( + """ + -------------------------------------------------------------------------------------------------- + GdUnit4 Comandline Tool + --------------------------------------------------------------------------------------------------""".dedent(), + Color.DARK_SALMON + ) + + var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd") + var result := cmd_parser.parse(get_cmdline_args()) + if result.is_error(): + console_error(result.error_message()) + show_options() + console_error("Abnormal exit with %d" % RETURN_ERROR) + quit(RETURN_ERROR) + return + if result.is_empty(): + show_help() + return + # build runner config by given commands + var commands :Array[CmdCommand] = [] + @warning_ignore("unsafe_cast") + commands.append_array(result.value() as Array) + result = ( + CmdCommandHandler.new(_cmd_options) + .register_cb("-help", show_help) + .register_cb("--help-advanced", show_advanced_help) + .register_cb("-a", add_test_suite) + .register_cbv("-a", add_test_suites) + .register_cb("-i", skip_test_suite) + .register_cbv("-i", skip_test_suites) + .register_cb("-rd", set_report_dir) + .register_cb("-rc", set_report_count) + .register_cb("--selftest", run_self_test) + .register_cb("-c", disable_fail_fast) + .register_cb("-conf", load_test_config) + .register_cb("--info", show_version) + .register_cb("--ignoreHeadlessMode", check_headless_mode) + .execute(commands) + ) + if result.is_error(): + console_error(result.error_message()) + quit(RETURN_ERROR) + return + + if DisplayServer.get_name() == "headless": + if _headless_mode_ignore: + console_warning(""" + Headless mode is ignored by option '--ignoreHeadlessMode'" + + Please note that tests that use UI interaction do not work correctly in headless mode. + Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore + have no effect in the test! + """.dedent() + ) + else: + console_error(""" + Headless mode is not supported! + + Please note that tests that use UI interaction do not work correctly in headless mode. + Godot 'InputEvents' are not transported by the Godot engine in headless mode and therefore + have no effect in the test! + + You can run with '--ignoreHeadlessMode' to swtich off this check. + """.dedent() + ) + console_error( + "Abnormal exit with %d" % RETURN_ERROR_HEADLESS_NOT_SUPPORTED + ) + quit(RETURN_ERROR_HEADLESS_NOT_SUPPORTED) + return + + _test_cases = discover_tests() + if _test_cases.is_empty(): + console_info("No test cases found, abort test run!", Color.YELLOW) + console_info("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) + quit(RETURN_SUCCESS) + return + _state = RUN + + +func discover_tests() -> Array[GdUnitTestCase]: + var gdunit_test_discover_added := GdUnitSignals.instance().gdunit_test_discover_added + + _test_cases = _runner_config.test_cases() + var scanner := GdUnitTestSuiteScanner.new() + for path in _included_tests: + var scripts := scanner.scan(path) + for script in scripts: + GdUnitTestDiscoverer.discover_tests(script, func(test: GdUnitTestCase) -> void: + if not is_skipped(test): + #_console.println_message("discoverd %s" % test.display_name) + _test_cases.append(test) + gdunit_test_discover_added.emit(test) + ) + + return _test_cases + + +func add_test_suite(path: String) -> void: + _included_tests.append(path) + + +func add_test_suites(paths: PackedStringArray) -> void: + _included_tests.append_array(paths) + + +func skip_test_suite(path: String) -> void: + _excluded_tests.append(path) + + +func skip_test_suites(paths: PackedStringArray) -> void: + _excluded_tests.append_array(paths) + + +func is_skipped(test: GdUnitTestCase) -> bool: + for skipped_info in _excluded_tests: + + # is suite skipped by full path or suite name + if skipped_info == test.suite_name or test.source_file.contains(skipped_info): + return true + var skip_file := skipped_info.replace("res://", "") + + # check for skipped single test + if not skip_file.contains(":"): + continue + var parts: PackedStringArray = skip_file.rsplit(":") + var skipped_suite := parts[0] + var skipped_test := parts[1] + # is suite skipped by full path or suite name + if (skipped_suite == test.suite_name or test.source_file.contains(skipped_suite)) and skipped_test == test.test_name: + return true + + return false + + +func _on_send_message(message: String) -> void: + _console.color(Color.CORNFLOWER_BLUE).println_message(message) + + +func _on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.SESSION_START: + _console_reporter.test_session = _test_session + GdUnitEvent.SESSION_CLOSE: + _console_reporter.test_session = null + + +func report_exit_code() -> int: + if _console_reporter.total_error_count() + _console_reporter.total_failure_count() > 0: + console_info("Exit code: %d" % RETURN_ERROR, Color.FIREBRICK) + return RETURN_ERROR + if _console_reporter.total_orphan_count() > 0: + console_info("Exit code: %d" % RETURN_WARNING, Color.GOLDENROD) + return RETURN_WARNING + console_info("Exit code: %d" % RETURN_SUCCESS, Color.DARK_SALMON) + return RETURN_SUCCESS diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid new file mode 100644 index 00000000..7dba971d --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestCIRunner.gd.uid @@ -0,0 +1 @@ +uid://calfs70rbig0l diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd new file mode 100644 index 00000000..5af6c9b9 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd @@ -0,0 +1,87 @@ +extends "res://addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd" +## Runner implementation used by the editor UI.[br] +## [br] +## This runner connects to a GdUnit server via TCP to report test results.[br] +## Test results are reported in real-time and displayed in the editor UI.[br] +## [br] +## The runner uses an RPC message protocol to communicate status and events:[br] +## - Messages to report progress[br] +## - Events to report test results[br] + +## The TCP client used to connect to the GdUnit server +@onready var _client: GdUnitTcpClient = $GdUnitTcpClient +@onready var _version_label: Control = %Version + + +func _init() -> void: + super() + # We set the default max report history to 1 + max_report_history = 1 + + +func _ready() -> void: + super() + GdUnit4Version.init_version_label(_version_label) + + var config_result := _runner_config.load_config() + if config_result.is_error(): + push_error(config_result.error_message()) + _state = EXIT + return + @warning_ignore("return_value_discarded") + _client.connect("connection_failed", _on_connection_failed) + GdUnitSignals.instance().gdunit_message.connect(_on_send_message) + var result := _client.start("127.0.0.1", _runner_config.server_port()) + if result.is_error(): + push_error(result.error_message()) + return + + +## Cleanup and quit the runner.[br] +## [br] +## [param code] The exit code to return. +func quit(code: int) -> void: + if code != RETURN_SUCCESS: + _state = EXIT + await GdUnitMemoryObserver.gc_on_guarded_instances() + + +## Called when the TCP connection to the GdUnit server fails.[br] +## Stops the test execution.[br] +## [br] +## [param message] The error message describing the failure. +func _on_connection_failed(message: String) -> void: + prints("_on_connection_failed", message) + _state = STOP + + +## Initializes the test runner.[br] +## Waits for TCP client connection and then scans for test suites.[br] +## Reports the number of found test suites via TCP message. +func init_runner() -> void: + # wait until client is connected to the GdUnitServer + if _client.is_client_connected(): + await gdUnitInit() + _state = RUN + + +## Initializes the GdUnit framework.[br] +## Sends initial message about number of test suites. +func gdUnitInit() -> void: + #enable_manuall_polling() + _test_cases = _runner_config.test_cases() + await get_tree().process_frame + + +## Sends a message via TCP to the GdUnit server.[br] +## [br] +## [param message] The message to send. +func _on_send_message(message: String) -> void: + _client.send(RPCMessage.of(message)) + + +## Handles GdUnit events by sending them via TCP to the server.[br] +## [br] +## [param event] The event to send. +func _on_gdunit_event(event: GdUnitEvent) -> void: + _client.send(RPCGdUnitEvent.of(event)) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid new file mode 100644 index 00000000..62334ce6 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd.uid @@ -0,0 +1 @@ +uid://bi04qg8kl1bqq diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn new file mode 100644 index 00000000..eaa6f1ab --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=3 format=3 uid="uid://belidlfknh74r"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpClient.gd" id="2"] + +[node name="Control" type="Node"] +script = ExtResource("1") + +[node name="GdUnitTcpClient" type="Node" parent="."] +script = ExtResource("2") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +custom_minimum_size = Vector2(0, 24) +layout_direction = 2 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 0 +size_flags_horizontal = 3 +size_flags_vertical = 10 +alignment = 2 + +[node name="Version" type="RichTextLabel" parent="HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(128, 0) +layout_mode = 2 +size_flags_horizontal = 10 +bbcode_enabled = true +scroll_active = false +shortcut_keys_enabled = false +horizontal_alignment = 1 +justification_flags = 0 diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd new file mode 100644 index 00000000..c789847b --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd @@ -0,0 +1,169 @@ +## +## @since GdUnit4 5.1.0 +## +## Represents a test execution session in GdUnit4.[br] +## [br] +## [i]A test session encapsulates a complete test execution cycle, managing the collection +## of test cases to be executed and providing communication channels for test events +## and messages. This class serves as the central coordination point for test execution +## and allows hooks and other components to interact with the running test session.[/i][br] +## [br] +## [b][u]Key Features[/u][/b][br] +## - [i][b]Test Case Management[/b][/i]: Maintains a collection of test cases to be executed[br] +## - [i][b]Event Broadcasting[/b][/i]: Forwards GdUnit events to session-specific listeners[br] +## - [i][b]Message Communication[/b][/i]: Provides a channel for sending messages during test execution[br] +## - [i][b]Hook Integration[/b][/i]: Passed to test session hooks for startup and shutdown operations[br] +## [br] +## [b][u]Usage in Test Hooks[/u][/b] +## [codeblock] +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## # Access test cases +## print("Running %d test cases" % session.test_cases.size()) +## +## # Send status messages +## session.send_message("Custom hook initialized") +## +## # Listen for test events +## session.test_event.connect(_on_test_event) +## +## return GdUnitResult.success() +## +## func _on_test_event(event: GdUnitEvent) -> void: +## print("Test event received: %s" % event.type) +## [/codeblock] +## [br] +## [b][u]Event Flow[/u][/b][br] +## 1. Session is created with a collection of test cases[br] +## 2. Session connects to the global GdUnit event system[br] +## 3. During test execution, events are automatically forwarded to session listeners[br] +## 4. Hooks and other components can subscribe to session events[br] +## 5. Messages can be sent through the session for logging and communication[br] +class_name GdUnitTestSession +extends RefCounted + + +## Emitted when a test execution event occurs.[br] +## [br] +## [i]This signal forwards events from the global GdUnit event system to session-specific +## listeners. It allows hooks and other session components to react to test events +## without directly connecting to the global event system.[/i][br] +## [br] +## [u]Common event types include:[/u][br] +## - Test suite start/end events[br] +## - Test case start/end events[br] +## - Test assertion events[br] +## - Test failure/error events[br] +## +## [param event] The test event containing details about test execution, timing, and results +@warning_ignore("unused_signal") +signal test_event(event: GdUnitEvent) + + +## [b][color=red]@readonly: Should not be modified directly during test execution![/color][/b][br] +## Collection of test cases to be executed in this session.[br] +## [br] +## This array contains all the test cases that will be run during the session. +## Test hooks can access this collection to: +## - Get the total number of tests to be executed +## - Access individual test case metadata +## - Perform setup/teardown based on test case requirements +## - Generate reports or statistics about the test suite +## +## The collection is typically populated before session startup and remains +## constant during test execution. +var _test_cases : Array[GdUnitTestCase] = [] + + +## [b][color=red]@readonly: The report path should not be modified after session creation![/color][/b][br] +## The file system path where test reports for this session will be generated.[br] +## [br] +## [i]This property provides centralized access to the report output location, +## allowing test hooks, reporters, and other components to reference the same +## report path without coupling to specific reporter implementations.[/i][br] +## [br] +## [b][u]Common use cases include:[/u][/b][br] +## - Test hooks generating additional report files in the same directory[br] +## - Custom reporters creating supplementary output files[br] +## - Post-processing scripts that need to locate generated reports[br] +## - Cleanup operations that need to manage report artifacts[br] +## [br] +## [b][u]Example Usage:[/u][/b] +## [codeblock] +## # In a test hook +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## var report_dir = session.report_path.get_base_dir() +## var custom_report = report_dir.path_join("custom_metrics.json") +## # Generate additional reports in the same location +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Reports available at: " + session.report_path) +## return GdUnitResult.success() +## [/codeblock] +## [br] +## The path is set during session initialization and remains constant throughout +## the test execution lifecycle. +var report_path: String: + get: + return report_path + + +## Initializes the test session and sets up event forwarding.[br] +## [br] +## [i]This constructor automatically connects to the global GdUnit event system +## and forwards all events to the session's test_event signal. This allows +## session-specific components to listen for test events without managing +## global signal connections.[/i] +func _init(test_cases: Array[GdUnitTestCase], session_report_path: String) -> void: + # We build a copy to prevent a user is modifing the tests + _test_cases = test_cases.duplicate(true) + report_path = session_report_path + GdUnitSignals.instance().gdunit_event.connect(func(event: GdUnitEvent) -> void: + test_event.emit(event) + ) + + +## Finds a test case by its unique identifier.[br] +## [br] +## [i]Searches through all test cases to find a test with the matching GUID.[/i][br] +## [br] +## [param id] The GUID of the test to find[br] +## Returns the matching test case or null if not found. +func find_test_by_id(id: GdUnitGUID) -> GdUnitTestCase: + for test in _test_cases: + if test.guid.equals(id): + return test + + return null + + +## Sends a message through the GdUnit messaging system.[br] +## [br] +## [i]This method provides a convenient way for test hooks and other session +## components to send messages that will be handled by the GdUnit framework.[/i] +## [br][br] +## [b][u]Messages are typically used for:[/u][/b][br] +## - Status updates during test execution[br] +## - Progress reporting from test hooks[br] +## - Debug information and logging[br] +## - User notifications and alerts[br] +## [br] +## The message will be processed by the global GdUnit message system and +## may be displayed in the test runner UI, logged to files, or handled +## by other registered message handlers. +## [br] +## [b][u]Example Usage:[/u][/b] +## [codeblock] +## # In a test hook +## func startup(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Database connection established") +## return GdUnitResult.success() +## +## func shutdown(session: GdUnitTestSession) -> GdUnitResult: +## session.send_message("Generated test report: report.html") +## return GdUnitResult.success() +## ``` +## [/codeblock] +## [param message] The message text to send through the GdUnit messaging system +func send_message(message: String) -> void: + GdUnitSignals.instance().gdunit_message.emit(message) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid new file mode 100644 index 00000000..03c78148 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSession.gd.uid @@ -0,0 +1 @@ +uid://d1um8x1nfq6nb diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd new file mode 100644 index 00000000..6551e085 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd @@ -0,0 +1,180 @@ +extends Node +## The base test runner implementation.[br] +## [br] +## This class provides the core functionality to execute test suites with following features:[br] +## - Loading and initialization of test suites[br] +## - Executing test suites and managing test states[br] +## - Event dispatching and test reporting[br] +## - Support for headless mode[br] +## - Plugin version verification[br] +## [br] +## Supported by specialized runners:[br] +## - [b]GdUnitTestRunner[/b]: Used in the editor, connects via tcp to report test results[br] +## - [b]GdUnitCLRunner[/b]: A command line interface runner, writes test reports to file[br] +## The test runner runs checked default in fail-fast mode, it stops checked first test failure. + +## Overall test run status codes used by the runners +const RETURN_SUCCESS = 0 +const RETURN_ERROR = 100 +const RETURN_ERROR_HEADLESS_NOT_SUPPORTED = 103 +const RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED = 104 +const RETURN_WARNING = 101 + +## Specifies the Node name under which the runner is registered +const GDUNIT_RUNNER = "GdUnitRunner" + +## The current runner configuration +@warning_ignore("unused_private_class_variable") +var _runner_config := GdUnitRunnerConfig.new() + +## The test suite executor instance +var _executor: GdUnitTestSuiteExecutor +var _hooks : GdUnitTestSessionHookService + +## Current runner state +var _state := READY + +## Current tests to be processed +var _test_cases: Array[GdUnitTestCase] = [] + + +## Configured report base path (can be set on CI test runner) +var report_base_path: String = GdUnitFileAccess.current_dir() + "reports": + get: + return report_base_path + + +## Current session report path +var report_path: String: + get: + return "%s/%s%d" % [report_base_path, GdUnitConstants.REPORT_DIR_PREFIX, current_report_history_index] + + +## Current report history index, if max_report_history > 1 we scan for the next index over the existing reports +var current_report_history_index: int: + get: + if max_report_history > 1: + return GdUnitFileAccess.find_last_path_index(report_base_path, GdUnitConstants.REPORT_DIR_PREFIX) + 1 + else: + return 1 + + +## Controls how many report historys will be hold +var max_report_history: int = GdUnitConstants.DEFAULT_REPORT_HISTORY_COUNT: + get: + return max_report_history + set(value): + max_report_history = value + + +# holds the current test session context +var _test_session: GdUnitTestSession + +## Runner state machine +enum { + READY, + INIT, + RUN, + STOP, + EXIT +} + +func _init() -> void: + if OS.get_cmdline_args().size() == 1: + DisplayServer.window_set_title("GdUnit4 Runner (Debug Mode)") + else: + DisplayServer.window_set_title("GdUnit4 Runner (Release Mode)") + if not Engine.is_embedded_in_editor(): + # minimize scene window checked debug mode + DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED) + # store current runner instance to engine meta data to can be access in as a singleton + Engine.set_meta(GDUNIT_RUNNER, self) + + +# Called when the node enters the scene tree for the first time. +func _ready() -> void: + if Engine.get_version_info().hex < 0x40300: + printerr("The GdUnit4 plugin requires Godot version 4.3 or higher to run.") + quit(RETURN_ERROR_GODOT_VERSION_NOT_SUPPORTED) + return + _executor = GdUnitTestSuiteExecutor.new() + + GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + _state = INIT + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + Engine.remove_meta(GDUNIT_RUNNER) + + +## Main test runner loop. Is called every frame to manage the test execution. +func _process(_delta: float) -> void: + match _state: + INIT: + await init_runner() + RUN: + _hooks = GdUnitTestSessionHookService.instance() + _test_session = GdUnitTestSession.new(_test_cases, report_path) + GdUnitSignals.instance().gdunit_event.emit(GdUnitSessionStart.new()) + # process next test suite + set_process(false) + var result := await _hooks.execute_startup(_test_session) + if result.is_error(): + push_error(result.error_message()) + await _executor.run_and_wait(_test_cases) + result = await _hooks.execute_shutdown(_test_session) + if result.is_error(): + push_error(result.error_message()) + _state = STOP + set_process(true) + GdUnitSignals.instance().gdunit_event.emit(GdUnitSessionClose.new()) + cleanup_report_history() + STOP: + _state = EXIT + # give the engine small amount time to finish the rpc + await get_tree().create_timer(0.1).timeout + await quit(get_exit_code()) + + +## Used by the inheriting runners to initialize test execution +func init_runner() -> void: + await get_tree().process_frame + + +func cleanup_report_history() -> int: + return GdUnitFileAccess.delete_path_index_lower_equals_than( + report_path.get_base_dir(), + GdUnitConstants.REPORT_DIR_PREFIX, + current_report_history_index-1-max_report_history) + + +## Returns the exit code when the test run is finished.[br] +## Abstract method to be implemented by the inheriting runners. +func get_exit_code() -> int: + return RETURN_SUCCESS + + +## Quits the test runner with given exit code. +func quit(code: int) -> void: + await get_tree().process_frame + await get_tree().physics_frame + get_tree().quit(code) + + +func prints_warning(message: String) -> void: + prints(message) + + +## Default event handler to process test events.[br] +## Should be overridden by concrete runner implementation. +@warning_ignore("unused_parameter") +func _on_gdunit_event(event: GdUnitEvent) -> void: + pass + + +## Event bridge from C# GdUnit4.ITestEventListener.cs[br] +## Used to handle test events from C# tests. +# gdlint: disable=function-name +func PublishEvent(data: Dictionary) -> void: + _on_gdunit_event(GdUnitEvent.new().deserialize(data)) diff --git a/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid new file mode 100644 index 00000000..2aad1f14 --- /dev/null +++ b/addons/gdUnit4/src/core/runners/GdUnitTestSessionRunner.gd.uid @@ -0,0 +1 @@ +uid://j58ly6b5wy6x diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd index 931fe846..20325ac1 100644 --- a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd @@ -10,7 +10,7 @@ const DEFAULT_TEMP_TS_GD =""" @warning_ignore('return_value_discarded') # TestSuite generated from - const __source = '${source_resource_path}' + const __source: String = '${source_resource_path}' """ diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid index 3f2a8e38..76f99573 100644 --- a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteDefaultTemplate.gd.uid @@ -1 +1 @@ -uid://de1d6i0cl3g70 +uid://1vff42r7mww3 diff --git a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid index 2c81fd17..92c886ba 100644 --- a/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid +++ b/addons/gdUnit4/src/core/templates/test_suite/GdUnitTestSuiteTemplate.gd.uid @@ -1 +1 @@ -uid://fy8b2o01fsn4 +uid://c2818yqqryxs4 diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid index dab190e5..b52fea1f 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadContext.gd.uid @@ -1 +1 @@ -uid://b20641qojct62 +uid://cswl465sy52f0 diff --git a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid index b0a44429..961dc0ce 100644 --- a/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid +++ b/addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd.uid @@ -1 +1 @@ -uid://sjmg81pivo7e +uid://cyv5806buvk6h diff --git a/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd new file mode 100644 index 00000000..be5d1e5b --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd @@ -0,0 +1,227 @@ +@tool +class_name GdUnitCSIMessageWriter +extends GdUnitMessageWritter +## A message writer implementation using ANSI/CSI escape codes for console output.[br] +## [br] +## This writer provides formatted message output using CSI (Control Sequence Introducer) codes.[br] +## It supports:[br] +## - Color using RGB values[br] +## - Text styles (bold, italic, underline)[br] +## - Cursor positioning and text alignment[br] +## [br] +## Used primarily for console-based test execution and CI/CD environments. + + +enum { + COLOR_TABLE, + COLOR_RGB +} + +const CSI_BOLD = "" +const CSI_ITALIC = "" +const CSI_UNDERLINE = "" +const CSI_RESET = "" + +# Control Sequence Introducer +var _debug_show_color_codes := false +var _color_mode := COLOR_TABLE + +## Current cursor position in the line +var _current_pos := 0 + +# Pre-compiled regex patterns for tag matching +var _tag_regex: RegEx + + +## Constructs CSI style codes based on flags.[br] +## [br] +## [param flags] The style flags to apply (BOLD, ITALIC, UNDERLINE).[br] +## Returns the corresponding CSI codes. +func _apply_style_flags(flags: int) -> String: + var _style := "" + if flags & BOLD: + _style += CSI_BOLD + if flags & ITALIC: + _style += CSI_ITALIC + if flags & UNDERLINE: + _style += CSI_UNDERLINE + return _style + + +## Converts a color string (named or hex) to a Color object +func _parse_color(color_str: String) -> Color: + return Color.from_string(color_str.strip_edges().to_lower(), Color.WHITE) + + +## Generates CSI color code for foreground color +func _color_to_csi_fg(c: Color) -> String: + return "[38;2;%d;%d;%dm" % [c.r8 * c.a, c.g8 * c.a, c.b8 * c.a] + + +## Generates CSI color code for background color +func _color_to_csi_bg(c: Color) -> String: + return "[48;2;%d;%d;%dm" % [c.r8 * c.a, c.g8 * c.a, c.b8 * c.a] + + +func _init_regex_patterns() -> void: + if not _tag_regex: + _tag_regex = RegEx.new() + # Match all richtext tags: [tag], [tag=value], [/tag] + _tag_regex.compile(r"\[/?(?:color|bgcolor|b|i|u)(?:=[^\]]+)?\]") + + +func _extract_color_from_tag(tag: String, tag_assign: String) -> Color: + var tag_assign_length := tag_assign.length() + var color_value := tag.substr(tag_assign_length, tag.length() - tag_assign_length - 1) + return _parse_color(color_value) + + +## Optimized richtext to CSI conversion using regex and lookup processing +func _bbcode_tags_to_csi_codes(message: String) -> String: + _init_regex_patterns() + + var result := "" + var last_pos := 0 + var color_stack: Array[Color] = [] + var bgcolor_stack: Array[Color] = [] + + # Find all richtext tags + var matches := _tag_regex.search_all(message) + + for match in matches: + var start_pos := match.get_start() + var end_pos := match.get_end() + var tag := match.get_string(0) + + # Add text before this tag + result += message.substr(last_pos, start_pos - last_pos) + + # Process the tag + if tag.begins_with("[color="): + var fg_color := _extract_color_from_tag(tag, "[color=") + color_stack.push_back(fg_color) + result += _color_to_csi_fg(fg_color) + elif tag.begins_with("[bgcolor="): + var bg_color := _extract_color_from_tag(tag, "[bgcolor=") + bgcolor_stack.push_back(bg_color) + result += _color_to_csi_bg(bg_color) + elif tag == "[b]": + result += CSI_BOLD + elif tag == "[i]": + result += CSI_ITALIC + elif tag == "[u]": + result += CSI_UNDERLINE + elif tag == "[/color]": + result += CSI_RESET + if color_stack.size() > 0: + color_stack.pop_back() + # Restore remaining styles and colors + if color_stack.size() > 0: + result += _color_to_csi_fg(color_stack[-1]) + if bgcolor_stack.size() > 0: + result += _color_to_csi_bg(bgcolor_stack[-1]) + elif tag == "[/bgcolor]": + result += CSI_RESET + if bgcolor_stack.size() > 0: + bgcolor_stack.pop_back() + # Restore remaining styles and colors + if color_stack.size() > 0: + result += _color_to_csi_fg(color_stack[-1]) + if bgcolor_stack.size() > 0: + result += _color_to_csi_bg(bgcolor_stack[-1]) + elif tag in ["[/b]", "[/i]", "[/u]"]: + result += CSI_RESET + # Restore remaining colors after style reset + if color_stack.size() > 0: + result += _color_to_csi_fg(color_stack[-1]) + if bgcolor_stack.size() > 0: + result += _color_to_csi_bg(bgcolor_stack[-1]) + + last_pos = end_pos + + # Add remaining text after last tag + result += message.substr(last_pos) + + return result + + +## Implementation of basic message output with formatting. +func _print_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + var text := _bbcode_tags_to_csi_codes(_message) + var indent_text := "".lpad(_indent * 2) + var _style := _apply_style_flags(_flags) + printraw("%s[38;2;%d;%d;%dm%s%s" % [indent_text, _color.r8, _color.g8, _color.b8, _style, text] ) + _current_pos += _indent * 2 + text.length() + + +## Implementation of line-ending message output with formatting. +func _println_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + _print_message(_message, _color, _indent, _flags) + prints() + _current_pos = 0 + + +## Implementation of positioned message output with formatting. +func _print_at(_message: String, cursor_pos: int, _color: Color, _effect: Effect, _align: Align, _flags: int) -> void: + if _align == Align.RIGHT: + cursor_pos = cursor_pos - _message.length() + + if cursor_pos > _current_pos: + printraw("[%dG" % cursor_pos) # Move cursor to absolute position + else: + _message = " " + _message + + var _style := _apply_style_flags(_flags) + printraw("[38;2;%d;%d;%dm%s%s" % [_color.r8, _color.g8, _color.b8, _style, _message] ) + _current_pos = cursor_pos + _message.length() + + +## Writes a line break and returns self for chaining. +func new_line() -> GdUnitCSIMessageWriter: + prints() + return self + + +## Saves the current cursor position.[br] +## Returns self for chaining. +func save_cursor() -> GdUnitCSIMessageWriter: + printraw("") + return self + + +## Restores previously saved cursor position.[br] +## Returns self for chaining. +func restore_cursor() -> GdUnitCSIMessageWriter: + printraw("") + return self + + +## Clears screen content and resets cursor position. +func clear() -> void: + printraw("") # Clear screen and move cursor to home + _current_pos = 0 + + +## Debug method to display the available color table.[br] +## Shows both 6x6x6 color cube and RGB color modes. +@warning_ignore("return_value_discarded") +func _print_color_table() -> void: + color(Color.ANTIQUE_WHITE).println_message("Color Table 6x6x6") + _debug_show_color_codes = true + for green in range(0, 6): + for red in range(0, 6): + for blue in range(0, 6): + color(Color8(red*42, green*42, blue*42)).println_message("████████ ") + new_line() + new_line() + + color(Color.ANTIQUE_WHITE).println_message("Color Table RGB") + _color_mode = COLOR_RGB + for green in range(0, 6): + for red in range(0, 6): + for blue in range(0, 6): + color(Color8(red*42, green*42, blue*42)).println_message("████████ ") + new_line() + new_line() + _color_mode = COLOR_TABLE + _debug_show_color_codes = false diff --git a/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid new file mode 100644 index 00000000..d5f12f35 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitCSIMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://k3kgm7osx44n diff --git a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd new file mode 100644 index 00000000..2ae94a46 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd @@ -0,0 +1,214 @@ +@tool +class_name GdUnitMessageWritter +extends RefCounted +## Base interface class for writing formatted messages to different outputs.[br] +## [br] +## This class defines the interface and common functionality for writing formatted messages.[br] +## It provides a fluent API for message formatting and supports different output targets.[br] +## [br] +## The class provides formatting options for:[br] +## - Text colors[br] +## - Text styles (bold, italic, underline)[br] +## - Text effects (e.g., wave)[br] +## - Text alignment[br] +## - Indentation[br] +## [br] +## Two concrete implementations are available:[br] +## - [GdUnitRichTextMessageWriter] writing to a [RichTextLabel][br] +## - [GdUnitCSIMessageWriter] writing to console using CSI codes[br] +## [br] +## Example usage:[br] +## [codeblock] +## writer.color(Color.RED).style(BOLD).println_message("Test failed!") +## writer.color(Color.GREEN).align(Align.RIGHT).print_at("Success", 80) +## [/codeblock] + + +## Text style flag for bold formatting +const BOLD = 0x1 +## Text style flag for italic formatting +const ITALIC = 0x2 +## Text style flag for underline formatting +const UNDERLINE = 0x4 + + +## Represents special text effects that can be applied to the output +enum Effect { + ## No special effect applied + NONE, + ## Applies a wave animation to the text + WAVE +} + + +## Controls text alignment at the specified cursor position +enum Align { + ## Aligns text to the left of the cursor position + LEFT, + ## Aligns text to the right of the cursor position, accounting for text length + RIGHT +} + + +## The current text color to be used for the next output operation +var _current_color := Color.WHITE + +## The current indentation level to be used for the next output operation.[br] +## Each level represents two spaces of indentation. +var _current_indent := 0 + +## The current text style flags (BOLD, ITALIC, UNDERLINE) to be used for the next output operation +var _current_flags := 0 + +## The current text alignment to be used for the next output operation +var _current_align := Align.LEFT + +## The current text effect to be used for the next output operation +var _current_effect := Effect.NONE + + +## Sets the text color for the next output operation.[br] +## [br] +## [param value] The color to be used for the text. +## Returns self for method chaining. +func color(value: Color) -> GdUnitMessageWritter: + _current_color = value + return self + + +## Sets the indentation level for the next output operation.[br] +## [br] +## [param value] The number of indentation levels, where each level equals two spaces. +## Returns self for method chaining. +func indent(value: int) -> GdUnitMessageWritter: + _current_indent = value + return self + + +## Sets text style flags for the next output operation.[br] +## [br] +## [param value] A combination of style flags (BOLD, ITALIC, UNDERLINE). +## Returns self for method chaining. +func style(value: int) -> GdUnitMessageWritter: + _current_flags = value + return self + + +## Sets text effect for the next output operation.[br] +## [br] +## [param value] The effect to apply to the text (NONE, WAVE). +## Returns self for method chaining. +func effect(value: Effect) -> GdUnitMessageWritter: + _current_effect = value + return self + + +## Sets text alignment for the next output operation.[br] +## [br] +## [param value] The alignment to use (LEFT, RIGHT). +## Returns self for method chaining. +func align(value: Align) -> GdUnitMessageWritter: + _current_align = value + return self + + +## Resets all formatting options to their default values.[br] +## [br] +## Defaults:[br] +## - color: Color.WHITE[br] +## - indent: 0[br] +## - flags: 0[br] +## - align: LEFT[br] +## - effect: NONE[br] +## Returns self for method chaining. +func reset() -> GdUnitMessageWritter: + _current_color = Color.WHITE + _current_indent = 0 + _current_flags = 0 + _current_align = Align.LEFT + _current_effect = Effect.NONE + return self + + +## Prints a warning message in golden color.[br] +## [br] +## [param message] The warning message to print. +func prints_warning(message: String) -> void: + color(Color.GOLDENROD).println_message(message) + + +## Prints an error message in crimson color.[br] +## [br] +## [param message] The error message to print. +func prints_error(message: String) -> void: + color(Color.CRIMSON).println_message(message) + + +## Prints a message with current formatting settings.[br] +## [br] +## [param message] The text to print. +func print_message(message: String) -> void: + _print_message(message, _current_color, _current_indent, _current_flags) + reset() + + +## Prints a message with current formatting settings followed by a newline.[br] +## [br] +## [param message] The text to print. +func println_message(message: String) -> void: + _println_message(message, _current_color, _current_indent, _current_flags) + reset() + + +## Prints a message at a specific column position with current formatting settings.[br] +## [br] +## [param message] The text to print.[br] +## [param cursor_pos] The column position where the text should start. +func print_at(message: String, cursor_pos: int) -> void: + _print_at(message, cursor_pos, _current_color, _current_effect, _current_align, _current_flags) + reset() + + +## Internal implementation of print_message.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param message] The text to print.[br] +## [param color] The color to use.[br] +## [param indent] The indentation level.[br] +## [param flags] The style flags to apply. +func _print_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + pass + + +## Internal implementation of println_message.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param message] The text to print.[br] +## [param color] The color to use.[br] +## [param indent] The indentation level.[br] +## [param flags] The style flags to apply. +func _println_message(_message: String, _color: Color, _indent: int, _flags: int) -> void: + pass + + +## Internal implementation of print_at.[br] +## [br] +## To be overridden by concrete formatters.[br] +## [br] +## [param message] The text to print.[br] +## [param cursor_pos] The column position.[br] +## [param color] The color to use.[br] +## [param effect] The effect to apply.[br] +## [param align] The text alignment.[br] +## [param flags] The style flags to apply. +func _print_at(_message: String, _cursor_pos: int, _color: Color, _effect: Effect, _align: Align, _flags: int) -> void: + pass + + +## Clears all output content.[br] +## [br] +## To be overridden by concrete formatters. +func clear() -> void: + pass diff --git a/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid new file mode 100644 index 00000000..3347ea24 --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://du462w7gv1gst diff --git a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd new file mode 100644 index 00000000..64793bbe --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd @@ -0,0 +1,115 @@ +@tool +class_name GdUnitRichTextMessageWriter +extends GdUnitMessageWritter +## A message writer implementation using [RichTextLabel] for the test report UI.[br] +## [br] +## This writer implementation writes formatted messages to a [RichTextLabel] using BBCode.[br] +## It supports:[br] +## - Text formatting using BBCode (bold, italic, underline)[br] +## - Text coloring using push colors[br] +## - Text indentation using push indent[br] +## - Text effects like wave[br] +## - Basic cursor positioning[br] +## [br] +## Used to format test reports in the editor UI. + + +## The [RichTextLabel] instance to write formatted messages +var _output: RichTextLabel + +## Tracks current position in characters from line start +var _current_pos := 0 + + +## Creates a new message writer for the given [RichTextLabel].[br] +## [br] +## [param output] The [RichTextLabel] used for output. +func _init(output: RichTextLabel) -> void: + _output = output + + +## Applies text style flags by wrapping text in BBCode tags.[br] +## [br] +## Available styles:[br] +## - BOLD: [b]text[/b][br] +## - ITALIC: [i]text[/i][br] +## - UNDERLINE: [u]text[/u][br] +## [br] +## [param message] The text to format.[br] +## [param flags] The text style flags to apply. +func _apply_flags(message: String, flags: int) -> String: + if flags & BOLD: + message = "[b]%s[/b]" % message + if flags & ITALIC: + message = "[i]%s[/i]" % message + if flags & UNDERLINE: + message = "[u]%s[/u]" % message + return message + + +## Writes a message with formatting.[br] +## [br] +## [param message] The text to write.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param flags] The text style flags to apply. +func _print_message(message: String, _color: Color, _indent: int, flags: int) -> void: + for i in _indent: + _output.push_indent(1) + _output.push_color(_color) + message = _apply_flags(message, flags) + _output.append_text(message) + _output.pop() + for i in _indent: + _output.pop() + _current_pos += _indent * 2 + message.length() + + +## Writes a message with formatting followed by a line break.[br] +## [br] +## [param message] The text to write.[br] +## [param _color] The color to use.[br] +## [param _indent] The indentation level.[br] +## [param flags] The text style flags to apply. +func _println_message(message: String, _color: Color, _indent: int, flags: int) -> void: + _print_message(message, _color, _indent, flags) + _output.newline() + _current_pos = 0 + + +## Writes a message at a specific column position.[br] +## [br] +## [param message] The text to write.[br] +## [param cursor_pos] The column position from line start.[br] +## [param _color] The color to use.[br] +## [param _effect] The text effect to apply (e.g. wave).[br] +## [param _align] The text alignment (left or right).[br] +## [param flags] The text style flags to apply. +func _print_at(message: String, cursor_pos: int, _color: Color, _effect: Effect, _align: Align, flags: int) -> void: + if _align == Align.RIGHT: + cursor_pos = cursor_pos - message.length() + + var spaces := cursor_pos - _current_pos + if spaces > 0: + _output.append_text("".lpad(spaces)) + _current_pos += spaces + else: + _output.append_text(" ") + _current_pos += 1 + + _output.push_color(_color) + message = _apply_flags(message, flags) + match _effect: + Effect.NONE: + pass + Effect.WAVE: + message = "[wave]%s[/wave]" % message + _output.append_text(message) + _output.pop() + _current_pos += message.length() + + +## Clears all written content from the [RichTextLabel]. +func clear() -> void: + _output.clear() + _current_pos = 0 diff --git a/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid new file mode 100644 index 00000000..a2ba7bcd --- /dev/null +++ b/addons/gdUnit4/src/core/writers/GdUnitRichTextMessageWriter.gd.uid @@ -0,0 +1 @@ +uid://0m5cuc7dd8l1 diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs new file mode 100644 index 00000000..14a1355b --- /dev/null +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs @@ -0,0 +1,216 @@ +// Copyright (c) 2025 Mike Schulze +// MIT License - See LICENSE file in the repository root for full license text +#pragma warning disable IDE1006 +namespace gdUnit4.addons.gdUnit4.src.dotnet; +#pragma warning restore IDE1006 + +#if GDUNIT4NET_API_V5 +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using GdUnit4; +using GdUnit4.Api; + +using Godot; +using Godot.Collections; + +/// +/// The GdUnit4 GDScript - C# API wrapper. +/// +public partial class GdUnit4CSharpApi : GdUnit4NetApiGodotBridge +{ + /// + /// The signal to be emitted when the execution is completed. + /// + [Signal] +#pragma warning disable CA1711 + public delegate void ExecutionCompletedEventHandler(); +#pragma warning restore CA1711 + +#pragma warning disable CA2213, SA1201 + private CancellationTokenSource? executionCts; +#pragma warning restore CA2213, SA1201 + + /// + /// Indicates if the API loaded. + /// + /// Returns true if the API already loaded. + public static bool IsApiLoaded() + => true; + + /// + /// Runs test discovery on the given script. + /// + /// The script to be scanned. + /// The list of tests discovered as dictionary. + public static Array DiscoverTests(CSharpScript sourceScript) + { + try + { + // Get the list of test case descriptors from the API + var testCaseDescriptors = DiscoverTestsFromScript(sourceScript); + + // Convert each TestCaseDescriptor to a Dictionary + return testCaseDescriptors + .Select(descriptor => new Dictionary + { + ["guid"] = descriptor.Id.ToString(), + ["managed_type"] = descriptor.ManagedType, + ["test_name"] = descriptor.ManagedMethod, + ["source_file"] = sourceScript.ResourcePath, + ["line_number"] = descriptor.LineNumber, + ["attribute_index"] = descriptor.AttributeIndex, + ["require_godot_runtime"] = descriptor.RequireRunningGodotEngine, + ["code_file_path"] = descriptor.CodeFilePath ?? string.Empty, + ["simple_name"] = descriptor.SimpleName, + ["fully_qualified_name"] = descriptor.FullyQualifiedName, + ["assembly_location"] = descriptor.AssemblyPath + }) + .Aggregate(new Array(), (array, dict) => + { + array.Add(dict); + return array; + }); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + GD.PrintErr($"Error discovering tests: {e.Message}\n{e.StackTrace}"); +#pragma warning disable IDE0028 // Do not catch general exception types + return new Array(); +#pragma warning restore IDE0028 // Do not catch general exception types + } + } + + /// + public override void _Notification(int what) + { + if (what != NotificationPredelete) + return; + executionCts?.Dispose(); + executionCts = null; + } + + /// + /// Executes the tests and using the listener for reporting the results. + /// + /// A list of tests to be executed. + /// The listener to report the results. + public void ExecuteAsync(Array tests, Callable listener) + { + try + { + // Cancel any ongoing execution + executionCts?.Cancel(); + executionCts?.Dispose(); + + // Create new cancellation token source + executionCts = new CancellationTokenSource(); + + Debug.Assert(tests != null, nameof(tests) + " != null"); + var testSuiteNodes = new List { BuildTestSuiteNodeFrom(tests) }; + ExecuteAsync(testSuiteNodes, listener, executionCts.Token) + .GetAwaiter() + .OnCompleted(() => EmitSignal(SignalName.ExecutionCompleted)); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + GD.PrintErr($"Error executing tests: {e.Message}\n{e.StackTrace}"); + Task.Run(() => { }).GetAwaiter().OnCompleted(() => EmitSignal(SignalName.ExecutionCompleted)); + } + } + + /// + /// Will cancel the current test execution. + /// + public void CancelExecution() + { + try + { + executionCts?.Cancel(); + } +#pragma warning disable CA1031 + catch (Exception e) +#pragma warning restore CA1031 + { + GD.PrintErr($"Error cancelling execution: {e.Message}"); + } + } + + // Convert a set of Tests stored as Dictionaries to TestSuiteNode + // all tests are assigned to a single test suit + internal static TestSuiteNode BuildTestSuiteNodeFrom(Array tests) + { + if (tests.Count == 0) + throw new InvalidOperationException("Cant build 'TestSuiteNode' from an empty test set."); + + // Create a suite ID + var suiteId = Guid.NewGuid(); + var firstTest = tests[0]; + var managedType = firstTest["managed_type"].AsString(); + var assemblyLocation = firstTest["assembly_location"].AsString(); + var sourceFile = firstTest["source_file"].AsString(); + + // Create TestCaseNodes for each test in the suite + var testCaseNodes = tests + .Select(test => new TestCaseNode + { + Id = Guid.Parse(test["guid"].AsString()), + ParentId = suiteId, + ManagedMethod = test["test_name"].AsString(), + LineNumber = test["line_number"].AsInt32(), + AttributeIndex = test["attribute_index"].AsInt32(), + RequireRunningGodotEngine = test["require_godot_runtime"].AsBool() + }) + .ToList(); + + return new TestSuiteNode + { + Id = suiteId, + ParentId = Guid.Empty, + ManagedType = managedType, + AssemblyPath = assemblyLocation, + SourceFile = sourceFile, + Tests = testCaseNodes + }; + } +} +#else +using Godot; +using Godot.Collections; + +public partial class GdUnit4CSharpApi : RefCounted +{ + [Signal] + public delegate void ExecutionCompletedEventHandler(); + + public static bool IsApiLoaded() + { + GD.PushWarning("No `gdunit4.api` dependency found, check your project dependencies."); + return false; + } + + + public static string Version() + => "Unknown"; + + public static Array DiscoverTests(CSharpScript sourceScript) => new(); + + public void ExecuteAsync(Array tests, Callable listener) + { + } + + public static bool IsTestSuite(CSharpScript script) + => false; + + public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) + => new(); +} +#endif diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd new file mode 100644 index 00000000..3d8ba25f --- /dev/null +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd @@ -0,0 +1,114 @@ +## GdUnit4CSharpApiLoader +## +## A bridge class that handles communication between GDScript and C# for the GdUnit4 testing framework. +## This loader acts as a compatibility layer to safely access the .NET API and ensure that calls +## only proceed when the .NET environment is properly configured and available. +## [br] +## The class handles: +## - Verification of .NET runtime availability +## - Loading the C# wrapper script +## - Checking for the GdUnit4Api assembly +## - Providing proxy methods to access GdUnit4 functionality in C# +@static_unload +class_name GdUnit4CSharpApiLoader +extends RefCounted + +## Cached reference to the loaded C# wrapper script +static var _gdUnit4NetWrapper: Script + +## Cached instance of the API (singleton pattern) +static var _api_instance: RefCounted + + +class TestEventListener extends RefCounted: + + func publish_event(event: Dictionary) -> void: + var test_event := GdUnitEvent.new().deserialize(event) + GdUnitSignals.instance().gdunit_event.emit(test_event) + +static var _test_event_listener := TestEventListener.new() + + +## Returns an instance of the GdUnit4CSharpApi wrapper.[br] +## @return Script: The loaded C# wrapper or null if .NET is not supported +static func instance() -> Script: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return null + + return _gdUnit4NetWrapper + + +## Returns or creates a single instance of the API [br] +## This improves performance by reusing the same object +static func api_instance() -> RefCounted: + if _api_instance == null and is_api_loaded(): + @warning_ignore("unsafe_method_access") + _api_instance = instance().new() + return _api_instance + + +static func is_engine_version_supported(engine_version: int = Engine.get_version_info().hex) -> bool: + return engine_version >= 0x40200 + + +## Checks if the .NET environment is properly configured and available.[br] +## @return bool: True if .NET is fully supported and the assembly is found +static func is_api_loaded() -> bool: + # If the wrapper is already loaded we don't need to check again + if _gdUnit4NetWrapper != null: + return true + + # First we check if this is a Godot .NET runtime instance + if not ClassDB.class_exists("CSharpScript") or not is_engine_version_supported(): + return false + # Second we check the C# project file exists + var assembly_name: String = ProjectSettings.get_setting("dotnet/project/assembly_name") + if assembly_name.is_empty() or not FileAccess.file_exists("res://%s.csproj" % assembly_name): + return false + + # Finally load the wrapper and check if the GdUnit4 assembly can be found + _gdUnit4NetWrapper = load("res://addons/gdUnit4/src/dotnet/GdUnit4CSharpApi.cs") + @warning_ignore("unsafe_method_access") + return _gdUnit4NetWrapper.call("IsApiLoaded") + + +## Returns the version of the GdUnit4 .NET assembly.[br] +## @return String: The version string or "unknown" if .NET is not supported +static func version() -> String: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return "unknown" + @warning_ignore("unsafe_method_access") + return instance().Version() + + +static func discover_tests(source_script: Script) -> Array[GdUnitTestCase]: + var tests: Array = _gdUnit4NetWrapper.call("DiscoverTests", source_script) + + return Array(tests.map(GdUnitTestCase.from_dict), TYPE_OBJECT, "RefCounted", GdUnitTestCase) + + +static func execute(tests: Array[GdUnitTestCase]) -> void: + var net_api := api_instance() + if net_api == null: + push_warning("Execute C# tests not supported!") + return + var tests_as_dict: Array[Dictionary] = Array(tests.map(GdUnitTestCase.to_dict), TYPE_DICTIONARY, "", null) + + net_api.call("ExecuteAsync", tests_as_dict, _test_event_listener.publish_event) + @warning_ignore("unsafe_property_access") + await net_api.ExecutionCompleted + + +static func create_test_suite(source_path: String, line_number: int, test_suite_path: String) -> GdUnitResult: + if not GdUnit4CSharpApiLoader.is_api_loaded(): + return GdUnitResult.error("Can't create test suite. No .NET support found.") + @warning_ignore("unsafe_method_access") + var result: Dictionary = instance().CreateTestSuite(source_path, line_number, test_suite_path) + if result.has("error"): + return GdUnitResult.error(str(result.get("error"))) + return GdUnitResult.success(result) + + +static func is_csharp_file(resource_path: String) -> bool: + var ext := resource_path.get_extension() + return ext == "cs" and GdUnit4CSharpApiLoader.is_api_loaded() diff --git a/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid new file mode 100644 index 00000000..7e91d829 --- /dev/null +++ b/addons/gdUnit4/src/dotnet/GdUnit4CSharpApiLoader.gd.uid @@ -0,0 +1 @@ +uid://yjqvv8qmpsfw diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd b/addons/gdUnit4/src/doubler/CallableDoubler.gd index 14a5947d..1bc78a1a 100644 --- a/addons/gdUnit4/src/doubler/CallableDoubler.gd +++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd @@ -58,20 +58,8 @@ static func callable_functions() -> PackedStringArray: ## Callable functions stubing ## ----------------------------------------------------------------------------------------------------------------------------------------- -@warning_ignore("untyped_declaration") -func bind(arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> Callable: - # save - var bind_values: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) - _cb = _cb.bindv(bind_values) +func bind(...varargs: Array) -> Callable: + _cb = _cb.bindv(varargs) return _cb @@ -80,34 +68,19 @@ func bindv(caller_args: Array) -> Callable: return _cb -@warning_ignore("untyped_declaration", "native_method_override", "unused_parameter") -func call(arg0=null, - arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> Variant: - - # This is a placeholder function signanture without any functionallity! - # It is used by the function doubler to double function signature of Callable:call() - # The doubled function calls direct _cb.callv() see GdUnitSpyFunctionDoubler:TEMPLATE_CALLABLE_CALL template - assert(false) - return null +@warning_ignore("native_method_override") +func call(...varargs: Array) -> Variant: + return _cb.callv(varargs) # Is not supported, see class description -#func call_deferred(a) -> void: -# pass +#func call_deferred(...varargs: Array) -> void: +# return _cb.call_deferred(varargs) # Is not supported, see class description -#func callv(a) -> void: -# pass - +#func callv(arguments: Array) -> Variant: +# return _cb.callv(arguments) func get_bound_arguments() -> Array: @@ -150,60 +123,33 @@ func is_valid() -> bool: return _cb.is_valid() -@warning_ignore("untyped_declaration") -func rpc(arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> void: - - var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) - match args.size(): - 0: _cb.rpc(0) - 1: _cb.rpc(args[0]) - 2: _cb.rpc(args[0], args[1]) - 3: _cb.rpc(args[0], args[1], args[2]) - 4: _cb.rpc(args[0], args[1], args[2], args[3]) - 5: _cb.rpc(args[0], args[1], args[2], args[3], args[4]) - 6: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5]) - 7: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6]) - 8: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) - 9: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]) - 10: _cb.rpc(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]) +func rpc(...varargs: Array) -> void: + match varargs.size(): + 0: _cb.rpc() + 1: _cb.rpc(varargs[0]) + 2: _cb.rpc(varargs[0], varargs[1]) + 3: _cb.rpc(varargs[0], varargs[1], varargs[2]) + 4: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) + 5: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) + 6: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) + 7: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) + 8: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) + 9: _cb.rpc(varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) @warning_ignore("untyped_declaration") -func rpc_id(peer_id: int, - arg0=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg1=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg2=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg3=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg4=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg5=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg6=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg7=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg8=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE, - arg9=GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) -> void: - - var args: Array = GdArrayTools.filter_value([arg0,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9], GdObjects.TYPE_VARARG_PLACEHOLDER_VALUE) - match args.size(): - 0: _cb.rpc_id(peer_id) - 1: _cb.rpc_id(peer_id, args[0]) - 2: _cb.rpc_id(peer_id, args[0], args[1]) - 3: _cb.rpc_id(peer_id, args[0], args[1], args[2]) - 4: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3]) - 5: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4]) - 6: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5]) - 7: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6]) - 8: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]) - 9: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]) - 10: _cb.rpc_id(peer_id, args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]) - +func rpc_id(peer_id: int, ...varargs: Array) -> void: + match varargs.size(): + 0: _cb.rpc_id(peer_id ) + 1: _cb.rpc_id(peer_id, varargs[0]) + 2: _cb.rpc_id(peer_id, varargs[0], varargs[1]) + 3: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2]) + 4: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4]) + 5: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5]) + 6: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6]) + 7: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7]) + 8: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8]) + 9: _cb.rpc_id(peer_id, varargs[0], varargs[1], varargs[2], varargs[3], varargs[4], varargs[5], varargs[6], varargs[7], varargs[8], varargs[9]) func unbind(argcount: int) -> Callable: _cb = _cb.unbind(argcount) diff --git a/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid index 5558cf6d..2d1ed3b1 100644 --- a/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid +++ b/addons/gdUnit4/src/doubler/CallableDoubler.gd.uid @@ -1 +1 @@ -uid://ccamvmddkpo6n +uid://c6o7cdywxfvmw diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd new file mode 100644 index 00000000..d9459dc3 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd @@ -0,0 +1,4 @@ +@abstract class_name GdFunctionDoubler +extends RefCounted + +@abstract func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray diff --git a/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid new file mode 100644 index 00000000..9c3f79f4 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://o28h6licvkyl diff --git a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd similarity index 71% rename from addons/gdUnit4/src/core/GdUnitClassDoubler.gd rename to addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd index 8a96a29b..31aa06bc 100644 --- a/addons/gdUnit4/src/core/GdUnitClassDoubler.gd +++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd @@ -2,13 +2,12 @@ class_name GdUnitClassDoubler extends RefCounted - const DOUBLER_INSTANCE_ID_PREFIX := "gdunit_doubler_instance_id_" -const DOUBLER_TEMPLATE :GDScript = preload("res://addons/gdUnit4/src/core/GdUnitObjectInteractionsTemplate.gd") const EXCLUDE_VIRTUAL_FUNCTIONS = [ # we have to exclude notifications because NOTIFICATION_PREDELETE is try # to delete already freed spy/mock resources and will result in a conflict "_notification", + "notification", # https://github.com/godotengine/godot/issues/67461 "get_name", "get_path", @@ -29,32 +28,29 @@ static func check_leaked_instances() -> void: ## we check that all registered spy/mock instances are removed from the engine meta data for key in Engine.get_meta_list(): if key.begins_with(DOUBLER_INSTANCE_ID_PREFIX): - var instance :Variant = Engine.get_meta(key) + var instance: Variant = Engine.get_meta(key) push_error("GdUnit internal error: an spy/mock instance '%s', class:'%s' is not removed from the engine and will lead in a leaked instance!" % [instance, instance.__SOURCE_CLASS]) + await (Engine.get_main_loop() as SceneTree).process_frame # loads the doubler template # class_info = { "class_name": <>, "class_path" : <>} -static func load_template(template :String, class_info :Dictionary, instance :Object) -> PackedStringArray: - # store instance id +static func load_template(template: String, class_info: Dictionary) -> PackedStringArray: var clazz_name: String = class_info.get("class_name") var source_code := template\ - .replace("${instance_id}", "%s%d" % [DOUBLER_INSTANCE_ID_PREFIX, abs(instance.get_instance_id())])\ - .replace("${source_class}", clazz_name) + .replace("${source_class}", clazz_name)\ + # Replace template class_name DoubledClass with source class name + .replace("SourceClassName", clazz_name.replace(".", "_")) var lines := GdScriptParser.to_unix_format(source_code).split("\n") - # replace template class_name with Doubled name and extends form source class - @warning_ignore("return_value_discarded") - lines.insert(0, "class_name Doubled%s" % clazz_name.replace(".", "_")) @warning_ignore("return_value_discarded") lines.insert(1, extends_clazz(class_info)) - # append Object interactions stuff - lines.append_array(GdScriptParser.to_unix_format(DOUBLER_TEMPLATE.source_code).split("\n")) + lines.insert(0, "@warning_ignore_start('unsafe_call_argument', 'shadowed_variable', 'untyped_declaration', 'native_method_override', 'int_as_enum_without_cast')") return lines -static func extends_clazz(class_info :Dictionary) -> String: - var clazz_name :String = class_info.get("class_name") - var clazz_path :PackedStringArray = class_info.get("class_path", []) +static func extends_clazz(class_info: Dictionary) -> String: + var clazz_name: String = class_info.get("class_name") + var clazz_path: PackedStringArray = class_info.get("class_path", []) # is inner class? if clazz_path.size() > 1: return "extends %s" % clazz_name @@ -64,7 +60,7 @@ static func extends_clazz(class_info :Dictionary) -> String: # double all functions of given instance -static func double_functions(instance :Object, clazz_name :String, clazz_path :PackedStringArray, func_doubler: GdFunctionDoubler, exclude_functions :Array) -> PackedStringArray: +static func double_functions(instance: Object, clazz_name: String, clazz_path: PackedStringArray, func_doubler: GdFunctionDoubler, exclude_functions: Array) -> PackedStringArray: var doubled_source := PackedStringArray() var parser := GdScriptParser.new() var exclude_override_functions := EXCLUDE_VIRTUAL_FUNCTIONS + EXCLUDE_FUNCTIONS + exclude_functions @@ -76,19 +72,19 @@ static func double_functions(instance :Object, clazz_name :String, clazz_path :P if result.is_error(): push_error(result.error_message()) return PackedStringArray() - var class_descriptor :GdClassDescriptor = result.value() + var class_descriptor: GdClassDescriptor = result.value() for func_descriptor in class_descriptor.functions(): if instance != null and not instance.has_method(func_descriptor.name()): #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) continue if functions.has(func_descriptor.name()) or exclude_override_functions.has(func_descriptor.name()): continue - doubled_source += func_doubler.double(func_descriptor, instance is CallableDoubler) + doubled_source += func_doubler.double(func_descriptor) functions.append(func_descriptor.name()) # double regular class functions var clazz_functions := GdObjects.extract_class_functions(clazz_name, clazz_path) - for method : Dictionary in clazz_functions: + for method: Dictionary in clazz_functions: var func_descriptor := GdFunctionDescriptor.extract_from(method) # exclude private core functions if func_descriptor.is_private(): @@ -104,16 +100,16 @@ static func double_functions(instance :Object, clazz_name :String, clazz_path :P #prints("no virtual func implemented",clazz_name, func_descriptor.name() ) continue functions.append(func_descriptor.name()) - doubled_source.append_array(func_doubler.double(func_descriptor, instance is CallableDoubler)) + doubled_source.append_array(func_doubler.double(func_descriptor)) return doubled_source # GD-110 -static func is_invalid_method_descriptior(method :Dictionary) -> bool: - var return_info :Dictionary = method["return"] - var type :int = return_info["type"] - var usage :int = return_info["usage"] - var clazz_name :String = return_info["class_name"] +static func is_invalid_method_descriptior(method: Dictionary) -> bool: + var return_info: Dictionary = method["return"] + var type: int = return_info["type"] + var usage: int = return_info["usage"] + var clazz_name: String = return_info["class_name"] # is method returning a type int with a given 'class_name' we have an enum # and the PROPERTY_USAGE_CLASS_IS_ENUM must be set if type == TYPE_INT and not clazz_name.is_empty() and not (usage & PROPERTY_USAGE_CLASS_IS_ENUM): diff --git a/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid new file mode 100644 index 00000000..d1c10b2f --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitClassDoubler.gd.uid @@ -0,0 +1 @@ +uid://dvtlfu2xqa3r4 diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd new file mode 100644 index 00000000..a1a75f11 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd @@ -0,0 +1,336 @@ +class_name GdUnitFunctionDoublerBuilder +extends RefCounted + +const TYPE_VOID = GdObjects.TYPE_VOID +const TYPE_VARIANT = GdObjects.TYPE_VARIANT +const TYPE_VARARG = GdObjects.TYPE_VARARG +const TYPE_FUNC = GdObjects.TYPE_FUNC +const TYPE_FUZZER = GdObjects.TYPE_FUZZER +const TYPE_ENUM = GdObjects.TYPE_ENUM + +const DEFAULT_TYPED_RETURN_VALUES := { + TYPE_NIL: "null", + TYPE_BOOL: "false", + TYPE_INT: "0", + TYPE_FLOAT: "0.0", + TYPE_STRING: "\"\"", + TYPE_STRING_NAME: "&\"\"", + TYPE_VECTOR2: "Vector2.ZERO", + TYPE_VECTOR2I: "Vector2i.ZERO", + TYPE_RECT2: "Rect2()", + TYPE_RECT2I: "Rect2i()", + TYPE_VECTOR3: "Vector3.ZERO", + TYPE_VECTOR3I: "Vector3i.ZERO", + TYPE_VECTOR4: "Vector4.ZERO", + TYPE_VECTOR4I: "Vector4i.ZERO", + TYPE_TRANSFORM2D: "Transform2D()", + TYPE_PLANE: "Plane()", + TYPE_QUATERNION: "Quaternion()", + TYPE_AABB: "AABB()", + TYPE_BASIS: "Basis()", + TYPE_TRANSFORM3D: "Transform3D()", + TYPE_PROJECTION: "Projection()", + TYPE_COLOR: "Color()", + TYPE_NODE_PATH: "NodePath()", + TYPE_RID: "RID()", + TYPE_OBJECT: "null", + TYPE_CALLABLE: "Callable()", + TYPE_SIGNAL: "Signal()", + TYPE_DICTIONARY: "Dictionary()", + TYPE_ARRAY: "Array()", + TYPE_PACKED_BYTE_ARRAY: "PackedByteArray()", + TYPE_PACKED_INT32_ARRAY: "PackedInt32Array()", + TYPE_PACKED_INT64_ARRAY: "PackedInt64Array()", + TYPE_PACKED_FLOAT32_ARRAY: "PackedFloat32Array()", + TYPE_PACKED_FLOAT64_ARRAY: "PackedFloat64Array()", + TYPE_PACKED_STRING_ARRAY: "PackedStringArray()", + TYPE_PACKED_VECTOR2_ARRAY: "PackedVector2Array()", + TYPE_PACKED_VECTOR3_ARRAY: "PackedVector3Array()", + TYPE_PACKED_VECTOR4_ARRAY: "PackedVector4Array()", + TYPE_PACKED_COLOR_ARRAY: "PackedColorArray()", + GdObjects.TYPE_VARIANT: "null", + GdObjects.TYPE_ENUM: "0" +} + + +# @GlobalScript enums +# needs to manually map because of https://github.com/godotengine/godot/issues/73835 +const DEFAULT_ENUM_RETURN_VALUES = { + "Side" : "SIDE_LEFT", + "Corner" : "CORNER_TOP_LEFT", + "Orientation" : "HORIZONTAL", + "ClockDirection" : "CLOCKWISE", + "HorizontalAlignment" : "HORIZONTAL_ALIGNMENT_LEFT", + "VerticalAlignment" : "VERTICAL_ALIGNMENT_TOP", + "InlineAlignment" : "INLINE_ALIGNMENT_TOP_TO", + "EulerOrder" : "EULER_ORDER_XYZ", + "Key" : "KEY_NONE", + "KeyModifierMask" : "KEY_CODE_MASK", + "MouseButton" : "MOUSE_BUTTON_NONE", + "MouseButtonMask" : "MOUSE_BUTTON_MASK_LEFT", + "JoyButton" : "JOY_BUTTON_INVALID", + "JoyAxis" : "JOY_AXIS_INVALID", + "MIDIMessage" : "MIDI_MESSAGE_NONE", + "Error" : "OK", + "PropertyHint" : "PROPERTY_HINT_NONE", + "Variant.Type" : "TYPE_NIL", + "Vector2.Axis" : "Vector2.AXIS_X", + "Vector2i.Axis" : "Vector2i.AXIS_X", + "Vector3.Axis" : "Vector3.AXIS_X", + "Vector3i.Axis" : "Vector3i.AXIS_X", + "Vector4.Axis" : "Vector4.AXIS_X", + "Vector4i.Axis" : "Vector4i.AXIS_X", +} + + +static var def_constructor := """ + func _init({constructor_args}) -> void: + __init_doubler() + super({args}) + """.dedent() + + +static var def_verify_block := """ + # verify block + var __verifier := __get_verifier() + if __verifier != null: + if __verifier.is_verify_interactions(): + __verifier.verify_interactions("{func_name}", __args) + {default_return} + else: + __verifier.save_function_interaction("{func_name}", __args) + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_prepare_block := """ + if __is_prepare_return_value(): + __save_function_return_value("{func_name}", __args) + {default_return} + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_void_prepare_block := """ + if __is_prepare_return_value(): + push_error("Mocking functions with return type void is not allowed!") + return + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_mock_return := """ + if __is_do_not_call_real_func("{func_name}", __args): + return __return_mock_value("{func_name}", __args, {default_return}) + """.dedent().indent("\t").trim_suffix("\n") + + +static var def_void_mock_return := """ + if __is_do_not_call_real_func("{func_name}", __args): + return + """.dedent().indent("\t").trim_suffix("\n") + + +var fd: GdFunctionDescriptor +var func_args: Array +var default_return: String +var verify_block: String = "" +var prepare_block: String = "" +var mock_return: String = "" + + +func _init(descriptor: GdFunctionDescriptor) -> void: + # verify all default types are covered + for type_key in TYPE_MAX: + if not DEFAULT_TYPED_RETURN_VALUES.has(type_key): + push_error("missing default definitions! Expexting %d bud is %d" % [DEFAULT_TYPED_RETURN_VALUES.size(), TYPE_MAX]) + prints("missing default definition for type", type_key) + assert(DEFAULT_TYPED_RETURN_VALUES.has(type_key), "Missing Type default definition!") + + fd = descriptor + func_args = argument_names() + default_return = default_return_value() + + +func build_func_signature() -> String: + var return_type := ":" if fd._return_type == TYPE_VARIANT else " -> %s:" % fd.return_type_as_string() + return "{static}func {func_name}({args}){return_type}".format({ + "static" : "static " if fd.is_static() else "", + "func_name": fd.name(), + "args": arguments_full_quilified(), + "return_type": return_type + }) + + +func arguments_full_quilified() -> String: + var collect := PackedStringArray() + for arg in fd.args(): + var name := argument_name(arg) + if arg.has_default(): + var signature := "{argument_name}{arg_typed}={arg_value}".format({ + "argument_name" : name, + "arg_typed" : ":"+GdObjects.type_as_string(arg.type()) if arg.type() == GdObjects.TYPE_VARIANT else "", + "arg_value" : arg.value_as_string() + }) + collect.push_back(signature) + else: + collect.push_back(name) + if fd.is_vararg(): + var arg_descriptor := fd.varargs()[0] + collect.push_back("...%s_: Array" % arg_descriptor.name()) + return ", ".join(collect) + + +func argument_name(arg: GdFunctionArgument) -> String: + return arg.name() + "_" + + +func argument_names() -> PackedStringArray: + return fd.args().map(argument_name) + + +func argument_default(arg :GdFunctionArgument) -> String: + return (arg.value_as_string() + if arg.has_default() + else DEFAULT_TYPED_RETURN_VALUES.get(arg.type(), "null")) + + +func build_constructor_arguments() -> String: + var arguments := PackedStringArray() + for arg in fd.args(): + var default_value := argument_default(arg) + var arg_signature := "{name}:{type}={default}".format({ + "name" : argument_name(arg), + "type" : "Variant" if default_value == "null" else "", + "default" : default_value + }) + arguments.append(arg_signature) + if fd.is_vararg(): + arguments.append("...varargs: Array") + return ", ".join(arguments) + + +func build_arguments() -> String: + return "\tvar __args := [{args}]{varargs}".format({ + "args" : ", ".join(func_args), + "varargs" : " + varargs_" if fd.is_vararg() else "" + }) + + +func build_super_calls() -> String: + if !fd.is_vararg(): + return 'super(%s)\n' % ", ".join(func_args) + + var match_block := "match varargs_.size():\n" + for index in range(0, 11): + match_block += '{index}: super({args})\n'.format({ + "index" : index, + "args" : ", ".join(func_args + build_vararg_list(index)) + }).indent("\t") + match_block += '_: push_error("To many varradic arguments.")\n'.indent("\t") + match_block += "return\n" if is_void_func() else "return %s\n" % default_return + return match_block + + +func build_vararg_list(count: int) -> Array: + var arg_list := [] + for index in count: + arg_list.append("varargs_[%d]" % index) + return arg_list + + +func default_return_value() -> String: + var return_type: Variant = fd.return_type() + if return_type == GdObjects.TYPE_ENUM: + var enum_class := fd._return_class + if DEFAULT_ENUM_RETURN_VALUES.has(enum_class): + return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0") + + var enum_path := enum_class.split(".") + if enum_path.size() >= 2: + var keys := ClassDB.class_get_enum_constants(enum_path[0], enum_path[1]) + if not keys.is_empty(): + return "%s.%s" % [enum_path[0], keys[0]] + var enum_value: Variant = get_enum_default(enum_class) + if enum_value != null: + return str(enum_value) + # we need fallback for @GlobalScript enums, + return DEFAULT_ENUM_RETURN_VALUES.get(fd._return_class, "0") + return DEFAULT_TYPED_RETURN_VALUES.get(return_type, "invalid") + + +# Determine the enum default by reflection +func get_enum_default(value: String) -> Variant: + var script := GDScript.new() + script.source_code = """ + extends RefCounted + + static func get_enum_default() -> Variant: + return %s.values()[0] + + """.dedent() % value + var err := script.reload() + if err != OK: + push_error("Cant get enum values form '%s', %s" % [value, error_string(err)]) + return 0 + @warning_ignore("unsafe_method_access") + return script.new().call("get_enum_default") + + +func is_void_func() -> bool: + return fd.return_type() == TYPE_NIL or fd.return_type() == TYPE_VOID + + +func with_verify_block() -> GdUnitFunctionDoublerBuilder: + verify_block = def_verify_block.format({ + "func_name" : fd.name(), + "default_return" : "return" if is_void_func() else "return " + default_return + }) + return self + + +func with_prepare_block() -> GdUnitFunctionDoublerBuilder: + if fd.return_type() == TYPE_NIL or fd.return_type() == GdObjects.TYPE_VOID: + prepare_block = def_void_prepare_block + return self + + prepare_block = def_prepare_block.format({ + "func_name" : fd.name(), + "default_return" : "return" if is_void_func() else "return " + default_return + }) + return self + + +func with_mocked_return_value() -> GdUnitFunctionDoublerBuilder: + if is_void_func(): + mock_return = def_void_mock_return.format({ + "func_name" : fd.name(), + }) + else: + mock_return = def_mock_return.format({ + "func_name" : fd.name(), + "default_return" : '"no_arg"' if is_void_func() else default_return + }) + return self + + +func build() -> PackedStringArray: + if fd.name() == "_init": + return [def_constructor.format({ + "constructor_args" : build_constructor_arguments(), + "args" : ", ".join(func_args) + })] + + var func_body: PackedStringArray = [] + func_body.append(build_func_signature()) + func_body.append(build_arguments()) + if not prepare_block.is_empty(): + func_body.append(prepare_block) + func_body.append(verify_block) + if not mock_return.is_empty(): + func_body.append(mock_return) + func_body.append("") + var super_calls := build_super_calls() + if not is_void_func(): + super_calls = super_calls.replace("super(", "return super(" ) + if fd.is_coroutine(): + super_calls = super_calls.replace("super(", "await super(" ) + func_body.append(super_calls.indent("\t")) + return func_body diff --git a/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid new file mode 100644 index 00000000..76f41c81 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitFunctionDoublerBuilder.gd.uid @@ -0,0 +1 @@ +uid://d0gu2bl276yma diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd new file mode 100644 index 00000000..d735bc56 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd @@ -0,0 +1,10 @@ +class_name GdUnitMockFunctionDoubler +extends GdFunctionDoubler + + +func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray: + return GdUnitFunctionDoublerBuilder.new(func_descriptor)\ + .with_prepare_block()\ + .with_verify_block()\ + .with_mocked_return_value()\ + .build() diff --git a/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid new file mode 100644 index 00000000..213959b3 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitMockFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://dt5imoxi1ivhq diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd new file mode 100644 index 00000000..789eb327 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd @@ -0,0 +1,53 @@ +class_name GdUnitObjectInteractions +extends RefCounted + + +static func verify(interaction_object: Object, interactions_times: int) -> Variant: + if not _is_mock_or_spy(interaction_object): + return interaction_object + + _get_verifier(interaction_object).do_verify_interactions(interactions_times) + return interaction_object + + +static func verify_no_interactions(interaction_object: Object) -> GdUnitAssert: + var assert_tool := GdUnitAssertImpl.new("") + if not _is_mock_or_spy(interaction_object): + return assert_tool.report_success() + + var summary := _get_verifier(interaction_object).verify_no_interactions() + if summary.is_empty(): + return assert_tool.report_success() + return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary)) + + +static func verify_no_more_interactions(interaction_object: Object) -> GdUnitAssert: + var assert_tool := GdUnitAssertImpl.new("") + if not _is_mock_or_spy(interaction_object): + return assert_tool + + var summary := _get_verifier(interaction_object).verify_no_more_interactions() + if summary.is_empty(): + return assert_tool + return assert_tool.report_error(GdAssertMessages.error_no_more_interactions(summary)) + + +static func reset(interaction_object: Object) -> Object: + if not _is_mock_or_spy(interaction_object): + return interaction_object + + _get_verifier(interaction_object).reset_interactions() + return interaction_object + + +static func _is_mock_or_spy(instance: Object) -> bool: + if instance != null and instance.has_method("__get_verifier"): + return true + + push_error("Error: The given object '%s' is not a mock or spy instance!" % instance) + return false + + +static func _get_verifier(interaction_object: Object) -> GdUnitObjectInteractionsVerifier: + @warning_ignore("unsafe_method_access") + return interaction_object.__get_verifier() diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid new file mode 100644 index 00000000..c42f8be3 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd.uid @@ -0,0 +1 @@ +uid://dqnpoju1aekta diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd new file mode 100644 index 00000000..36aa9783 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd @@ -0,0 +1,84 @@ +class_name GdUnitObjectInteractionsVerifier + +var expected_interactions: int = -1 +var saved_interactions := Dictionary() +var verified_interactions := Array() + + +func save_function_interaction(func_name: String, args :Array[Variant]) -> void: + var function_args := [func_name] + args + var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for index in saved_interactions.keys().size(): + var key: Variant = saved_interactions.keys()[index] + if matcher.is_match(key): + saved_interactions[key] += 1 + return + saved_interactions[function_args] = 1 + + +func is_verify_interactions() -> bool: + return expected_interactions != -1 + + +func do_verify_interactions(interactions_times: int = 1) -> void: + expected_interactions = interactions_times + + +func verify_interactions(func_name: String, args: Array[Variant]) -> void: + var summary := Dictionary() + var total_interactions := 0 + var function_args := [func_name] + args + var matcher := GdUnitArgumentMatchers.to_matcher(function_args, true) + for index in saved_interactions.keys().size(): + var key: Variant = saved_interactions.keys()[index] + if matcher.is_match(key): + var interactions: int = saved_interactions.get(key, 0) + total_interactions += interactions + summary[key] = interactions + # add as verified + verified_interactions.append(key) + + var assert_tool := GdUnitAssertImpl.new("") + if total_interactions != expected_interactions: + var __expected_summary := {function_args : expected_interactions} + var error_message: String + # if no interactions macht collect not verified interactions for failure report + if summary.is_empty(): + var __current_summary := verify_no_more_interactions() + error_message = GdAssertMessages.error_validate_interactions(__current_summary, __expected_summary) + else: + error_message = GdAssertMessages.error_validate_interactions(summary, __expected_summary) + @warning_ignore("return_value_discarded") + assert_tool.report_error(error_message) + else: + @warning_ignore("return_value_discarded") + assert_tool.report_success() + expected_interactions = -1 + + +func verify_no_interactions() -> Dictionary: + var summary := Dictionary() + if not saved_interactions.is_empty(): + for index in saved_interactions.keys().size(): + var func_call: Variant = saved_interactions.keys()[index] + summary[func_call] = saved_interactions[func_call] + return summary + + +func verify_no_more_interactions() -> Dictionary: + var summary := Dictionary() + var called_functions: Array[Variant] = saved_interactions.keys() + if called_functions != verified_interactions: + # collect the not verified functions + var called_but_not_verified := called_functions.duplicate() + for index in verified_interactions.size(): + called_but_not_verified.erase(verified_interactions[index]) + + for index in called_but_not_verified.size(): + var not_verified: Variant = called_but_not_verified[index] + summary[not_verified] = saved_interactions[not_verified] + return summary + + +func reset_interactions() -> void: + saved_interactions.clear() diff --git a/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid new file mode 100644 index 00000000..e0a44b63 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitObjectInteractionsVerifier.gd.uid @@ -0,0 +1 @@ +uid://mx32fl26kcdv diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd new file mode 100644 index 00000000..091241da --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd @@ -0,0 +1,8 @@ +class_name GdUnitSpyFunctionDoubler +extends GdFunctionDoubler + + +func double(func_descriptor: GdFunctionDescriptor) -> PackedStringArray: + return GdUnitFunctionDoublerBuilder.new(func_descriptor)\ + .with_verify_block()\ + .build() diff --git a/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid new file mode 100644 index 00000000..ef88b2a0 --- /dev/null +++ b/addons/gdUnit4/src/doubler/GdUnitSpyFunctionDoubler.gd.uid @@ -0,0 +1 @@ +uid://bbyhhuy8113m2 diff --git a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid index 2631c661..578ce5f3 100644 --- a/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid +++ b/addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd.uid @@ -1 +1 @@ -uid://b27wsjlcch2bi +uid://cx4fl8vhtadto diff --git a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid index ac7344ce..fecc471b 100644 --- a/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/FloatFuzzer.gd.uid @@ -1 +1 @@ -uid://s7xumc3dus5e +uid://m0toqy7leklg diff --git a/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid index 8bece86b..ceeca79b 100644 --- a/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/Fuzzer.gd.uid @@ -1 +1 @@ -uid://d3ickae3nx13j +uid://br2qgptogixyk diff --git a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid index 39a8be89..d562334f 100644 --- a/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/IntFuzzer.gd.uid @@ -1 +1 @@ -uid://csehhhyca1ua8 +uid://dptsthe3tjyxa diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd index ca165d33..d0634949 100644 --- a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd @@ -2,28 +2,28 @@ class_name StringFuzzer extends Fuzzer -const DEFAULT_CHARSET = "a-zA-Z0-9+-_" +const DEFAULT_CHARSET = "\\w\\p{L}\\p{N}+-_'" -var _min_length :int -var _max_length :int -var _charset :PackedByteArray +var _min_length: int +var _max_length: int +var _charset: PackedInt32Array -func _init(min_length :int, max_length :int, pattern :String = DEFAULT_CHARSET) -> void: - assert(min_length>0 and min_length < max_length) - assert(not null or not pattern.is_empty()) +func _init(min_length: int, max_length: int, pattern: String = DEFAULT_CHARSET) -> void: _min_length = min_length - _max_length = max_length + _max_length = max_length + 1 # +1 for inclusive + assert(not null or not pattern.is_empty()) + assert(_min_length > 0 and _min_length < _max_length) _charset = StringFuzzer.extract_charset(pattern) -static func extract_charset(pattern :String) -> PackedByteArray: +static func extract_charset(pattern: String) -> PackedInt32Array: var reg := RegEx.new() if reg.compile(pattern) != OK: - push_error("Invalid pattern to generate Strings! Use e.g 'a-zA-Z0-9+-_'") - return PackedByteArray() + push_error("Invalid pattern to generate Strings! Use e.g '\\w\\p{L}\\p{N}+-_'") + return PackedInt32Array() - var charset := Array() + var charset := PackedInt32Array() var char_before := -1 var index := 0 while index < pattern.length(): @@ -45,21 +45,21 @@ static func extract_charset(pattern :String) -> PackedByteArray: continue char_before = char_current charset.append(char_current) - return PackedByteArray(charset) + return charset -static func build_chars(from :int, to :int) -> Array[int]: - var characters :Array[int] = [] +static func build_chars(from: int, to: int) -> PackedInt32Array: + var characters := PackedInt32Array() for character in range(from+1, to+1): characters.append(character) return characters func next_value() -> String: - var value := PackedByteArray() + var value := PackedInt32Array() var max_char := len(_charset) - var length :int = max(_min_length, randi() % _max_length) + var length: int = max(_min_length, randi() % _max_length) for i in length: @warning_ignore("return_value_discarded") value.append(_charset[randi() % max_char]) - return value.get_string_from_utf8() + return value.to_byte_array().get_string_from_utf32() diff --git a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid index 8149acc7..d19f488e 100644 --- a/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/StringFuzzer.gd.uid @@ -1 +1 @@ -uid://ce1beaiph5a0t +uid://djuhpoeijubuc diff --git a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid index b9925148..a30f72e2 100644 --- a/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/Vector2Fuzzer.gd.uid @@ -1 +1 @@ -uid://s1k2yktpdqjl +uid://d4f0tjvqrvv8d diff --git a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid index b9eefada..dd137b8c 100644 --- a/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid +++ b/addons/gdUnit4/src/fuzzers/Vector3Fuzzer.gd.uid @@ -1 +1 @@ -uid://dxoid7carbar6 +uid://dr8kd48a7dmgf diff --git a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid index 0ff2b3c2..e07334ab 100644 --- a/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/AnyArgumentMatcher.gd.uid @@ -1 +1 @@ -uid://cqiyhljh4po5i +uid://j3cf8pj7e2hx diff --git a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid index e69c14e9..52b9b2b6 100644 --- a/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/AnyBuildInTypeArgumentMatcher.gd.uid @@ -1 +1 @@ -uid://db87ltuc7iqtg +uid://dutfy1fdybu6g diff --git a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid index 401d44c2..5d3daaff 100644 --- a/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/AnyClazzArgumentMatcher.gd.uid @@ -1 +1 @@ -uid://bnkpo6wrugocw +uid://br5inj0c3yeka diff --git a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid index 87aea744..71f32a45 100644 --- a/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/ChainedArgumentMatcher.gd.uid @@ -1 +1 @@ -uid://cfrhqrwjb7xbn +uid://cqs4p1xlav5pq diff --git a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid index 55705c87..882a249a 100644 --- a/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/EqualsArgumentMatcher.gd.uid @@ -1 +1 @@ -uid://bvn0n5lcfg0kt +uid://rjupecr2iu8f diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd index aa43b80f..d5adc4bb 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd @@ -4,5 +4,10 @@ extends RefCounted @warning_ignore("unused_parameter") -func is_match(value :Variant) -> bool: +func is_match(value: Variant) -> bool: return true + + +func _to_string() -> String: + assert(false, "`_to_string()` Is not implemented!") + return "" diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid index 196768f4..5a05a8b9 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatcher.gd.uid @@ -1 +1 @@ -uid://whr18ncgthx +uid://bch01ucnnvm5 diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd index a40eacee..6a70cc15 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd @@ -4,9 +4,9 @@ extends RefCounted const TYPE_ANY = TYPE_MAX + 100 -static func to_matcher(arguments :Array[Variant], auto_deep_check_mode := false) -> ChainedArgumentMatcher: - var matchers :Array[Variant] = [] - for arg :Variant in arguments: +static func to_matcher(arguments: Array[Variant], auto_deep_check_mode := false) -> ChainedArgumentMatcher: + var matchers: Array[Variant] = [] + for arg: Variant in arguments: # argument is already a matcher if arg is GdUnitArgumentMatcher: matchers.append(arg) @@ -20,13 +20,23 @@ static func any() -> GdUnitArgumentMatcher: return AnyArgumentMatcher.new() -static func by_type(type :int) -> GdUnitArgumentMatcher: +static func by_type(type: int) -> GdUnitArgumentMatcher: return AnyBuildInTypeArgumentMatcher.new([type]) -static func by_types(types :PackedInt32Array) -> GdUnitArgumentMatcher: +static func by_types(types: PackedInt32Array) -> GdUnitArgumentMatcher: return AnyBuildInTypeArgumentMatcher.new(types) -static func any_class(clazz :Object) -> GdUnitArgumentMatcher: +static func any_class(clazz: Object) -> GdUnitArgumentMatcher: return AnyClazzArgumentMatcher.new(clazz) + + +static func is_variant_string_matching(value: Variant) -> GdUnitResult: + if value is String or value is StringName: + return GdUnitResult.success() + if value is GdUnitArgumentMatcher: + if str(value) == "any()" or str(value) == "any_string()": + return GdUnitResult.success() + return GdUnitResult.error("Only 'any()' and 'any_string()' argument matchers are allowed!") + return GdUnitResult.error("Only String or StringName types are allowed!") diff --git a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid index 36fa4d3c..18b987c1 100644 --- a/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid +++ b/addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd.uid @@ -1 +1 @@ -uid://bp6sj0q15upe6 +uid://knmpyvp6ugc5 diff --git a/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid index ec3591fb..6a24597a 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid +++ b/addons/gdUnit4/src/mocking/GdUnitMock.gd.uid @@ -1 +1 @@ -uid://j0yefbhyv2p4 +uid://6ypywv8vtlp8 diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd index bc26d657..537e9239 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd @@ -9,51 +9,57 @@ static func is_push_errors() -> bool: return GdUnitSettings.is_report_push_errors() -@warning_ignore("unsafe_method_access", "unsafe_cast") static func build(clazz :Variant, mock_mode :String, debug_write := false) -> Variant: var push_errors := is_push_errors() if not is_mockable(clazz, push_errors): return null # mocking a scene? if GdObjects.is_scene(clazz): - return mock_on_scene(clazz as PackedScene, debug_write) - elif typeof(clazz) == TYPE_STRING and clazz.ends_with(".tscn"): - return mock_on_scene(load(clazz as String) as PackedScene, debug_write) + var packed_scene: PackedScene = clazz + return mock_on_scene(packed_scene, debug_write) + elif typeof(clazz) == TYPE_STRING and str(clazz).ends_with(".tscn"): + var packed_scene: PackedScene = load(str(clazz)) + return mock_on_scene(packed_scene, debug_write) # mocking a script var instance := create_instance(clazz) + if instance == null: + push_error("Can't create instance of class %s" % clazz) var mock := mock_on_script(instance, clazz, [ "get_script"], debug_write) if not instance is RefCounted: instance.free() if mock == null: return null - var mock_instance: Variant = mock.new() - mock_instance.__set_script(mock) - mock_instance.__set_singleton() - mock_instance.__set_mode(mock_mode) + var mock_instance: Object = mock.new() + @warning_ignore("unsafe_method_access") + mock_instance.__init(mock, mock_mode) return register_auto_free(mock_instance) -@warning_ignore("unsafe_method_access", "unsafe_cast") static func create_instance(clazz: Variant) -> Object: - if typeof(clazz) == TYPE_OBJECT and (clazz as Object).is_class("GDScriptNativeClass"): - return clazz.new() - elif (clazz is GDScript) || (typeof(clazz) == TYPE_STRING and clazz.ends_with(".gd")): - var script: GDScript = null - if clazz is GDScript: - script = clazz - else: - script = load(clazz as String) - - var args := GdObjects.build_function_default_arguments(script, "_init") - return script.callv("new", args) - elif typeof(clazz) == TYPE_STRING and ClassDB.can_instantiate(clazz as String): - return ClassDB.instantiate(clazz as String) + match typeof(clazz): + TYPE_OBJECT: + var obj: Object = clazz + if clazz is GDScript: + var script: GDScript = clazz + var args := GdObjects.build_function_default_arguments(script, "_init") + return script.callv("new", args) + elif obj.is_class("GDScriptNativeClass"): + @warning_ignore("unsafe_method_access") + return obj.new() + TYPE_STRING: + var clazz_name: String = clazz + if clazz_name.ends_with(".gd"): + var script: GDScript = load(clazz_name) + var args := GdObjects.build_function_default_arguments(script, "_init") + return script.callv("new", args) + elif ClassDB.can_instantiate(clazz_name): + return ClassDB.instantiate(clazz_name) + push_error("Can't create a mock validation instance from class: `%s`" % clazz) return null -@warning_ignore("unsafe_method_access") -static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Variant: +static func mock_on_scene(scene: PackedScene, debug_write: bool) -> Variant: var push_errors := is_push_errors() if not scene.can_instantiate(): if push_errors: @@ -61,20 +67,21 @@ static func mock_on_scene(scene :PackedScene, debug_write :bool) -> Variant: return null var scene_instance := scene.instantiate() # we can only mock checked a scene with attached script - if scene_instance.get_script() == null: + var scene_script: Script = scene_instance.get_script() + if scene_script == null: if push_errors: push_error("Can't create a mockable instance for a scene without script '%s'" % scene.resource_path) @warning_ignore("return_value_discarded") GdUnitTools.free_instance(scene_instance) return null - var script_path :String = scene_instance.get_script().get_path() + var script_path := scene_script.get_path() var mock := mock_on_script(scene_instance, script_path, GdUnitClassDoubler.EXLCUDE_SCENE_FUNCTIONS, debug_write) if mock == null: return null scene_instance.set_script(mock) - scene_instance.__set_singleton() - scene_instance.__set_mode(GdUnitMock.CALL_REAL_FUNC) + @warning_ignore("unsafe_method_access") + scene_instance.__init(mock, GdUnitMock.CALL_REAL_FUNC) return register_auto_free(scene_instance) @@ -88,14 +95,20 @@ static func get_class_info(clazz :Variant) -> Dictionary: static func mock_on_script(instance :Object, clazz :Variant, function_excludes :PackedStringArray, debug_write :bool) -> GDScript: - var push_errors := is_push_errors() - var function_doubler := GdUnitMockFunctionDoubler.new(push_errors) + var function_doubler := GdUnitMockFunctionDoubler.new() var class_info := get_class_info(clazz) - var lines := load_template(MOCK_TEMPLATE.source_code, class_info, instance) - var clazz_name :String = class_info.get("class_name") var clazz_path :PackedStringArray = class_info.get("class_path", [clazz_name]) + var mock_template := MOCK_TEMPLATE.source_code.format({ + "instance_id" : abs(instance.get_instance_id()), + "gdunit_source_class": clazz_name if clazz_path.is_empty() else clazz_path[0] + }) + var lines := load_template(mock_template, class_info) lines += double_functions(instance, clazz_name, clazz_path, function_doubler, function_excludes) + # We disable warning/errors for inferred_declaration + if Engine.get_version_info().hex >= 0x40400: + lines.insert(0, '@warning_ignore_start("inferred_declaration")') + lines.append('@warning_ignore_restore("inferred_declaration")') var mock := GDScript.new() mock.source_code = "\n".join(lines) diff --git a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid index c4c3a8c9..3b5f4856 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid +++ b/addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd.uid @@ -1 +1 @@ -uid://dr2haljdfj0s5 +uid://devdv6fxsu4cm diff --git a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd deleted file mode 100644 index f7464210..00000000 --- a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd +++ /dev/null @@ -1,86 +0,0 @@ -class_name GdUnitMockFunctionDoubler -extends GdFunctionDoubler - - -const TEMPLATE_FUNC_WITH_RETURN_VALUE = """ - var args__: Array = ["$(func_name)", $(arguments)] - - if $(instance)__is_prepare_return_value(): - $(instance)__save_function_return_value(args__) - return ${default_return_value} - if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args__) - return ${default_return_value} - else: - $(instance)__save_function_interaction(args__) - - if $(instance)__do_call_real_func("$(func_name)", args__): - return $(await)super($(arguments)) - return $(instance)__get_mocked_return_value_or_default(args__, ${default_return_value}) - -""" - - -const TEMPLATE_FUNC_WITH_RETURN_VOID = """ - var args__: Array = ["$(func_name)", $(arguments)] - - if $(instance)__is_prepare_return_value(): - if $(push_errors): - push_error(\"Mocking a void function '$(func_name)() -> void:' is not allowed.\") - return - if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args__) - return - else: - $(instance)__save_function_interaction(args__) - - if $(instance)__do_call_real_func("$(func_name)"): - $(await)super($(arguments)) - -""" - - -const TEMPLATE_FUNC_VARARG_RETURN_VALUE = """ - var varargs__: Array = __filter_vargs([$(varargs)]) - var args__: Array = ["$(func_name)", $(arguments)] + varargs__ - - if $(instance)__is_prepare_return_value(): - if $(push_errors): - push_error(\"Mocking a void function '$(func_name)() -> void:' is not allowed.\") - $(instance)__save_function_return_value(args__) - return ${default_return_value} - if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args__) - return ${default_return_value} - else: - $(instance)__save_function_interaction(args__) - - if $(instance)__do_call_real_func("$(func_name)", args__): - match varargs__.size(): - 0: return $(await)super($(arguments)) - 1: return $(await)super($(arguments), varargs__[0]) - 2: return $(await)super($(arguments), varargs__[0], varargs__[1]) - 3: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2]) - 4: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3]) - 5: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4]) - 6: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5]) - 7: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6]) - 8: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6], varargs__[7]) - 9: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6], varargs__[7], varargs__[8]) - 10: return $(await)super($(arguments), varargs__[0], varargs__[1], varargs__[2], varargs__[3], varargs__[4], varargs__[5], varargs__[6], varargs__[7], varargs__[8], varargs__[9]) - return __get_mocked_return_value_or_default(args__, ${default_return_value}) - -""" - - -func _init(push_errors :bool = false) -> void: - super._init(push_errors) - - -func get_template(fd: GdFunctionDescriptor, _is_callable: bool) -> String: - if fd.is_vararg(): - return TEMPLATE_FUNC_VARARG_RETURN_VALUE - var return_type :Variant = fd.return_type() - if return_type is StringName: - return TEMPLATE_FUNC_WITH_RETURN_VALUE - return TEMPLATE_FUNC_WITH_RETURN_VOID if (return_type == TYPE_NIL or return_type == GdObjects.TYPE_VOID) else TEMPLATE_FUNC_WITH_RETURN_VALUE diff --git a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid deleted file mode 100644 index ad8edccc..00000000 --- a/addons/gdUnit4/src/mocking/GdUnitMockFunctionDoubler.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://br6nddh00hj7f diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd index de43da98..774ca3e3 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd @@ -1,64 +1,74 @@ +class_name DoubledMockClassSourceClassName ################################################################################ # internal mocking stuff ################################################################################ -const __INSTANCE_ID = "${instance_id}" -const __SOURCE_CLASS = "${source_class}" -var __mock_working_mode := GdUnitMock.RETURN_DEFAULTS -var __excluded_methods :PackedStringArray = [] -var __do_return_value :Variant = null -var __prepare_return_value := false +const __INSTANCE_ID := "gdunit_doubler_instance_id_{instance_id}" -#{ = { -# = -# } -#} -var __mocked_return_values := Dictionary() +class GdUnitMockDoublerState: + const __SOURCE_CLASS := "{gdunit_source_class}" -static func __instance() -> Object: - return Engine.get_meta(__INSTANCE_ID) + var excluded_methods := PackedStringArray() + var working_mode := GdUnitMock.RETURN_DEFAULTS + var is_prepare_return := false + var return_values := Dictionary() + var return_value: Variant = null -func _notification(what :int) -> void: - if what == NOTIFICATION_PREDELETE: - if Engine.has_meta(__INSTANCE_ID): - Engine.remove_meta(__INSTANCE_ID) + func _init(working_mode_ := GdUnitMock.RETURN_DEFAULTS) -> void: + working_mode = working_mode_ -func __instance_id() -> String: - return __INSTANCE_ID +var __mock_state := GdUnitMockDoublerState.new() +@warning_ignore("unused_private_class_variable") +var __verifier_instance := GdUnitObjectInteractionsVerifier.new() -func __set_singleton() -> void: - # store self need to mock static functions +func __init(__script: GDScript, mock_working_mode: String) -> void: + super.set_script(__script) + __init_doubler() + __mock_state.working_mode = mock_working_mode + + +static func __doubler_state() -> GdUnitMockDoublerState: + if Engine.has_meta(__INSTANCE_ID): + return Engine.get_meta(__INSTANCE_ID).__mock_state + return null + + +func __init_doubler() -> void: Engine.set_meta(__INSTANCE_ID, self) -func __release_double() -> void: - # we need to release the self reference manually to prevent orphan nodes - Engine.remove_meta(__INSTANCE_ID) +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE and Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) + + +static func __get_verifier() -> GdUnitObjectInteractionsVerifier: + return Engine.get_meta(__INSTANCE_ID).__verifier_instance -func __is_prepare_return_value() -> bool: - return __prepare_return_value +static func __is_prepare_return_value() -> bool: + return __doubler_state().is_prepare_return -func __sort_by_argument_matcher(__left_args :Array, __right_args :Array) -> bool: +static func __sort_by_argument_matcher(__left_args: Array, __right_args: Array) -> bool: for __index in __left_args.size(): - var __larg :Variant = __left_args[__index] + var __larg: Variant = __left_args[__index] if __larg is GdUnitArgumentMatcher: return false return true # we need to sort by matcher arguments so that they are all at the end of the list -func __sort_dictionary(__unsorted_args :Dictionary) -> Dictionary: +static func __sort_dictionary(__unsorted_args: Dictionary) -> Dictionary: # only need to sort if contains more than one entry if __unsorted_args.size() <= 1: return __unsorted_args - var __sorted_args := __unsorted_args.keys() + var __sorted_args: Array = __unsorted_args.keys() __sorted_args.sort_custom(__sort_by_argument_matcher) var __sorted_result := {} for __index in __sorted_args.size(): @@ -67,28 +77,28 @@ func __sort_dictionary(__unsorted_args :Dictionary) -> Dictionary: return __sorted_result -func __save_function_return_value(__fuction_args :Array) -> void: - var __func_name :String = __fuction_args[0] - var __func_args :Array = __fuction_args.slice(1) - var __mocked_return_value_by_args :Dictionary = __mocked_return_values.get(__func_name, {}) - __mocked_return_value_by_args[__func_args] = __do_return_value - __mocked_return_values[__func_name] = __sort_dictionary(__mocked_return_value_by_args) - __do_return_value = null - __prepare_return_value = false +static func __save_function_return_value(__func_name: String, __func_args: Array) -> void: + var doubler_state := __doubler_state() + var mocked_return_value_by_args: Dictionary = doubler_state.return_values.get(__func_name, {}) + mocked_return_value_by_args[__func_args] = doubler_state.return_value + doubler_state.return_values[__func_name] = __sort_dictionary(mocked_return_value_by_args) + doubler_state.return_value = null + doubler_state.is_prepare_return = false -@warning_ignore("unsafe_method_access") -func __is_mocked_args_match(__func_args :Array, __mocked_args :Array) -> bool: + +static func __is_mocked_args_match(__func_args: Array, __mocked_args: Array) -> bool: var __is_matching := false for __index in __mocked_args.size(): - var __fuction_args :Variant = __mocked_args[__index] + var __fuction_args: Array = __mocked_args[__index] if __func_args.size() != __fuction_args.size(): continue __is_matching = true for __arg_index in __func_args.size(): - var __func_arg :Variant = __func_args[__arg_index] - var __mock_arg :Variant = __fuction_args[__arg_index] + var __func_arg: Variant = __func_args[__arg_index] + var __mock_arg: Variant = __fuction_args[__arg_index] if __mock_arg is GdUnitArgumentMatcher: + @warning_ignore("unsafe_method_access") __is_matching = __is_matching and __mock_arg.is_match(__func_arg) else: __is_matching = __is_matching and typeof(__func_arg) == typeof(__mock_arg) and __func_arg == __mock_arg @@ -99,45 +109,35 @@ func __is_mocked_args_match(__func_args :Array, __mocked_args :Array) -> bool: return __is_matching -@warning_ignore("unsafe_method_access") -func __get_mocked_return_value_or_default(__fuction_args :Array, __default_return_value :Variant) -> Variant: - var __func_name :String = __fuction_args[0] - if not __mocked_return_values.has(__func_name): +static func __return_mock_value(__func_name: String, __func_args: Array, __default_return_value: Variant) -> Variant: + var doubler_state := __doubler_state() + if not doubler_state.return_values.has(__func_name): return __default_return_value - var __func_args :Array = __fuction_args.slice(1) - var __mocked_args :Array = __mocked_return_values.get(__func_name).keys() + @warning_ignore("unsafe_method_access") + var __mocked_args: Array = doubler_state.return_values.get(__func_name).keys() for __index in __mocked_args.size(): - var __margs :Variant = __mocked_args[__index] + var __margs: Variant = __mocked_args[__index] if __is_mocked_args_match(__func_args, [__margs]): - return __mocked_return_values[__func_name][__margs] + return doubler_state.return_values[__func_name][__margs] return __default_return_value -func __set_script(__script :GDScript) -> void: - super.set_script(__script) - - -func __set_mode(mock_working_mode :String) -> Object: - __mock_working_mode = mock_working_mode - return self - - -@warning_ignore("unsafe_method_access") -func __do_call_real_func(__func_name :String, __func_args := []) -> bool: - var __is_call_real_func := __mock_working_mode == GdUnitMock.CALL_REAL_FUNC and not __excluded_methods.has(__func_name) +static func __is_do_not_call_real_func(__func_name: String, __func_args := []) -> bool: + var doubler_state := __doubler_state() + var __is_call_real_func: bool = doubler_state.working_mode == GdUnitMock.CALL_REAL_FUNC and not doubler_state.excluded_methods.has(__func_name) # do not call real funcions for mocked functions - if __is_call_real_func and __mocked_return_values.has(__func_name): - var __fuction_args :Array = __func_args.slice(1) - var __mocked_args :Array = __mocked_return_values.get(__func_name).keys() - return not __is_mocked_args_match(__fuction_args, __mocked_args) - return __is_call_real_func + if __is_call_real_func and doubler_state.return_values.has(__func_name): + @warning_ignore("unsafe_method_access") + var __mocked_args: Array = doubler_state.return_values.get(__func_name).keys() + return __is_mocked_args_match(__func_args, __mocked_args) + return !__is_call_real_func -func __exclude_method_call(exluded_methods :PackedStringArray) -> void: - __excluded_methods.append_array(exluded_methods) +func __exclude_method_call(exluded_methods: PackedStringArray) -> void: + __doubler_state().excluded_methods.append_array(exluded_methods) -func __do_return(mock_do_return_value :Variant) -> Object: - __do_return_value = mock_do_return_value - __prepare_return_value = true +func __do_return(mock_do_return_value: Variant) -> Object: + __doubler_state().return_value = mock_do_return_value + __doubler_state().is_prepare_return = true return self diff --git a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid index 5e468af4..01f3c33a 100644 --- a/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid +++ b/addons/gdUnit4/src/mocking/GdUnitMockImpl.gd.uid @@ -1 +1 @@ -uid://bp58ahqt2tnpb +uid://dyeukucg26ye diff --git a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid index 7c9af210..9113b5d3 100644 --- a/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid +++ b/addons/gdUnit4/src/monitor/ErrorLogEntry.gd.uid @@ -1 +1 @@ -uid://kmqt45xu3sie +uid://dc01q4bwjrlbj diff --git a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid index 580af924..fdb90719 100644 --- a/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid +++ b/addons/gdUnit4/src/monitor/GdUnitMonitor.gd.uid @@ -1 +1 @@ -uid://bv75e8nc5qd1s +uid://ith4leeqnc2e diff --git a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid index f3434c55..3a4c14a9 100644 --- a/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid +++ b/addons/gdUnit4/src/monitor/GdUnitOrphanNodesMonitor.gd.uid @@ -1 +1 @@ -uid://b7c0vvfp3likq +uid://bwi5ola574pol diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd index 2ea2234e..16db4582 100644 --- a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd @@ -52,8 +52,25 @@ func erase_log_entry(entry: ErrorLogEntry) -> void: _entries.erase(entry) +func collect_full_logs() -> PackedStringArray: + await (Engine.get_main_loop() as SceneTree).process_frame + await (Engine.get_main_loop() as SceneTree).physics_frame + + var file := FileAccess.open(_godot_log_file, FileAccess.READ) + file.seek(_eof) + var records := PackedStringArray() + while not file.eof_reached(): + @warning_ignore("return_value_discarded") + records.append(file.get_line()) + + return records + + func _collect_log_entries(force_collect_reports: bool) -> Array[ErrorLogEntry]: var file := FileAccess.open(_godot_log_file, FileAccess.READ) + if not file: + # Log file might not be available. + return [] file.seek(_eof) var records := PackedStringArray() while not file.eof_reached(): diff --git a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid index b13a8e25..e501bf48 100644 --- a/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid +++ b/addons/gdUnit4/src/monitor/GodotGdErrorMonitor.gd.uid @@ -1 +1 @@ -uid://buayb5ysixad0 +uid://ddu7re3nhfovr diff --git a/addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs b/addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs deleted file mode 100644 index 557cc01a..00000000 --- a/addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Reflection; -using System.Linq; - -using Godot; -using Godot.Collections; -using GdUnit4; - - -// GdUnit4 GDScript - C# API wrapper -public partial class GdUnit4CSharpApi : Godot.GodotObject -{ - private static Type? apiType; - - private static Type GetApiType() - { - if (apiType == null) - { - var assembly = Assembly.Load("gdUnit4Api"); - apiType = GdUnit4NetVersion() < new Version(4, 2, 2) ? - assembly.GetType("GdUnit4.GdUnit4MonoAPI") : - assembly.GetType("GdUnit4.GdUnit4NetAPI"); - Godot.GD.PrintS($"GdUnit4CSharpApi type:{apiType} loaded."); - } - return apiType!; - } - - private static Version GdUnit4NetVersion() - { - var assembly = Assembly.Load("gdUnit4Api"); - return assembly.GetName().Version!; - } - - private static T InvokeApiMethod(string methodName, params object[] args) - { - var method = GetApiType().GetMethod(methodName)!; - return (T)method.Invoke(null, args)!; - } - - public static string Version() => GdUnit4NetVersion().ToString(); - - public static bool IsTestSuite(string classPath) => InvokeApiMethod("IsTestSuite", classPath); - - public static RefCounted Executor(Node listener) => InvokeApiMethod("Executor", listener); - - public static CsNode? ParseTestSuite(string classPath) => InvokeApiMethod("ParseTestSuite", classPath); - - public static Dictionary CreateTestSuite(string sourcePath, int lineNumber, string testSuitePath) => - InvokeApiMethod("CreateTestSuite", sourcePath, lineNumber, testSuitePath); -} diff --git a/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd b/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd deleted file mode 100644 index d6c815c8..00000000 --- a/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd +++ /dev/null @@ -1,70 +0,0 @@ -extends RefCounted -class_name GdUnit4CSharpApiLoader - - -static func instance() -> Object: - return GdUnitSingleton.instance("GdUnit4CSharpApi", func() -> Object: - if not GdUnit4CSharpApiLoader.is_mono_supported(): - return null - @warning_ignore("unsafe_method_access") - return load("res://addons/gdUnit4/src/mono/GdUnit4CSharpApi.cs").new() - ) - - -static func is_engine_version_supported(engine_version :int = Engine.get_version_info().hex) -> bool: - return engine_version >= 0x40200 - - -# test is Godot mono running -static func is_mono_supported() -> bool: - return ClassDB.class_exists("CSharpScript") and is_engine_version_supported() - - -static func version() -> String: - if not GdUnit4CSharpApiLoader.is_mono_supported(): - return "unknown" - @warning_ignore("unsafe_method_access") - return instance().Version() - - -static func create_test_suite(source_path :String, line_number :int, test_suite_path :String) -> GdUnitResult: - if not GdUnit4CSharpApiLoader.is_mono_supported(): - return GdUnitResult.error("Can't create test suite. No C# support found.") - @warning_ignore("unsafe_method_access") - var result: Dictionary = instance().CreateTestSuite(source_path, line_number, test_suite_path) - if result.has("error"): - return GdUnitResult.error(str(result.get("error"))) - return GdUnitResult.success(result) - - -static func is_test_suite(resource_path :String) -> bool: - if not is_csharp_file(resource_path) or not GdUnit4CSharpApiLoader.is_mono_supported(): - return false - - if resource_path.is_empty(): - if GdUnitSettings.is_report_push_errors(): - push_error("Can't create test suite. Missing resource path.") - return false - @warning_ignore("unsafe_method_access") - return instance().IsTestSuite(resource_path) - - -static func parse_test_suite(source_path :String) -> Node: - if not GdUnit4CSharpApiLoader.is_mono_supported(): - if GdUnitSettings.is_report_push_errors(): - push_error("Can't create test suite. No c# support found.") - return null - @warning_ignore("unsafe_method_access") - return instance().ParseTestSuite(source_path) - - -static func create_executor(listener :Node) -> RefCounted: - if not GdUnit4CSharpApiLoader.is_mono_supported(): - return null - @warning_ignore("unsafe_method_access") - return instance().Executor(listener) - - -static func is_csharp_file(resource_path :String) -> bool: - var ext := resource_path.get_extension() - return ext == "cs" and GdUnit4CSharpApiLoader.is_mono_supported() diff --git a/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd.uid b/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd.uid deleted file mode 100644 index 042bbcbd..00000000 --- a/addons/gdUnit4/src/mono/GdUnit4CSharpApiLoader.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://fuv4d1k0mkyc diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd b/addons/gdUnit4/src/network/GdUnitServer.gd index ea638c51..6d878a01 100644 --- a/addons/gdUnit4/src/network/GdUnitServer.gd +++ b/addons/gdUnit4/src/network/GdUnitServer.gd @@ -31,12 +31,12 @@ func _on_gdunit_runner_stop(client_id: int) -> void: _server.disconnect_client(client_id) -func _receive_rpc_data(p_rpc: Variant) -> void: +func _receive_rpc_data(p_rpc: RPC) -> void: if p_rpc is RPCMessage: - GdUnitSignals.instance().gdunit_message.emit(p_rpc.message()) + var rpc_message: RPCMessage = p_rpc + GdUnitSignals.instance().gdunit_message.emit(rpc_message.message()) return if p_rpc is RPCGdUnitEvent: - GdUnitSignals.instance().gdunit_event.emit(p_rpc.event()) + var rpc_event: RPCGdUnitEvent = p_rpc + GdUnitSignals.instance().gdunit_event.emit(rpc_event.event()) return - if p_rpc is RPCGdUnitTestSuite: - GdUnitSignals.instance().gdunit_add_test_suite.emit(p_rpc.dto()) diff --git a/addons/gdUnit4/src/network/GdUnitServer.gd.uid b/addons/gdUnit4/src/network/GdUnitServer.gd.uid index 7d97d567..61029602 100644 --- a/addons/gdUnit4/src/network/GdUnitServer.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitServer.gd.uid @@ -1 +1 @@ -uid://bpvjnrbi863rp +uid://bou8b3qboxmmw diff --git a/addons/gdUnit4/src/network/GdUnitServer.tscn b/addons/gdUnit4/src/network/GdUnitServer.tscn index 7058bbf2..4dbe8c49 100644 --- a/addons/gdUnit4/src/network/GdUnitServer.tscn +++ b/addons/gdUnit4/src/network/GdUnitServer.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=3 format=3 uid="uid://cn5mp3tmi2gb1"] -[ext_resource type="Script" uid="uid://bpvjnrbi863rp" path="res://addons/gdUnit4/src/network/GdUnitServer.gd" id="1"] -[ext_resource type="Script" uid="uid://cvwx65s7t1d8c" path="res://addons/gdUnit4/src/network/GdUnitTcpServer.gd" id="2"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitServer.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/network/GdUnitTcpServer.gd" id="2"] [node name="Control" type="Node"] script = ExtResource("1") diff --git a/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid b/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid index 325df57a..95037212 100644 --- a/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitServerConstants.gd.uid @@ -1 +1 @@ -uid://otwwesb4g30i +uid://dvt8r46i4soe3 diff --git a/addons/gdUnit4/src/network/GdUnitTask.gd.uid b/addons/gdUnit4/src/network/GdUnitTask.gd.uid index ba297989..4e3c3bd5 100644 --- a/addons/gdUnit4/src/network/GdUnitTask.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitTask.gd.uid @@ -1 +1 @@ -uid://daw8hm8lcaw6h +uid://cphown6d22alc diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd b/addons/gdUnit4/src/network/GdUnitTcpClient.gd index e48145eb..2cf32a37 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpClient.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd @@ -1,33 +1,40 @@ class_name GdUnitTcpClient -extends Node +extends GdUnitTcpNode -signal connection_succeeded(message :String) -signal connection_failed(message :String) +signal connection_succeeded(message: String) +signal connection_failed(message: String) -var _host :String -var _port :int -var _client_id :int -var _connected :bool -var _stream :StreamPeerTCP +var _client_name: String +var _debug := false +var _host: String +var _port: int +var _client_id: int +var _connected: bool +var _stream: StreamPeerTCP + + +func _init(client_name := "GdUnit4 TCP Client", debug := false) -> void: + _client_name = client_name + _debug = debug func _ready() -> void: _connected = false _stream = StreamPeerTCP.new() - _stream.set_big_endian(true) + #_stream.set_big_endian(true) func stop() -> void: - console("Client: disconnect from server") + console("Disconnecting from server") if _stream != null: - rpc_send(RPCClientDisconnect.new().with_id(_client_id)) + rpc_send(_stream, RPCClientDisconnect.new().with_id(_client_id)) if _stream != null: _stream.disconnect_from_host() _connected = false -func start(host :String, port :int) -> GdUnitResult: +func start(host: String, port: int) -> GdUnitResult: _host = host _port = port if _connected: @@ -42,7 +49,7 @@ func start(host :String, port :int) -> GdUnitResult: return GdUnitResult.success("GdUnit4: Client connected checked port %d" % port) -func _process(_delta :float) -> void: +func _process(_delta: float) -> void: match _stream.get_status(): StreamPeerTCP.STATUS_NONE: return @@ -53,7 +60,7 @@ func _process(_delta :float) -> void: for retry in 10: @warning_ignore("return_value_discarded") _stream.poll() - console("wait to connect ..") + console("Waiting to connect ..") if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTING: await get_tree().create_timer(0.500).timeout if _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED: @@ -61,25 +68,25 @@ func _process(_delta :float) -> void: return set_process(true) _stream.disconnect_from_host() - console("connection failed") + console("Connection failed") connection_failed.emit("Connect to TCP Server %s:%d faild!" % [_host, _port]) StreamPeerTCP.STATUS_CONNECTED: if not _connected: - var rpc_ :RPC = null + var rpc_data :RPC = null set_process(false) - while rpc_ == null: + while rpc_data == null: await get_tree().create_timer(0.500).timeout - rpc_ = rpc_receive() + rpc_data = rpc_receive() set_process(true) - _client_id = (rpc_ as RPCClientConnect).client_id() + _client_id = (rpc_data as RPCClientConnect).client_id() console("Connected to Server: %d" % _client_id) connection_succeeded.emit("Connect to TCP Server %s:%d success." % [_host, _port]) _connected = true process_rpc() StreamPeerTCP.STATUS_ERROR: - console("connection failed") + console("Connection failed") _stream.disconnect_from_host() connection_failed.emit("Connect to TCP Server %s:%d faild!" % [_host, _port]) return @@ -91,44 +98,27 @@ func is_client_connected() -> bool: func process_rpc() -> void: if _stream.get_available_bytes() > 0: - var rpc_ := rpc_receive() - if rpc_ is RPCClientDisconnect: + var rpc_data := rpc_receive() + if rpc_data is RPCClientDisconnect: stop() -func rpc_send(p_rpc :RPC) -> void: - if _stream != null: - var data := GdUnitServerConstants.JSON_RESPONSE_DELIMITER + p_rpc.serialize() + GdUnitServerConstants.JSON_RESPONSE_DELIMITER - @warning_ignore("return_value_discarded") - _stream.put_data(data.to_utf8_buffer()) +func send(data: RPC) -> void: + rpc_send(_stream, data) func rpc_receive() -> RPC: - if _stream != null: - while _stream.get_available_bytes() > 0: - var available_bytes := _stream.get_available_bytes() - var data := _stream.get_data(available_bytes) - var received_data: PackedByteArray = data[1] - # data send by Godot has this magic header of 12 bytes - var header := Array(received_data.slice(0, 4)) - if header == [0, 0, 0, 124]: - received_data = received_data.slice(12, available_bytes) - var decoded := received_data.get_string_from_utf8() - if decoded == "": - #prints("decoded is empty", available_bytes, received_data.get_string_from_utf8()) - return null - return RPC.deserialize(decoded) - return null + return receive_packages(_stream).front() -func console(_message :String) -> void: - #prints("TCP Client:", _message) - pass +func console(value: Variant) -> void: + if _debug: + print(_client_name, ": ", value) -func _on_connection_failed(message :String) -> void: - console("connection faild: " + message) +func _on_connection_failed(message: String) -> void: + console("Connection faild by: " + message) -func _on_connection_succeeded(message :String) -> void: - console("connected: " + message) +func _on_connection_succeeded(message: String) -> void: + console("Connected: " + message) diff --git a/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid index 210d9e8a..ba4d61a9 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitTcpClient.gd.uid @@ -1 +1 @@ -uid://cra3gsh4gftcw +uid://cghxfus00xqqa diff --git a/addons/gdUnit4/src/network/GdUnitTcpNode.gd b/addons/gdUnit4/src/network/GdUnitTcpNode.gd new file mode 100644 index 00000000..156c764b --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpNode.gd @@ -0,0 +1,73 @@ +class_name GdUnitTcpNode +extends Node + + +func rpc_send(stream: StreamPeerTCP, data: RPC) -> void: + var package_buffer := StreamPeerBuffer.new() + var buffer := data.serialize().to_utf16_buffer() + package_buffer.put_u32(0xDEADBEEF) + package_buffer.put_u32(buffer.size()) + var status_code := package_buffer.put_data(buffer) + if status_code != OK: + push_error("'rpc_send:' Can't put_data(), error: %s" % error_string(status_code)) + return + stream.put_data(package_buffer.data_array) + + +func receive_packages(stream: StreamPeerTCP, rpc_cb: Callable = noop) -> Array[RPC]: + var received_packages: Array[RPC] = [] + var package_buffer := StreamPeerBuffer.new() + if stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return received_packages + + while stream.get_status() == StreamPeerTCP.STATUS_CONNECTED and stream.get_available_bytes() > 0: + var buffer := stream.get_data(8) + var status_code: int = buffer[0] + if status_code != OK: + push_error("'receive_packages:' Can't get_data(%d) for available_bytes, error: %s" + % [stream.get_available_bytes(), error_string(status_code)]) + return received_packages + + var data_package: PackedByteArray + package_buffer.data_array = buffer[1] + package_buffer.seek(0) + + if package_buffer.get_u32() == 0xDEADBEEF: + var size := package_buffer.get_u32() + if stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: + return received_packages + if stream.get_available_bytes() < size: + prints("size check:", + package_buffer.get_size(), ":", + package_buffer.get_position(), + "to read:", + size, + "available size:", + stream.get_available_bytes()) + push_error("'receive_packages:' Can't receive data get_data(%d) for package, error: %s" % [size, error_string(status_code)]) + return received_packages + + buffer = stream.get_data(size) + package_buffer.data_array = buffer[1] + + var rpc_data := package_buffer.get_data(size) + status_code = rpc_data[0] + if status_code != OK: + push_error("'receive_packages:' Can't get_data(%d) for package, error: %s" % [size, error_string(status_code)]) + continue + data_package = rpc_data[1] + else: + data_package = buffer[1] + + var json := data_package.get_string_from_utf16() + if json.is_empty(): + push_warning("json is empty, can't process data") + continue + var data := RPC.deserialize(json) + received_packages.append(data) + rpc_cb.call(data) + return received_packages + + +static func noop(_rpc_data: RPC) -> void: + pass diff --git a/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid new file mode 100644 index 00000000..8d4d4867 --- /dev/null +++ b/addons/gdUnit4/src/network/GdUnitTcpNode.gd.uid @@ -0,0 +1 @@ +uid://oi0ifevw4c4y diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd b/addons/gdUnit4/src/network/GdUnitTcpServer.gd index d681de9d..4687ac19 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd @@ -2,29 +2,24 @@ class_name GdUnitTcpServer extends Node -signal client_connected(client_id :int) -signal client_disconnected(client_id :int) +signal client_connected(client_id: int) +signal client_disconnected(client_id: int) @warning_ignore("unused_signal") signal rpc_data(rpc_data: RPC) -var _server :TCPServer +var _server: TCPServer +var _server_name: String +class TcpConnection extends GdUnitTcpNode: + var _id: int + var _stream: StreamPeerTCP -class TcpConnection extends Node: - var _id :int - # we do use untyped here because we using a mock for testing and the static type is break the mock - @warning_ignore("untyped_declaration") - var _stream - var _readBuffer :String = "" - - @warning_ignore("unsafe_method_access") - func _init(p_server :Variant) -> void: - assert(p_server is TCPServer) - _stream = p_server.take_connection() - _stream.set_big_endian(true) + func _init(tcp_server: TCPServer) -> void: + _stream = tcp_server.take_connection() + #_stream.set_big_endian(true) _id = _stream.get_instance_id() - rpc_send(RPCClientConnect.new().with_id(_id)) + rpc_send(_stream, RPCClientConnect.new().with_id(_id)) func _ready() -> void: @@ -32,11 +27,8 @@ class TcpConnection extends Node: func close() -> void: - if _stream != null: - @warning_ignore("unsafe_method_access") + if _stream != null and _stream.get_status() == StreamPeerTCP.STATUS_CONNECTED: _stream.disconnect_from_host() - _readBuffer = "" - _stream = null queue_free() @@ -48,60 +40,26 @@ class TcpConnection extends Node: return get_parent() - func rpc_send(p_rpc: RPC) -> void: - @warning_ignore("unsafe_method_access") - _stream.put_var(p_rpc.serialize(), true) - - func _process(_delta: float) -> void: - @warning_ignore("unsafe_method_access") if _stream == null or _stream.get_status() != StreamPeerTCP.STATUS_CONNECTED: return - receive_packages() - - - @warning_ignore("unsafe_method_access") - func receive_packages() -> void: - var available_bytes :int = _stream.get_available_bytes() - if available_bytes > 0: - var partial_data :Array = _stream.get_partial_data(available_bytes) - # Check for read error. - if partial_data[0] != OK: - push_error("Error getting data from stream: %s " % partial_data[0]) - return - else: - var received_data: PackedByteArray = partial_data[1] - for package in _read_next_data_packages(received_data): - var rpc_ := RPC.deserialize(package) - if rpc_ is RPCClientDisconnect: - close() - server().rpc_data.emit(rpc_) - - - func _read_next_data_packages(data_package: PackedByteArray) -> PackedStringArray: - _readBuffer += data_package.get_string_from_utf8() - var json_array := _readBuffer.split(GdUnitServerConstants.JSON_RESPONSE_DELIMITER) - # We need to check if the current data is terminated by the delemiter (data packets can be split unspecifically). - # If not, store the last part in _readBuffer and complete it on the next data packet that is received - if not _readBuffer.ends_with(GdUnitServerConstants.JSON_RESPONSE_DELIMITER): - _readBuffer = json_array[-1] - json_array.remove_at(json_array.size()-1) - else: - # Reset the buffer if a completely terminated packet was received - _readBuffer = "" - # remove empty packages - for index in json_array.size(): - if index < json_array.size() and json_array[index].is_empty(): - json_array.remove_at(index) - return json_array + receive_packages(_stream, func(rpc_data: RPC) -> void: + server().rpc_data.emit(rpc_data) + # is client disconnecting we close the server after a timeout of 1 second + if rpc_data is RPCClientDisconnect: + close() + ) - func console(_message :String) -> void: - #print_debug("TCP Connection:", _message) + func console(_value: Variant) -> void: + #print_debug("TCP Server: ", value) pass -@warning_ignore("return_value_discarded") +func _init(server_name := "GdUnit4 TCP Server") -> void: + _server_name = server_name + + func _ready() -> void: _server = TCPServer.new() client_connected.connect(_on_client_connected) @@ -113,8 +71,7 @@ func _notification(what: int) -> void: stop() -func start() -> GdUnitResult: - var server_port := GdUnitServerConstants.GD_TEST_SERVER_PORT +func start(server_port := GdUnitServerConstants.GD_TEST_SERVER_PORT) -> GdUnitResult: var err := OK for retry in GdUnitServerConstants.DEFAULT_SERVER_START_RETRY_TIMES: err = _server.listen(server_port, "127.0.0.1") @@ -128,7 +85,7 @@ func start() -> GdUnitResult: if err == ERR_ALREADY_IN_USE: return GdUnitResult.error("GdUnit4: Can't establish server, the server is already in use. Error: %s, " % error_string(err)) return GdUnitResult.error("GdUnit4: Can't establish server. Error: %s." % error_string(err)) - prints("GdUnit4: Test server successfully started checked port: %d" % server_port) + console("Successfully started checked port: %d" % server_port) return GdUnitResult.success(server_port) @@ -151,7 +108,7 @@ func _process(_delta: float) -> void: if _server != null and not _server.is_listening(): return # check if connection is ready to be used - if _server.is_connection_available(): + if _server != null and _server.is_connection_available(): add_child(TcpConnection.new(_server)) @@ -159,14 +116,14 @@ func _on_client_connected(client_id: int) -> void: console("Client connected %d" % client_id) -@warning_ignore("unsafe_method_access") func _on_client_disconnected(client_id: int) -> void: for connection in get_children(): + @warning_ignore("unsafe_method_access") if connection is TcpConnection and connection.id() == client_id: + @warning_ignore("unsafe_method_access") connection.close() remove_child(connection) -func console(_message: String) -> void: - #print_debug("TCP Server:", _message) - pass +func console(value: Variant) -> void: + print(_server_name, ": ", value) diff --git a/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid b/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid index b42715c4..b5ed67a5 100644 --- a/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid +++ b/addons/gdUnit4/src/network/GdUnitTcpServer.gd.uid @@ -1 +1 @@ -uid://cvwx65s7t1d8c +uid://clfnvhrjrv8w6 diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd b/addons/gdUnit4/src/network/rpc/RPC.gd index c3ddaaf1..6569cb1c 100644 --- a/addons/gdUnit4/src/network/rpc/RPC.gd +++ b/addons/gdUnit4/src/network/rpc/RPC.gd @@ -2,23 +2,36 @@ class_name RPC extends RefCounted +var _data: Dictionary = {} + + +func _init(obj: Object = null) -> void: + if obj != null: + if obj.has_method("serialize"): + _data = obj.call("serialize") + else: + _data = inst_to_dict(obj) + + +func get_data() -> Object: + return dict_to_inst(_data) + + func serialize() -> String: return JSON.stringify(inst_to_dict(self)) # using untyped version see comments below -static func deserialize(json_value :String) -> Object: +static func deserialize(json_value: String) -> Object: var json := JSON.new() var err := json.parse(json_value) if err != OK: - push_error("Can't deserialize JSON, error at line %d: %s \n json: '%s'" % [json.get_error_line(), json.get_error_message(), json_value]) + push_error("Can't deserialize JSON, error at line %d:\n error: %s \n json: '%s'" + % [json.get_error_line(), json.get_error_message(), json_value]) return null - var result :Dictionary = json.get_data() + var result: Dictionary = json.get_data() if not typeof(result) == TYPE_DICTIONARY: - push_error("Can't deserialize JSON, error at line %d: %s \n json: '%s'" % [result.error_line, result.error_string, json_value]) + push_error("Can't deserialize JSON. Expecting dictionary, error at line %d:\n error: %s \n json: '%s'" + % [result.error_line, result.error_string, json_value]) return null return dict_to_inst(result) - -# this results in orpan node, for more details https://github.com/godotengine/godot/issues/50069 -#func deserialize2(data :Dictionary) -> RPC: -# return dict_to_inst(data) as RPC diff --git a/addons/gdUnit4/src/network/rpc/RPC.gd.uid b/addons/gdUnit4/src/network/rpc/RPC.gd.uid index 61d57c94..99972c67 100644 --- a/addons/gdUnit4/src/network/rpc/RPC.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPC.gd.uid @@ -1 +1 @@ -uid://casowu1l3gtn4 +uid://cx0qfmxtj4bwt diff --git a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd index 115fea26..6b494cfe 100644 --- a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd +++ b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd @@ -1,11 +1,11 @@ class_name RPCClientConnect extends RPC -var _client_id :int +var _client_id: int -func with_id(p_client_id :int) -> RPCClientConnect: - _client_id = p_client_id +func with_id(id: int) -> RPCClientConnect: + _client_id = id return self diff --git a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid index 403cd5b0..be2f995a 100644 --- a/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCClientConnect.gd.uid @@ -1 +1 @@ -uid://b1dn3g0wgtlq2 +uid://bdio43gdelu1i diff --git a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd index 52ef259d..7445b9de 100644 --- a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd +++ b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd @@ -1,11 +1,11 @@ class_name RPCClientDisconnect extends RPC -var _client_id :int +var _client_id: int -func with_id(p_client_id :int) -> RPCClientDisconnect: - _client_id = p_client_id +func with_id(id: int) -> RPCClientDisconnect: + _client_id = id return self diff --git a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid index 39a05642..3afe53ee 100644 --- a/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCClientDisconnect.gd.uid @@ -1 +1 @@ -uid://dmw41gyqwujnd +uid://di7wbudqfl7m1 diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd index e692aff9..dbf55c63 100644 --- a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd @@ -1,18 +1,14 @@ class_name RPCGdUnitEvent extends RPC -var _event :Dictionary - -static func of(p_event :GdUnitEvent) -> RPCGdUnitEvent: - var rpc := RPCGdUnitEvent.new() - rpc._event = p_event.serialize() - return rpc +static func of(p_event: GdUnitEvent) -> RPCGdUnitEvent: + return RPCGdUnitEvent.new(p_event) func event() -> GdUnitEvent: - return GdUnitEvent.new().deserialize(_event) + return GdUnitEvent.new().deserialize(_data) func _to_string() -> String: - return "RPCGdUnitEvent: " + str(_event) + return "RPCGdUnitEvent: " + str(_data) diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid index f787589d..8e059d97 100644 --- a/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCGdUnitEvent.gd.uid @@ -1 +1 @@ -uid://dtqk1dgbd8w63 +uid://b27fgxch4ycbf diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd b/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd deleted file mode 100644 index 96c4d96d..00000000 --- a/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd +++ /dev/null @@ -1,18 +0,0 @@ -class_name RPCGdUnitTestSuite -extends RPC - -var _data :Dictionary - - -static func of(test_suite :Node) -> RPCGdUnitTestSuite: - var rpc := RPCGdUnitTestSuite.new() - rpc._data = GdUnitTestSuiteDto.new().serialize(test_suite) - return rpc - - -func dto() -> GdUnitResourceDto: - return GdUnitTestSuiteDto.new().deserialize(_data) - - -func _to_string() -> String: - return "RPCGdUnitTestSuite: " + str(_data) diff --git a/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd.uid b/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd.uid deleted file mode 100644 index 83b429cb..00000000 --- a/addons/gdUnit4/src/network/rpc/RPCGdUnitTestSuite.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://da2iv1r8745lu diff --git a/addons/gdUnit4/src/network/rpc/RPCMessage.gd b/addons/gdUnit4/src/network/rpc/RPCMessage.gd index ddc49257..1db0470d 100644 --- a/addons/gdUnit4/src/network/rpc/RPCMessage.gd +++ b/addons/gdUnit4/src/network/rpc/RPCMessage.gd @@ -1,12 +1,12 @@ class_name RPCMessage extends RPC -var _message :String +var _message: String -static func of(p_message :String) -> RPCMessage: +static func of(msg :String) -> RPCMessage: var rpc := RPCMessage.new() - rpc._message = p_message + rpc._message = msg return rpc diff --git a/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid b/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid index be38eed1..1bafd738 100644 --- a/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid +++ b/addons/gdUnit4/src/network/rpc/RPCMessage.gd.uid @@ -1 +1 @@ -uid://svvgnbhb4s3m +uid://b6el6katlkf8w diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd deleted file mode 100644 index 9cfd1fe1..00000000 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd +++ /dev/null @@ -1,27 +0,0 @@ -class_name GdUnitResourceDto -extends Resource - -var _name :String -var _path :String - - -func serialize(resource :Node) -> Dictionary: - var serialized := Dictionary() - serialized["name"] = resource.get_name() - @warning_ignore("unsafe_method_access") - serialized["resource_path"] = resource.ResourcePath() - return serialized - - -func deserialize(data :Dictionary) -> GdUnitResourceDto: - _name = data.get("name", "n.a.") - _path = data.get("resource_path", "") - return self - - -func name() -> String: - return _name - - -func path() -> String: - return _path diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd.uid b/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd.uid deleted file mode 100644 index 17d1fd6d..00000000 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitResourceDto.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c1j1elip17wre diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd deleted file mode 100644 index cfb093e0..00000000 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd +++ /dev/null @@ -1,47 +0,0 @@ -class_name GdUnitTestCaseDto -extends GdUnitResourceDto - -var _line_number :int = -1 -var _script_path: String -var _test_case_names :PackedStringArray = [] - - -@warning_ignore("unsafe_method_access") -func serialize(test_case :Node) -> Dictionary: - var serialized := super.serialize(test_case) - if test_case.has_method("line_number"): - serialized["line_number"] = test_case.line_number() - else: - serialized["line_number"] = test_case.get("LineNumber") - if test_case.has_method("script_path"): - serialized["script_path"] = test_case.script_path() - else: - # TODO 'script_path' needs to be implement in c# the the - # serialized["script_path"] = test_case.get("ScriptPath") - serialized["script_path"] = serialized["resource_path"] - if test_case.has_method("test_case_names"): - serialized["test_case_names"] = test_case.test_case_names() - elif test_case.has_method("TestCaseNames"): - serialized["test_case_names"] = test_case.TestCaseNames() - return serialized - - -func deserialize(data :Dictionary) -> GdUnitTestCaseDto: - @warning_ignore("return_value_discarded") - super.deserialize(data) - _line_number = data.get("line_number", -1) - _script_path = data.get("script_path", data.get("resource_path", "")) - _test_case_names = data.get("test_case_names", []) - return self - - -func line_number() -> int: - return _line_number - - -func script_path() -> String: - return _script_path - - -func test_case_names() -> PackedStringArray: - return _test_case_names diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd.uid b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd.uid deleted file mode 100644 index 4275cc2a..00000000 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestCaseDto.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://du74tav3y4h4t diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd deleted file mode 100644 index c5d4501c..00000000 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd +++ /dev/null @@ -1,43 +0,0 @@ -class_name GdUnitTestSuiteDto -extends GdUnitResourceDto - - -# Dictionary[String, GdUnitTestCaseDto] -var _test_cases_by_name := Dictionary() - - -static func of(test_suite :Node) -> GdUnitTestSuiteDto: - var dto := GdUnitTestSuiteDto.new() - return dto.deserialize(dto.serialize(test_suite)) - - -func serialize(test_suite :Node) -> Dictionary: - var serialized := super.serialize(test_suite) - var test_cases_ := Array() - serialized["test_cases"] = test_cases_ - for test_case in test_suite.get_children(): - test_cases_.append(GdUnitTestCaseDto.new().serialize(test_case)) - return serialized - - -func deserialize(data :Dictionary) -> GdUnitResourceDto: - @warning_ignore("return_value_discarded") - super.deserialize(data) - var test_cases_ :Array = data.get("test_cases", []) - for test_case :Dictionary in test_cases_: - add_test_case(GdUnitTestCaseDto.new().deserialize(test_case)) - return self - - -func add_test_case(test_case :GdUnitTestCaseDto) -> void: - _test_cases_by_name[test_case.name()] = test_case - - -func test_case_count() -> int: - return _test_cases_by_name.size() - - -func test_cases() -> Array[GdUnitTestCaseDto]: - var test_cases_ :Array[GdUnitTestCaseDto] = [] - test_cases_.append_array(_test_cases_by_name.values()) - return test_cases_ diff --git a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd.uid b/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd.uid deleted file mode 100644 index 65016198..00000000 --- a/addons/gdUnit4/src/network/rpc/dtos/GdUnitTestSuiteDto.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://c2ynl3ml3arpo diff --git a/addons/gdUnit4/src/report/GdUnitByPathReport.gd.uid b/addons/gdUnit4/src/report/GdUnitByPathReport.gd.uid deleted file mode 100644 index 4190139b..00000000 --- a/addons/gdUnit4/src/report/GdUnitByPathReport.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://chef78rauc60b diff --git a/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd.uid b/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd.uid deleted file mode 100644 index fac7ad1e..00000000 --- a/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://devw7c4ax306y diff --git a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd deleted file mode 100644 index 94bee48f..00000000 --- a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd +++ /dev/null @@ -1,146 +0,0 @@ -class_name GdUnitHtmlReport -extends GdUnitReportSummary - -const REPORT_DIR_PREFIX = "report_" - -var _report_path :String -var _iteration :int - - -func _init(report_path :String, max_reports: int) -> void: - if max_reports > 1: - _iteration = GdUnitFileAccess.find_last_path_index(report_path, REPORT_DIR_PREFIX) + 1 - else: - _iteration = 1 - _report_path = "%s/%s%d" % [report_path, REPORT_DIR_PREFIX, _iteration] - @warning_ignore("return_value_discarded") - DirAccess.make_dir_recursive_absolute(_report_path) - - -func add_testsuite_report(p_resource_path: String, p_suite_name: String, p_test_count: int) -> void: - _reports.append(GdUnitTestSuiteReport.new(p_resource_path, p_suite_name, p_test_count)) - - -@warning_ignore("shadowed_variable") -func add_testcase(resource_path :String, suite_name :String, test_name: String) -> void: - for report:GdUnitTestSuiteReport in _reports: - if report.resource_path() == resource_path: - var test_report := GdUnitTestCaseReport.new(resource_path, suite_name, test_name) - report.add_or_create_test_report(test_report) - - -func add_testsuite_reports( - p_resource_path :String, - p_error_count :int, - p_failure_count :int, - p_orphan_count :int, - p_duration :int, - p_reports :Array = []) -> void: - - for report:GdUnitTestSuiteReport in _reports: - if report.resource_path() == p_resource_path: - report.set_reports(p_reports) - update_summary_counters(p_error_count, p_failure_count, p_orphan_count, 0, 0, p_duration) - - -func add_testcase_reports( - p_resource_path: String, - p_test_name: String, - p_reports: Array[GdUnitReport]) -> void: - - for report:GdUnitTestSuiteReport in _reports: - if report.resource_path() == p_resource_path: - report.add_testcase_reports(p_test_name, p_reports) - - -func update_testsuite_counters( - p_resource_path :String, - p_error_count: int, - p_failure_count: int, - p_orphan_count: int, - p_is_skipped: bool, - p_is_flaky: bool, - p_duration: int) -> void: - - for report:GdUnitTestSuiteReport in _reports: - if report.resource_path() == p_resource_path: - report.update_testsuite_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration) - update_summary_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, 0) - - -func set_testcase_counters( - p_resource_path: String, - p_test_name: String, - p_error_count: int, - p_failure_count: int, - p_orphan_count: int, - p_is_skipped: bool, - p_is_flaky: bool, - p_duration: int) -> void: - - for report:GdUnitTestSuiteReport in _reports: - if report.resource_path() == p_resource_path: - report.set_testcase_counters(p_test_name, p_error_count, p_failure_count, p_orphan_count, - p_is_skipped, p_is_flaky, p_duration) - - -func update_summary_counters( - p_error_count: int, - p_failure_count: int, - p_orphan_count: int, - p_is_skipped: bool, - p_is_flaky: bool, - p_duration: int) -> void: - - _error_count += p_error_count - _failure_count += p_failure_count - _orphan_count += p_orphan_count - _skipped_count += p_is_skipped as int - _flaky_count += p_is_flaky as int - _duration += p_duration - - -func write() -> String: - var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/index.html") - var to_write := GdUnitHtmlPatterns.build(template, self, "") - to_write = apply_path_reports(_report_path, to_write, _reports) - to_write = apply_testsuite_reports(_report_path, to_write, _reports) - # write report - var index_file := "%s/index.html" % _report_path - FileAccess.open(index_file, FileAccess.WRITE).store_string(to_write) - @warning_ignore("return_value_discarded") - GdUnitFileAccess.copy_directory("res://addons/gdUnit4/src/report/template/css/", _report_path + "/css") - return index_file - - -func delete_history(max_reports :int) -> int: - return GdUnitFileAccess.delete_path_index_lower_equals_than(_report_path.get_base_dir(), REPORT_DIR_PREFIX, _iteration-max_reports) - - -func apply_path_reports(report_dir :String, template :String, report_summaries :Array) -> String: - #Dictionary[String, Array[GdUnitReportSummary]] - var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(report_summaries) - var table_records := PackedStringArray() - var paths :Array[String] = [] - paths.append_array(path_report_mapping.keys()) - paths.sort() - for report_path in paths: - var reports: Array[GdUnitReportSummary] = path_report_mapping.get(report_path) - var report := GdUnitByPathReport.new(report_path, reports) - var report_link :String = report.write(report_dir).replace(report_dir, ".") - @warning_ignore("return_value_discarded") - table_records.append(report.create_record(report_link)) - return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) - - -func apply_testsuite_reports(report_dir: String, template: String, test_suite_reports: Array[GdUnitReportSummary]) -> String: - var table_records := PackedStringArray() - for report: GdUnitTestSuiteReport in test_suite_reports: - var report_link :String = report.write(report_dir).replace(report_dir, ".") - @warning_ignore("return_value_discarded") - table_records.append(report.create_record(report_link) as String) - return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) - - -func iteration() -> int: - return _iteration diff --git a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd.uid b/addons/gdUnit4/src/report/GdUnitHtmlReport.gd.uid deleted file mode 100644 index 62aa6b3b..00000000 --- a/addons/gdUnit4/src/report/GdUnitHtmlReport.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://0bo1toay2ch0 diff --git a/addons/gdUnit4/src/report/GdUnitReportSummary.gd b/addons/gdUnit4/src/report/GdUnitReportSummary.gd deleted file mode 100644 index 20a24e7a..00000000 --- a/addons/gdUnit4/src/report/GdUnitReportSummary.gd +++ /dev/null @@ -1,140 +0,0 @@ -class_name GdUnitReportSummary -extends RefCounted - -const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") - -const CHARACTERS_TO_ENCODE := { - '<' : '<', - '>' : '>' -} - -var _resource_path :String -var _name :String -var _test_count := 0 -var _failure_count := 0 -var _error_count := 0 -var _orphan_count := 0 -var _skipped_count := 0 -var _flaky_count := 0 -var _duration := 0 -var _reports :Array[GdUnitReportSummary] = [] - -func name() -> String: - return _name - - -func name_html_encoded() -> String: - return html_encode(_name) - - -func path() -> String: - return _resource_path.get_base_dir().replace("res://", "") - - -func resource_path() -> String: - return _resource_path - - -func suite_count() -> int: - return _reports.size() - - -func suite_executed_count() -> int: - var executed := _reports.size() - for report in _reports: - if report.test_count() == report.skipped_count(): - executed -= 1 - return executed - - -func test_count() -> int: - var count := _test_count - for report in _reports: - count += report.test_count() - return count - - -func test_executed_count() -> int: - return test_count() - skipped_count() - - -func success_count() -> int: - return test_count() - error_count() - failure_count() - flaky_count() - skipped_count() - - -func error_count() -> int: - return _error_count - - -func failure_count() -> int: - return _failure_count - - -func skipped_count() -> int: - return _skipped_count - - -func flaky_count() -> int: - return _flaky_count - - -func orphan_count() -> int: - return _orphan_count - - -func duration() -> int: - return _duration - - -func get_reports() -> Array: - return _reports - - -func add_report(report :GdUnitReportSummary) -> void: - _reports.append(report) - - -func report_state() -> String: - return calculate_state(error_count(), failure_count(), orphan_count(), flaky_count(), skipped_count()) - - -func succes_rate() -> String: - return calculate_succes_rate(test_count(), error_count(), failure_count()) - - -func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int, p_flaky_count: int, p_skipped_count: int) -> String: - if p_skipped_count > 0: - return "SKIPPED" - if p_error_count > 0: - return "ERROR" - if p_failure_count > 0: - return "FAILED" - if p_flaky_count > 0: - return "FLAKY" - if p_orphan_count > 0: - return "WARNING" - return "PASSED" - - -func calculate_succes_rate(p_test_count :int, p_error_count :int, p_failure_count :int) -> String: - if p_failure_count == 0: - return "100%" - var count := p_test_count-p_failure_count-p_error_count - if count < 0: - return "0%" - return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" - - -func create_summary(_report_dir :String) -> String: - return "" - - -func html_encode(value: String) -> String: - for key: String in CHARACTERS_TO_ENCODE.keys(): - @warning_ignore("unsafe_cast") - value = value.replace(key, CHARACTERS_TO_ENCODE[key] as String) - return value - - -func convert_rtf_to_html(bbcode :String) -> String: - return GdUnitTools.richtext_normalize(bbcode) diff --git a/addons/gdUnit4/src/report/GdUnitReportSummary.gd.uid b/addons/gdUnit4/src/report/GdUnitReportSummary.gd.uid deleted file mode 100644 index f69a55d8..00000000 --- a/addons/gdUnit4/src/report/GdUnitReportSummary.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dfl24l8q06jau diff --git a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd deleted file mode 100644 index 5eec799e..00000000 --- a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd +++ /dev/null @@ -1,52 +0,0 @@ -class_name GdUnitTestCaseReport -extends GdUnitReportSummary - -var _suite_name :String -var _failure_reports :Array[GdUnitReport] - - -@warning_ignore("shadowed_variable") -func _init(p_resource_path: String, p_suite_name: String, p_test_name: String) -> void: - _resource_path = p_resource_path - _suite_name = p_suite_name - _name = p_test_name - - -func suite_name() -> String: - return _suite_name - - -func failure_report() -> String: - var html_report := "" - for report in get_test_reports(): - html_report += convert_rtf_to_html(str(report)) - return html_report - - -func create_record(_report_dir :String) -> String: - return GdUnitHtmlPatterns.TABLE_RECORD_TESTCASE\ - .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state().to_lower())\ - .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report_state())\ - .replace(GdUnitHtmlPatterns.TESTCASE_NAME, name())\ - .replace(GdUnitHtmlPatterns.SKIPPED_COUNT, str(skipped_count()))\ - .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(orphan_count()))\ - .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(_duration))\ - .replace(GdUnitHtmlPatterns.FAILURE_REPORT, failure_report()) - - -func add_testcase_reports(reports: Array[GdUnitReport]) -> void: - _failure_reports.append_array(reports) - - -func set_testcase_counters(p_error_count: int, p_failure_count: int, p_orphan_count: int, - p_is_skipped: bool, p_is_flaky: bool, p_duration: int) -> void: - _error_count = p_error_count - _failure_count = p_failure_count - _orphan_count = p_orphan_count - _skipped_count = p_is_skipped - _flaky_count = p_is_flaky as int - _duration = p_duration - - -func get_test_reports() -> Array[GdUnitReport]: - return _failure_reports diff --git a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd.uid b/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd.uid deleted file mode 100644 index 9bfe40b2..00000000 --- a/addons/gdUnit4/src/report/GdUnitTestCaseReport.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://3jaec6plxkdn diff --git a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd deleted file mode 100644 index 4a62087b..00000000 --- a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd +++ /dev/null @@ -1,124 +0,0 @@ -class_name GdUnitTestSuiteReport -extends GdUnitReportSummary - -var _time_stamp: int -var _failure_reports: Array[GdUnitReport] = [] - - -func _init(p_resource_path: String, p_name: String, p_test_count: int) -> void: - _resource_path = p_resource_path - _name = p_name - _test_count = p_test_count - _time_stamp = Time.get_unix_time_from_system() as int - - -func create_record(report_link :String) -> String: - return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_TESTSUITE, self, report_link) - - -func output_path(report_dir :String) -> String: - return "%s/test_suites/%s.%s.html" % [report_dir, path().replace("/", "."), name()] - - -func failure_report() -> String: - var html_report := "" - for report in _failure_reports: - html_report += convert_rtf_to_html(str(report)) - return html_report - - -func test_suite_failure_report() -> String: - return GdUnitHtmlPatterns.TABLE_REPORT_TESTSUITE\ - .replace(GdUnitHtmlPatterns.REPORT_STATE, report_state().to_lower())\ - .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report_state())\ - .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(orphan_count()))\ - .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(_duration))\ - .replace(GdUnitHtmlPatterns.FAILURE_REPORT, failure_report()) - - -func write(report_dir :String) -> String: - var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/suite_report.html") - template = GdUnitHtmlPatterns.build(template, self, "") - - var report_output_path := output_path(report_dir) - var test_report_table := PackedStringArray() - if not _failure_reports.is_empty(): - @warning_ignore("return_value_discarded") - test_report_table.append(test_suite_failure_report()) - for test_report: GdUnitTestCaseReport in _reports: - @warning_ignore("return_value_discarded") - test_report_table.append(test_report.create_record(report_output_path)) - - template = template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTCASES, "\n".join(test_report_table)) - - var dir := report_output_path.get_base_dir() - if not DirAccess.dir_exists_absolute(dir): - @warning_ignore("return_value_discarded") - DirAccess.make_dir_recursive_absolute(dir) - FileAccess.open(report_output_path, FileAccess.WRITE).store_string(template) - return report_output_path - - -func set_duration(p_duration :int) -> void: - _duration = p_duration - - -func time_stamp() -> int: - return _time_stamp - - -func duration() -> int: - return _duration - - -func set_skipped(skipped :int) -> void: - _skipped_count += skipped - - -func set_orphans(orphans :int) -> void: - _orphan_count = orphans - - -func set_failed(count :int) -> void: - _failure_count += count - - -func set_reports(failure_reports :Array[GdUnitReport]) -> void: - _failure_reports = failure_reports - - -func add_or_create_test_report(test_report: GdUnitTestCaseReport) -> void: - _reports.append(test_report) - - -func update_testsuite_counters(p_error_count: int, p_failure_count: int, p_orphan_count: int, - p_is_skipped: bool, p_is_flaky: bool, p_duration: int) -> void: - _error_count += p_error_count - _failure_count += p_failure_count - _orphan_count += p_orphan_count - _skipped_count += p_is_skipped as int - _flaky_count += p_is_flaky as int - _duration += p_duration - - -func set_testcase_counters(test_name: String, p_error_count: int, p_failure_count: int, p_orphan_count: int, - p_is_skipped: bool, p_is_flaky: bool, p_duration: int) -> void: - if _reports.is_empty(): - return - var test_report:GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: - return report.name() == test_name - ).back() - if test_report: - test_report.set_testcase_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration) - - -func add_testcase_reports(test_name: String, reports: Array[GdUnitReport] ) -> void: - if reports.is_empty(): - return - # we lookup to latest matching report because of flaky tests could be retry the tests - # and resultis in multipe report entries with the same name - var test_report:GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: - return report.name() == test_name - ).back() - if test_report: - test_report.add_testcase_reports(reports) diff --git a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd.uid b/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd.uid deleted file mode 100644 index edc3c1b6..00000000 --- a/addons/gdUnit4/src/report/GdUnitTestSuiteReport.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b7kde1pwh8xfc diff --git a/addons/gdUnit4/src/report/JUnitXmlReport.gd.uid b/addons/gdUnit4/src/report/JUnitXmlReport.gd.uid deleted file mode 100644 index 0a3afe44..00000000 --- a/addons/gdUnit4/src/report/JUnitXmlReport.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cv1buimuubvwj diff --git a/addons/gdUnit4/src/report/XmlElement.gd.uid b/addons/gdUnit4/src/report/XmlElement.gd.uid deleted file mode 100644 index 4abc0902..00000000 --- a/addons/gdUnit4/src/report/XmlElement.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://rgc3iyesp1a0 diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd new file mode 100644 index 00000000..b55b964f --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd @@ -0,0 +1,202 @@ +class_name GdUnitReportSummary +extends RefCounted + +var _resource_path: String +var _name: String +var _test_count := 0 +var _failure_count := 0 +var _error_count := 0 +var _orphan_count := 0 +var _skipped_count := 0 +var _flaky_count := 0 +var _duration := 0 +var _reports: Array[GdUnitReportSummary] = [] +var _text_formatter: Callable + + +func _init(text_formatter: Callable) -> void: + _text_formatter = text_formatter + + +func name() -> String: + return _name + + +func path() -> String: + return _resource_path.get_base_dir().replace("res://", "") + + +func get_resource_path() -> String: + return _resource_path + + +func suite_count() -> int: + return _reports.size() + + +func suite_executed_count() -> int: + var executed := _reports.size() + for report in _reports: + if report.test_count() == report.skipped_count(): + executed -= 1 + return executed + + +func test_count() -> int: + var count := _test_count + for report in _reports: + count += report.test_count() + return count + + +func test_executed_count() -> int: + return test_count() - skipped_count() + + +func success_count() -> int: + return test_count() - error_count() - failure_count() - flaky_count() - skipped_count() + + +func error_count() -> int: + return _error_count + + +func failure_count() -> int: + return _failure_count + + +func skipped_count() -> int: + return _skipped_count + + +func flaky_count() -> int: + return _flaky_count + + +func orphan_count() -> int: + return _orphan_count + + +func duration() -> int: + return _duration + + +func get_reports() -> Array: + return _reports + + +func add_report(report: GdUnitReportSummary) -> void: + _reports.append(report) + + +func report_state() -> String: + return calculate_state(error_count(), failure_count(), orphan_count(), flaky_count(), skipped_count()) + + +func succes_rate() -> String: + return calculate_succes_rate(test_count(), error_count(), failure_count()) + + +@warning_ignore("shadowed_variable") +func add_testcase(resource_path: String, suite_name: String, test_name: String) -> void: + for report: GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == resource_path: + var test_report := GdUnitTestCaseReport.new(resource_path, suite_name, test_name, _text_formatter) + report.add_or_create_test_report(test_report) + + +func add_reports( + p_resource_path: String, + p_test_name: String, + p_reports: Array[GdUnitReport]) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.add_testcase_reports(p_test_name, p_reports) + + +func add_testsuite_report(p_resource_path: String, p_suite_name: String, p_test_count: int) -> void: + _reports.append(GdUnitTestSuiteReport.new(p_resource_path, p_suite_name, p_test_count, _text_formatter)) + + +func add_testsuite_reports( + p_resource_path: String, + p_reports: Array = []) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.set_reports(p_reports) + + +func set_counters( + p_resource_path: String, + p_test_name: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + + for report: GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report.set_testcase_counters(p_test_name, p_error_count, p_failure_count, p_orphan_count, + p_is_skipped, p_is_flaky, p_duration) + + +func update_testsuite_counters( + p_resource_path: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + + for report:GdUnitTestSuiteReport in _reports: + if report.get_resource_path() == p_resource_path: + report._update_testsuite_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, p_duration) + _update_summary_counters(p_error_count, p_failure_count, p_orphan_count, p_skipped_count, p_flaky_count, 0) + + +func _update_summary_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_skipped_count + _flaky_count += p_flaky_count + _duration += p_duration + + +func calculate_state(p_error_count :int, p_failure_count :int, p_orphan_count :int, p_flaky_count: int, p_skipped_count: int) -> String: + if p_error_count > 0: + return "ERROR" + if p_failure_count > 0: + return "FAILED" + if p_flaky_count > 0: + return "FLAKY" + if p_orphan_count > 0: + return "WARNING" + if p_skipped_count > 0: + return "SKIPPED" + return "PASSED" + + +func calculate_succes_rate(p_test_count :int, p_error_count :int, p_failure_count :int) -> String: + if p_failure_count == 0: + return "100%" + var count := p_test_count-p_failure_count-p_error_count + if count < 0: + return "0%" + return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" + + +func create_summary(_report_dir :String) -> String: + return "" diff --git a/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid new file mode 100644 index 00000000..6de596a5 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportSummary.gd.uid @@ -0,0 +1 @@ +uid://cxt40diesh6pc diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd new file mode 100644 index 00000000..bf4c96ea --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd @@ -0,0 +1,12 @@ +class_name GdUnitReportWriter +extends RefCounted + + +func write(_report_path: String, _report: GdUnitReportSummary) -> String: + assert(false, "'write' is not implemented!") + return "" + + +func output_format() -> String: + assert(false, "'output_format' is not implemented!") + return "" diff --git a/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid new file mode 100644 index 00000000..4a5e1129 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitReportWriter.gd.uid @@ -0,0 +1 @@ +uid://cbquy7r2ye33l diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd new file mode 100644 index 00000000..2801696c --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd @@ -0,0 +1,47 @@ +class_name GdUnitTestCaseReport +extends GdUnitReportSummary + + +var _suite_name: String +var _failure_reports: Array[GdUnitReport] = [] + + +func _init(p_resource_path: String, p_suite_name: String, p_test_name: String, text_formatter: Callable) -> void: + _resource_path = p_resource_path + _suite_name = p_suite_name + _name = p_test_name + _text_formatter = text_formatter + + +func suite_name() -> String: + return _suite_name + + +func failure_report() -> String: + var report_message := "" + for report in get_test_reports(): + report_message += _text_formatter.call(str(report)) + "\n" + return report_message + + +func add_testcase_reports(reports: Array[GdUnitReport]) -> void: + _failure_reports.append_array(reports) + + +func set_testcase_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + _error_count = p_error_count + _failure_count = p_failure_count + _orphan_count = p_orphan_count + _skipped_count = p_is_skipped + _flaky_count = p_is_flaky as int + _duration = p_duration + + +func get_test_reports() -> Array[GdUnitReport]: + return _failure_reports diff --git a/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid new file mode 100644 index 00000000..69af8bb9 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestCaseReport.gd.uid @@ -0,0 +1 @@ +uid://hhn8rwdgali0 diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd new file mode 100644 index 00000000..46b81931 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd @@ -0,0 +1,112 @@ +class_name GdUnitTestReporter +extends RefCounted + + +var _statistics := {} +var _summary := {} + + +func init_summary() -> void: + _summary["suite_count"] = 0 + _summary["total_count"] = 0 + _summary["error_count"] = 0 + _summary["failed_count"] = 0 + _summary["skipped_count"] = 0 + _summary["flaky_count"] = 0 + _summary["orphan_nodes"] = 0 + _summary["elapsed_time"] = 0 + + +func init_statistics() -> void: + _statistics.clear() + + +func add_test_statistics(event: GdUnitEvent) -> void: + _statistics[event.guid()] = { + "error_count" : event.error_count(), + "failed_count" : event.failed_count(), + "skipped_count" : event.skipped_count(), + "flaky_count" : event.is_flaky() as int, + "orphan_nodes" : event.orphan_nodes() + } + + +func build_test_suite_statisitcs(event: GdUnitEvent) -> Dictionary: + var statistic := { + "total_count" : _statistics.size(), + "error_count" : event.error_count(), + "failed_count" : event.failed_count(), + "skipped_count" : event.skipped_count(), + "flaky_count" : 0, + "orphan_nodes" : event.orphan_nodes() + } + _summary["suite_count"] += 1 + _summary["total_count"] += _statistics.size() + _summary["error_count"] += event.error_count() + _summary["failed_count"] += event.failed_count() + _summary["skipped_count"] += event.skipped_count() + _summary["orphan_nodes"] += event.orphan_nodes() + _summary["elapsed_time"] += event.elapsed_time() + + for key: String in ["error_count", "failed_count", "skipped_count", "flaky_count", "orphan_nodes"]: + var value: int = _statistics.values().reduce(get_value.bind(key), 0 ) + statistic[key] += value + _summary[key] += value + + return statistic + + +func get_value(acc: int, value: Dictionary, key: String) -> int: + return acc + value[key] + + +func processed_suite_count() -> int: + return _summary["suite_count"] + + +func total_test_count() -> int: + return _summary["total_count"] + + +func total_flaky_count() -> int: + return _summary["flaky_count"] + + +func total_error_count() -> int: + return _summary["error_count"] + + +func total_failure_count() -> int: + return _summary["failed_count"] + + +func total_skipped_count() -> int: + return _summary["skipped_count"] + + +func total_orphan_count() -> int: + return _summary["orphan_nodes"] + + +func elapsed_time() -> int: + return _summary["elapsed_time"] + + +func error_count(statistics: Dictionary) -> int: + return statistics["error_count"] + + +func failed_count(statistics: Dictionary) -> int: + return statistics["failed_count"] + + +func orphan_nodes(statistics: Dictionary) -> int: + return statistics["orphan_nodes"] + + +func skipped_count(statistics: Dictionary) -> int: + return statistics["skipped_count"] + + +func flaky_count(statistics: Dictionary) -> int: + return statistics["flaky_count"] diff --git a/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid new file mode 100644 index 00000000..40ce8574 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestReporter.gd.uid @@ -0,0 +1 @@ +uid://2ympkqu6j2hl diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd new file mode 100644 index 00000000..9be06825 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd @@ -0,0 +1,96 @@ +class_name GdUnitTestSuiteReport +extends GdUnitReportSummary + +var _time_stamp: int +var _failure_reports: Array[GdUnitReport] = [] + + +func _init(p_resource_path: String, p_name: String, p_test_count: int, text_formatter: Callable) -> void: + _resource_path = p_resource_path + _name = p_name + _test_count = p_test_count + _time_stamp = Time.get_unix_time_from_system() as int + _text_formatter = text_formatter + + +func failure_report() -> String: + var report_message := "" + for report in _failure_reports: + report_message += _text_formatter.call(str(report)) + return report_message + + +func set_duration(p_duration :int) -> void: + _duration = p_duration + + +func time_stamp() -> int: + return _time_stamp + + +func duration() -> int: + return _duration + + +func set_skipped(skipped :int) -> void: + _skipped_count += skipped + + +func set_orphans(orphans :int) -> void: + _orphan_count = orphans + + +func set_failed(count :int) -> void: + _failure_count += count + + +func set_reports(failure_reports :Array[GdUnitReport]) -> void: + _failure_reports = failure_reports + + +func add_or_create_test_report(test_report: GdUnitTestCaseReport) -> void: + _reports.append(test_report) + + +func _update_testsuite_counters( + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_skipped_count: int, + p_flaky_count: int, + p_duration: int) -> void: + _error_count += p_error_count + _failure_count += p_failure_count + _orphan_count += p_orphan_count + _skipped_count += p_skipped_count + _flaky_count += p_flaky_count + _duration += p_duration + + +func set_testcase_counters( + test_name: String, + p_error_count: int, + p_failure_count: int, + p_orphan_count: int, + p_is_skipped: bool, + p_is_flaky: bool, + p_duration: int) -> void: + if _reports.is_empty(): + return + var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.set_testcase_counters(p_error_count, p_failure_count, p_orphan_count, p_is_skipped, p_is_flaky, p_duration) + + +func add_testcase_reports(test_name: String, reports: Array[GdUnitReport]) -> void: + if reports.is_empty(): + return + # we lookup to latest matching report because of flaky tests could be retry the tests + # and resultis in multipe report entries with the same name + var test_report: GdUnitTestCaseReport = _reports.filter(func (report: GdUnitTestCaseReport) -> bool: + return report.name() == test_name + ).back() + if test_report: + test_report.add_testcase_reports(reports) diff --git a/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid new file mode 100644 index 00000000..7fa6c605 --- /dev/null +++ b/addons/gdUnit4/src/reporters/GdUnitTestSuiteReport.gd.uid @@ -0,0 +1 @@ +uid://ttn5oyc7esh5 diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd new file mode 100644 index 00000000..8b54aac0 --- /dev/null +++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd @@ -0,0 +1,234 @@ +@tool +class_name GdUnitConsoleTestReporter + + +var test_session: GdUnitTestSession: + get: + return test_session + set(value): + # disconnect first possible connected listener + if test_session != null: + test_session.test_event.disconnect(on_gdunit_event) + # add listening to current session + test_session = value + if test_session != null: + test_session.test_event.connect(on_gdunit_event) + + +var _writer: GdUnitMessageWritter +var _reporter: GdUnitTestReporter = GdUnitTestReporter.new() +var _status_indent := 86 +var _detailed: bool +var _text_color: Color = Color.ANTIQUE_WHITE +var _function_color: Color = Color.ANTIQUE_WHITE +var _engine_type_color: Color = Color.ANTIQUE_WHITE + + +func _init(writer: GdUnitMessageWritter, detailed := false) -> void: + _writer = writer + _writer.clear() + _detailed = detailed + if _detailed: + _status_indent = 20 + init_colors() + + +func init_colors() -> void: + if Engine.is_editor_hint(): + var settings := EditorInterface.get_editor_settings() + _text_color = settings.get_setting("text_editor/theme/highlighting/text_color") + _function_color = settings.get_setting("text_editor/theme/highlighting/function_color") + _engine_type_color = settings.get_setting("text_editor/theme/highlighting/engine_type_color") + + +func clear() -> void: + _writer.clear() + + +func on_gdunit_event(event: GdUnitEvent) -> void: + match event.type(): + GdUnitEvent.INIT: + _reporter.init_summary() + + GdUnitEvent.STOP: + _print_summary() + println_message(build_executed_test_suite_msg(processed_suite_count(), processed_suite_count()), Color.DARK_SALMON) + println_message(build_executed_test_case_msg(total_test_count(), total_skipped_count()), Color.DARK_SALMON) + println_message("Total execution time: %s" % LocalTime.elapsed(elapsed_time()), Color.DARK_SALMON) + # We need finally to set the wave effect to enable the animations + _writer.effect(GdUnitMessageWritter.Effect.WAVE).print_at("", 0) + + GdUnitEvent.TESTSUITE_BEFORE: + _reporter.init_statistics() + print_message("Run Test Suite: ", Color.DARK_TURQUOISE) + println_message(event.resource_path(), _engine_type_color) + + GdUnitEvent.TESTSUITE_AFTER: + if not event.reports().is_empty(): + _writer.color(Color.DARK_SALMON)\ + .style(GdUnitMessageWritter.BOLD)\ + .println_message(event.suite_name()+":finalze") + _print_failure_report(event.reports()) + _print_statistics(_reporter.build_test_suite_statisitcs(event)) + _print_status(event) + println_message("") + if _detailed: + println_message("") + + GdUnitEvent.TESTCASE_BEFORE: + var test := test_session.find_test_by_id(event.guid()) + _print_test_path(test, event.guid()) + if _detailed: + _writer.color(Color.FOREST_GREEN).print_at("STARTED", _status_indent) + println_message("") + + GdUnitEvent.TESTCASE_AFTER: + _reporter.add_test_statistics(event) + if _detailed: + var test := test_session.find_test_by_id(event.guid()) + _print_test_path(test, event.guid()) + _print_status(event) + _print_failure_report(event.reports()) + if _detailed: + println_message("") + + +func _print_test_path(test: GdUnitTestCase, uid: GdUnitGUID) -> void: + if test == null: + prints_warning("Can't print full test info, the test by uid: '%s' was not discovered." % uid) + _writer.indent(1).color(_engine_type_color).print_message("Test ID: %s" % uid) + return + + var suite_name := test.source_file if _detailed else test.suite_name + _writer.indent(1).color(_engine_type_color).print_message(suite_name) + print_message(" > ") + print_message(test.display_name, _function_color) + + +func _print_status(event: GdUnitEvent) -> void: + if event.is_flaky() and event.is_success(): + var retries: int = event.statistic(GdUnitEvent.RETRY_COUNT) + _writer.color(Color.GREEN_YELLOW)\ + .style(GdUnitMessageWritter.ITALIC)\ + .print_at("FLAKY (%d retries)" % retries, _status_indent) + elif event.is_success(): + _writer.color(Color.FOREST_GREEN).print_at("PASSED", _status_indent) + elif event.is_skipped(): + _writer.color(Color.GOLDENROD).style(GdUnitMessageWritter.ITALIC).print_at("SKIPPED", _status_indent) + elif event.is_failed() or event.is_error(): + var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) + var message := "FAILED (retry %d)" % retries if retries > 1 else "FAILED" + _writer.color(Color.FIREBRICK)\ + .style(GdUnitMessageWritter.BOLD)\ + .effect(GdUnitMessageWritter.Effect.WAVE)\ + .print_at(message, _status_indent) + elif event.is_warning(): + _writer.color(Color.GOLDENROD)\ + .style(GdUnitMessageWritter.UNDERLINE)\ + .print_at("WARNING", _status_indent) + + println_message(" %s" % LocalTime.elapsed(event.elapsed_time()), Color.CORNFLOWER_BLUE) + + +func _print_failure_report(reports: Array[GdUnitReport]) -> void: + for report in reports: + if ( + report.is_failure() + or report.is_error() + or report.is_warning() + or report.is_skipped() + ): + _writer.indent(1)\ + .color(Color.DARK_TURQUOISE)\ + .style(GdUnitMessageWritter.BOLD | GdUnitMessageWritter.UNDERLINE)\ + .println_message("Report:") + var text := str(report) + for line in text.split("\n", false): + _writer.indent(2).color(Color.DARK_TURQUOISE).println_message(line) + + if not reports.is_empty(): + println_message("") + + +func _print_statistics(statistics: Dictionary) -> void: + print_message("Statistics:", Color.DODGER_BLUE) + print_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" %\ + [statistics["total_count"], + statistics["error_count"], + statistics["failed_count"], + statistics["flaky_count"], + statistics["skipped_count"], + statistics["orphan_nodes"]]) + + +func _print_summary() -> void: + print_message("Overall Summary:", Color.DODGER_BLUE) + _writer\ + .println_message(" %d test cases | %d errors | %d failures | %d flaky | %d skipped | %d orphans |" % [ + total_test_count(), + total_error_count(), + total_failure_count(), + total_flaky_count(), + total_skipped_count(), + total_orphan_count() + ]) + + +func build_executed_test_suite_msg(executed_count: int, total_count: int) -> String: + if executed_count == total_count: + return "Executed test suites: (%d/%d)" % [executed_count, total_count] + return "Executed test suites: (%d/%d), %d skipped" % [executed_count, total_count, (total_count - executed_count)] + + +func build_executed_test_case_msg(total_count: int, p_skipped_count: int) -> String: + if p_skipped_count == 0: + return "Executed test cases : (%d/%d)" % [total_count, total_count] + return "Executed test cases : (%d/%d), %d skipped" % [total_count-p_skipped_count, total_count, p_skipped_count] + + +func print_message(message: String, color: Color = _text_color) -> void: + _writer.color(color).print_message(message) + + +func println_message(message: String, color: Color = _text_color) -> void: + _writer.color(color).println_message(message) + + +func prints_warning(message: String) -> void: + _writer.prints_warning(message) + + +func prints_error(message: String) -> void: + _writer.prints_error(message) + + +func total_test_count() -> int: + return _reporter.total_test_count() + + +func total_error_count() -> int: + return _reporter.total_error_count() + + +func total_failure_count() -> int: + return _reporter.total_failure_count() + + +func total_flaky_count() -> int: + return _reporter.total_flaky_count() + + +func total_skipped_count() -> int: + return _reporter.total_skipped_count() + + +func total_orphan_count() -> int: + return _reporter.total_orphan_count() + + +func processed_suite_count() -> int: + return _reporter.processed_suite_count() + + +func elapsed_time() -> int: + return _reporter.elapsed_time() diff --git a/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid new file mode 100644 index 00000000..df22f258 --- /dev/null +++ b/addons/gdUnit4/src/reporters/console/GdUnitConsoleTestReporter.gd.uid @@ -0,0 +1 @@ +uid://b6xkrfskcc6a2 diff --git a/addons/gdUnit4/src/report/GdUnitByPathReport.gd b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd similarity index 86% rename from addons/gdUnit4/src/report/GdUnitByPathReport.gd rename to addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd index d857ad64..74b961b4 100644 --- a/addons/gdUnit4/src/report/GdUnitByPathReport.gd +++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd @@ -19,7 +19,7 @@ static func sort_reports_by_path(report_summaries :Array[GdUnitReportSummary]) - func path() -> String: - return _resource_path.replace("res://", "") + return _resource_path.replace("res://", "").trim_suffix("/") func create_record(report_link :String) -> String: @@ -28,7 +28,7 @@ func create_record(report_link :String) -> String: func write(report_dir :String) -> String: calculate_summary() - var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/report/template/folder_report.html") + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/folder_report.html") var path_report := GdUnitHtmlPatterns.build(template, self, "") path_report = apply_testsuite_reports(report_dir, path_report, _reports) @@ -44,9 +44,9 @@ func write(report_dir :String) -> String: func apply_testsuite_reports(report_dir :String, template :String, test_suite_reports :Array[GdUnitReportSummary]) -> String: var table_records := PackedStringArray() for report:GdUnitTestSuiteReport in test_suite_reports: - var report_link := report.output_path(report_dir).replace(report_dir, "..") + var report_link := GdUnitHtmlReportWriter.create_output_path(report_dir, report.path(), report.name()).replace(report_dir, "..") @warning_ignore("return_value_discarded") - table_records.append(report.create_record(report_link)) + table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report)) return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) diff --git a/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid new file mode 100644 index 00000000..f6ecfc8b --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitByPathReport.gd.uid @@ -0,0 +1 @@ +uid://b153cs0yh0lwm diff --git a/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd similarity index 78% rename from addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd rename to addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd index a9da106c..b8be904f 100644 --- a/addons/gdUnit4/src/report/GdUnitHtmlPatterns.gd +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd @@ -36,7 +36,7 @@ const TABLE_RECORD_PATH = """ ${duration}
-
+
@@ -78,6 +78,11 @@ ${failure-report} """ +const CHARACTERS_TO_ENCODE := { + '<' : '<', + '>' : '>' +} + const TABLE_BY_PATHS = "${report_table_paths}" const TABLE_BY_TESTSUITES = "${report_table_testsuites}" const TABLE_BY_TESTCASES = "${report_table_tests}" @@ -120,8 +125,8 @@ static func build(template: String, report: GdUnitReportSummary, report_link: St return template\ .replace(PATH, get_report_path(report))\ .replace(BREADCRUMP_PATH_LINK, get_path_as_link(report))\ - .replace(RESOURCE_PATH, report.resource_path())\ - .replace(TESTSUITE_NAME, report.name_html_encoded())\ + .replace(RESOURCE_PATH, report.get_resource_path())\ + .replace(TESTSUITE_NAME, html_encoded(report.name()))\ .replace(TESTSUITE_COUNT, str(report.suite_count()))\ .replace(TESTCASE_COUNT, str(report.test_count()))\ .replace(FAILURE_COUNT, str(report.error_count() + report.failure_count()))\ @@ -161,3 +166,34 @@ static func calculate_percentage(p_test_count: int, count: int) -> String: if count <= 0: return "0%" return "%d" % (( 0 if count < 0 else count) * 100.0 / p_test_count) + "%" + + +static func html_encoded(value: String) -> String: + for key: String in CHARACTERS_TO_ENCODE.keys(): + @warning_ignore("unsafe_cast") + value = value.replace(key, CHARACTERS_TO_ENCODE[key] as String) + return value + + +static func create_suite_record(report_link: String, report: GdUnitTestSuiteReport) -> String: + return GdUnitHtmlPatterns.build(GdUnitHtmlPatterns.TABLE_RECORD_TESTSUITE, report, report_link) + + +static func create_test_failure_report(_report_dir :String, report: GdUnitTestCaseReport) -> String: + return GdUnitHtmlPatterns.TABLE_RECORD_TESTCASE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\ + .replace(GdUnitHtmlPatterns.TESTCASE_NAME, report.name())\ + .replace(GdUnitHtmlPatterns.SKIPPED_COUNT, str(report.skipped_count()))\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, report.failure_report()) + + +static func create_suite_failure_report(report: GdUnitTestSuiteReport) -> String: + return GdUnitHtmlPatterns.TABLE_REPORT_TESTSUITE\ + .replace(GdUnitHtmlPatterns.REPORT_STATE, report.report_state().to_lower())\ + .replace(GdUnitHtmlPatterns.REPORT_STATE_LABEL, report.report_state())\ + .replace(GdUnitHtmlPatterns.ORPHAN_COUNT, str(report.orphan_count()))\ + .replace(GdUnitHtmlPatterns.DURATION, LocalTime.elapsed(report._duration))\ + .replace(GdUnitHtmlPatterns.FAILURE_REPORT, report.failure_report()) diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid new file mode 100644 index 00000000..255109ed --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlPatterns.gd.uid @@ -0,0 +1 @@ +uid://dreawonplort diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd new file mode 100644 index 00000000..a3d2bfff --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd @@ -0,0 +1,72 @@ +class_name GdUnitHtmlReportWriter +extends GdUnitReportWriter + + +func output_format() -> String: + return "HTML" + + +func write(report_path: String, report: GdUnitReportSummary) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/index.html") + var to_write := GdUnitHtmlPatterns.build(template, report, "") + to_write = _apply_path_reports(report_path, to_write, report.get_reports()) + to_write = _apply_testsuite_reports(report_path, to_write, report.get_reports()) + # write report + DirAccess.make_dir_recursive_absolute(report_path) + var html_report_file := "%s/index.html" % report_path + FileAccess.open(html_report_file, FileAccess.WRITE).store_string(to_write) + @warning_ignore("return_value_discarded") + GdUnitFileAccess.copy_directory("res://addons/gdUnit4/src/reporters/html/template/css/", report_path + "/css") + return html_report_file + + +func _apply_path_reports(report_dir: String, template: String, report_summaries: Array) -> String: + #Dictionary[String, Array[GdUnitReportSummary]] + var path_report_mapping := GdUnitByPathReport.sort_reports_by_path(report_summaries) + var table_records := PackedStringArray() + var paths: Array[String] = [] + paths.append_array(path_report_mapping.keys()) + paths.sort() + for report_at_path in paths: + var reports: Array[GdUnitReportSummary] = path_report_mapping.get(report_at_path) + var report := GdUnitByPathReport.new(report_at_path, reports) + var report_link: String = report.write(report_dir).replace(report_dir, ".") + @warning_ignore("return_value_discarded") + table_records.append(report.create_record(report_link)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_PATHS, "\n".join(table_records)) + + +func _apply_testsuite_reports(report_dir: String, template: String, test_suite_reports: Array[GdUnitReportSummary]) -> String: + var table_records := PackedStringArray() + for report: GdUnitTestSuiteReport in test_suite_reports: + var report_link: String = _write(report_dir, report).replace(report_dir, ".") + @warning_ignore("return_value_discarded") + table_records.append(GdUnitHtmlPatterns.create_suite_record(report_link, report)) + return template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTSUITES, "\n".join(table_records)) + + +func _write(report_dir :String, report: GdUnitTestSuiteReport) -> String: + var template := GdUnitHtmlPatterns.load_template("res://addons/gdUnit4/src/reporters/html/template/suite_report.html") + template = GdUnitHtmlPatterns.build(template, report, "") + + var report_output_path := create_output_path(report_dir, report.path(), report.name()) + var test_report_table := PackedStringArray() + if not report._failure_reports.is_empty(): + @warning_ignore("return_value_discarded") + test_report_table.append(GdUnitHtmlPatterns.create_suite_failure_report(report)) + for test_report: GdUnitTestCaseReport in report._reports: + @warning_ignore("return_value_discarded") + test_report_table.append(GdUnitHtmlPatterns.create_test_failure_report(report_output_path, test_report)) + + template = template.replace(GdUnitHtmlPatterns.TABLE_BY_TESTCASES, "\n".join(test_report_table)) + + var dir := report_output_path.get_base_dir() + if not DirAccess.dir_exists_absolute(dir): + @warning_ignore("return_value_discarded") + DirAccess.make_dir_recursive_absolute(dir) + FileAccess.open(report_output_path, FileAccess.WRITE).store_string(template) + return report_output_path + + +static func create_output_path(report_dir :String, path: String, name: String) -> String: + return "%s/test_suites/%s.%s.html" % [report_dir, path.replace("/", "."), name] diff --git a/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid new file mode 100644 index 00000000..d56c2aef --- /dev/null +++ b/addons/gdUnit4/src/reporters/html/GdUnitHtmlReportWriter.gd.uid @@ -0,0 +1 @@ +uid://d0gn8sltoi8nm diff --git a/addons/gdUnit4/src/reporters/html/template/.gdignore b/addons/gdUnit4/src/reporters/html/template/.gdignore new file mode 100644 index 00000000..e69de29b diff --git a/addons/gdUnit4/src/report/template/css/breadcrumb.css b/addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css similarity index 100% rename from addons/gdUnit4/src/report/template/css/breadcrumb.css rename to addons/gdUnit4/src/reporters/html/template/css/breadcrumb.css diff --git a/addons/gdUnit4/src/report/template/css/logo.png b/addons/gdUnit4/src/reporters/html/template/css/logo.png similarity index 100% rename from addons/gdUnit4/src/report/template/css/logo.png rename to addons/gdUnit4/src/reporters/html/template/css/logo.png diff --git a/addons/gdUnit4/src/report/template/css/styles.css b/addons/gdUnit4/src/reporters/html/template/css/styles.css similarity index 100% rename from addons/gdUnit4/src/report/template/css/styles.css rename to addons/gdUnit4/src/reporters/html/template/css/styles.css diff --git a/addons/gdUnit4/src/report/template/folder_report.html b/addons/gdUnit4/src/reporters/html/template/folder_report.html similarity index 100% rename from addons/gdUnit4/src/report/template/folder_report.html rename to addons/gdUnit4/src/reporters/html/template/folder_report.html diff --git a/addons/gdUnit4/src/report/template/index.html b/addons/gdUnit4/src/reporters/html/template/index.html similarity index 100% rename from addons/gdUnit4/src/report/template/index.html rename to addons/gdUnit4/src/reporters/html/template/index.html diff --git a/addons/gdUnit4/src/report/template/suite_report.html b/addons/gdUnit4/src/reporters/html/template/suite_report.html similarity index 100% rename from addons/gdUnit4/src/report/template/suite_report.html rename to addons/gdUnit4/src/reporters/html/template/suite_report.html diff --git a/addons/gdUnit4/src/report/JUnitXmlReport.gd b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd similarity index 68% rename from addons/gdUnit4/src/report/JUnitXmlReport.gd rename to addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd index 8921564b..0b9674f3 100644 --- a/addons/gdUnit4/src/report/JUnitXmlReport.gd +++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd @@ -1,7 +1,7 @@ # This class implements the JUnit XML file format # based checked https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd -class_name JUnitXmlReport -extends RefCounted +class_name JUnitXmlReportWriter +extends GdUnitReportWriter const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd") @@ -22,42 +22,40 @@ const ATTR_TYPE := "type" const HEADER := '\n' -var _report_path :String -var _iteration :int +func output_format() -> String: + return "XML" -func _init(path :String, iteration :int) -> void: - _iteration = iteration - _report_path = path - -func write(report :GdUnitReportSummary) -> String: - var result_file: String = "%s/results.xml" % _report_path +func write(report_path: String, report: GdUnitReportSummary) -> String: + var result_file: String = "%s/results.xml" % report_path + DirAccess.make_dir_recursive_absolute(report_path) var file := FileAccess.open(result_file, FileAccess.WRITE) if file == null: push_warning("Can't saving the result to '%s'\n Error: %s" % [result_file, error_string(FileAccess.get_open_error())]) - file.store_string(build_junit_report(report)) + else: + file.store_string(build_junit_report(report_path, report)) return result_file -func build_junit_report(report :GdUnitReportSummary) -> String: +func build_junit_report(report_path: String, report: GdUnitReportSummary) -> String: var iso8601_datetime := Time.get_date_string_from_system() var test_suites := XmlElement.new("testsuites")\ .attribute(ATTR_ID, iso8601_datetime)\ - .attribute(ATTR_NAME, "report_%s" % _iteration)\ + .attribute(ATTR_NAME, report_path.get_file())\ .attribute(ATTR_TESTS, report.test_count())\ .attribute(ATTR_FAILURES, report.failure_count())\ .attribute(ATTR_SKIPPED, report.skipped_count())\ .attribute(ATTR_FLAKY, report.flaky_count())\ - .attribute(ATTR_TIME, JUnitXmlReport.to_time(report.duration()))\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\ .add_childs(build_test_suites(report)) var as_string := test_suites.to_xml() test_suites.dispose() return HEADER + as_string -func build_test_suites(summary :GdUnitReportSummary) -> Array: - var test_suites :Array[XmlElement] = [] +func build_test_suites(summary: GdUnitReportSummary) -> Array: + var test_suites: Array[XmlElement] = [] for index in summary.get_reports().size(): var suite_report :GdUnitTestSuiteReport = summary.get_reports()[index] var iso8601_datetime := Time.get_datetime_string_from_unix_time(suite_report.time_stamp()) @@ -72,49 +70,49 @@ func build_test_suites(summary :GdUnitReportSummary) -> Array: .attribute(ATTR_ERRORS, suite_report.error_count())\ .attribute(ATTR_SKIPPED, suite_report.skipped_count())\ .attribute(ATTR_FLAKY, suite_report.flaky_count())\ - .attribute(ATTR_TIME, JUnitXmlReport.to_time(suite_report.duration()))\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(suite_report.duration()))\ .add_childs(build_test_cases(suite_report))) return test_suites -func build_test_cases(suite_report :GdUnitTestSuiteReport) -> Array: - var test_cases :Array[XmlElement] = [] +func build_test_cases(suite_report: GdUnitTestSuiteReport) -> Array: + var test_cases: Array[XmlElement] = [] for index in suite_report.get_reports().size(): var report :GdUnitTestCaseReport = suite_report.get_reports()[index] test_cases.append( XmlElement.new("testcase")\ - .attribute(ATTR_NAME, JUnitXmlReport.encode_xml(report.name()))\ + .attribute(ATTR_NAME, JUnitXmlReportWriter.encode_xml(report.name()))\ .attribute(ATTR_CLASSNAME, report.suite_name())\ - .attribute(ATTR_TIME, JUnitXmlReport.to_time(report.duration()))\ + .attribute(ATTR_TIME, JUnitXmlReportWriter.to_time(report.duration()))\ .add_childs(build_reports(report))) return test_cases func build_reports(test_report: GdUnitTestCaseReport) -> Array: - var failure_reports :Array[XmlElement] = [] + var failure_reports: Array[XmlElement] = [] for report: GdUnitReport in test_report.get_test_reports(): if report.is_failure(): failure_reports.append(XmlElement.new("failure")\ - .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [test_report._resource_path, report.line_number()])\ - .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ + .attribute(ATTR_MESSAGE, "FAILED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\ .text(convert_rtf_to_text(report.message()))) elif report.is_error(): failure_reports.append(XmlElement.new("error")\ - .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [test_report._resource_path, report.line_number()])\ - .attribute(ATTR_TYPE, JUnitXmlReport.to_type(report.type()))\ + .attribute(ATTR_MESSAGE, "ERROR: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ + .attribute(ATTR_TYPE, JUnitXmlReportWriter.to_type(report.type()))\ .text(convert_rtf_to_text(report.message()))) elif report.is_skipped(): failure_reports.append(XmlElement.new("skipped")\ - .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [test_report._resource_path, report.line_number()])\ + .attribute(ATTR_MESSAGE, "SKIPPED: %s:%d" % [test_report.get_resource_path(), report.line_number()])\ .text(convert_rtf_to_text(report.message()))) return failure_reports -func convert_rtf_to_text(bbcode :String) -> String: +func convert_rtf_to_text(bbcode: String) -> String: return GdUnitTools.richtext_normalize(bbcode) -static func to_type(type :int) -> String: +static func to_type(type: int) -> String: match type: GdUnitReport.SUCCESS: return "SUCCESS" @@ -133,11 +131,11 @@ static func to_type(type :int) -> String: return "UNKNOWN" -static func to_time(duration :int) -> String: +static func to_time(duration: int) -> String: return "%4.03f" % (duration / 1000.0) -static func encode_xml(value :String) -> String: +static func encode_xml(value: String) -> String: return value.xml_escape(true) diff --git a/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid new file mode 100644 index 00000000..18df0ee7 --- /dev/null +++ b/addons/gdUnit4/src/reporters/xml/JUnitXmlReportWriter.gd.uid @@ -0,0 +1 @@ +uid://drb733xw634rd diff --git a/addons/gdUnit4/src/report/XmlElement.gd b/addons/gdUnit4/src/reporters/xml/XmlElement.gd similarity index 100% rename from addons/gdUnit4/src/report/XmlElement.gd rename to addons/gdUnit4/src/reporters/xml/XmlElement.gd diff --git a/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid new file mode 100644 index 00000000..8fbd8da7 --- /dev/null +++ b/addons/gdUnit4/src/reporters/xml/XmlElement.gd.uid @@ -0,0 +1 @@ +uid://6vceptdvowpg diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd index 3bd4b2ea..5d8b6318 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd @@ -38,16 +38,14 @@ static func build(to_spy: Variant, debug_write := false) -> Variant: var spy := spy_on_script(to_spy, excluded_functions, debug_write) if spy == null: return null - var spy_instance :Object = spy.new() + var spy_instance: Object = spy.new() + @warning_ignore("unsafe_method_access") + # we do not call the original implementation for _ready and all input function, this is actualy done by the engine + spy_instance.__init(["_input", "_gui_input", "_input_event", "_unhandled_input"]) @warning_ignore("unsafe_cast") copy_properties(to_spy as Object, spy_instance) @warning_ignore("return_value_discarded") GdUnitObjectInteractions.reset(spy_instance) - @warning_ignore("unsafe_method_access") - spy_instance.__set_singleton(to_spy) - # we do not call the original implementation for _ready and all input function, this is actualy done by the engine - @warning_ignore("unsafe_method_access") - spy_instance.__exclude_method_call([ "_input", "_gui_input", "_input_event", "_unhandled_input"]) return register_auto_free(spy_instance) @@ -60,7 +58,7 @@ static func get_class_info(clazz :Variant) -> Dictionary: } -static func spy_on_script(instance :Variant, function_excludes :PackedStringArray, debug_write :bool) -> GDScript: +static func spy_on_script(instance: Variant, function_excludes: PackedStringArray, debug_write: bool) -> GDScript: if GdArrayTools.is_array_type(instance): if GdUnitSettings.is_verbose_assert_errors(): push_error("Can't build spy checked type '%s'! Spy checked Container Built-In Type not supported!" % type_string(typeof(instance))) @@ -72,10 +70,20 @@ static func spy_on_script(instance :Variant, function_excludes :PackedStringArra if GdUnitSettings.is_verbose_assert_errors(): push_error("Can't build spy for class type '%s'! Using an instance instead e.g. 'spy()'" % [clazz_name]) return null + + @warning_ignore("unsafe_method_access") + var spy_template := SPY_TEMPLATE.source_code.format({ + "instance_id" : abs(instance.get_instance_id()), + "gdunit_source_class": clazz_name if clazz_path.is_empty() else clazz_path[0] + }) @warning_ignore("unsafe_cast") - var lines := load_template(SPY_TEMPLATE.source_code, class_info, instance as Object) + var lines := load_template(spy_template, class_info) @warning_ignore("unsafe_cast") lines += double_functions(instance as Object, clazz_name, clazz_path, GdUnitSpyFunctionDoubler.new(), function_excludes) + # We disable warning/errors for inferred_declaration + if Engine.get_version_info().hex >= 0x40400: + lines.insert(0, '@warning_ignore_start("inferred_declaration")') + lines.append('@warning_ignore_restore("inferred_declaration")') var spy := GDScript.new() spy.source_code = "\n".join(lines) @@ -106,8 +114,23 @@ static func spy_on_scene(scene :Node, debug_write :bool) -> Object: scene_script.free() if spy == null: return null - # replace original script whit spy + + # we need to restore the original script properties to apply after script exchange + var original_properties := {} + for p in scene.get_property_list(): + var property_name: String = p["name"] + var usage: int = p["usage"] + if (usage & PROPERTY_USAGE_SCRIPT_VARIABLE) == PROPERTY_USAGE_SCRIPT_VARIABLE: + original_properties[property_name] = scene.get(property_name) + + # exchage with spy scene.set_script(spy) + # apply original script properties to the spy + for property_name: String in original_properties.keys(): + scene.set(property_name, original_properties[property_name]) + + @warning_ignore("unsafe_method_access") + scene.__init() return register_auto_free(scene) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid index e436594c..63068f4f 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid +++ b/addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd.uid @@ -1 +1 @@ -uid://dl5h47ho5haor +uid://lnb8k70lrdt1 diff --git a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd deleted file mode 100644 index c20061b1..00000000 --- a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd +++ /dev/null @@ -1,97 +0,0 @@ -class_name GdUnitSpyFunctionDoubler -extends GdFunctionDoubler - - -const TEMPLATE_RETURN_VARIANT = """ - var args__: Array = ["$(func_name)", $(arguments)] - - if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args__) - return ${default_return_value} - else: - $(instance)__save_function_interaction(args__) - - if $(instance)__do_call_real_func("$(func_name)"): - return $(await)super($(arguments)) - return ${default_return_value} - -""" - - -const TEMPLATE_RETURN_VOID = """ - var args__: Array = ["$(func_name)", $(arguments)] - - if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args__) - return - else: - $(instance)__save_function_interaction(args__) - - if $(instance)__do_call_real_func("$(func_name)"): - $(await)super($(arguments)) - -""" - - -const TEMPLATE_RETURN_VOID_VARARG = """ - var varargs__: Array = __filter_vargs([$(varargs)]) - var args__: Array = ["$(func_name)", $(arguments)] + varargs__ - - if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args__) - return - else: - $(instance)__save_function_interaction(args__) - - $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs__) - -""" - - -const TEMPLATE_RETURN_VARIANT_VARARG = """ - var varargs__: Array = __filter_vargs([$(varargs)]) - var args__: Array = ["$(func_name)", $(arguments)] + varargs__ - - if $(instance)__is_verify_interactions(): - $(instance)__verify_interactions(args__) - return ${default_return_value} - else: - $(instance)__save_function_interaction(args__) - - return $(await)$(instance)__call_func("$(func_name)", [$(arguments)] + varargs__) - -""" - - -const TEMPLATE_CALLABLE_CALL = """ - var used_arguments__ := __filter_vargs([$(arguments)]) - - if __is_verify_interactions(): - __verify_interactions(["call", used_arguments__]) - return ${default_return_value} - else: - # append possible binded values to complete the original argument list - var args__ := used_arguments__.duplicate() - args__.append_array(super.get_bound_arguments()) - __save_function_interaction(["call", args__]) - - if __do_call_real_func("call"): - return _cb.callv(used_arguments__) - return ${default_return_value} - -""" - - -func _init(push_errors :bool = false) -> void: - super._init(push_errors) - - -func get_template(fd: GdFunctionDescriptor, is_callable: bool) -> String: - if is_callable and fd.name() == "call": - return TEMPLATE_CALLABLE_CALL - if fd.is_vararg(): - return TEMPLATE_RETURN_VOID_VARARG if fd.return_type() == TYPE_NIL else TEMPLATE_RETURN_VARIANT_VARARG - var return_type :Variant = fd.return_type() - if return_type is StringName: - return TEMPLATE_RETURN_VARIANT - return TEMPLATE_RETURN_VOID if (return_type == TYPE_NIL or return_type == GdObjects.TYPE_VOID) else TEMPLATE_RETURN_VARIANT diff --git a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid deleted file mode 100644 index b8864b63..00000000 --- a/addons/gdUnit4/src/spy/GdUnitSpyFunctionDoubler.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bwjrsh0vhg3ya diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd index 3b61e869..e0bcaf2b 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd @@ -1,44 +1,46 @@ +class_name DoubledSpyClassSourceClassName -const __INSTANCE_ID = "${instance_id}" -const __SOURCE_CLASS = "${source_class}" +const __INSTANCE_ID := "gdunit_doubler_instance_id_{instance_id}" -var __instance_delegator :Object -var __excluded_methods :PackedStringArray = [] +class GdUnitSpyDoublerState: + const __SOURCE_CLASS := "{gdunit_source_class}" -static func __instance() -> Variant: - return Engine.get_meta(__INSTANCE_ID) + var excluded_methods := PackedStringArray() + func _init(excluded_methods__ := PackedStringArray()) -> void: + excluded_methods = excluded_methods__ -func _notification(what :int) -> void: - if what == NOTIFICATION_PREDELETE: - if Engine.has_meta(__INSTANCE_ID): - Engine.remove_meta(__INSTANCE_ID) +var __spy_state := GdUnitSpyDoublerState.new() +@warning_ignore("unused_private_class_variable") +var __verifier_instance := GdUnitObjectInteractionsVerifier.new() -func __instance_id() -> String: - return __INSTANCE_ID +func __init(__excluded_methods := PackedStringArray()) -> void: + __init_doubler() + __spy_state.excluded_methods = __excluded_methods -func __set_singleton(delegator :Object) -> void: - # store self need to mock static functions - Engine.set_meta(__INSTANCE_ID, self) - __instance_delegator = delegator + +static func __doubler_state() -> GdUnitSpyDoublerState: + if Engine.has_meta(__INSTANCE_ID): + return Engine.get_meta(__INSTANCE_ID).__spy_state + return null -func __release_double() -> void: - # we need to release the self reference manually to prevent orphan nodes - Engine.remove_meta(__INSTANCE_ID) - __instance_delegator = null +func __init_doubler() -> void: + Engine.set_meta(__INSTANCE_ID, self) -func __do_call_real_func(func_name :String) -> bool: - return not __excluded_methods.has(func_name) +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE and Engine.has_meta(__INSTANCE_ID): + Engine.remove_meta(__INSTANCE_ID) -func __exclude_method_call(exluded_methods :PackedStringArray) -> void: - __excluded_methods.append_array(exluded_methods) +static func __get_verifier() -> GdUnitObjectInteractionsVerifier: + return Engine.get_meta(__INSTANCE_ID).__verifier_instance -func __call_func(func_name :String, arguments :Array) -> Variant: - return __instance_delegator.callv(func_name, arguments) +static func __do_call_real_func(__func_name: String) -> bool: + @warning_ignore("unsafe_method_access") + return not __doubler_state().excluded_methods.has(__func_name) diff --git a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid index 93720a0e..9eee162c 100644 --- a/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid +++ b/addons/gdUnit4/src/spy/GdUnitSpyImpl.gd.uid @@ -1 +1 @@ -uid://dobfm3i0vhdkw +uid://o7r8i8uino0m diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd b/addons/gdUnit4/src/ui/GdUnitConsole.gd index 572146ae..eb1e6254 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.gd +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd @@ -8,35 +8,24 @@ const TITLE = "gdUnit4 ${version} Console" @onready var title: RichTextLabel = $VBoxContainer/Header/header_title @onready var output: RichTextLabel = $VBoxContainer/Console/TextEdit -var _text_color: Color -var _function_color: Color -var _engine_type_color: Color -var _statistics := {} -var _summary := { - "total_count": 0, - "error_count": 0, - "failed_count": 0, - "skipped_count": 0, - "flaky_count": 0, - "orphan_nodes": 0 -} + +var _test_reporter: GdUnitConsoleTestReporter @warning_ignore("return_value_discarded") func _ready() -> void: - init_colors() GdUnitFonts.init_fonts(output) GdUnit4Version.init_version_label(title) GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) GdUnitSignals.instance().gdunit_message.connect(_on_gdunit_message) GdUnitSignals.instance().gdunit_client_connected.connect(_on_gdunit_client_connected) GdUnitSignals.instance().gdunit_client_disconnected.connect(_on_gdunit_client_disconnected) - output.clear() + _test_reporter = GdUnitConsoleTestReporter.new(GdUnitRichTextMessageWriter.new(output)) func _notification(what: int) -> void: if what == EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED: - init_colors() + _test_reporter.init_colors() if what == NOTIFICATION_PREDELETE: var instance := GdUnitSignals.instance() if instance.gdunit_event.is_connected(_on_gdunit_event): @@ -49,85 +38,29 @@ func _notification(what: int) -> void: instance.gdunit_client_disconnected.disconnect(_on_gdunit_client_disconnected) -func init_colors() -> void: - var settings := EditorInterface.get_editor_settings() - _text_color = settings.get_setting("text_editor/theme/highlighting/text_color") - _function_color = settings.get_setting("text_editor/theme/highlighting/function_color") - _engine_type_color = settings.get_setting("text_editor/theme/highlighting/engine_type_color") - - -func init_statistics(event: GdUnitEvent) -> void: - _statistics["total_count"] = event.total_count() - _statistics["error_count"] = 0 - _statistics["failed_count"] = 0 - _statistics["skipped_count"] = 0 - _statistics["flaky_count"] = 0 - _statistics["orphan_nodes"] = 0 - _summary["total_count"] += event.total_count() - - -func reset_statistics() -> void: - for k: String in _statistics.keys(): - _statistics[k] = 0 - for k: String in _summary.keys(): - _summary[k] = 0 - - -func update_statistics(event: GdUnitEvent) -> void: - _statistics["error_count"] += event.error_count() - _statistics["failed_count"] += event.failed_count() - _statistics["skipped_count"] += event.is_skipped() as int - _statistics["flaky_count"] += event.is_flaky() as int - _statistics["orphan_nodes"] += event.orphan_nodes() - _summary["error_count"] += event.error_count() - _summary["failed_count"] += event.failed_count() - _summary["skipped_count"] += event.is_skipped() as int - _summary["flaky_count"] += event.is_flaky() as int - _summary["orphan_nodes"] += event.orphan_nodes() - - -func print_message(message: String, color: Color=_text_color, indent:=0) -> void: - for i in indent: - output.push_indent(1) - output.push_color(color) - output.append_text(message) - output.pop() - for i in indent: - output.pop() - - -func println_message(message: String, color: Color=_text_color, indent:=-1) -> void: - print_message(message, color, indent) - output.newline() - - -func line_number(report: GdUnitReport) -> String: - return str(report._line_number) if report._line_number != -1 else "" - - func setup_update_notification(control: Button) -> void: if not GdUnitSettings.is_update_notification_enabled(): - print_message("The search for updates is deactivated.", Color.CORNFLOWER_BLUE) + _test_reporter.println_message("The search for updates is deactivated.", Color.CORNFLOWER_BLUE) return - println_message("Searching for updates.", Color.CORNFLOWER_BLUE) + _test_reporter.print_message("Searching for updates... ", Color.CORNFLOWER_BLUE) var update_client := GdUnitUpdateClient.new() add_child(update_client) var response :GdUnitUpdateClient.HttpResponse = await update_client.request_latest_version() if response.status() != 200: - println_message("Information cannot be retrieved from GitHub!", Color.INDIAN_RED) - println_message("Error: %s" % response.response(), Color.INDIAN_RED) + _test_reporter.println_message("Information cannot be retrieved from GitHub!", Color.INDIAN_RED) + _test_reporter.println_message("Error: %s" % response.response(), Color.INDIAN_RED) return var latest_version := update_client.extract_latest_version(response) if not latest_version.is_greater(GdUnit4Version.current()): - println_message("GdUnit4 is up-to-date.", Color.FOREST_GREEN) + _test_reporter.println_message("GdUnit4 is up-to-date.", Color.FOREST_GREEN) return - println_message("A new update is available %s" % latest_version, Color.YELLOW) - println_message("Open the GdUnit4 settings and check the update tab.", Color.YELLOW) + _test_reporter.println_message("A new update is available %s" % latest_version, Color.YELLOW) + _test_reporter.println_message("Open the GdUnit4 settings and check the update tab.", Color.YELLOW) control.icon = GdUnitUiTools.get_icon("Notification", Color.YELLOW) - var tween :=create_tween() + var tween := create_tween() tween.tween_property(control, "self_modulate", Color.VIOLET, .2).set_trans(Tween.TransitionType.TRANS_LINEAR) tween.tween_property(control, "self_modulate", Color.YELLOW, .2).set_trans(Tween.TransitionType.TRANS_BOUNCE) tween.parallel() @@ -139,92 +72,20 @@ func setup_update_notification(control: Button) -> void: func _on_gdunit_event(event: GdUnitEvent) -> void: match event.type(): - GdUnitEvent.INIT: - reset_statistics() - - GdUnitEvent.STOP: - print_message("Summary:", Color.DODGER_BLUE) - println_message("| %d total | %d error | %d failed | %d flaky | %d skipped | %d orphans |" %\ - [_summary["total_count"], - _summary["error_count"], - _summary["failed_count"], - _summary["flaky_count"], - _summary["skipped_count"], - _summary["orphan_nodes"]], - _text_color, 1) - print_message("[wave][/wave]") - - GdUnitEvent.TESTSUITE_BEFORE: - init_statistics(event) - print_message("Execute: ", Color.DODGER_BLUE) - println_message(event._suite_name, _engine_type_color) - - GdUnitEvent.TESTSUITE_AFTER: - if not event.reports().is_empty(): - println_message("\t" + event._suite_name, _engine_type_color) - for report: GdUnitReport in event.reports(): - println_message("line %s: %s" % [line_number(report), report._message], _text_color, 2) - if event.is_success() and event.is_flaky(): - print_message("[wave]FLAKY[/wave]", Color.GREEN_YELLOW) - elif event.is_success(): - print_message("[wave]PASSED[/wave]", Color.LIGHT_GREEN) - else: - print_message("[shake rate=5 level=10][b]FAILED[/b][/shake]", Color.FIREBRICK) - print_message(" | %d total | %d error | %d failed | %d flaky | %d skipped | %d orphans |" %\ - [_statistics["total_count"], - _statistics["error_count"], - _statistics["failed_count"], - _statistics["flaky_count"], - _statistics["skipped_count"], - _statistics["orphan_nodes"]]) - println_message("%+12s" % LocalTime.elapsed(event.elapsed_time())) - println_message(" ") - - - GdUnitEvent.TESTCASE_BEFORE: - var spaces := "-%d" % (80 - event._suite_name.length()) - print_message(event._suite_name, _engine_type_color, 1) - print_message(":") - print_message(("%" + spaces + "s") % event._test_name, _function_color) - - GdUnitEvent.TESTCASE_AFTER: - var reports := event.reports() - if event.is_flaky() and event.is_success(): - var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) - print_message("[wave]FLAKY[/wave] (%d retries)" % retries, Color.GREEN_YELLOW) - elif event.is_success(): - print_message("PASSED", Color.LIGHT_GREEN) - elif event.is_skipped(): - print_message("SKIPPED", Color.GOLDENROD) - elif event.is_error() or event.is_failed(): - var retries :int = event.statistic(GdUnitEvent.RETRY_COUNT) - if retries > 1: - print_message("[wave]FAILED[/wave] (retry %d)" % retries, Color.FIREBRICK) - else: - print_message("[wave]FAILED[/wave]", Color.FIREBRICK) - elif event.is_warning(): - print_message("WARNING", Color.YELLOW) - println_message(" %+12s" % LocalTime.elapsed(event.elapsed_time())) - - for report: GdUnitReport in event.reports(): - println_message("line %s: %s" % [line_number(report), report._message], _text_color, 2) - - GdUnitEvent.TESTCASE_STATISTICS: - update_statistics(event) + GdUnitEvent.SESSION_START: + _test_reporter.test_session = GdUnitTestSession.new(GdUnitTestDiscoverGuard.instance().get_discovered_tests(), "") + GdUnitEvent.SESSION_CLOSE: + _test_reporter.test_session = null func _on_gdunit_client_connected(client_id: int) -> void: - output.clear() - output.append_text("[color=#9887c4]GdUnit Test Client connected with id %d[/color]\n" % client_id) - output.newline() + _test_reporter.clear() + _test_reporter.println_message("GdUnit Test Client connected with id: %d" % client_id, Color.hex(0x9887c4)) func _on_gdunit_client_disconnected(client_id: int) -> void: - output.append_text("[color=#9887c4]GdUnit Test Client disconnected with id %d[/color]\n" % client_id) - output.newline() + _test_reporter.println_message("GdUnit Test Client disconnected with id: %d" % client_id, Color.hex(0x9887c4)) func _on_gdunit_message(message: String) -> void: - output.newline() - output.append_text(message) - output.newline() + _test_reporter.println_message(message, Color.CORNFLOWER_BLUE) diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid index e7c472ad..8b316595 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitConsole.gd.uid @@ -1 +1 @@ -uid://6g5u60ljn4tj +uid://dm710bg0dhn4o diff --git a/addons/gdUnit4/src/ui/GdUnitConsole.tscn b/addons/gdUnit4/src/ui/GdUnitConsole.tscn index b501fee4..c3c7e29f 100644 --- a/addons/gdUnit4/src/ui/GdUnitConsole.tscn +++ b/addons/gdUnit4/src/ui/GdUnitConsole.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://dm0wvfyeew7vd"] -[ext_resource type="Script" uid="uid://6g5u60ljn4tj" path="res://addons/gdUnit4/src/ui/GdUnitConsole.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitConsole.gd" id="1"] [node name="Control" type="Control"] use_parent_material = true @@ -29,17 +29,17 @@ size_flags_horizontal = 3 size_flags_vertical = 3 [node name="Header" type="PanelContainer" parent="VBoxContainer"] +auto_translate_mode = 2 custom_minimum_size = Vector2(0, 32) layout_mode = 2 -auto_translate = false localize_numeral_system = false mouse_filter = 2 [node name="header_title" type="RichTextLabel" parent="VBoxContainer/Header"] +auto_translate_mode = 2 layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 -auto_translate = false localize_numeral_system = false mouse_filter = 2 bbcode_enabled = true @@ -57,5 +57,8 @@ use_parent_material = true layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +focus_mode = 2 bbcode_enabled = true scroll_following = true +context_menu_enabled = true +selection_enabled = true diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd b/addons/gdUnit4/src/ui/GdUnitFonts.gd index 24483454..0cb0f5d5 100644 --- a/addons/gdUnit4/src/ui/GdUnitFonts.gd +++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd @@ -1,28 +1,31 @@ +@tool class_name GdUnitFonts extends RefCounted -const FONT_MONO = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf" -const FONT_MONO_BOLT = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf" -const FONT_MONO_BOLT_ITALIC = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf" -const FONT_MONO_ITALIC = "res://addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf" - static func init_fonts(item: CanvasItem) -> float: - # add a default fallback font - item.set("theme_override_fonts/font", load_and_resize_font(FONT_MONO, 16)) - item.set("theme_override_fonts/normal_font", load_and_resize_font(FONT_MONO, 16)) + # set default size item.set("theme_override_font_sizes/font_size", 16) + if Engine.is_editor_hint(): + var base_control := EditorInterface.get_base_control() + # source modules/mono/editor/GodotTools/GodotTools/Build/BuildOutputView.cs + # https://github.com/godotengine/godot/blob/9ee1873ae1e09c217ac24a5800007f63cb895615/editor/editor_log.cpp#L65 + var output_source_mono := base_control.get_theme_font("output_source_mono", "EditorFonts") + var output_source_bold_italic := base_control.get_theme_font("output_source_bold_italic", "EditorFonts") + var output_source_italic := base_control.get_theme_font("output_source_italic", "EditorFonts") + var output_source_bold := base_control.get_theme_font("output_source_bold", "EditorFonts") + var output_source := base_control.get_theme_font("output_source", "EditorFonts") var settings := EditorInterface.get_editor_settings() var scale_factor := EditorInterface.get_editor_scale() var font_size: float = settings.get_setting("interface/editor/main_font_size") + font_size *= scale_factor - var font_mono := load_and_resize_font(FONT_MONO, font_size) - item.set("theme_override_fonts/normal_font", font_mono) - item.set("theme_override_fonts/bold_font", load_and_resize_font(FONT_MONO_BOLT, font_size)) - item.set("theme_override_fonts/italics_font", load_and_resize_font(FONT_MONO_ITALIC, font_size)) - item.set("theme_override_fonts/bold_italics_font", load_and_resize_font(FONT_MONO_BOLT_ITALIC, font_size)) - item.set("theme_override_fonts/mono_font", font_mono) + item.set("theme_override_fonts/normal_font", output_source) + item.set("theme_override_fonts/bold_font", output_source_bold) + item.set("theme_override_fonts/italics_font", output_source_italic) + item.set("theme_override_fonts/bold_italics_font", output_source_bold_italic) + item.set("theme_override_fonts/mono_font", output_source_mono) item.set("theme_override_font_sizes/font_size", font_size) item.set("theme_override_font_sizes/normal_font_size", font_size) item.set("theme_override_font_sizes/bold_font_size", font_size) @@ -31,13 +34,3 @@ static func init_fonts(item: CanvasItem) -> float: item.set("theme_override_font_sizes/mono_font_size", font_size) return font_size return 16.0 - - -static func load_and_resize_font(font_resource: String, size: float) -> FontFile: - var font: FontFile = ResourceLoader.load(font_resource, "FontFile") - if font == null: - push_error("Can't load font '%s'" % font_resource) - return null - var resized_font: FontFile = font.duplicate() - resized_font.fixed_size = int(size) - return resized_font diff --git a/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid index 2eae6ff4..734f7c91 100644 --- a/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitFonts.gd.uid @@ -1 +1 @@ -uid://cic8tb4hofnai +uid://1r0ywt2xtb25 diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd b/addons/gdUnit4/src/ui/GdUnitInspector.gd index 55bcba3e..a6d53f9b 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd @@ -2,15 +2,11 @@ class_name GdUnitInspecor extends Panel -const ScriptEditorContextMenuHandler = preload("res://addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd") -const EditorFileSystemContextMenuHandler = preload("res://addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd") var _command_handler := GdUnitCommandHandler.instance() func _ready() -> void: - if Engine.is_editor_hint(): - _getEditorThemes() @warning_ignore("return_value_discarded") GdUnitCommandHandler.instance().gdunit_runner_start.connect(func() -> void: var control :Control = get_parent_control() @@ -21,65 +17,15 @@ func _ready() -> void: if tab_container.get_tab_title(tab_index) == "GdUnit": tab_container.set_current_tab(tab_index) ) - if Engine.is_editor_hint(): - add_script_editor_context_menu() - add_file_system_dock_context_menu() + # propagete the test_counters_changed signal to the progress bar + @warning_ignore("unsafe_property_access", "unsafe_method_access") + %MainPanel.test_counters_changed.connect(%ProgressBar._on_test_counter_changed) func _process(_delta: float) -> void: _command_handler._do_process() -func _getEditorThemes() -> void: - # example to access current theme - #var editiorTheme := interface.get_base_control().theme - # setup inspector button icons - #var stylebox_types :PackedStringArray = editiorTheme.get_stylebox_type_list() - #for stylebox_type in stylebox_types: - #prints("stylebox_type", stylebox_type) - # if "Tree" == stylebox_type: - # prints(editiorTheme.get_stylebox_list(stylebox_type)) - #var style:StyleBoxFlat = editiorTheme.get_stylebox("panel", "Tree") - #style.bg_color = Color.RED - #var locale = interface.get_editor_settings().get_setting("interface/editor/editor_language") - #sessions_label.add_theme_color_override("font_color", get_color("contrast_color_2", "Editor")) - #status_label.add_theme_color_override("font_color", get_color("contrast_color_2", "Editor")) - #no_sessions_label.add_theme_color_override("font_color", get_color("contrast_color_2", "Editor")) - pass - - -# Context menu registrations ---------------------------------------------------------------------- -func add_file_system_dock_context_menu() -> void: - var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: - if script == null: - return false - return GdObjects.is_test_suite(script) == is_ts - var menu :Array[GdUnitContextMenuItem] = [ - GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Testsuites", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE)), - GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Testsuites", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG)), - ] - add_child(EditorFileSystemContextMenuHandler.new(menu)) - - -func add_script_editor_context_menu() -> void: - var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: - return GdObjects.is_test_suite(script) == is_ts - var menu :Array[GdUnitContextMenuItem] = [ - GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), - GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Tests", "PlayStart", is_test_suite.bind(true),_command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG)), - GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.CREATE_TEST, "Create Test", "New", is_test_suite.bind(false), _command_handler.command(GdUnitCommandHandler.CMD_CREATE_TESTCASE)) - ] - add_child(ScriptEditorContextMenuHandler.new(menu)) - - -func _on_MainPanel_run_testsuite(test_suite_paths: Array, debug: bool) -> void: - _command_handler.cmd_run_test_suites(test_suite_paths, debug) - - -func _on_MainPanel_run_testcase(resource_path: String, test_case: String, test_param_index: int, debug: bool) -> void: - _command_handler.cmd_run_test_case(resource_path, test_case, test_param_index, debug) - - @warning_ignore("redundant_await") func _on_status_bar_request_discover_tests() -> void: await _command_handler.cmd_discover_tests() diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid index 65f4a620..eb4459ff 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitInspector.gd.uid @@ -1 +1 @@ -uid://dpiqmjh6sedtd +uid://dlur1qir5aduv diff --git a/addons/gdUnit4/src/ui/GdUnitInspector.tscn b/addons/gdUnit4/src/ui/GdUnitInspector.tscn index 01961d44..2dcb9505 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspector.tscn +++ b/addons/gdUnit4/src/ui/GdUnitInspector.tscn @@ -1,12 +1,12 @@ [gd_scene load_steps=8 format=3 uid="uid://mpo5o6d4uybu"] -[ext_resource type="PackedScene" uid="uid://dx7xy4dgi3wwb" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn" id="1"] -[ext_resource type="PackedScene" uid="uid://dva3tonxsxrlk" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn" id="2"] -[ext_resource type="PackedScene" uid="uid://c22l4odk7qesc" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn" id="3"] -[ext_resource type="PackedScene" uid="uid://djp8ait0bxpsc" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn" id="4"] -[ext_resource type="Script" uid="uid://dpiqmjh6sedtd" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"] -[ext_resource type="PackedScene" uid="uid://bqfpidewtpeg0" path="res://addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn" id="7"] -[ext_resource type="PackedScene" uid="uid://cn5mp3tmi2gb1" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn" id="1"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn" id="2"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn" id="3"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn" id="4"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/GdUnitInspector.gd" id="5"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn" id="7"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/network/GdUnitServer.tscn" id="7_721no"] [node name="GdUnit" type="Panel"] use_parent_material = true @@ -26,18 +26,21 @@ layout_mode = 0 anchor_right = 1.0 anchor_bottom = 1.0 size_flags_vertical = 11 +theme_override_constants/separation = 0 [node name="Header" type="VBoxContainer" parent="VBoxContainer"] use_parent_material = true clip_contents = true layout_mode = 2 size_flags_horizontal = 9 +size_flags_vertical = 0 [node name="ToolBar" parent="VBoxContainer/Header" instance=ExtResource("1")] layout_mode = 2 size_flags_vertical = 1 [node name="ProgressBar" parent="VBoxContainer/Header" instance=ExtResource("2")] +unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 5 max_value = 0.0 @@ -47,6 +50,7 @@ layout_mode = 2 size_flags_horizontal = 11 [node name="MainPanel" parent="VBoxContainer" instance=ExtResource("7")] +unique_name_in_owner = true layout_mode = 2 [node name="Monitor" parent="VBoxContainer" instance=ExtResource("4")] @@ -55,13 +59,13 @@ layout_mode = 2 [node name="event_server" parent="." instance=ExtResource("7_721no")] [connection signal="request_discover_tests" from="VBoxContainer/Header/StatusBar" to="." method="_on_status_bar_request_discover_tests"] -[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]] -[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]] -[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]] -[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]] -[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [4]] -[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [4]] +[connection signal="select_error_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [7]] +[connection signal="select_error_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [7]] +[connection signal="select_failure_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [6]] +[connection signal="select_failure_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [6]] +[connection signal="select_flaky_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [5]] +[connection signal="select_flaky_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [5]] +[connection signal="select_skipped_next" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_next_item_by_state" binds= [2]] +[connection signal="select_skipped_prevous" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_select_previous_item_by_state" binds= [2]] [connection signal="tree_view_mode_changed" from="VBoxContainer/Header/StatusBar" to="VBoxContainer/MainPanel" method="_on_status_bar_tree_view_mode_changed"] -[connection signal="run_testcase" from="VBoxContainer/MainPanel" to="." method="_on_MainPanel_run_testcase"] -[connection signal="run_testsuite" from="VBoxContainer/MainPanel" to="." method="_on_MainPanel_run_testsuite"] [connection signal="jump_to_orphan_nodes" from="VBoxContainer/Monitor" to="VBoxContainer/MainPanel" method="select_first_orphan"] diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd index 8b0e06b9..8dd0265d 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd +++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd @@ -16,3 +16,16 @@ enum SORT_MODE { NAME_DESCENDING, EXECUTION_TIME } + + +enum STATE { + INITIAL, + RUNNING, + SKIPPED, + SUCCESS, + WARNING, + FLAKY, + FAILED, + ERROR, + ABORDED, +} diff --git a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid index e8a1abe4..cdf3a7e4 100644 --- a/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitInspectorTreeConstants.gd.uid @@ -1 +1 @@ -uid://gpao4sfhw15o +uid://catqq5jt2ac2e diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd b/addons/gdUnit4/src/ui/GdUnitUiTools.gd index dd8f9f1b..0bcdb1d2 100644 --- a/addons/gdUnit4/src/ui/GdUnitUiTools.gd +++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd @@ -46,6 +46,8 @@ static func get_spinner() -> AnimatedTexture: static func get_color_animated_icon(icon_name :String, from :Color, to :Color) -> AnimatedTexture: + if not Engine.is_editor_hint(): + return null var texture := AnimatedTexture.new() texture.frames = 8 texture.speed_scale = 2.5 @@ -124,6 +126,7 @@ static func _merge_images_scaled(image1: Image, offset1: Vector2i, image2: Image # Create a new Image for the merged result var merged_image := Image.create(image1.get_width(), image1.get_height(), false, image1.get_format()) merged_image.blend_rect(image1, Rect2(Vector2.ZERO, image1.get_size()), offset1) + @warning_ignore("narrowing_conversion") image2.resize(image2.get_width()/1.3, image2.get_height()/1.3) merged_image.blend_rect(image2, Rect2(Vector2.ZERO, image2.get_size()), offset2) return merged_image diff --git a/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid index e9b54558..bf36456e 100644 --- a/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid +++ b/addons/gdUnit4/src/ui/GdUnitUiTools.gd.uid @@ -1 +1 @@ -uid://be0s45g6dqb6f +uid://dug2bl60wnj6b diff --git a/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid b/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid index 9213ee32..89d0f274 100644 --- a/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid +++ b/addons/gdUnit4/src/ui/ScriptEditorControls.gd.uid @@ -1 +1 @@ -uid://cfe1tjhth84my +uid://1n82qpd4r6x5 diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd index 29e7d620..044b9efc 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd @@ -2,10 +2,20 @@ extends Control var _context_menus := Dictionary() +var _command_handler := GdUnitCommandHandler.instance() -func _init(context_menus: Array[GdUnitContextMenuItem]) -> void: +func _init() -> void: set_name("EditorFileSystemContextMenuHandler") + + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + if script == null: + return false + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + var context_menus :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Testsuites", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Testsuites", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG)), + ] for menu in context_menus: _context_menus[menu.id] = menu var popup := _menu_popup() @@ -19,44 +29,44 @@ func _init(context_menus: Array[GdUnitContextMenuItem]) -> void: func on_context_menu_show(context_menu: PopupMenu, file_tree: Tree) -> void: context_menu.add_separator() var current_index := context_menu.get_item_count() - var selected_test_suites := collect_testsuites(_context_menus.values()[0] as GdUnitContextMenuItem, file_tree) for menu_id: int in _context_menus.keys(): var menu_item: GdUnitContextMenuItem = _context_menus[menu_id] - if selected_test_suites.size() != 0: - context_menu.add_item(menu_item.name, menu_id) - #context_menu.set_item_icon_modulate(current_index, Color.MEDIUM_PURPLE) - context_menu.set_item_disabled(current_index, !menu_item.is_enabled(null)) - context_menu.set_item_icon(current_index, GdUnitUiTools.get_icon(menu_item.icon)) - current_index += 1 + + context_menu.add_item(menu_item.name, menu_id) + #context_menu.set_item_icon_modulate(current_index, Color.MEDIUM_PURPLE) + context_menu.set_item_disabled(current_index, !menu_item.is_enabled(null)) + context_menu.set_item_icon(current_index, GdUnitUiTools.get_icon(menu_item.icon)) + current_index += 1 func on_context_menu_pressed(id: int, file_tree: Tree) -> void: if !_context_menus.has(id): return var menu_item: GdUnitContextMenuItem = _context_menus[id] - var selected_test_suites := collect_testsuites(menu_item, file_tree) - menu_item.execute([selected_test_suites]) + var test_suites := collect_testsuites(menu_item, file_tree) + + menu_item.execute([test_suites]) -func collect_testsuites(_menu_item: GdUnitContextMenuItem, file_tree: Tree) -> PackedStringArray: +func collect_testsuites(_menu_item: GdUnitContextMenuItem, file_tree: Tree) -> Array[Script]: var file_system := EditorInterface.get_resource_filesystem() var selected_item := file_tree.get_selected() - var selected_test_suites := PackedStringArray() + var selected_test_suites: Array[Script] = [] + var suite_scaner := GdUnitTestSuiteScanner.new() while selected_item: var resource_path: String = selected_item.get_metadata(0) var file_type := file_system.get_file_type(resource_path) var is_dir := DirAccess.dir_exists_absolute(resource_path) if is_dir: - @warning_ignore("return_value_discarded") - selected_test_suites.append(resource_path) + selected_test_suites.append_array(suite_scaner.scan_directory(resource_path)) elif is_dir or file_type == "GDScript" or file_type == "CSharpScript": # find a performant way to check if the selected item a testsuite - var resource := ResourceLoader.load(resource_path, "Script", ResourceLoader.CACHE_MODE_REUSE) + var resource: Script = ResourceLoader.load(resource_path, "Script", ResourceLoader.CACHE_MODE_REUSE) if _menu_item.is_visible(resource): @warning_ignore("return_value_discarded") - selected_test_suites.append(resource_path) + selected_test_suites.append(resource) selected_item = file_tree.get_next_selected(selected_item) return selected_test_suites diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid index 7d17fdea..ff582094 100644 --- a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandler.gd.uid @@ -1 +1 @@ -uid://jx03hwns1oyy +uid://cq7lcsiitaupx diff --git a/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx new file mode 100644 index 00000000..108450e5 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/EditorFileSystemContextMenuHandlerV44.gdx @@ -0,0 +1,47 @@ +@tool +extends EditorContextMenuPlugin + +var _context_menus := Dictionary() +var _command_handler := GdUnitCommandHandler.instance() + + +func _init() -> void: + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + if script == null: + return false + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + _context_menus[GdUnitContextMenuItem.MENU_ID.TEST_RUN] = GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Testsuites", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE)) + _context_menus[GdUnitContextMenuItem.MENU_ID.TEST_DEBUG] = GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Testsuites", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTSUITE_DEBUG)) + + # setup shortcuts + for menu_item: GdUnitContextMenuItem in _context_menus.values(): + var cb := func call(files: Array) -> void: + menu_item.execute([files]) + add_menu_shortcut(menu_item.shortcut(), cb) + + +func _popup_menu(paths: PackedStringArray) -> void: + var test_suites: Array[Script] = [] + var suite_scaner := GdUnitTestSuiteScanner.new() + + for resource_path in paths: + # directories and test-suites are valid to enable the menu + if DirAccess.dir_exists_absolute(resource_path): + test_suites.append_array(suite_scaner.scan_directory(resource_path)) + continue + + var file_type := resource_path.get_extension() + if file_type == "gd" or file_type == "cs": + var script: Script = ResourceLoader.load(resource_path, "Script", ResourceLoader.CACHE_MODE_REUSE) + if GdUnitTestSuiteScanner.is_test_suite(script): + test_suites.append(script) + + # no direcory or test-suites selected? + if test_suites.is_empty(): + return + + for menu_item: GdUnitContextMenuItem in _context_menus.values(): + @warning_ignore("unused_parameter") + var cb := func call(files: Array) -> void: + menu_item.execute([test_suites]) + add_context_menu_item(menu_item.name, cb, GdUnitUiTools.get_icon(menu_item.icon)) diff --git a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid index 32c4c87d..655c8391 100644 --- a/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid +++ b/addons/gdUnit4/src/ui/menu/GdUnitContextMenuItem.gd.uid @@ -1 +1 @@ -uid://dv715xd4xwpx8 +uid://cr4b1qj3gpmy2 diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd index c32568c5..550c6d61 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd @@ -3,10 +3,19 @@ extends Control var _context_menus := Dictionary() var _editor: ScriptEditor +var _command_handler := GdUnitCommandHandler.instance() -func _init(context_menus: Array[GdUnitContextMenuItem]) -> void: +func _init() -> void: set_name("ScriptEditorContextMenuHandler") + + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + var context_menus :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Tests", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.CREATE_TEST, "Create Test", "New", is_test_suite.bind(false), _command_handler.command(GdUnitCommandHandler.CMD_CREATE_TESTCASE)) + ] for menu in context_menus: _context_menus[menu.id] = menu _editor = EditorInterface.get_script_editor() diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid index e68121f8..ab3bd591 100644 --- a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandler.gd.uid @@ -1 +1 @@ -uid://bncfnqqm2s6y +uid://d3x1ev5shdb5q diff --git a/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx new file mode 100644 index 00000000..a879e378 --- /dev/null +++ b/addons/gdUnit4/src/ui/menu/ScriptEditorContextMenuHandlerV44.gdx @@ -0,0 +1,33 @@ +@tool +extends EditorContextMenuPlugin + +var _context_menus := Dictionary() +var _editor: ScriptEditor +var _command_handler := GdUnitCommandHandler.instance() + + +func _init() -> void: + var is_test_suite := func is_visible(script: Script, is_ts: bool) -> bool: + return GdUnitTestSuiteScanner.is_test_suite(script) == is_ts + var context_menus :Array[GdUnitContextMenuItem] = [ + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_RUN, "Run Tests", "Play", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.TEST_DEBUG, "Debug Tests", "PlayStart", is_test_suite.bind(true), _command_handler.command(GdUnitCommandHandler.CMD_RUN_TESTCASE_DEBUG)), + GdUnitContextMenuItem.new(GdUnitContextMenuItem.MENU_ID.CREATE_TEST, "Create Test", "New", is_test_suite.bind(false), _command_handler.command(GdUnitCommandHandler.CMD_CREATE_TESTCASE)) + ] + for menu in context_menus: + _context_menus[menu.id] = menu + _editor = EditorInterface.get_script_editor() + @warning_ignore("return_value_discarded") + + +func _popup_menu(paths: PackedStringArray) -> void: + var script_path := paths[0] + var script: Script = ResourceLoader.load(script_path, "Script", ResourceLoader.CACHE_MODE_REUSE) + + for menu_id: int in _context_menus.keys(): + var menu_item: GdUnitContextMenuItem = _context_menus[menu_id] + if menu_item.is_visible(script): + add_context_menu_item(menu_item.name, + func call(files: Array) -> void: + menu_item.execute([script_path]), + GdUnitUiTools.get_icon(menu_item.icon)) diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid index 356d1f72..7edd2ccd 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.gd.uid @@ -1 +1 @@ -uid://rq8ph4i273km +uid://dcndxbt413dpn diff --git a/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn index fb8ef9e7..0262b8cd 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorMonitor.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=6 format=3 uid="uid://djp8ait0bxpsc"] -[ext_resource type="Script" uid="uid://rq8ph4i273km" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.gd" id="3"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorMonitor.gd" id="3"] [sub_resource type="Image" id="Image_sx31i"] data = { diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd index 4c18f114..d368ed3f 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd @@ -1,47 +1,49 @@ @tool extends ProgressBar + @onready var status: Label = $Label @onready var style: StyleBoxFlat = get("theme_override_styles/fill") +var _state: GdUnitInspectorTreeConstants.STATE func _ready() -> void: - @warning_ignore("return_value_discarded") - GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) style.bg_color = Color.DARK_GREEN value = 0 max_value = 0 update_text() -func progress_init(p_max_value: int) -> void: - value = 0 - max_value = p_max_value - style.bg_color = Color.DARK_GREEN - update_text() +func update_text() -> void: + status.text = "%d:%d" % [value, max_value] -func progress_update(p_value: int, is_failed: bool) -> void: - value += p_value +func _on_test_counter_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE) -> void: + value = index + max_value = total update_text() - if is_failed: - style.bg_color = Color.DARK_RED + # inital state + if index == 0: + style.bg_color = Color.DARK_GREEN -func update_text() -> void: - status.text = "%d:%d" % [value, max_value] + # do only update the state is higher prio than current state + if state <= _state: + return + _state = state + if is_flaky(state): + style.bg_color = Color.WEB_GREEN + if is_failed(state): + style.bg_color = Color.DARK_RED -func _on_gdunit_event(event: GdUnitEvent) -> void: - match event.type(): - GdUnitEvent.INIT: - progress_init(event.total_count()) - GdUnitEvent.DISCOVER_END: - progress_init(event.total_count()) +func is_failed(state: GdUnitInspectorTreeConstants.STATE) -> bool: + return state in [ + GdUnitInspectorTreeConstants.STATE.FAILED, + GdUnitInspectorTreeConstants.STATE.ERROR, + GdUnitInspectorTreeConstants.STATE.ABORDED] - GdUnitEvent.TESTCASE_STATISTICS: - progress_update(1, event.is_failed() or event.is_error()) - GdUnitEvent.TESTSUITE_AFTER: - progress_update(0, event.is_failed() or event.is_error()) +func is_flaky(state: GdUnitInspectorTreeConstants.STATE) -> bool: + return state == GdUnitInspectorTreeConstants.STATE.FLAKY diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid index fcf12be9..3211d0b3 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd.uid @@ -1 +1 @@ -uid://dj5pav6hk4vli +uid://c71226knir1y diff --git a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn index 370fa4da..1824230a 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorProgressBar.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://dva3tonxsxrlk"] -[ext_resource type="Script" uid="uid://dj5pav6hk4vli" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorProgressBar.gd" id="1"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ayfir"] bg_color = Color(0, 0.392157, 0, 1) diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd index d3f36d83..dfed5204 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd @@ -7,6 +7,8 @@ signal select_error_next() signal select_error_prevous() signal select_flaky_next() signal select_flaky_prevous() +signal select_skipped_next() +signal select_skipped_prevous() signal request_discover_tests() @warning_ignore("unused_signal") @@ -15,19 +17,22 @@ signal tree_view_mode_changed(flat :bool) @onready var _errors: Label = %error_value @onready var _failures: Label = %failure_value @onready var _flaky_value: Label = %flaky_value -@onready var _button_failure_up: Button = %btn_failure_up -@onready var _button_failure_down: Button = %btn_failure_down +@onready var _skipped_value: Label = %skipped_value +#@onready var _button_failure_up: Button = %btn_failure_up +#@onready var _button_failure_down: Button = %btn_failure_down @onready var _button_sync: Button = %btn_tree_sync -@onready var _button_view_mode: Button = %btn_tree_mode -@onready var _button_sort_mode: Button = %btn_tree_sort +@onready var _button_view_mode: MenuButton = %btn_tree_mode +@onready var _button_sort_mode: MenuButton = %btn_tree_sort @onready var _icon_errors: TextureRect = %icon_errors @onready var _icon_failures: TextureRect = %icon_failures @onready var _icon_flaky: TextureRect = %icon_flaky +@onready var _icon_skipped: TextureRect = %icon_skipped var total_failed := 0 var total_errors := 0 var total_flaky := 0 +var total_skipped := 0 var icon_mappings := { @@ -46,12 +51,15 @@ var icon_mappings := { func _ready() -> void: _failures.text = "0" _errors.text = "0" + _flaky_value.text = "0" + _skipped_value.text = "0" _icon_failures.texture = GdUnitUiTools.get_icon("StatusError", Color.SKY_BLUE) _icon_errors.texture = GdUnitUiTools.get_icon("StatusError", Color.DARK_RED) _icon_flaky.texture = GdUnitUiTools.get_icon("CheckBox", Color.GREEN_YELLOW) + _icon_skipped.texture = GdUnitUiTools.get_icon("CheckBox", Color.WEB_GRAY) - _button_failure_up.icon = GdUnitUiTools.get_icon("ArrowUp") - _button_failure_down.icon = GdUnitUiTools.get_icon("ArrowDown") + #_button_failure_up.icon = GdUnitUiTools.get_icon("ArrowUp") + #_button_failure_down.icon = GdUnitUiTools.get_icon("ArrowDown") _button_sync.icon = GdUnitUiTools.get_icon("Loop") _set_sort_mode_menu_options() _set_view_mode_menu_options() @@ -105,13 +113,15 @@ func normalise(value: String) -> String: return " ".join(parts) -func status_changed(errors: int, failed: int, flaky: int) -> void: +func status_changed(errors: int, failed: int, flaky: int, skipped: int) -> void: total_failed += failed total_errors += errors total_flaky += flaky + total_skipped += skipped _failures.text = str(total_failed) _errors.text = str(total_errors) _flaky_value.text = str(total_flaky) + _skipped_value.text = str(total_skipped) func disable_buttons(value :bool) -> void: @@ -129,24 +139,17 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: disable_buttons(false) GdUnitEvent.INIT: - total_failed = 0 total_errors = 0 + total_failed = 0 total_flaky = 0 - status_changed(0, 0, 0) - GdUnitEvent.TESTCASE_BEFORE: - pass - GdUnitEvent.TESTCASE_STATISTICS: - if event.is_error(): - status_changed(event.error_count(), 0, event.is_flaky()) - else: - status_changed(0, event.failed_count(), event.is_flaky()) - GdUnitEvent.TESTSUITE_BEFORE: - pass + total_skipped = 0 + status_changed(total_errors, total_failed, total_flaky, total_skipped) + + GdUnitEvent.TESTCASE_AFTER: + status_changed(event.error_count(), event.failed_count(), event.is_flaky(), event.is_skipped()) + GdUnitEvent.TESTSUITE_AFTER: - if event.is_error(): - status_changed(event.error_count(), 0, 0) - else: - status_changed(0, event.failed_count(), 0) + status_changed(event.error_count(), event.failed_count(), event.is_flaky(), 0) func _on_btn_error_up_pressed() -> void: @@ -173,6 +176,14 @@ func _on_btn_flaky_down_pressed() -> void: select_flaky_next.emit() +func _on_btn_skipped_up_pressed() -> void: + select_skipped_prevous.emit() + + +func _on_btn_skipped_down_pressed() -> void: + select_skipped_next.emit() + + func _on_tree_sync_pressed() -> void: request_discover_tests.emit() diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid index 56ae2bc7..53a3d755 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd.uid @@ -1 +1 @@ -uid://cu2mxgqtv325l +uid://cvqyv45dvlqko diff --git a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn index 7ff60e59..baf6a9fb 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorStatusBar.tscn @@ -1,116 +1,116 @@ -[gd_scene load_steps=32 format=3 uid="uid://c22l4odk7qesc"] +[gd_scene load_steps=30 format=3 uid="uid://c22l4odk7qesc"] -[ext_resource type="Script" uid="uid://cu2mxgqtv325l" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd" id="3"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorStatusBar.gd" id="3"] [sub_resource type="Image" id="Image_mb3ih"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 160, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 213, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 75, 224, 224, 224, 188, 224, 224, 224, 238, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 224, 224, 224, 96, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 133, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 226, 226, 226, 95, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 77, 224, 224, 224, 255, 224, 224, 224, 253, 225, 225, 225, 117, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 212, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 129, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 189, 224, 224, 224, 255, 224, 224, 224, 113, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 159, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 255, 224, 224, 224, 185, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 242, 224, 224, 224, 255, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 25, 224, 224, 224, 255, 224, 224, 224, 238, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 243, 224, 224, 224, 254, 233, 233, 233, 23, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 229, 229, 229, 29, 224, 224, 224, 255, 224, 224, 224, 236, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 189, 224, 224, 224, 255, 225, 225, 225, 68, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 160, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 121, 224, 224, 224, 255, 224, 224, 224, 181, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 72, 224, 224, 224, 121, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 43, 224, 224, 224, 213, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 36, 225, 225, 225, 124, 224, 224, 224, 254, 224, 224, 224, 255, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 125, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 95, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 237, 224, 224, 224, 185, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 213, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 225, 225, 225, 159, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 160, 230, 230, 230, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 213, 225, 225, 225, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 75, 224, 224, 224, 188, 224, 224, 224, 238, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 224, 224, 224, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 133, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 245, 226, 226, 226, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 77, 224, 224, 224, 255, 224, 224, 224, 253, 225, 225, 225, 117, 224, 224, 224, 32, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 212, 225, 225, 225, 42, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 129, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 189, 224, 224, 224, 255, 224, 224, 224, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 159, 230, 230, 230, 10, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 73, 224, 224, 224, 255, 224, 224, 224, 185, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 242, 224, 224, 224, 255, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 25, 224, 224, 224, 255, 224, 224, 224, 238, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 243, 224, 224, 224, 254, 233, 233, 233, 23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 229, 229, 229, 29, 224, 224, 224, 255, 224, 224, 224, 236, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 189, 224, 224, 224, 255, 225, 225, 225, 68, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 10, 224, 224, 224, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 121, 224, 224, 224, 255, 224, 224, 224, 181, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 72, 224, 224, 224, 121, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 43, 224, 224, 224, 213, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 227, 227, 227, 36, 225, 225, 225, 124, 224, 224, 224, 254, 224, 224, 224, 255, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 96, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 125, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 95, 224, 224, 224, 245, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 237, 224, 224, 224, 185, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 42, 224, 224, 224, 213, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 10, 225, 225, 225, 159, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_jvn24"] +[sub_resource type="ImageTexture" id="ImageTexture_wo03e"] image = SubResource("Image_mb3ih") -[sub_resource type="Image" id="Image_wo03e"] +[sub_resource type="Image" id="Image_ixycx"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_k82x4"] -image = SubResource("Image_wo03e") +[sub_resource type="ImageTexture" id="ImageTexture_c80wp"] +image = SubResource("Image_ixycx") -[sub_resource type="Image" id="Image_ixycx"] +[sub_resource type="Image" id="Image_eis20"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_bs7qq"] -image = SubResource("Image_ixycx") +[sub_resource type="ImageTexture" id="ImageTexture_t2qd7"] +image = SubResource("Image_eis20") -[sub_resource type="Image" id="Image_c80wp"] +[sub_resource type="Image" id="Image_jh28t"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 196, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 232, 232, 232, 22, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 194, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 176, 224, 224, 224, 200, 224, 224, 224, 253, 224, 224, 224, 255, 225, 225, 225, 199, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 252, 224, 224, 224, 255, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 171, 224, 224, 224, 195, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 195, 225, 225, 225, 175, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 196, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 215, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 224, 224, 224, 198, 224, 224, 224, 201, 224, 224, 224, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_0ck6a"] -image = SubResource("Image_c80wp") +[sub_resource type="ImageTexture" id="ImageTexture_1mh1t"] +image = SubResource("Image_jh28t") -[sub_resource type="Image" id="Image_eis20"] +[sub_resource type="Image" id="Image_lpjla"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 3, 224, 224, 224, 105, 224, 224, 224, 192, 224, 224, 224, 244, 224, 224, 224, 238, 224, 224, 224, 197, 224, 224, 224, 105, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 233, 233, 233, 23, 225, 225, 225, 207, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 198, 226, 226, 226, 26, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 6, 224, 224, 224, 205, 224, 224, 224, 255, 224, 224, 224, 218, 225, 225, 225, 83, 237, 237, 237, 14, 237, 237, 237, 14, 224, 224, 224, 82, 224, 224, 224, 220, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 102, 224, 224, 224, 255, 224, 224, 224, 218, 227, 227, 227, 18, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 16, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 101, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 198, 224, 224, 224, 255, 225, 225, 225, 84, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 86, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 1, 255, 255, 255, 4, 224, 224, 224, 238, 224, 224, 224, 255, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 229, 229, 229, 19, 224, 224, 224, 255, 224, 224, 224, 233, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 160, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 159, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 237, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 10, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 230, 230, 230, 10, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 90, 224, 224, 224, 255, 224, 224, 224, 185, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 42, 224, 224, 224, 245, 224, 224, 224, 245, 225, 225, 225, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 22, 224, 224, 224, 224, 224, 224, 224, 255, 224, 224, 224, 98, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 96, 226, 226, 226, 95, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 230, 230, 230, 20, 224, 224, 224, 88, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 2, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 200, 227, 227, 227, 18, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 236, 224, 224, 224, 195, 224, 224, 224, 96, 255, 255, 255, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 3, 224, 224, 224, 105, 224, 224, 224, 192, 224, 224, 224, 244, 224, 224, 224, 238, 224, 224, 224, 197, 224, 224, 224, 105, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233, 233, 233, 23, 225, 225, 225, 207, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 198, 226, 226, 226, 26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 6, 224, 224, 224, 205, 224, 224, 224, 255, 224, 224, 224, 218, 225, 225, 225, 83, 237, 237, 237, 14, 237, 237, 237, 14, 224, 224, 224, 82, 224, 224, 224, 220, 224, 224, 224, 255, 224, 224, 224, 197, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 102, 224, 224, 224, 255, 224, 224, 224, 218, 227, 227, 227, 18, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 224, 224, 224, 16, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 198, 224, 224, 224, 255, 225, 225, 225, 84, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 226, 226, 226, 86, 224, 224, 224, 255, 224, 224, 224, 194, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 255, 255, 255, 4, 224, 224, 224, 238, 224, 224, 224, 255, 227, 227, 227, 18, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 229, 229, 229, 19, 224, 224, 224, 255, 224, 224, 224, 233, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 160, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 159, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 237, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 10, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 212, 230, 230, 230, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 90, 224, 224, 224, 255, 224, 224, 224, 185, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225, 225, 225, 42, 224, 224, 224, 245, 224, 224, 224, 245, 225, 225, 225, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232, 232, 232, 22, 224, 224, 224, 224, 224, 224, 224, 255, 224, 224, 224, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 96, 226, 226, 226, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 230, 230, 20, 224, 224, 224, 88, 224, 224, 224, 221, 224, 224, 224, 255, 225, 225, 225, 199, 255, 255, 255, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 200, 227, 227, 227, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 236, 224, 224, 224, 195, 224, 224, 224, 96, 255, 255, 255, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_t7ac1"] -image = SubResource("Image_eis20") +[sub_resource type="ImageTexture" id="ImageTexture_bq8kn"] +image = SubResource("Image_lpjla") -[sub_resource type="Image" id="Image_t2qd7"] +[sub_resource type="Image" id="Image_bwbka"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_03vfp"] -image = SubResource("Image_t2qd7") +[sub_resource type="ImageTexture" id="ImageTexture_8lbfl"] +image = SubResource("Image_bwbka") -[sub_resource type="Image" id="Image_jh28t"] +[sub_resource type="Image" id="Image_ki3oo"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 237, 247, 245, 248, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 237, 247, 245, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_fv3i4"] -image = SubResource("Image_jh28t") +[sub_resource type="ImageTexture" id="ImageTexture_ivm1h"] +image = SubResource("Image_ki3oo") -[sub_resource type="Image" id="Image_1mh1t"] +[sub_resource type="Image" id="Image_uqb0l"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 249, 249, 255, 230, 246, 246, 252, 230, 249, 249, 255, 230, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 237, 246, 246, 252, 255, 246, 246, 252, 248, 255, 255, 255, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 236, 246, 246, 252, 254, 246, 246, 252, 247, 255, 255, 255, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 253, 231, 246, 246, 253, 232, 246, 246, 252, 230, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 243, 246, 246, 252, 255, 246, 246, 252, 242, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 242, 246, 246, 252, 253, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 249, 249, 255, 230, 246, 246, 252, 230, 249, 249, 255, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 237, 246, 246, 252, 255, 246, 246, 252, 248, 0, 0, 0, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 236, 246, 246, 252, 254, 246, 246, 252, 247, 0, 0, 0, 0, 246, 246, 252, 254, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 253, 231, 246, 246, 253, 232, 246, 246, 252, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 243, 246, 246, 252, 255, 246, 246, 252, 242, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 242, 246, 246, 252, 253, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 246, 246, 252, 244, 246, 246, 252, 255, 246, 246, 252, 241, 246, 246, 252, 230, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 246, 246, 252, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_ab51p"] -image = SubResource("Image_1mh1t") +[sub_resource type="ImageTexture" id="ImageTexture_j00vj"] +image = SubResource("Image_uqb0l") -[sub_resource type="Image" id="Image_lpjla"] +[sub_resource type="Image" id="Image_0oden"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 122, 111, 23, 255, 121, 107, 126, 255, 120, 108, 206, 255, 120, 107, 240, 255, 120, 107, 240, 255, 120, 108, 206, 255, 121, 107, 124, 255, 128, 116, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 108, 80, 255, 120, 107, 240, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 121, 107, 239, 255, 123, 109, 77, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 108, 78, 255, 120, 107, 254, 255, 120, 107, 255, 255, 120, 107, 240, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 240, 255, 120, 107, 255, 255, 120, 107, 254, 255, 122, 109, 75, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 121, 107, 239, 255, 120, 107, 255, 255, 122, 107, 107, 255, 121, 109, 42, 255, 120, 107, 233, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 231, 255, 121, 108, 40, 255, 121, 107, 112, 255, 120, 107, 255, 255, 120, 107, 238, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 124, 255, 120, 107, 255, 255, 120, 107, 240, 255, 121, 109, 42, 255, 255, 255, 0, 255, 121, 109, 42, 255, 120, 107, 233, 255, 120, 107, 232, 255, 124, 112, 41, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 119, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 207, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 233, 255, 121, 109, 42, 255, 255, 255, 0, 255, 121, 109, 42, 255, 121, 109, 42, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 202, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 233, 255, 121, 109, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 108, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 232, 255, 121, 109, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 122, 110, 44, 255, 120, 107, 234, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 108, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 207, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 231, 255, 124, 112, 41, 255, 255, 255, 0, 255, 125, 108, 45, 255, 122, 110, 44, 255, 255, 255, 0, 255, 125, 107, 43, 255, 120, 107, 233, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 200, 255, 255, 255, 0, 255, 255, 255, 0, 255, 120, 108, 123, 255, 120, 107, 255, 255, 120, 107, 240, 255, 121, 108, 40, 255, 255, 255, 0, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 234, 255, 125, 107, 43, 255, 255, 255, 0, 255, 125, 107, 43, 255, 120, 107, 242, 255, 120, 107, 255, 255, 121, 108, 116, 255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 116, 22, 255, 120, 107, 238, 255, 120, 107, 255, 255, 121, 107, 112, 255, 125, 108, 45, 255, 120, 107, 235, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 233, 255, 125, 107, 43, 255, 120, 107, 117, 255, 120, 107, 255, 255, 120, 107, 235, 255, 128, 113, 18, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 107, 76, 255, 120, 107, 254, 255, 120, 107, 255, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 242, 255, 120, 107, 255, 255, 120, 107, 253, 255, 120, 109, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 123, 109, 77, 255, 121, 107, 239, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 255, 255, 120, 107, 236, 255, 122, 108, 71, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 121, 109, 21, 255, 121, 107, 122, 255, 121, 107, 203, 255, 120, 107, 238, 255, 120, 107, 238, 255, 120, 107, 202, 255, 120, 107, 119, 255, 128, 115, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 232, 151, 12, 11, 242, 151, 12, 11, 250, 151, 12, 11, 254, 151, 12, 11, 254, 151, 12, 11, 250, 151, 12, 11, 242, 151, 12, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 238, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 10, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 232, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 240, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 151, 12, 11, 241, 151, 12, 11, 255, 151, 12, 11, 253, 151, 11, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 242, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 241, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 250, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 250, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 10, 234, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 250, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 10, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 250, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 242, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 234, 0, 0, 0, 0, 151, 12, 11, 234, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 10, 241, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 10, 232, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 241, 151, 12, 11, 234, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 234, 151, 12, 11, 241, 151, 12, 11, 255, 151, 12, 11, 253, 151, 11, 10, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 254, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 237, 151, 12, 11, 253, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 255, 151, 12, 11, 253, 151, 12, 11, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151, 12, 11, 232, 151, 12, 11, 242, 151, 12, 11, 250, 151, 12, 11, 253, 151, 12, 11, 253, 151, 12, 11, 250, 151, 12, 11, 241, 151, 11, 10, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_2rpr0"] -image = SubResource("Image_lpjla") +[sub_resource type="ImageTexture" id="ImageTexture_suo5c"] +image = SubResource("Image_0oden") -[sub_resource type="Image" id="Image_bq8kn"] +[sub_resource type="Image" id="Image_ipq44"] data = { "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 178, 224, 224, 224, 194, 230, 230, 230, 20, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", @@ -120,9 +120,9 @@ data = { } [sub_resource type="ImageTexture" id="ImageTexture_1oriu"] -image = SubResource("Image_bq8kn") +image = SubResource("Image_ipq44") -[sub_resource type="Image" id="Image_bwbka"] +[sub_resource type="Image" id="Image_d5kq4"] data = { "data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 195, 231, 231, 231, 21, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 195, 224, 224, 224, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), "format": "RGBA8", @@ -132,55 +132,43 @@ data = { } [sub_resource type="ImageTexture" id="ImageTexture_ikyhk"] -image = SubResource("Image_bwbka") +image = SubResource("Image_d5kq4") -[sub_resource type="Image" id="Image_8lbfl"] +[sub_resource type="Image" id="Image_8d0da"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 232, 147, 198, 222, 242, 147, 197, 222, 250, 147, 197, 222, 254, 147, 197, 222, 254, 147, 197, 222, 250, 147, 198, 222, 242, 147, 198, 223, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 238, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 198, 222, 253, 147, 198, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 198, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 232, 147, 198, 222, 253, 147, 197, 222, 255, 147, 198, 222, 240, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 147, 198, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 223, 232, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 198, 223, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 255, 255, 255, 0, 255, 255, 255, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 255, 255, 255, 0, 255, 255, 255, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 223, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 198, 223, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 255, 255, 255, 0, 255, 255, 255, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 198, 222, 234, 255, 255, 255, 0, 147, 198, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 198, 222, 241, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 223, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 198, 222, 241, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 223, 231, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 237, 147, 198, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 237, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 147, 198, 222, 232, 147, 198, 222, 242, 147, 198, 222, 250, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 250, 147, 197, 222, 241, 147, 198, 223, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), -"format": "RGBA8", -"height": 16, -"mipmaps": false, -"width": 16 -} - -[sub_resource type="ImageTexture" id="ImageTexture_i2d73"] -image = SubResource("Image_8lbfl") - -[sub_resource type="Image" id="Image_ki3oo"] -data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 178, 224, 224, 224, 194, 230, 230, 230, 20, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 194, 224, 224, 224, 179, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 223, 232, 147, 197, 222, 242, 147, 197, 222, 250, 147, 197, 222, 254, 147, 197, 222, 254, 147, 197, 222, 250, 147, 197, 222, 242, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 238, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 240, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 0, 0, 0, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 147, 198, 222, 234, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 198, 222, 234, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 250, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 250, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 242, 147, 197, 222, 255, 147, 197, 222, 254, 147, 198, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 234, 0, 0, 0, 0, 147, 197, 222, 234, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 241, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 232, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 241, 147, 197, 222, 234, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 234, 147, 197, 222, 241, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 221, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 237, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 254, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 197, 222, 237, 147, 197, 222, 253, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 255, 147, 197, 222, 253, 147, 197, 222, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147, 198, 222, 232, 147, 197, 222, 242, 147, 197, 222, 250, 147, 197, 222, 253, 147, 197, 222, 253, 147, 197, 222, 250, 147, 197, 222, 241, 147, 197, 222, 232, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_mph2m"] -image = SubResource("Image_ki3oo") +[sub_resource type="ImageTexture" id="ImageTexture_qagbu"] +image = SubResource("Image_8d0da") -[sub_resource type="Image" id="Image_ivm1h"] +[sub_resource type="Image" id="Image_oy0ff"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 181, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 180, 224, 224, 224, 195, 231, 231, 231, 21, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 195, 224, 224, 224, 178, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 195, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 231, 231, 231, 21, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 194, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 211, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 255, 224, 224, 224, 255, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 237, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 247, 171, 255, 57, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 58, 232, 170, 254, 57, 232, 0, 0, 0, 0, 170, 253, 58, 234, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 170, 254, 58, 232, 170, 253, 57, 251, 170, 253, 57, 251, 170, 254, 58, 236, 170, 253, 57, 253, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 170, 254, 58, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 251, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 170, 254, 58, 242, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 251, 170, 253, 57, 255, 170, 254, 57, 247, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 232, 170, 253, 57, 244, 171, 255, 58, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 255, 170, 253, 57, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 170, 254, 57, 237, 170, 253, 57, 252, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 255, 170, 253, 57, 252, 170, 254, 57, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_k6fqi"] -image = SubResource("Image_ivm1h") +[sub_resource type="ImageTexture" id="ImageTexture_a4rkr"] +image = SubResource("Image_oy0ff") -[sub_resource type="Image" id="Image_uqb0l"] +[sub_resource type="Image" id="Image_iahim"] data = { -"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 144, 239, 151, 76, 142, 239, 151, 228, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 152, 128, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 143, 239, 152, 229, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 152, 128, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 244, 153, 45, 143, 239, 152, 175, 149, 255, 170, 12, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 244, 153, 45, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 130, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 244, 153, 45, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 144, 244, 155, 23, 151, 244, 151, 22, 255, 255, 255, 0, 142, 244, 153, 45, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 144, 244, 155, 23, 143, 239, 151, 213, 142, 239, 152, 212, 145, 240, 152, 67, 142, 239, 151, 235, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 142, 240, 152, 128, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 151, 244, 151, 22, 142, 239, 152, 212, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 142, 240, 152, 128, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 146, 243, 158, 21, 143, 239, 151, 211, 142, 239, 151, 255, 143, 240, 151, 177, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 146, 243, 158, 21, 143, 239, 152, 141, 153, 255, 153, 5, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 255, 142, 239, 151, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 142, 239, 151, 228, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 151, 225, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 143, 241, 154, 73, 142, 239, 151, 226, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 239, 151, 255, 142, 240, 151, 225, 142, 241, 153, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 237, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 247, 130, 141, 130, 231, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 243, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 131, 232, 129, 140, 130, 232, 0, 0, 0, 0, 129, 139, 131, 234, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 130, 140, 131, 232, 129, 139, 130, 251, 129, 139, 130, 251, 129, 139, 130, 236, 129, 139, 130, 253, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 129, 139, 130, 242, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 129, 140, 130, 232, 129, 139, 130, 251, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 129, 139, 130, 242, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 130, 232, 129, 139, 130, 251, 129, 139, 130, 255, 129, 139, 130, 247, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 140, 130, 232, 129, 139, 130, 244, 131, 141, 131, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 255, 129, 139, 130, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 252, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 130, 139, 130, 237, 129, 139, 130, 252, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 255, 129, 139, 130, 252, 129, 139, 130, 237, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), "format": "RGBA8", "height": 16, "mipmaps": false, "width": 16 } -[sub_resource type="ImageTexture" id="ImageTexture_04e57"] -image = SubResource("Image_uqb0l") +[sub_resource type="ImageTexture" id="ImageTexture_idt7c"] +image = SubResource("Image_iahim") [node name="StatusBar" type="PanelContainer"] clip_contents = true @@ -195,6 +183,7 @@ script = ExtResource("3") [node name="VBoxContainer" type="VBoxContainer" parent="."] layout_mode = 2 +size_flags_vertical = 0 [node name="tree_tools" type="HBoxContainer" parent="VBoxContainer"] layout_mode = 2 @@ -203,7 +192,7 @@ size_flags_vertical = 0 [node name="Label" type="Label" parent="VBoxContainer/tree_tools"] layout_mode = 2 size_flags_horizontal = 0 -text = "Statisitics" +text = "Statistics" [node name="tree_buttons" type="HBoxContainer" parent="VBoxContainer/tree_tools"] layout_mode = 2 @@ -218,212 +207,271 @@ layout_mode = 2 unique_name_in_owner = true layout_mode = 2 tooltip_text = "Run discover tests." -disabled = true -icon = SubResource("ImageTexture_jvn24") +icon = SubResource("ImageTexture_wo03e") [node name="btn_tree_sort" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"] unique_name_in_owner = true layout_mode = 2 tooltip_text = "Sets tree sorting mode." -disabled = true -icon = SubResource("ImageTexture_k82x4") +icon = SubResource("ImageTexture_c80wp") flat = false item_count = 4 popup/item_0/text = "Unsorted" -popup/item_0/icon = SubResource("ImageTexture_bs7qq") +popup/item_0/icon = SubResource("ImageTexture_t2qd7") popup/item_0/checkable = 1 +popup/item_0/id = 0 popup/item_1/text = "Name ascending" -popup/item_1/icon = SubResource("ImageTexture_k82x4") +popup/item_1/icon = SubResource("ImageTexture_c80wp") popup/item_1/checkable = 1 popup/item_1/checked = true popup/item_1/id = 1 popup/item_2/text = "Name descending" -popup/item_2/icon = SubResource("ImageTexture_0ck6a") +popup/item_2/icon = SubResource("ImageTexture_1mh1t") popup/item_2/checkable = 1 popup/item_2/id = 2 popup/item_3/text = "Execution time" -popup/item_3/icon = SubResource("ImageTexture_t7ac1") +popup/item_3/icon = SubResource("ImageTexture_bq8kn") popup/item_3/checkable = 1 popup/item_3/id = 3 [node name="btn_tree_mode" type="MenuButton" parent="VBoxContainer/tree_tools/tree_buttons"] unique_name_in_owner = true layout_mode = 2 -tooltip_text = "Sets tree presentaion mode." -disabled = true -icon = SubResource("ImageTexture_03vfp") +tooltip_text = "Sets tree presentation mode." +icon = SubResource("ImageTexture_8lbfl") flat = false item_count = 2 popup/item_0/text = "Tree" -popup/item_0/icon = SubResource("ImageTexture_fv3i4") +popup/item_0/icon = SubResource("ImageTexture_ivm1h") popup/item_0/checkable = 1 popup/item_0/checked = true +popup/item_0/id = 0 popup/item_1/text = "Flat" -popup/item_1/icon = SubResource("ImageTexture_ab51p") +popup/item_1/icon = SubResource("ImageTexture_j00vj") popup/item_1/checkable = 1 popup/item_1/id = 1 [node name="HSeparator" type="HSeparator" parent="VBoxContainer"] layout_mode = 2 +theme_override_constants/separation = 0 [node name="status_bar" type="HFlowContainer" parent="VBoxContainer"] +layout_direction = 2 layout_mode = 2 -last_wrap_alignment = 1 +size_flags_vertical = 2 -[node name="errors" type="HBoxContainer" parent="VBoxContainer/status_bar"] +[node name="error" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) layout_mode = 2 -size_flags_vertical = 4 +size_flags_vertical = 0 +theme_override_constants/separation = -2 -[node name="error_value" type="Label" parent="VBoxContainer/status_bar/errors"] -unique_name_in_owner = true -use_parent_material = true -custom_minimum_size = Vector2(24, 0) +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/error"] layout_mode = 2 -size_flags_horizontal = 2 -text = "0" -horizontal_alignment = 2 -justification_flags = 0 +size_flags_horizontal = 3 -[node name="icon_errors" type="TextureRect" parent="VBoxContainer/status_bar/errors"] +[node name="icon_errors" type="TextureRect" parent="VBoxContainer/status_bar/error/icon"] unique_name_in_owner = true layout_mode = 2 +size_flags_horizontal = 10 size_flags_vertical = 4 -texture = SubResource("ImageTexture_2rpr0") -stretch_mode = 2 +tooltip_text = "Error Tests" +texture = SubResource("ImageTexture_suo5c") +stretch_mode = 3 -[node name="Label" type="Label" parent="VBoxContainer/status_bar/errors"] +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/error/icon"] layout_mode = 2 -text = "Errors" -justification_flags = 0 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Jump to the previous error test" +icon = SubResource("ImageTexture_1oriu") -[node name="navigation" type="HBoxContainer" parent="VBoxContainer/status_bar/errors"] +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/error"] auto_translate_mode = 2 layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 4 localize_numeral_system = false -[node name="btn_error_up" type="Button" parent="VBoxContainer/status_bar/errors/navigation"] +[node name="error_value" type="Label" parent="VBoxContainer/status_bar/error/counter"] +unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(32, 0) layout_mode = 2 -size_flags_vertical = 3 -tooltip_text = "Shows the total test errors." -icon = SubResource("ImageTexture_1oriu") +size_flags_horizontal = 10 +text = "0" +horizontal_alignment = 2 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 -[node name="btn_error_down" type="Button" parent="VBoxContainer/status_bar/errors/navigation"] +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/error/counter"] layout_mode = 2 -size_flags_horizontal = 0 -size_flags_vertical = 3 -tooltip_text = "Shows the total test errors." +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next error test" icon = SubResource("ImageTexture_ikyhk") [node name="VSeparator" type="VSeparator" parent="VBoxContainer/status_bar"] layout_mode = 2 -[node name="failures" type="HBoxContainer" parent="VBoxContainer/status_bar"] +[node name="failure" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) layout_mode = 2 -size_flags_vertical = 4 +theme_override_constants/separation = -2 -[node name="failure_value" type="Label" parent="VBoxContainer/status_bar/failures"] -unique_name_in_owner = true -use_parent_material = true -custom_minimum_size = Vector2(24, 0) +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/failure"] layout_mode = 2 -size_flags_horizontal = 0 -text = "0" -horizontal_alignment = 2 -vertical_alignment = 1 -justification_flags = 160 -max_lines_visible = 1 +size_flags_horizontal = 3 -[node name="icon_failures" type="TextureRect" parent="VBoxContainer/status_bar/failures"] +[node name="icon_failures" type="TextureRect" parent="VBoxContainer/status_bar/failure/icon"] unique_name_in_owner = true layout_mode = 2 +size_flags_horizontal = 10 size_flags_vertical = 4 -texture = SubResource("ImageTexture_i2d73") -stretch_mode = 2 +tooltip_text = "Failed Tests" +texture = SubResource("ImageTexture_qagbu") +stretch_mode = 3 -[node name="Label" type="Label" parent="VBoxContainer/status_bar/failures"] +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/failure/icon"] layout_mode = 2 -text = "Failures" -justification_flags = 0 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Jump to the previous failed test" +icon = SubResource("ImageTexture_1oriu") -[node name="navigation" type="HBoxContainer" parent="VBoxContainer/status_bar/failures"] +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/failure"] auto_translate_mode = 2 layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 4 localize_numeral_system = false -[node name="btn_failure_up" type="Button" parent="VBoxContainer/status_bar/failures/navigation"] +[node name="failure_value" type="Label" parent="VBoxContainer/status_bar/failure/counter"] unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(32, 0) layout_mode = 2 -size_flags_vertical = 3 -tooltip_text = "Shows the total test errors." -icon = SubResource("ImageTexture_mph2m") +size_flags_horizontal = 10 +text = "0" +horizontal_alignment = 2 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 -[node name="btn_failure_down" type="Button" parent="VBoxContainer/status_bar/failures/navigation"] -unique_name_in_owner = true +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/failure/counter"] layout_mode = 2 -size_flags_horizontal = 0 -size_flags_vertical = 3 -tooltip_text = "Shows the total test errors." -icon = SubResource("ImageTexture_k6fqi") +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next failed test" +icon = SubResource("ImageTexture_ikyhk") [node name="VSeparator2" type="VSeparator" parent="VBoxContainer/status_bar"] layout_mode = 2 -[node name="flaky" type="HBoxContainer" parent="VBoxContainer/status_bar"] +[node name="flaky" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +theme_override_constants/separation = -2 + +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="icon_flaky" type="TextureRect" parent="VBoxContainer/status_bar/flaky/icon"] +unique_name_in_owner = true layout_mode = 2 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Flaky Tests" +texture = SubResource("ImageTexture_a4rkr") +stretch_mode = 3 + +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/flaky/icon"] +layout_mode = 2 +size_flags_horizontal = 10 size_flags_vertical = 4 +tooltip_text = "Jump to the previous flaky test" +icon = SubResource("ImageTexture_1oriu") + +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"] +auto_translate_mode = 2 +layout_mode = 2 +localize_numeral_system = false -[node name="flaky_value" type="Label" parent="VBoxContainer/status_bar/flaky"] +[node name="flaky_value" type="Label" parent="VBoxContainer/status_bar/flaky/counter"] unique_name_in_owner = true use_parent_material = true -custom_minimum_size = Vector2(24, 0) +custom_minimum_size = Vector2(32, 0) layout_mode = 2 -size_flags_horizontal = 0 +size_flags_horizontal = 10 text = "0" horizontal_alignment = 2 -vertical_alignment = 1 -justification_flags = 160 -max_lines_visible = 1 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 + +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/flaky/counter"] +layout_mode = 2 +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next flaky test" +icon = SubResource("ImageTexture_ikyhk") + +[node name="VSeparator3" type="VSeparator" parent="VBoxContainer/status_bar"] +layout_mode = 2 + +[node name="skipped" type="VBoxContainer" parent="VBoxContainer/status_bar"] +custom_minimum_size = Vector2(0, 48) +layout_mode = 2 +theme_override_constants/separation = -2 + +[node name="icon" type="HBoxContainer" parent="VBoxContainer/status_bar/skipped"] +layout_mode = 2 +size_flags_horizontal = 3 -[node name="icon_flaky" type="TextureRect" parent="VBoxContainer/status_bar/flaky"] +[node name="icon_skipped" type="TextureRect" parent="VBoxContainer/status_bar/skipped/icon"] unique_name_in_owner = true layout_mode = 2 +size_flags_horizontal = 10 size_flags_vertical = 4 -texture = SubResource("ImageTexture_04e57") -stretch_mode = 2 +tooltip_text = "Skipped Tests" +texture = SubResource("ImageTexture_idt7c") +stretch_mode = 3 -[node name="Label" type="Label" parent="VBoxContainer/status_bar/flaky"] +[node name="btn_up" type="Button" parent="VBoxContainer/status_bar/skipped/icon"] layout_mode = 2 -text = "Flaky" -justification_flags = 0 +size_flags_horizontal = 10 +size_flags_vertical = 4 +tooltip_text = "Jump to the previous skipped test" +icon = SubResource("ImageTexture_1oriu") -[node name="navigation" type="HBoxContainer" parent="VBoxContainer/status_bar/flaky"] +[node name="counter" type="HBoxContainer" parent="VBoxContainer/status_bar/skipped"] auto_translate_mode = 2 layout_mode = 2 -size_flags_horizontal = 4 -size_flags_vertical = 4 localize_numeral_system = false -[node name="btn_flaky_up" type="Button" parent="VBoxContainer/status_bar/flaky/navigation"] +[node name="skipped_value" type="Label" parent="VBoxContainer/status_bar/skipped/counter"] +unique_name_in_owner = true +use_parent_material = true +custom_minimum_size = Vector2(32, 0) layout_mode = 2 -size_flags_vertical = 3 -tooltip_text = "Shows the total test errors." -icon = SubResource("ImageTexture_1oriu") +size_flags_horizontal = 10 +text = "0" +horizontal_alignment = 2 +justification_flags = 0 +visible_characters = 3 +visible_ratio = 3.0 -[node name="btn_flaky_down" type="Button" parent="VBoxContainer/status_bar/flaky/navigation"] +[node name="btn_down" type="Button" parent="VBoxContainer/status_bar/skipped/counter"] layout_mode = 2 -size_flags_horizontal = 0 -size_flags_vertical = 3 -tooltip_text = "Shows the total test errors." +size_flags_horizontal = 4 +size_flags_vertical = 4 +tooltip_text = "Jump to the next skipped test" icon = SubResource("ImageTexture_ikyhk") [connection signal="pressed" from="VBoxContainer/tree_tools/tree_buttons/btn_tree_sync" to="." method="_on_tree_sync_pressed"] -[connection signal="pressed" from="VBoxContainer/status_bar/errors/navigation/btn_error_up" to="." method="_on_btn_error_up_pressed"] -[connection signal="pressed" from="VBoxContainer/status_bar/errors/navigation/btn_error_down" to="." method="_on_btn_error_down_pressed"] -[connection signal="pressed" from="VBoxContainer/status_bar/failures/navigation/btn_failure_up" to="." method="_on_failure_up_pressed"] -[connection signal="pressed" from="VBoxContainer/status_bar/failures/navigation/btn_failure_down" to="." method="_on_failure_down_pressed"] -[connection signal="pressed" from="VBoxContainer/status_bar/flaky/navigation/btn_flaky_up" to="." method="_on_btn_flaky_up_pressed"] -[connection signal="pressed" from="VBoxContainer/status_bar/flaky/navigation/btn_flaky_down" to="." method="_on_btn_flaky_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/error/icon/btn_up" to="." method="_on_btn_error_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/error/counter/btn_down" to="." method="_on_btn_error_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/failure/icon/btn_up" to="." method="_on_failure_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/failure/counter/btn_down" to="." method="_on_failure_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/flaky/icon/btn_up" to="." method="_on_btn_flaky_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/flaky/counter/btn_down" to="." method="_on_btn_flaky_down_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/skipped/icon/btn_up" to="." method="_on_btn_skipped_up_pressed"] +[connection signal="pressed" from="VBoxContainer/status_bar/skipped/counter/btn_down" to="." method="_on_btn_skipped_down_pressed"] diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd index 3407048d..0cca901a 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd @@ -5,6 +5,8 @@ signal run_overall_pressed(debug: bool) signal run_pressed(debug: bool) signal stop_pressed() +const InspectorTreeMainPanel := preload("res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd") + @onready var _version_label: Control = %version @onready var _button_wiki: Button = %help @onready var _tool_button: Button = %tool @@ -14,7 +16,6 @@ signal stop_pressed() @onready var _button_stop: Button = %stop - const SETTINGS_SHORTCUT_MAPPING := { GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST: GdUnitShortcut.ShortCut.RERUN_TESTS, GdUnitSettings.SHORTCUT_INSPECTOR_RERUN_TEST_DEBUG: GdUnitShortcut.ShortCut.RERUN_TESTS_DEBUG, @@ -23,11 +24,16 @@ const SETTINGS_SHORTCUT_MAPPING := { } -@warning_ignore("return_value_discarded") func _ready() -> void: + var inspector :InspectorTreeMainPanel = get_parent().get_parent().find_child("MainPanel", false, false) + if inspector == null: + push_error("Internal error, can't connect to the test inspector!") + else: + inspector.tree_item_selected.connect(_on_inspector_selected) + run_pressed.connect(inspector._on_run_pressed) + GdUnit4Version.init_version_label(_version_label) var command_handler := GdUnitCommandHandler.instance() - run_pressed.connect(command_handler._on_run_pressed) run_overall_pressed.connect(command_handler._on_run_overall_pressed) stop_pressed.connect(command_handler._on_stop_pressed) command_handler.gdunit_runner_start.connect(_on_gdunit_runner_start) @@ -37,7 +43,6 @@ func _ready() -> void: init_shortcuts(command_handler) - func init_buttons() -> void: _button_run_overall.icon = GdUnitUiTools.get_run_overall_icon() _button_run_overall.visible = GdUnitSettings.is_inspector_toolbar_button_show() @@ -46,6 +51,9 @@ func init_buttons() -> void: _button_stop.icon = GdUnitUiTools.get_icon("Stop") _tool_button.icon = GdUnitUiTools.get_icon("Tools") _button_wiki.icon = GdUnitUiTools.get_icon("HelpSearch") + # Set run buttons initial disabled + _button_run.disabled = true + _button_run_debug.disabled = true func init_shortcuts(command_handler: GdUnitCommandHandler) -> void: @@ -58,6 +66,12 @@ func init_shortcuts(command_handler: GdUnitCommandHandler) -> void: GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed.bind(command_handler)) +func _on_inspector_selected(item: TreeItem) -> void: + var button_disabled := item == null + _button_run.disabled = button_disabled + _button_run_debug.disabled = button_disabled + + func _on_runoverall_pressed(debug:=false) -> void: run_overall_pressed.emit(debug) @@ -79,8 +93,6 @@ func _on_gdunit_runner_start() -> void: func _on_gdunit_runner_stop(_client_id: int) -> void: _button_run_overall.disabled = false - _button_run.disabled = false - _button_run_debug.disabled = false _button_stop.disabled = true @@ -89,8 +101,9 @@ func _on_gdunit_settings_changed(_property: GdUnitProperty) -> void: func _on_wiki_pressed() -> void: - @warning_ignore("return_value_discarded") - OS.shell_open("https://mikeschulze.github.io/gdUnit4/") + var status := OS.shell_open("https://mikeschulze.github.io/gdUnit4/%s" % GdUnit4Version.current().documentation_version()) + if status != OK: + push_error("Can't open GdUnit4 documentaion page: %s" % error_string(status)) func _on_btn_tool_pressed() -> void: diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid index a2c223cb..5c6c7953 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.gd.uid @@ -1 +1 @@ -uid://bk7xe6iefegdl +uid://dfexdwfncg7ws diff --git a/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn index 559f5a3b..5d75da02 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorToolBar.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=22 format=3 uid="uid://dx7xy4dgi3wwb"] -[ext_resource type="Script" uid="uid://bk7xe6iefegdl" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.gd" id="3"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorToolBar.gd" id="3"] [sub_resource type="Image" id="Image_c7rhl"] data = { diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd index 205105e2..536fe87f 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd @@ -1,8 +1,10 @@ @tool extends VSplitContainer -signal run_testcase(test_suite_resource_path: String, test_case: String, test_param_index: int, run_debug: bool) -signal run_testsuite() +## Will be emitted when the test index counter is changed +signal test_counters_changed(index: int, total: int, state: GdUnitInspectorTreeConstants.STATE) +signal tree_item_selected(item: TreeItem) + const CONTEXT_MENU_RUN_ID = 0 const CONTEXT_MENU_DEBUG_ID = 1 @@ -43,73 +45,71 @@ enum GdUnitType { FOLDER, TEST_SUITE, TEST_CASE, - TEST_CASE_PARAMETERIZED -} - - -enum STATE { - INITIAL, - RUNNING, - SUCCESS, - WARNING, - FLAKY, - FAILED, - ERROR, - ABORDED, - SKIPPED + TEST_GROUP } -const META_GDUNIT_ORIGINAL_INDEX = "gdunit_original_index" +const META_GDUNIT_PROGRESS_COUNT_MAX := "gdUnit_progress_count_max" +const META_GDUNIT_PROGRESS_INDEX := "gdUnit_progress_index" +const META_TEST_CASE := "gdunit_test_case" const META_GDUNIT_NAME := "gdUnit_name" const META_GDUNIT_STATE := "gdUnit_state" const META_GDUNIT_TYPE := "gdUnit_type" -const META_GDUNIT_TOTAL_TESTS := "gdUnit_suite_total_tests" const META_GDUNIT_SUCCESS_TESTS := "gdUnit_suite_success_tests" const META_GDUNIT_REPORT := "gdUnit_report" const META_GDUNIT_ORPHAN := "gdUnit_orphan" const META_GDUNIT_EXECUTION_TIME := "gdUnit_execution_time" -const META_RESOURCE_PATH := "resource_path" -const META_LINE_NUMBER := "line_number" -const META_SCRIPT_PATH := "script_path" -const META_TEST_PARAM_INDEX := "test_param_index" +const META_GDUNIT_ORIGINAL_INDEX = "gdunit_original_index" +const STATE = GdUnitInspectorTreeConstants.STATE -var _tree_root: TreeItem -var _item_hash := Dictionary() -var _tree_view_mode_flat := GdUnitSettings.get_inspector_tree_view_mode() == GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT +var _tree_root: TreeItem +var _current_selected_item: TreeItem = null +var _current_tree_view_mode := GdUnitSettings.get_inspector_tree_view_mode() +var _run_test_recovery := true -func _build_cache_key(resource_path: String, test_name: String) -> Array: - return [resource_path, test_name] +## Used for debugging purposes only +func print_tree_item_ids(parent: TreeItem) -> TreeItem: + for child in parent.get_children(): + if child.has_meta(META_TEST_CASE): + var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE) + prints(test_case.guid, test_case.test_name) -func get_tree_item(resource_path: String, item_name: String) -> TreeItem: - var key := _build_cache_key(resource_path, item_name) - return _item_hash.get(key, null) + if child.get_child_count() > 0: + print_tree_item_ids(child) + return null -func remove_tree_item(resource_path: String, item_name: String) -> bool: - var key := _build_cache_key(resource_path, item_name) - var item :TreeItem= _item_hash.get(key, null) - if item: - item.get_parent().remove_child(item) - item.free() - return _item_hash.erase(key) - return false +func _find_tree_item(parent: TreeItem, item_name: String) -> TreeItem: + for child in parent.get_children(): + if child.get_meta(META_GDUNIT_NAME) == item_name: + return child + return null -func add_tree_item_to_cache(resource_path: String, test_name: String, item: TreeItem) -> void: - var key := _build_cache_key(resource_path, test_name) - _item_hash[key] = item +func _find_tree_item_by_id(parent: TreeItem, id: GdUnitGUID) -> TreeItem: + for child in parent.get_children(): + if is_test_id(child, id): + return child + if child.get_child_count() > 0: + var item := _find_tree_item_by_id(child, id) + if item != null: + return item -func clear_tree_item_cache() -> void: - _item_hash.clear() + return null -func _find_by_resource_path(current: TreeItem, resource_path: String) -> TreeItem: - for item in current.get_children(): - if item.get_meta(META_RESOURCE_PATH) == resource_path: - return item +func _find_tree_item_by_test_suite(parent: TreeItem, suite_path: String, suite_name: String) -> TreeItem: + for child in parent.get_children(): + if child.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE: + var test_case: GdUnitTestCase = child.get_meta(META_TEST_CASE) + if test_case.suite_resource_path == suite_path and test_case.suite_name == suite_name: + return child + if child.get_child_count() > 0: + var item := _find_tree_item_by_test_suite(child, suite_path, suite_name) + if item != null: + return item return null @@ -179,6 +179,18 @@ func is_folder(item: TreeItem) -> bool: return item.has_meta(META_GDUNIT_TYPE) and item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER +func is_test_id(item: TreeItem, id: GdUnitGUID) -> bool: + if not item.has_meta(META_TEST_CASE): + return false + + var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE) + return test_case.guid.equals(id) + + +func disable_test_recovery() -> void: + _run_test_recovery = false + + @warning_ignore("return_value_discarded") func _ready() -> void: _context_menu.set_item_icon(CONTEXT_MENU_RUN_ID, GdUnitUiTools.get_icon("Play")) @@ -192,11 +204,14 @@ func _ready() -> void: _spinner.icon = GdUnitUiTools.get_spinner() init_tree() GdUnitSignals.instance().gdunit_settings_changed.connect(_on_settings_changed) - GdUnitSignals.instance().gdunit_add_test_suite.connect(do_add_test_suite) GdUnitSignals.instance().gdunit_event.connect(_on_gdunit_event) + GdUnitSignals.instance().gdunit_test_discover_added.connect(on_test_case_discover_added) + GdUnitSignals.instance().gdunit_test_discover_deleted.connect(on_test_case_discover_deleted) + GdUnitSignals.instance().gdunit_test_discover_modified.connect(on_test_case_discover_modified) var command_handler := GdUnitCommandHandler.instance() - command_handler.gdunit_runner_start.connect(_on_gdunit_runner_start) command_handler.gdunit_runner_stop.connect(_on_gdunit_runner_stop) + if _run_test_recovery: + GdUnitTestDiscoverer.restore_last_session() # we need current to manually redraw bacause of the animation bug @@ -208,6 +223,7 @@ func _process(_delta: float) -> void: func init_tree() -> void: cleanup_tree() + _tree.deselect_all() _tree.set_hide_root(true) _tree.ensure_cursor_is_visible() _tree.set_allow_reselect(true) @@ -219,6 +235,12 @@ func init_tree() -> void: _tree.set_column_expand_ratio(1, 0) _tree.set_column_custom_minimum_width(1, 100) _tree_root = _tree.create_item() + _tree_root.set_text(0, "tree_root") + _tree_root.set_meta(META_GDUNIT_NAME, "tree_root") + _tree_root.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0) + _tree_root.set_meta(META_GDUNIT_STATE, STATE.INITIAL) + _tree_root.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) # fix tree icon scaling var scale_factor := EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0 _tree.set("theme_override_constants/icon_max_width", 16 * scale_factor) @@ -226,11 +248,11 @@ func init_tree() -> void: func cleanup_tree() -> void: clear_reports() - clear_tree_item_cache() if not _tree_root: return _free_recursive() _tree.clear() + _current_selected_item = null func _free_recursive(items:=_tree_root.get_children()) -> void: @@ -239,12 +261,20 @@ func _free_recursive(items:=_tree_root.get_children()) -> void: item.call_deferred("free") -func sort_tree_items(parent :TreeItem) -> void: +func sort_tree_items(parent: TreeItem) -> void: + _sort_tree_items(parent, GdUnitSettings.get_inspector_tree_sort_mode()) + _tree.queue_redraw() + + +static func _sort_tree_items(parent: TreeItem, sort_mode: GdUnitInspectorTreeConstants.SORT_MODE) -> void: parent.visible = false var items := parent.get_children() + # first remove all childs before sorting + for item in items: + parent.remove_child(item) # do sort by selected sort mode - match GdUnitSettings.get_inspector_tree_sort_mode(): + match sort_mode: GdUnitInspectorTreeConstants.SORT_MODE.UNSORTED: items.sort_custom(sort_items_by_original_index) @@ -257,32 +287,42 @@ func sort_tree_items(parent :TreeItem) -> void: GdUnitInspectorTreeConstants.SORT_MODE.EXECUTION_TIME: items.sort_custom(sort_items_by_execution_time) + # readding sorted childs for item in items: - parent.remove_child(item) parent.add_child(item) if item.get_child_count() > 0: - sort_tree_items(item) + _sort_tree_items(item, sort_mode) parent.visible = true - _tree.queue_redraw() -func sort_items_by_name(a: TreeItem, b: TreeItem, ascending: bool) -> bool: +static func sort_items_by_name(a: TreeItem, b: TreeItem, ascending: bool) -> bool: var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE) var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE) - # Compare types first - if type_a != type_b: - return type_a == GdUnitType.FOLDER - var name_a :String = a.get_meta(META_GDUNIT_NAME) - var name_b :String = b.get_meta(META_GDUNIT_NAME) - return name_a.naturalnocasecmp_to(name_b) < 0 if ascending else name_a.naturalnocasecmp_to(name_b) > 0 + # Sort folders to the top + if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER: + return true + if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER: + return false + + # sort by name + var name_a: String = a.get_meta(META_GDUNIT_NAME) + var name_b: String = b.get_meta(META_GDUNIT_NAME) + var comparison := name_a.naturalnocasecmp_to(name_b) -func sort_items_by_execution_time(a: TreeItem, b: TreeItem) -> bool: + return comparison < 0 if ascending else comparison > 0 + + +static func sort_items_by_execution_time(a: TreeItem, b: TreeItem) -> bool: var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE) var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE) - # Compare types first - if type_a != type_b: - return type_a == GdUnitType.FOLDER + + # Sort folders to the top + if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER: + return true + if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER: + return false + var execution_time_a :int = a.get_meta(META_GDUNIT_EXECUTION_TIME) var execution_time_b :int = b.get_meta(META_GDUNIT_EXECUTION_TIME) # if has same execution time sort by name @@ -293,19 +333,164 @@ func sort_items_by_execution_time(a: TreeItem, b: TreeItem) -> bool: return execution_time_a > execution_time_b -func sort_items_by_original_index(a: TreeItem, b: TreeItem) -> bool: +static func sort_items_by_original_index(a: TreeItem, b: TreeItem) -> bool: var type_a: GdUnitType = a.get_meta(META_GDUNIT_TYPE) var type_b: GdUnitType = b.get_meta(META_GDUNIT_TYPE) - if type_a != type_b: - return type_a == GdUnitType.FOLDER + + # Sort folders to the top + if type_a == GdUnitType.FOLDER and type_b != GdUnitType.FOLDER: + return true + if type_b == GdUnitType.FOLDER and type_a != GdUnitType.FOLDER: + return false + var index_a :int = a.get_meta(META_GDUNIT_ORIGINAL_INDEX) var index_b :int = b.get_meta(META_GDUNIT_ORIGINAL_INDEX) + + # Sorting by index return index_a < index_b +func restructure_tree(parent: TreeItem, tree_mode: GdUnitInspectorTreeConstants.TREE_VIEW_MODE) -> void: + _current_tree_view_mode = tree_mode + + match tree_mode: + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT: + restructure_tree_to_flat(parent) + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE: + restructure_tree_to_tree(parent) + recalculate_counters(_tree_root) + # finally apply actual sort mode + sort_tree_items(_tree_root) + + +# Restructure into flat mode +func restructure_tree_to_flat(parent: TreeItem) -> void: + var folders := flatmap_folders(parent) + # Store current folder paths and their test suites + for folder_path: String in folders: + var test_suites: Array[TreeItem] = folders[folder_path] + if test_suites.is_empty(): + continue + + # Create flat folder and move test suites into it + var folder := _tree.create_item(parent) + folder.set_meta(META_GDUNIT_NAME, folder_path) + update_item_total_counter(folder) + set_state_initial(folder, GdUnitType.FOLDER) + + # Move test suites under the flat folder + for test_suite in test_suites: + var old_parent := test_suite.get_parent() + old_parent.remove_child(test_suite) + folder.add_child(test_suite) + + # Cleanup old folder structure + cleanup_empty_folders(parent) + + +# Restructure into hierarchical tree mode +func restructure_tree_to_tree(parent: TreeItem) -> void: + var items_to_process := parent.get_children().duplicate() + + for item: TreeItem in items_to_process: + if is_folder(item): + var folder_path: String = item.get_meta(META_GDUNIT_NAME) + var parts := folder_path.split("/") + + if parts.size() > 1: + var current_parent := parent + # Build folder hierarchy + for part in parts: + var next := _find_tree_item(current_parent, part) + if not next: + next = _tree.create_item(current_parent) + next.set_meta(META_GDUNIT_NAME, part) + set_state_initial(next, GdUnitType.FOLDER) + current_parent = next + + # Move test suites to deepest folder + var test_suites := item.get_children() + for test_suite in test_suites: + item.remove_child(test_suite) + current_parent.add_child(test_suite) + + # Remove the flat folder + item.get_parent().remove_child(item) + item.free() + + +func flatmap_folders(parent: TreeItem) -> Dictionary: + var folder_map := {} + + for item in parent.get_children(): + if is_folder(item): + var current_path: String = item.get_meta(META_GDUNIT_NAME) + # Get parent folder paths + var parent_path := get_parent_folder_path(item) + if parent_path: + current_path = parent_path + "/" + current_path + + # Collect direct children of this folder + var children: Array[TreeItem] = [] + for child in item.get_children(): + if is_test_suite(child): + children.append(child) + + # Add children to existing path or create new entry + if not children.is_empty(): + if folder_map.has(current_path): + @warning_ignore("unsafe_method_access") + folder_map[current_path].append_array(children) + else: + folder_map[current_path] = children + + # Recursively process subfolders + var sub_folders := flatmap_folders(item) + for path: String in sub_folders.keys(): + if folder_map.has(path): + @warning_ignore("unsafe_method_access") + folder_map[path].append_array(sub_folders[path]) + else: + folder_map[path] = sub_folders[path] + return folder_map + + +func get_parent_folder_path(item: TreeItem) -> String: + var path := "" + var parent := item.get_parent() + + while parent != _tree_root: + if is_folder(parent): + path = parent.get_meta(META_GDUNIT_NAME) + ("/" + path if path else "") + parent = parent.get_parent() + + return path + + +func cleanup_empty_folders(parent: TreeItem) -> void: + var folders: Array[TreeItem] = [] + # First collect all folders to avoid modification during iteration + for item in parent.get_children(): + if is_folder(item): + folders.append(item) + + # Process collected folders + for folder in folders: + cleanup_empty_folders(folder) + # Remove folder if it has no children after cleanup + if folder.get_child_count() == 0: + parent.remove_child(folder) + folder.free() + + func reset_tree_state(parent: TreeItem) -> void: + if parent == _tree_root: + _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, 0) + _tree_root.set_meta(META_GDUNIT_STATE, STATE.INITIAL) + test_counters_changed.emit(0, 0, STATE.INITIAL) + for item in parent.get_children(): - set_state_initial(item) + set_state_initial(item, get_item_type(item)) reset_tree_state(item) @@ -332,7 +517,8 @@ func do_collapse_all(collapse: bool, parent := _tree_root) -> void: do_collapse_all(collapse, item) -func set_state_initial(item: TreeItem) -> void: +func set_state_initial(item: TreeItem, type: GdUnitType) -> void: + item.set_text(0, str(item.get_meta(META_GDUNIT_NAME))) item.set_custom_color(0, Color.LIGHT_GRAY) item.set_tooltip_text(0, "") item.set_text_overrun_behavior(0, TextServer.OVERRUN_TRIM_CHAR) @@ -344,30 +530,44 @@ func set_state_initial(item: TreeItem) -> void: item.set_tooltip_text(1, "") item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) + item.set_meta(META_GDUNIT_TYPE, type) item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) + item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) and item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) > 0: + item.set_text(0, "(0/%d) %s" % [item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]) item.remove_meta(META_GDUNIT_REPORT) item.remove_meta(META_GDUNIT_ORPHAN) + set_item_icon_by_state(item) - init_item_counter(item) -func set_state_running(item: TreeItem) -> void: +func set_state_running(item: TreeItem, is_running: bool) -> void: if is_state_running(item): return - item.set_custom_color(0, Color.DARK_GREEN) - item.set_custom_color(1, Color.DARK_GREEN) - item.set_icon(0, ICON_SPINNER) - item.set_meta(META_GDUNIT_STATE, STATE.RUNNING) - item.collapsed = false + if is_item_state(item, STATE.INITIAL): + item.set_custom_color(0, Color.DARK_GREEN) + item.set_custom_color(1, Color.DARK_GREEN) + item.set_meta(META_GDUNIT_STATE, STATE.RUNNING) + item.collapsed = false + + if is_running: + item.set_icon(0, ICON_SPINNER) + else: + set_item_icon_by_state(item) + for child in item.get_children(): + set_item_icon_by_state(child) + var parent := item.get_parent() if parent != _tree_root: - set_state_running(parent) - # force scrolling to current test case - @warning_ignore("return_value_discarded") - select_item(item) + set_state_running(parent, is_running) func set_state_succeded(item: TreeItem) -> void: + # Do not overwrite higher states + if is_state_error(item) or is_state_failed(item): + return + if item == _tree_root: + return item.set_custom_color(0, Color.GREEN) item.set_custom_color(1, Color.GREEN) item.set_meta(META_GDUNIT_STATE, STATE.SUCCESS) @@ -382,9 +582,11 @@ func set_state_flaky(item: TreeItem, event: GdUnitEvent) -> void: var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT) item.set_meta(META_GDUNIT_STATE, STATE.FLAKY) if retry_count > 1: - item.set_text(0, "%s (%s retries)" % [ - item.get_meta(META_GDUNIT_NAME), - retry_count]) + var item_text: String = item.get_meta(META_GDUNIT_NAME) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)] + item.set_text(0, "%s (%s retries)" % [item_text, retry_count]) item.set_custom_color(0, Color.GREEN_YELLOW) item.set_custom_color(1, Color.GREEN_YELLOW) item.collapsed = false @@ -418,9 +620,11 @@ func set_state_failed(item: TreeItem, event: GdUnitEvent) -> void: return var retry_count := event.statistic(GdUnitEvent.RETRY_COUNT) if retry_count > 1: - item.set_text(0, "%s (%s retries)" % [ - item.get_meta(META_GDUNIT_NAME), - retry_count]) + var item_text: String = item.get_meta(META_GDUNIT_NAME) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + item_text = "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)] + item.set_text(0, "%s (%s retries)" % [item_text, retry_count]) item.set_meta(META_GDUNIT_STATE, STATE.FAILED) item.set_custom_color(0, Color.LIGHT_BLUE) item.set_custom_color(1, Color.LIGHT_BLUE) @@ -463,15 +667,15 @@ func set_state_orphan(item: TreeItem, event: GdUnitEvent) -> void: func update_state(item: TreeItem, event: GdUnitEvent, add_reports := true) -> void: # we do not show the root - if item == _tree_root: + if item == null: return - if event.is_success() and event.is_flaky(): + if event.is_skipped(): + set_state_skipped(item) + elif event.is_success() and event.is_flaky(): set_state_flaky(item, event) elif event.is_success(): set_state_succeded(item) - elif event.is_skipped(): - set_state_skipped(item) elif event.is_error(): set_state_error(item) elif event.is_failed(): @@ -482,8 +686,16 @@ func update_state(item: TreeItem, event: GdUnitEvent, add_reports := true) -> vo for report in event.reports(): add_report(item, report) set_state_orphan(item, event) - if is_folder(item): - update_state(item.get_parent(), event, false) + + var parent := item.get_parent() + if parent == null: + return + + var item_state: int = item.get_meta(META_GDUNIT_STATE) + var parent_state: int = parent.get_meta(META_GDUNIT_STATE) + if item_state <= parent_state: + return + update_state(item.get_parent(), event, false) func add_report(item: TreeItem, report: GdUnitReport) -> void: @@ -501,10 +713,6 @@ func abort_running(items:=_tree_root.get_children()) -> void: abort_running(item.get_children()) -func select_first_failure() -> TreeItem: - return select_item(_find_first_item_by_state(_tree_root, STATE.FAILED)) - - func _on_select_next_item_by_state(item_state: int) -> TreeItem: var current_selected := _tree.get_selected() # If nothing is selected, the first error is selected or the next one in the vicinity of the current selection is found @@ -555,157 +763,148 @@ func show_failed_report(selected_item: TreeItem) -> void: func update_test_suite(event: GdUnitEvent) -> void: - var item := get_tree_item(extract_resource_path(event), event.suite_name()) + var item := _find_tree_item_by_test_suite(_tree_root, event.resource_path(), event.suite_name()) if not item: - push_error("Internal Error: Can't find test suite %s" % event.suite_name()) - return - if event.type() == GdUnitEvent.TESTSUITE_BEFORE: - set_state_running(item) + push_error("[InspectorTreeMainPanel#update_test_suite] Internal Error: Can't find test suite item '{_suite_name}' for {_resource_path} ".format(event)) return if event.type() == GdUnitEvent.TESTSUITE_AFTER: - update_item_counter(item) update_item_elapsed_time_counter(item, event.elapsed_time()) - - update_state(item, event) - update_state(item.get_parent(), event, false) + update_state(item, event) + set_state_running(item, false) func update_test_case(event: GdUnitEvent) -> void: - var item := get_tree_item(extract_resource_path(event), event.test_name()) + var item := _find_tree_item_by_id(_tree_root, event.guid()) if not item: - push_error("Internal Error: Can't find test case %s:%s" % [event.suite_name(), event.test_name()]) + #push_error("Internal Error: Can't find test id %s" % [event.guid()]) return if event.type() == GdUnitEvent.TESTCASE_BEFORE: - set_state_running(item) + set_state_running(item, true) + # force scrolling to current test case + _tree.scroll_to_item(item, true) return + if event.type() == GdUnitEvent.TESTCASE_AFTER: update_item_elapsed_time_counter(item, event.elapsed_time()) if event.is_success() or event.is_warning(): - update_item_counter(item) - update_state(item, event) - - -func create_tree_item(test_suite: GdUnitTestSuiteDto) -> TreeItem: - var parent := _tree_root - var test_root_folder := GdUnitSettings.test_root_folder() - var resource_path := ProjectSettings.localize_path(test_suite.path()) - var test_base_path := "res://" - var test_relative_path := resource_path - if resource_path.contains(test_root_folder): - var path_elements := resource_path.split(test_root_folder) - test_base_path = path_elements[0] + "/" + test_root_folder - test_relative_path = path_elements[1] - test_relative_path = test_relative_path.replace("res://", "") - - if _tree_view_mode_flat: - var element := test_relative_path.get_base_dir().trim_prefix("/") - if element.is_empty(): - return _tree.create_item(parent) - test_base_path += "/" + element - parent = create_or_find_item(parent, test_base_path, element) - return _tree.create_item(parent) - - var elements := test_relative_path.split("/") - if elements[0] == "res://" or elements[0] == "": - elements.remove_at(0) - if elements.size() > 0: - elements.remove_at(elements.size() - 1) - for element in elements: - test_base_path += "/" + element - parent = create_or_find_item(parent, test_base_path, element) - return _tree.create_item(parent) - - -func create_or_find_item(parent: TreeItem, resource_path: String, item_name: String) -> TreeItem: - var item := _find_by_resource_path(parent, resource_path) - if item != null: - return item - item = _tree.create_item(parent) - item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index()) - item.set_text(0, item_name) - item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) - item.set_meta(META_GDUNIT_NAME, item_name) - item.set_meta(META_GDUNIT_TYPE, GdUnitType.FOLDER) - item.set_meta(META_RESOURCE_PATH, resource_path) - item.set_meta(META_GDUNIT_TOTAL_TESTS, 0) - item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) - set_item_icon_by_state(item) - item.collapsed = true - return item + update_item_processed_counter(item) + update_state(item, event) + update_progress_counters(item) -func create_item(parent: TreeItem, resource_path: String, item_name: String, type: GdUnitType) -> TreeItem: +func create_item(parent: TreeItem, test: GdUnitTestCase, item_name: String, type: GdUnitType) -> TreeItem: var item := _tree.create_item(parent) + item.collapsed = true item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index()) item.set_text(0, item_name) - item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) + match type: + GdUnitType.TEST_CASE: + item.set_meta(META_TEST_CASE, test) + GdUnitType.TEST_GROUP: + # We need to create a copy of the test record meta with a new uniqe guid + item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.test_name)) + GdUnitType.TEST_SUITE: + # We need to create a copy of the test record meta with a new uniqe guid + item.set_meta(META_TEST_CASE, GdUnitTestCase.from(test.suite_resource_path, test.source_file, test.line_number, test.suite_name)) + item.set_meta(META_GDUNIT_NAME, item_name) - item.set_meta(META_GDUNIT_TYPE, type) - item.set_meta(META_RESOURCE_PATH, resource_path) - item.set_meta(META_GDUNIT_TOTAL_TESTS, 0) - item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) - set_item_icon_by_state(item) - item.collapsed = true + set_state_initial(item, type) + update_item_total_counter(item) return item func set_item_icon_by_state(item :TreeItem) -> void: - var resource_path :String = item.get_meta(META_RESOURCE_PATH) + if item == _tree_root: + return var state :STATE = item.get_meta(META_GDUNIT_STATE) var is_orphan := is_item_state_orphan(item) + var resource_path := get_item_source_file(item) item.set_icon(0, get_icon_by_file_type(resource_path, state, is_orphan)) if item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER: item.set_icon_modulate(0, Color.SKY_BLUE) -func init_item_counter(item: TreeItem) -> void: - if item.has_meta(META_GDUNIT_TOTAL_TESTS) and item.get_meta(META_GDUNIT_TOTAL_TESTS) > 0: - item.set_text(0, "(0/%s) %s" % [ - item.get_meta(META_GDUNIT_TOTAL_TESTS), - item.get_meta(META_GDUNIT_NAME)]) - init_folder_counter(item.get_parent()) - - -func increment_item_counter(item: TreeItem, increment_count: int) -> void: - if item != _tree_root and item.get_meta(META_GDUNIT_TOTAL_TESTS) != 0: - var count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) - item.set_meta(META_GDUNIT_SUCCESS_TESTS, count + increment_count) - item.set_text(0, "(%s/%s) %s" % [ - item.get_meta(META_GDUNIT_SUCCESS_TESTS), - item.get_meta(META_GDUNIT_TOTAL_TESTS), - item.get_meta(META_GDUNIT_NAME)]) - if is_folder(item): - increment_item_counter(item.get_parent(), increment_count) +func update_item_total_counter(item: TreeItem) -> void: + if item == null: + return + var child_count := get_total_child_count(item) + if child_count > 0: + item.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, child_count) + item.set_text(0, "(0/%d) %s" % [child_count, item.get_meta(META_GDUNIT_NAME)]) -func init_folder_counter(item: TreeItem) -> void: - if item == _tree_root: - return - var type :GdUnitType = item.get_meta(META_GDUNIT_TYPE) - if type == GdUnitType.FOLDER: - var count :int = item.get_children().reduce(count_tests_total, 0) - item.set_meta(META_GDUNIT_TOTAL_TESTS, count) - item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) - item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) - init_item_counter(item) + update_item_total_counter(item.get_parent()) -func count_tests_total(accum: int, item: TreeItem) -> int: - return accum + item.get_meta(META_GDUNIT_TOTAL_TESTS) +func get_total_child_count(item: TreeItem) -> int: + var total_count := 0 + for child in item.get_children(): + total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX) else 1 + return total_count -func update_item_counter(item: TreeItem) -> void: +func update_item_processed_counter(item: TreeItem, add_count := 1) -> void: if item == _tree_root: return - var type :GdUnitType = item.get_meta(META_GDUNIT_TYPE) - match type: - GdUnitType.TEST_CASE: - increment_item_counter(item.get_parent(), 1) - GdUnitType.TEST_CASE_PARAMETERIZED: - increment_item_counter(item.get_parent(), 1) - GdUnitType.TEST_SUITE: - var count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) - increment_item_counter(item.get_parent(), count) + + var success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + add_count + item.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count) + if item.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + item.set_text(0, "(%d/%d) %s" % [success_count, item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX), item.get_meta(META_GDUNIT_NAME)]) + + update_item_processed_counter(item.get_parent(), add_count) + + +func update_progress_counters(item: TreeItem) -> void: + var index: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_INDEX) + 1 + var total_test: int = _tree_root.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) + var state: STATE = item.get_meta(META_GDUNIT_STATE) + test_counters_changed.emit(index, total_test, state) + _tree_root.set_meta(META_GDUNIT_PROGRESS_INDEX, index) + + +func recalculate_counters(parent: TreeItem) -> void: + # Reset the counter first + if parent.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + if parent.has_meta(META_GDUNIT_PROGRESS_INDEX): + parent.set_meta(META_GDUNIT_PROGRESS_INDEX, 0) + if parent.has_meta(META_GDUNIT_SUCCESS_TESTS): + parent.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) + + # Calculate new count based on children + var total_count := 0 + var success_count := 0 + var progress_index := 0 + + for child in parent.get_children(): + if child.get_child_count() > 0: + # Recursively update child counters first + recalculate_counters(child) + # Add child's counters to parent + if child.has_meta(META_GDUNIT_PROGRESS_COUNT_MAX): + total_count += child.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX) + if child.has_meta(META_GDUNIT_SUCCESS_TESTS): + success_count += child.get_meta(META_GDUNIT_SUCCESS_TESTS) + if child.has_meta(META_GDUNIT_PROGRESS_INDEX): + progress_index += child.get_meta(META_GDUNIT_PROGRESS_INDEX) + elif is_test_case(child): + # Count individual test cases + total_count += 1 + # Count completed tests + if is_state_success(child) or is_state_warning(child) or is_state_failed(child) or is_state_error(child): + progress_index += 1 + if is_state_success(child) or is_state_warning(child): + success_count += 1 + + # Update the counters + if total_count > 0: + parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_count) + parent.set_meta(META_GDUNIT_PROGRESS_INDEX, progress_index) + parent.set_meta(META_GDUNIT_SUCCESS_TESTS, success_count) + + # Update the display text + parent.set_text(0, "(%d/%d) %s" % [success_count, total_count, parent.get_meta(META_GDUNIT_NAME)]) func update_item_elapsed_time_counter(item: TreeItem, time: int) -> void: @@ -769,125 +968,128 @@ func get_icon_by_file_type(path: String, state: STATE, orphans: bool) -> Texture return ICON_FOLDER -func discover_test_suite_added(event: GdUnitEventTestDiscoverTestSuiteAdded) -> void: - # Check first if the test suite already exists - var item := get_tree_item(extract_resource_path(event), event.suite_name()) - if item != null: - return - # Otherwise create it - prints("Discovered test suite added: '%s' on %s" % [event.suite_name(), extract_resource_path(event)]) - do_add_test_suite(event.suite_dto()) +func on_test_case_discover_added(test_case: GdUnitTestCase) -> void: + var test_root_folder := GdUnitSettings.test_root_folder().replace("res://", "") + var fully_qualified_name := test_case.fully_qualified_name.trim_suffix(test_case.display_name) + var parts := fully_qualified_name.split(".", false) + parts.append(test_case.display_name) + # Skip tree structure until test root folder + var index := parts.find(test_root_folder) + if index != -1: + parts = parts.slice(index+1) + + match _current_tree_view_mode: + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT: + create_items_tree_mode_flat(test_case, parts) + GdUnitInspectorTreeConstants.TREE_VIEW_MODE.TREE: + create_items_tree_mode_tree(test_case, parts) -func discover_test_added(event: GdUnitEventTestDiscoverTestAdded) -> void: - # check if the test already exists - var test_name := event.test_case_dto().name() - var resource_path := extract_resource_path(event) - var item := get_tree_item(resource_path, test_name) +func create_items_tree_mode_tree(test_case: GdUnitTestCase, parts: PackedStringArray) -> void: + var parent := _tree_root + var is_suite_assigned := false + var suite_name := test_case.suite_name.split(".")[-1] + for item_name in parts: + var next := _find_tree_item(parent, item_name) + if next != null: + parent = next + continue + + if not is_suite_assigned and suite_name == item_name: + next = create_item(parent, test_case, item_name, GdUnitType.TEST_SUITE) + is_suite_assigned = true + elif item_name == test_case.display_name: + next = create_item(parent, test_case, item_name, GdUnitType.TEST_CASE) + # On grouped tests (parameterized tests) + elif item_name == test_case.test_name: + next = create_item(parent, test_case, item_name, GdUnitType.TEST_GROUP) + else: + next = create_item(parent, test_case, item_name, GdUnitType.FOLDER) + parent = next + + +func create_items_tree_mode_flat(test_case: GdUnitTestCase, parts: PackedStringArray) -> void: + # All parts except the last two (suite name and test name/display name) + var slice_index := -2 if parts[-1] == test_case.test_name else -3 + var path_parts := parts.slice(0, slice_index) + var folder_path := "/".join(path_parts) + + # Find or create flat folder + var folder_item: TreeItem + if folder_path.is_empty(): + folder_item = _tree_root + else: + folder_item = _find_tree_item(_tree_root, folder_path) + if folder_item == null: + folder_item = create_item(_tree_root, test_case, folder_path, GdUnitType.FOLDER) + + # Find suite under the flat folder (second to last part) + var suite_item := _find_tree_item(folder_item, test_case.suite_name) + if suite_item == null: + suite_item = create_item(folder_item, test_case, test_case.suite_name, GdUnitType.TEST_SUITE) + + # Add test case or group under the suite + if test_case.test_name != test_case.display_name: + # It's a parameterized test group + var group_item := _find_tree_item(suite_item, test_case.test_name) + if group_item == null: + group_item = create_item(suite_item, test_case, test_case.test_name, GdUnitType.TEST_GROUP) + create_item(group_item, test_case, test_case.display_name, GdUnitType.TEST_CASE) + else: + create_item(suite_item, test_case, test_case.display_name, GdUnitType.TEST_CASE) + + +func on_test_case_discover_deleted(test_case: GdUnitTestCase) -> void: + var item := _find_tree_item_by_id(_tree_root, test_case.guid) if item != null: - return + var parent := item.get_parent() + parent.remove_child(item) - item = get_tree_item(resource_path, event.suite_name()) - if not item: - push_error("Internal Error: Can't find test suite %s:%s" % [event.suite_name(), resource_path]) - return - prints("Discovered test added: '%s' on %s" % [event.test_name(), resource_path]) - # update test case count - var test_count :int = item.get_meta(META_GDUNIT_TOTAL_TESTS) - item.set_meta(META_GDUNIT_TOTAL_TESTS, test_count + 1) - init_item_counter(item) - # add new discovered test - add_test(item, event.test_case_dto()) - - -func discover_test_removed(event: GdUnitEventTestDiscoverTestRemoved) -> void: - var resource_path := extract_resource_path(event) - prints("Discovered test removed: '%s' on %s" % [event.test_name(), resource_path]) - var item := get_tree_item(resource_path, event.test_name()) - if not item: - push_error("Internal Error: Can't find test suite %s:%s" % [event.suite_name(), resource_path]) - return - # update test case count on test suite - var parent := item.get_parent() - var test_count :int = parent.get_meta(META_GDUNIT_TOTAL_TESTS) - parent.set_meta(META_GDUNIT_TOTAL_TESTS, test_count - 1) - init_item_counter(parent) - # finally remove the test - @warning_ignore("return_value_discarded") - remove_tree_item(resource_path, event.test_name()) - - -func do_add_test_suite(test_suite: GdUnitTestSuiteDto) -> void: - var item := create_tree_item(test_suite) - var suite_name := test_suite.name() - var resource_path := ProjectSettings.localize_path(test_suite.path()) - item.set_text(0, suite_name) - item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index()) - item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) - item.set_meta(META_GDUNIT_NAME, suite_name) - item.set_meta(META_GDUNIT_TYPE, GdUnitType.TEST_SUITE) - item.set_meta(META_GDUNIT_TOTAL_TESTS, test_suite.test_case_count()) - item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) - item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) - item.set_meta(META_RESOURCE_PATH, resource_path) - item.set_meta(META_LINE_NUMBER, 1) - item.collapsed = true - set_item_icon_by_state(item) - init_item_counter(item) - add_tree_item_to_cache(resource_path, suite_name, item) - for test_case in test_suite.test_cases(): - add_test(item, test_case) + # update the cached counters + var item_success_count: int = item.get_meta(META_GDUNIT_SUCCESS_TESTS) + var item_total_test_count: int = item.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + var total_test_count: int = parent.get_meta(META_GDUNIT_PROGRESS_COUNT_MAX, 0) + parent.set_meta(META_GDUNIT_PROGRESS_COUNT_MAX, total_test_count-item_total_test_count) + # propagate counter update to all parents + update_item_total_counter(parent) + update_item_processed_counter(parent, -item_success_count) -func add_test(parent: TreeItem, test_case: GdUnitTestCaseDto) -> void: - var item := _tree.create_item(parent) - var test_name := test_case.name() - var resource_path :String = parent.get_meta(META_RESOURCE_PATH) - var test_case_names := test_case.test_case_names() - item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index()) - item.set_text(0, test_name) - item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) - item.set_meta(META_GDUNIT_NAME, test_name) - item.set_meta(META_GDUNIT_TYPE, GdUnitType.TEST_CASE) - item.set_meta(META_RESOURCE_PATH, resource_path) - item.set_meta(META_GDUNIT_SUCCESS_TESTS, 0) - item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) - item.set_meta(META_GDUNIT_TOTAL_TESTS, test_case_names.size()) - item.set_meta(META_SCRIPT_PATH, test_case.script_path()) - item.set_meta(META_LINE_NUMBER, test_case.line_number()) - item.set_meta(META_TEST_PARAM_INDEX, -1) - set_item_icon_by_state(item) - init_item_counter(item) - add_tree_item_to_cache(resource_path, test_name, item) - if not test_case_names.is_empty(): - add_test_cases(item, test_case_names) - - -func add_test_cases(parent: TreeItem, test_case_names: PackedStringArray) -> void: - for index in test_case_names.size(): - var item := _tree.create_item(parent) - var test_case_name := test_case_names[index] - var resource_path :String = parent.get_meta(META_RESOURCE_PATH) - item.set_meta(META_GDUNIT_ORIGINAL_INDEX, item.get_index()) - item.set_text(0, test_case_name) - item.set_meta(META_GDUNIT_STATE, STATE.INITIAL) - item.set_meta(META_GDUNIT_NAME, test_case_name) - item.set_meta(META_GDUNIT_TOTAL_TESTS, 0) - item.set_meta(META_GDUNIT_TYPE, GdUnitType.TEST_CASE_PARAMETERIZED) - item.set_meta(META_GDUNIT_EXECUTION_TIME, 0) - item.set_meta(META_RESOURCE_PATH, resource_path) - item.set_meta(META_SCRIPT_PATH, parent.get_meta(META_SCRIPT_PATH)) - item.set_meta(META_LINE_NUMBER, parent.get_meta(META_LINE_NUMBER)) - item.set_meta(META_TEST_PARAM_INDEX, index) - set_item_icon_by_state(item) - add_tree_item_to_cache(resource_path, test_case_name, item) +func on_test_case_discover_modified(test_case: GdUnitTestCase) -> void: + var item := _find_tree_item_by_id(_tree_root, test_case.guid) + if item != null: + item.set_meta(META_TEST_CASE, test_case) + item.set_text(0, test_case.display_name) + item.set_meta(META_GDUNIT_NAME, test_case.display_name) func get_item_reports(item: TreeItem) -> Array[GdUnitReport]: return item.get_meta(META_GDUNIT_REPORT) +func get_item_test_line_number(item: TreeItem) -> int: + if item == null or not item.has_meta(META_TEST_CASE): + return -1 + + var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE) + return test_case.line_number + + +func get_item_source_file(item: TreeItem) -> String: + if item == null or not item.has_meta(META_TEST_CASE): + return "" + + var test_case: GdUnitTestCase = item.get_meta(META_TEST_CASE) + return test_case.source_file + + +func get_item_type(item: TreeItem) -> GdUnitType: + if item == null or not item.has_meta(META_GDUNIT_TYPE): + return GdUnitType.FOLDER + return item.get_meta(META_GDUNIT_TYPE) + + func _dump_tree_as_json(dump_name: String) -> void: var dict := _to_json(_tree_root) var file := FileAccess.open("res://%s.json" % dump_name, FileAccess.WRITE) @@ -896,7 +1098,7 @@ func _dump_tree_as_json(dump_name: String) -> void: func _to_json(parent :TreeItem) -> Dictionary: var item_as_dict := GdObjects.obj2dict(parent) - item_as_dict["TreeItem"]["childs"] = parent.get_children().map(func(item: TreeItem) -> Dictionary: + item_as_dict["TreeItem"]["childrens"] = parent.get_children().map(func(item: TreeItem) -> Dictionary: return _to_json(item)) return item_as_dict @@ -905,6 +1107,18 @@ func extract_resource_path(event: GdUnitEvent) -> String: return ProjectSettings.localize_path(event.resource_path()) +func collect_test_cases(item: TreeItem, tests: Array[GdUnitTestCase] = []) -> Array[GdUnitTestCase]: + for next in item.get_children(): + collect_test_cases(next, tests) + + if is_test_case(item): + var test: GdUnitTestCase = item.get_meta(META_TEST_CASE) + if not tests.has(test): + tests.append(test) + + return tests + + ################################################################################ # Tree signal receiver ################################################################################ @@ -920,18 +1134,9 @@ func _on_run_pressed(run_debug: bool) -> void: if item == null: print_rich("[color=GOLDENROD]Abort Testrun, no test suite selected![/color]") return - if item.get_meta(META_GDUNIT_TYPE) == GdUnitType.TEST_SUITE or item.get_meta(META_GDUNIT_TYPE) == GdUnitType.FOLDER: - var resource_path: String = item.get_meta(META_RESOURCE_PATH) - run_testsuite.emit([resource_path], run_debug) - return - var parent := item.get_parent() - var test_suite_resource_path: String = parent.get_meta(META_RESOURCE_PATH) - var test_case: String = item.get_meta(META_GDUNIT_NAME) - # handle parameterized test selection - var test_param_index: int = item.get_meta(META_TEST_PARAM_INDEX) - if test_param_index != -1: - test_case = parent.get_meta(META_GDUNIT_NAME) - run_testcase.emit(test_suite_resource_path, test_case, test_param_index, run_debug) + + var test_to_execute := collect_test_cases(item) + GdUnitCommandHandler.instance().cmd_run_tests(test_to_execute, run_debug) func _on_Tree_item_selected() -> void: @@ -940,17 +1145,16 @@ func _on_Tree_item_selected() -> void: if not _context_menu.is_item_disabled(CONTEXT_MENU_RUN_ID): var selected_item: TreeItem = _tree.get_selected() show_failed_report(selected_item) + _current_selected_item = _tree.get_selected() + tree_item_selected.emit(_current_selected_item) # Opens the test suite func _on_Tree_item_activated() -> void: var selected_item := _tree.get_selected() - if selected_item != null and selected_item.has_meta(META_LINE_NUMBER): - var script_path: String = ( - selected_item.get_meta(META_RESOURCE_PATH) if is_test_suite(selected_item) - else selected_item.get_meta(META_SCRIPT_PATH) - ) - var line_number: int = selected_item.get_meta(META_LINE_NUMBER) + var line_number := get_item_test_line_number(selected_item) + if line_number != -1: + var script_path := ProjectSettings.localize_path(get_item_source_file(selected_item)) var resource: Script = load(script_path) if selected_item.has_meta(META_GDUNIT_REPORT): @@ -972,21 +1176,21 @@ func _on_Tree_item_activated() -> void: # external signal receiver ################################################################################ func _on_gdunit_runner_start() -> void: - reset_tree_state(_tree_root) _context_menu.set_item_disabled(CONTEXT_MENU_RUN_ID, true) _context_menu.set_item_disabled(CONTEXT_MENU_DEBUG_ID, true) + reset_tree_state(_tree_root) clear_reports() -func _on_gdunit_runner_stop(_client_id: int) -> void: +func _on_gdunit_runner_stop(_id: int) -> void: _context_menu.set_item_disabled(CONTEXT_MENU_RUN_ID, false) _context_menu.set_item_disabled(CONTEXT_MENU_DEBUG_ID, false) abort_running() sort_tree_items(_tree_root) # wait until the tree redraw await get_tree().process_frame - @warning_ignore("return_value_discarded") - select_first_failure() + var failure_item := _find_first_item_by_state(_tree_root, STATE.FAILED) + select_item( failure_item if failure_item else _current_selected_item) func _on_gdunit_event(event: GdUnitEvent) -> void: @@ -998,26 +1202,13 @@ func _on_gdunit_event(event: GdUnitEvent) -> void: GdUnitEvent.DISCOVER_END: sort_tree_items(_tree_root) + select_item(_tree_root.get_first_child()) _discover_hint.visible = false _tree_root.visible = true #_dump_tree_as_json("tree_example_discovered") - GdUnitEvent.DISCOVER_SUITE_ADDED: - discover_test_suite_added(event as GdUnitEventTestDiscoverTestSuiteAdded) - - GdUnitEvent.DISCOVER_TEST_ADDED: - discover_test_added(event as GdUnitEventTestDiscoverTestAdded) - - GdUnitEvent.DISCOVER_TEST_REMOVED: - discover_test_removed(event as GdUnitEventTestDiscoverTestRemoved) - GdUnitEvent.INIT: - if not GdUnitSettings.is_test_discover_enabled(): - init_tree() - - GdUnitEvent.STOP: - sort_tree_items(_tree_root) - #_dump_tree_as_json("tree_example") + _on_gdunit_runner_start() GdUnitEvent.TESTCASE_BEFORE: update_test_case(event) @@ -1045,10 +1236,10 @@ func _on_context_m_index_pressed(index: int) -> void: func _on_settings_changed(property :GdUnitProperty) -> void: - if property.name() == GdUnitSettings.INSPECTOR_TREE_SORT_MODE: - sort_tree_items(_tree_root) - # _dump_tree_as_json("tree_sorted_by_%s" % GdUnitInspectorTreeConstants.SORT_MODE.keys()[property.value()]) + match property.name(): + GdUnitSettings.INSPECTOR_TREE_SORT_MODE: + sort_tree_items(_tree_root) + #_dump_tree_as_json("tree_sorted_by_%s" % GdUnitInspectorTreeConstants.SORT_MODE.keys()[property.value()]) - if property.name() == GdUnitSettings.INSPECTOR_TREE_VIEW_MODE: - _tree_view_mode_flat = property.value() == GdUnitInspectorTreeConstants.TREE_VIEW_MODE.FLAT - GdUnitCommandHandler.instance().cmd_discover_tests() + GdUnitSettings.INSPECTOR_TREE_VIEW_MODE: + restructure_tree(_tree_root, GdUnitSettings.get_inspector_tree_view_mode()) diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid index 3e9c0b51..e30f6deb 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid +++ b/addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd.uid @@ -1 +1 @@ -uid://dauvi8e3d56d3 +uid://bfxyodyfsh5dk diff --git a/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn index 1885ea20..a4247706 100644 --- a/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn +++ b/addons/gdUnit4/src/ui/parts/InspectorTreePanel.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=27 format=3 uid="uid://bqfpidewtpeg0"] -[ext_resource type="Script" uid="uid://dauvi8e3d56d3" path="res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/parts/InspectorTreeMainPanel.gd" id="1"] [sub_resource type="Image" id="Image_466oo"] data = { diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid index 64e26179..bfb37e65 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd.uid @@ -1 +1 @@ -uid://cmjcsb0dch3og +uid://c3erojmjbnikq diff --git a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn index fb7e6a89..7e62c438 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://pmnkxrhglak5"] -[ext_resource type="Script" uid="uid://cmjcsb0dch3og" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd" id="1_gki1u"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.gd" id="1_gki1u"] [node name="GdUnitInputMapper" type="Control"] modulate = Color(0.929099, 0.929099, 0.929099, 0.936189) diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd index 4a9a9465..14a8bd5b 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd @@ -10,15 +10,15 @@ const GdUnitUpdateClient = preload ("res://addons/gdUnit4/src/update/GdUnitUpdat @onready var _btn_install: Button = %btn_install_examples @onready var _progress_bar: ProgressBar = %ProgressBar @onready var _progress_text: Label = %progress_lbl -@onready var _properties_template: Node = $property_template -@onready var _properties_common: Node = % "common-content" -@onready var _properties_ui: Node = % "ui-content" -@onready var _properties_shortcuts: Node = % "shortcut-content" -@onready var _properties_report: Node = % "report-content" +@onready var _properties_template: Control = $property_template +@onready var _properties_common: Control = % "common-content" +@onready var _properties_ui: Control = % "ui-content" +@onready var _properties_shortcuts: Control = % "shortcut-content" +@onready var _properties_report: Control = % "report-content" @onready var _input_capture: GdUnitInputCapture = %GdUnitInputCapture @onready var _property_error: Window = % "propertyError" @onready var _tab_container: TabContainer = %Properties -@onready var _update_tab: = %Update +@onready var _update_tab: Control = %Update var _font_size: float @@ -41,7 +41,7 @@ func _sort_by_key(left: GdUnitProperty, right: GdUnitProperty) -> bool: return left.name() < right.name() -func setup_properties(properties_parent: Node, property_category: String) -> void: +func setup_properties(properties_parent: Control, property_category: String) -> void: # Do remove first potential previous added properties (could be happened when the dlg is opened at twice) for child in properties_parent.get_children(): properties_parent.remove_child(child) @@ -66,8 +66,9 @@ func setup_properties(properties_parent: Node, property_category: String) -> voi grid.columns = 4 grid.theme = theme_ - var sub_category: Node = _properties_template.get_child(3).duplicate() - sub_category.get_child(0).text = current_category.capitalize() + var sub_category: Control = _properties_template.get_child(3).duplicate() + var category_label: Label = sub_category.get_child(0) + category_label.text = current_category.capitalize() sub_category.custom_minimum_size.y = _font_size + 16 properties_parent.add_child(sub_category) properties_parent.add_child(grid) @@ -91,7 +92,7 @@ func setup_properties(properties_parent: Node, property_category: String) -> voi @warning_ignore("return_value_discarded") reset_btn.pressed.connect(_on_btn_property_reset_pressed.bind(property, input, reset_btn)) # property help text - var info: Node = _properties_template.get_child(2).duplicate() + var info: Label = _properties_template.get_child(2).duplicate() info.text = property.help() info_labels.append(info) grid.add_child(info) @@ -106,7 +107,6 @@ func setup_properties(properties_parent: Node, property_category: String) -> voi properties_parent.custom_minimum_size.x = min_size_overall -@warning_ignore("return_value_discarded") func _create_input_element(property: GdUnitProperty, reset_btn: Button) -> Node: if property.is_selectable_value(): var options := OptionButton.new() @@ -114,7 +114,7 @@ func _create_input_element(property: GdUnitProperty, reset_btn: Button) -> Node: for value in property.value_set(): options.add_item(value) options.item_selected.connect(_on_option_selected.bind(property, reset_btn)) - options.select(property.value()) + options.select(property.int_value()) return options if property.type() == TYPE_BOOL: var check_btn := CheckButton.new() @@ -131,7 +131,8 @@ func _create_input_element(property: GdUnitProperty, reset_btn: Button) -> Node: return input if property.type() == TYPE_PACKED_INT32_ARRAY: var key_input_button := Button.new() - key_input_button.text = to_shortcut(property.value()) + var value:PackedInt32Array = property.value() + key_input_button.text = to_shortcut(value) key_input_button.pressed.connect(_on_shortcut_change.bind(key_input_button, property, reset_btn)) return key_input_button return Control.new() @@ -150,7 +151,6 @@ func to_shortcut(keys: PackedInt32Array) -> String: return input_event.as_text() -@warning_ignore("return_value_discarded") func to_keys(input_event: InputEventKey) -> PackedInt32Array: var keys := PackedInt32Array() if input_event.ctrl_pressed: @@ -184,8 +184,8 @@ func _install_examples() -> void: var tmp_path := GdUnitFileAccess.create_temp_dir("download") var zip_file := tmp_path + "/examples.zip" var response: GdUnitUpdateClient.HttpResponse = await _update_client.request_zip_package(EAXAMPLE_URL, zip_file) - if response.code() != 200: - push_warning("Examples cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.code(), response.response()]) + if response.status() != 200: + push_warning("Examples cannot be retrieved from GitHub! \n Error code: %d : %s" % [response.status(), response.response()]) update_progress("Install examples failed! Try it later again.") await get_tree().create_timer(3).timeout stop_progress() @@ -199,20 +199,28 @@ func _install_examples() -> void: stop_progress() return update_progress("Refresh project") - await rescan(true) + await rescan() + await reimport("res://gdUnit4-examples/") + update_progress("Examples successfully installed") await get_tree().create_timer(3).timeout stop_progress() -func rescan(update_scripts:=false) -> void: - await get_tree().idle_frame +func rescan() -> void: + await get_tree().process_frame var fs := EditorInterface.get_resource_filesystem() fs.scan_sources() while fs.is_scanning(): await get_tree().create_timer(1).timeout - if update_scripts: - EditorInterface.get_resource_filesystem().update_script_classes() + + +func reimport(path: String) -> void: + await get_tree().process_frame + var files := DirAccess.get_files_at(path) + EditorInterface.get_resource_filesystem().reimport_files(files) + for directory in DirAccess.get_directories_at(path): + reimport(directory) func check_for_update() -> void: @@ -252,17 +260,19 @@ func _on_btn_close_pressed() -> void: func _on_btn_property_reset_pressed(property: GdUnitProperty, input: Node, reset_btn: Button) -> void: if input is CheckButton: - input.button_pressed = property.default() + var is_default_pressed: bool = property.default() + (input as CheckButton).button_pressed = is_default_pressed elif input is LineEdit: - input.text = str(property.default()) + (input as LineEdit).text = str(property.default()) # we have to update manually for text input fields because of no change event is emited _on_property_text_changed(property.default(), property, reset_btn) elif input is OptionButton: - input.select(0) + (input as OptionButton).select(0) _on_option_selected(0, property, reset_btn) elif input is Button: - input.text = to_shortcut(property.default()) - _on_property_text_changed(property.default(), property, reset_btn) + var value: PackedInt32Array = property.default() + (input as Button).text = to_shortcut(value) + _on_property_text_changed(value, property, reset_btn) func _on_property_text_changed(new_value: Variant, property: GdUnitProperty, reset_btn: Button) -> void: @@ -271,7 +281,7 @@ func _on_property_text_changed(new_value: Variant, property: GdUnitProperty, res var error: Variant = GdUnitSettings.update_property(property) if error: var label: Label = _property_error.get_child(0) as Label - label.set_text(error) + label.set_text(str(error)) var control := gui_get_focus_owner() _property_error.show() if control != null: diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid index dc3e779e..d92713c9 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd.uid @@ -1 +1 @@ -uid://cstv4pnqu72e4 +uid://cdptt8hmovee6 diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn index 8be23f94..08afee43 100644 --- a/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.tscn @@ -1,11 +1,12 @@ -[gd_scene load_steps=8 format=3 uid="uid://dwgat6j2u77g4"] +[gd_scene load_steps=9 format=3] -[ext_resource type="Script" uid="uid://cstv4pnqu72e4" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"] -[ext_resource type="Texture2D" uid="uid://bkh022wwqq7s3" path="res://addons/gdUnit4/src/ui/settings/logo.png" id="3_isfyl"] -[ext_resource type="PackedScene" uid="uid://dte0m2endcgtu" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn" id="4"] -[ext_resource type="PackedScene" uid="uid://0xyeci1tqebj" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn" id="5_n1jtv"] -[ext_resource type="PackedScene" uid="uid://pmnkxrhglak5" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"] -[ext_resource type="Script" uid="uid://bjrq7poxyhrd5" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsDialog.gd" id="2"] +[ext_resource type="Texture2D" path="res://addons/gdUnit4/src/ui/settings/logo.png" id="3_isfyl"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn" id="4"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn" id="4_nf72w"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn" id="5_n1jtv"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/ui/settings/GdUnitInputCapture.tscn" id="5_xu3j8"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="8_2ggr0"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hbbq5"] content_margin_left = 10.0 @@ -226,10 +227,14 @@ layout_mode = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +[node name="Hooks" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4_nf72w")] +visible = false +layout_mode = 2 + [node name="UI" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] visible = false layout_mode = 2 -metadata/_tab_index = 1 +metadata/_tab_index = 2 [node name="ui-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/UI"] unique_name_in_owner = true @@ -242,7 +247,7 @@ size_flags_vertical = 3 [node name="Shortcuts" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] visible = false layout_mode = 2 -metadata/_tab_index = 2 +metadata/_tab_index = 3 [node name="shortcut-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Shortcuts"] unique_name_in_owner = true @@ -255,7 +260,7 @@ size_flags_vertical = 3 [node name="Report" type="ScrollContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties"] visible = false layout_mode = 2 -metadata/_tab_index = 3 +metadata/_tab_index = 4 [node name="report-content" type="VBoxContainer" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties/Report"] unique_name_in_owner = true @@ -268,13 +273,13 @@ size_flags_vertical = 3 [node name="Templates" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("4")] visible = false layout_mode = 2 -metadata/_tab_index = 4 +metadata/_tab_index = 5 [node name="Update" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_n1jtv")] unique_name_in_owner = true visible = false layout_mode = 2 -metadata/_tab_index = 5 +metadata/_tab_index = 6 [node name="GdUnitInputCapture" parent="Panel/PanelContainer/v/MarginContainer/GridContainer/Properties" instance=ExtResource("5_xu3j8")] unique_name_in_owner = true diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd new file mode 100644 index 00000000..3b01f340 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd @@ -0,0 +1,255 @@ +@tool +extends ScrollContainer + + +@onready var _hooks_tree: Tree = %hooks_tree +@onready var _hook_description: RichTextLabel = %hook_description +@onready var _btn_move_up: Button = %hook_actions/btn_move_up +@onready var _btn_move_down: Button = %hook_actions/btn_move_down +@onready var _btn_delete: Button = %hook_actions/btn_delete_hook +@onready var _select_hook_dlg: FileDialog = %select_hook_dlg +@onready var _error_msg_popup :AcceptDialog = %error_msg_popup + +var _selected_hook_item: TreeItem = null +var _root: TreeItem +var _system_box_style: StyleBoxFlat +var _priority_box_style: StyleBoxFlat + +func _ready() -> void: + _setup_styles() + _setup_buttons() + _setup_tree() + _load_registered_hooks() + + +func _setup_styles() -> void: + _system_box_style = StyleBoxFlat.new() + _system_box_style.bg_color = Color(1.0, 0.76, 0.03, 1) + _system_box_style.corner_radius_top_left = 6 + _system_box_style.corner_radius_top_right = 6 + _system_box_style.corner_radius_bottom_left = 6 + _system_box_style.corner_radius_bottom_right = 6 + _priority_box_style = _system_box_style.duplicate() + _priority_box_style.bg_color = Color(0.26, 0.54, 0.89, 1) + + +func _setup_buttons() -> void: + #if Engine.is_editor_hint(): + # _btn_move_up.icon = GdUnitUiTools.get_icon("MoveUp") + # _btn_move_down.icon = GdUnitUiTools.get_icon("MoveDown") + # _btn_add.icon = GdUnitUiTools.get_icon("Add") + # _btn_delete.icon = GdUnitUiTools.get_icon("Remove") + pass + + +func _setup_tree() -> void: + _hooks_tree.clear() + _root = _hooks_tree.create_item() + _hooks_tree.set_columns(2) + _hooks_tree.set_column_custom_minimum_width(1, 32) + _hooks_tree.set_column_expand(1, false) + _hooks_tree.set_hide_root(true) + _hooks_tree.set_hide_folding(true) + _hooks_tree.set_select_mode(Tree.SELECT_SINGLE) + _hooks_tree.item_selected.connect(_on_hook_selected) + _hooks_tree.item_edited.connect(_on_item_edited) + + +func _load_registered_hooks() -> void: + var hook_service := GdUnitTestSessionHookService.instance() + for hook: GdUnitTestSessionHook in hook_service.enigne_hooks: + _create_hook_tree_item(hook) + + # Select first item if any + if _root.get_child_count() > 0: + var first_item: TreeItem = _root.get_first_child() + first_item.select(0) + _on_hook_selected() + + +func _create_hook_tree_item(hook: GdUnitTestSessionHook) -> TreeItem: + var item: TreeItem = _hooks_tree.create_item(_root) + item.set_custom_minimum_height(26) + # Column 0: Hook info with custom drawing + item.set_cell_mode(0, TreeItem.CELL_MODE_CUSTOM) + item.set_custom_draw_callback(0, _draw_hook_item) + item.set_editable(0, false) + item.set_metadata(0, hook) + # Column 1: Checkbox for enable/disable + item.set_cell_mode(1, TreeItem.CELL_MODE_CHECK) + item.set_checked(1, GdUnitTestSessionHookService.is_enabled(hook)) + item.set_editable(1, true) + item.set_custom_bg_color(1, _hook_bg_color(hook)) + item.set_tooltip_text(1, "Enable/Disable the Hook") + item.propagate_check(1) + + if _is_system_hook(hook): + item.set_tooltip_text(0, "System hook - (Read-only)") + else: + item.set_tooltip_text(0, "User hook") + return item + + +func _hook_bg_color(hook: GdUnitTestSessionHook) -> Color: + if _is_system_hook(hook): + return Color(0.133, 0.118, 0.090, 1) # Brownish background for system hooks + return Color(0.176, 0.196, 0.235, 1) # Dark background #2d3142 + + +func _draw_hook_item(item: TreeItem, rect: Rect2) -> void: + var hook := _get_hook(item) + var is_system := _is_system_hook(hook) + var is_selected := item == _selected_hook_item + + # Draw background + var bg_color := _hook_bg_color(hook) # Dark background #2d3142 + if is_selected: + bg_color = bg_color.lerp(Color(0.2, 0.4, 0.6, 0.3), 0.5) # Blue tint for selection + _hooks_tree.draw_rect(rect, bg_color) + + # Draw left border for system hooks + if is_system: + var border_rect := Rect2(rect.position.x, rect.position.y, 3, rect.size.y) + _hooks_tree.draw_rect(border_rect, Color(1.0, 0.76, 0.03, 1)) # Yellow border + + var font := _hooks_tree.get_theme_default_font() + + # Draw hook name + var hook_name := hook.name + var text_pos := Vector2(rect.position.x + ( 15 if is_system else 12), rect.position.y + 18) + var text_color := Color(0.95, 0.95, 0.95, 1) + _hooks_tree.draw_string(font, text_pos, hook_name, HORIZONTAL_ALIGNMENT_LEFT, -1, 14, text_color) + + # Draw system badge if needed + if is_system: + var badge_x := rect.position.x + rect.end.x - 100 + var badge_y := rect.position.y + 14 + var system_badge_rect := Rect2(badge_x, badge_y-8, 48, 16) + _hooks_tree.draw_style_box(_system_box_style, system_badge_rect) + + var system_text_pos := Vector2(badge_x + 4, badge_y + 4) + var system_font_size := 10 + _hooks_tree.draw_string(font, system_text_pos, "SYSTEM", HORIZONTAL_ALIGNMENT_CENTER, -1, system_font_size, Color(0.1, 0.1, 0.1, 1)) + + +func _create_hook_display_text(hook_name: String, priority: int, is_system: bool) -> String: + var text := hook_name + "\n" + text += "Priority: [color=#4299e1][bgcolor=#4299e1] " + str(priority) + " [/bgcolor][/color]" + + if is_system: + text += " [color=#1a202c][bgcolor=#ffc107] SYSTEM [/bgcolor][/color]" + + return text + + +func _update_hook_description() -> void: + if _selected_hook_item == null: + _hook_description.text = "[i]Select a hook to view its description[/i]" + return + _hook_description.text = _get_hook(_selected_hook_item).description + + +func _update_hook_buttons() -> void: + # Is nothing selected disable the move and delete buttons + if _selected_hook_item == null: + _btn_move_up.disabled = true + _btn_move_down.disabled = true + _btn_delete.disabled = true + return + + var hook := _get_hook(_selected_hook_item) + var is_system := _is_system_hook(hook) + + # Disable the move and delete buttons for system hooks by default + if is_system: + _btn_move_up.disabled = true + _btn_move_down.disabled = true + _btn_delete.disabled = true + return + + var prev_item: TreeItem = _selected_hook_item.get_prev() + var next_item: TreeItem = _selected_hook_item.get_next() + + if prev_item != null: + var prev_hook := _get_hook(prev_item) + _btn_move_up.disabled = _is_system_hook(prev_hook) + + _btn_move_down.disabled = next_item == null + _btn_delete.disabled = false + + +static func _get_hook(item: TreeItem) -> GdUnitTestSessionHook: + return item.get_metadata(0) + + +static func _is_system_hook(hook: GdUnitTestSessionHook) -> bool: + if hook == null: + return false + return hook.get_meta("SYSTEM_HOOK") + + +func _on_hook_selected() -> void: + _selected_hook_item = _hooks_tree.get_selected() + _update_hook_buttons() + _update_hook_description() + + +func _on_item_edited() -> void: + var selected_hook_item := _hooks_tree.get_selected() + if selected_hook_item != null: + var hook := _get_hook(selected_hook_item) + var is_enabled := selected_hook_item.is_checked(1) + GdUnitTestSessionHookService.instance().enable_hook(hook, is_enabled) + + +func _on_btn_add_hook_pressed() -> void: + _select_hook_dlg.show() + + +func _on_select_hook_dlg_file_selected(path: String) -> void: + _select_hook_dlg.set_current_path(path) + _on_select_hook_dlg_confirmed() + + +func _on_select_hook_dlg_confirmed() -> void: + _select_hook_dlg.hide() + var result := GdUnitTestSessionHookService.instance().load_hook(_select_hook_dlg.get_current_path()) + if result.is_error(): + _error_msg_popup.dialog_text = result.error_message() + _error_msg_popup.show() + return + + var hook: GdUnitTestSessionHook = result.value() + result = GdUnitTestSessionHookService.instance().register(hook) + if result.is_error(): + _error_msg_popup.dialog_text = result.error_message() + _error_msg_popup.show() + return + + var hook_added := _create_hook_tree_item(hook) + _hooks_tree.set_selected(hook_added, 0) + + +func _on_btn_delete_hook_pressed() -> void: + if _selected_hook_item != null: + _root.remove_child(_selected_hook_item) + GdUnitTestSessionHookService.instance()\ + .unregister(_get_hook(_selected_hook_item)) + _selected_hook_item = null + _update_hook_buttons() + + +func _on_btn_move_up_pressed() -> void: + var prev := _selected_hook_item.get_prev() + _selected_hook_item.move_before(prev) + GdUnitTestSessionHookService.instance()\ + .move_before(_get_hook(_selected_hook_item), _get_hook(prev)) + _update_hook_buttons() + + +func _on_btn_move_down_pressed() -> void: + var next := _selected_hook_item.get_next() + _selected_hook_item.move_after(next) + GdUnitTestSessionHookService.instance()\ + .move_after(_get_hook(_selected_hook_item), _get_hook(next)) + _update_hook_buttons() diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid new file mode 100644 index 00000000..4355ff14 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd.uid @@ -0,0 +1 @@ +uid://dc6wvyastotux diff --git a/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn new file mode 100644 index 00000000..a9a850c0 --- /dev/null +++ b/addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.tscn @@ -0,0 +1,148 @@ +[gd_scene load_steps=10 format=3 uid="uid://41l7a46fol5m"] + +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/settings/GdUnitSettingsTabHooks.gd" id="1_8yffn"] + +[sub_resource type="Image" id="Image_h5sr5"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 240, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 228, 228, 228, 37, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 179, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_77fm0"] +image = SubResource("Image_h5sr5") + +[sub_resource type="Image" id="Image_77fm0"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 180, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 181, 224, 224, 224, 193, 234, 234, 234, 12, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 193, 224, 224, 224, 180, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 200, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 1, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 1, 225, 225, 225, 174, 224, 224, 224, 255, 225, 225, 225, 199, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228, 228, 228, 37, 224, 224, 224, 239, 224, 224, 224, 255, 224, 224, 224, 122, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 239, 227, 227, 227, 36, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 74, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 253, 224, 224, 224, 253, 224, 224, 224, 73, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 123, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 1, 224, 224, 224, 173, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 172, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234, 234, 234, 12, 224, 224, 224, 255, 224, 224, 224, 255, 234, 234, 234, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_rewru"] +image = SubResource("Image_77fm0") + +[sub_resource type="Image" id="Image_kppp6"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_manhx"] +image = SubResource("Image_kppp6") + +[sub_resource type="Image" id="Image_rewru"] +data = { +"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 227, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 73, 224, 224, 224, 226, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 226, 226, 226, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_4h4u1"] +image = SubResource("Image_rewru") + +[node name="Hooks" type="ScrollContainer"] +custom_minimum_size = Vector2(400, 300) +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1_8yffn") +metadata/_tab_index = 1 + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="hooks_content" type="VBoxContainer" parent="HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="hooks_tree" type="Tree" parent="HBoxContainer/hooks_content"] +unique_name_in_owner = true +layout_direction = 2 +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +hide_folding = true +hide_root = true + +[node name="hook_description" type="RichTextLabel" parent="HBoxContainer/hooks_content"] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 120) +layout_mode = 2 +size_flags_vertical = 2 +bbcode_enabled = true +text = "The test result Html reporting hook." +scroll_active = false + +[node name="hook_actions" type="VBoxContainer" parent="HBoxContainer"] +unique_name_in_owner = true +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +size_flags_horizontal = 0 +theme_override_constants/separation = 5 + +[node name="btn_move_up" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Move hook up in priority" +disabled = true +icon = SubResource("ImageTexture_77fm0") +icon_alignment = 1 + +[node name="btn_move_down" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Move hook down in priority" +disabled = true +icon = SubResource("ImageTexture_rewru") +icon_alignment = 1 + +[node name="btn_add_hook" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Add new hook" +icon = SubResource("ImageTexture_manhx") +icon_alignment = 1 + +[node name="btn_delete_hook" type="Button" parent="HBoxContainer/hook_actions"] +layout_mode = 2 +tooltip_text = "Delete selected hook" +disabled = true +icon = SubResource("ImageTexture_4h4u1") +icon_alignment = 1 + +[node name="select_hook_dlg" type="FileDialog" parent="."] +unique_name_in_owner = true +disable_3d = true +title = "Open a File" +initial_position = 3 +current_screen = 0 +ok_button_text = "Open" +file_mode = 0 +filters = PackedStringArray("*.gd") + +[node name="error_msg_popup" type="AcceptDialog" parent="."] +unique_name_in_owner = true +initial_position = 3 +current_screen = 0 + +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_up" to="." method="_on_btn_move_up_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_move_down" to="." method="_on_btn_move_down_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_add_hook" to="." method="_on_btn_add_hook_pressed"] +[connection signal="pressed" from="HBoxContainer/hook_actions/btn_delete_hook" to="." method="_on_btn_delete_hook_pressed"] +[connection signal="confirmed" from="select_hook_dlg" to="." method="_on_select_hook_dlg_confirmed"] +[connection signal="file_selected" from="select_hook_dlg" to="." method="_on_select_hook_dlg_file_selected"] diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid index 2620ddd8..75567f51 100644 --- a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid +++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd.uid @@ -1 +1 @@ -uid://d0fytslwh68xp +uid://bm4782o8xvhx4 diff --git a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn index 2177a42b..5ccc4430 100644 --- a/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn +++ b/addons/gdUnit4/src/ui/templates/TestSuiteTemplate.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=2 format=3 uid="uid://dte0m2endcgtu"] -[ext_resource type="Script" uid="uid://d0fytslwh68xp" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/ui/templates/TestSuiteTemplate.gd" id="1"] [node name="TestSuiteTemplate" type="MarginContainer"] anchors_preset = 15 diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd b/addons/gdUnit4/src/update/GdMarkDownReader.gd index ce6885de..dab19e40 100644 --- a/addons/gdUnit4/src/update/GdMarkDownReader.gd +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd @@ -215,7 +215,7 @@ func process_tables(input: String) -> String: return "\n".join(bbcode) -class Table: +class GdUnitMDReaderTable: var _columns: int var _rows: Array[Row] = [] @@ -292,7 +292,7 @@ class Table: func parse_table(lines: Array) -> PackedStringArray: var line: String = lines[0] - var table := Table.new(line.count("|") + 1) + var table := GdUnitMDReaderTable.new(line.count("|") + 1) while not lines.is_empty(): line = lines.pop_front() if not table.parse_row(line): diff --git a/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid index 04af3fb0..57cf1105 100644 --- a/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid +++ b/addons/gdUnit4/src/update/GdMarkDownReader.gd.uid @@ -1 +1 @@ -uid://co6iiwkj427tu +uid://dl510u7rvuvw6 diff --git a/addons/gdUnit4/src/update/GdUnitPatch.gd.uid b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid index 04aa3657..cded37cb 100644 --- a/addons/gdUnit4/src/update/GdUnitPatch.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitPatch.gd.uid @@ -1 +1 @@ -uid://dqjkgpknnun26 +uid://bk020xoxjycd4 diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd b/addons/gdUnit4/src/update/GdUnitPatcher.gd index 1adae828..73d25c92 100644 --- a/addons/gdUnit4/src/update/GdUnitPatcher.gd +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd @@ -22,6 +22,7 @@ func _scan(scan_path :String, current :GdUnit4Version) -> void: func patch_count() -> int: var count := 0 for key :String in _patches.keys(): + @warning_ignore("unsafe_method_access") count += _patches[key].size() return count @@ -29,7 +30,7 @@ func patch_count() -> int: func execute() -> void: for key :String in _patches.keys(): for path :String in _patches[key]: - var patch :GdUnitPatch = load(key + "/" + path).new() + var patch :GdUnitPatch = (load(key + "/" + path) as GDScript).new() if patch: prints("execute patch", patch.version(), patch.get_script().resource_path) if not patch.execute(): diff --git a/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid index 3ceffbde..3c9f0b40 100644 --- a/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitPatcher.gd.uid @@ -1 +1 @@ -uid://c6ci6kgridst4 +uid://ts272bcr5vj0 diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd b/addons/gdUnit4/src/update/GdUnitUpdate.gd index 1b492fe8..97b7fdd1 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.gd +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd @@ -18,7 +18,7 @@ var _download_url :String func _ready() -> void: - init_progress(5) + init_progress(6) func _process(_delta :float) -> void: @@ -56,6 +56,8 @@ func message_h4(message: String, color: Color, show_spinner := true) -> void: if show_spinner: _progress_content.add_image(_spinner_img) _progress_content.append_text(" [font_size=16]%s[/font_size]" % _colored(message, color)) + if _debug_mode: + prints(message) @warning_ignore("return_value_discarded") @@ -81,7 +83,7 @@ func run_update() -> void: await update_progress("Uninstall GdUnit4.") disable_gdUnit() if not _debug_mode: - delete_directory("res://addons/gdUnit4/") + GdUnitFileAccess.delete_directory("res://addons/gdUnit4/") # give editor time to react on deleted files await get_tree().create_timer(1).timeout @@ -91,14 +93,73 @@ func run_update() -> void: else: copy_directory(tmp_path, "res://") + await update_progress("Patch invalid UID's") + await patch_uids() + + await rebuild_project() + await update_progress("New GdUnit version successfully installed, Restarting Godot please wait.") await get_tree().create_timer(3).timeout enable_gdUnit() hide() - delete_directory("res://addons/.gdunit_update") + GdUnitFileAccess.delete_directory("res://addons/.gdunit_update") restart_godot() +func patch_uids(path := "res://addons/gdUnit4/src/") -> void: + var to_reimport: PackedStringArray + for file in DirAccess.get_files_at(path): + var file_path := path.path_join(file) + var ext := file.get_extension() + + if ext == "tscn" or ext == "scn" or ext == "tres" or ext == "res": + message_h4("Patch GdUnit4 scene: '%s'" % file, Color.WEB_GREEN) + remove_uids_from_file(file_path) + elif FileAccess.file_exists(file_path + ".import"): + to_reimport.append(file_path) + + if not to_reimport.is_empty(): + message_h4("Reimport resources '%s'" % ", ".join(to_reimport), Color.WEB_GREEN) + if Engine.is_editor_hint(): + EditorInterface.get_resource_filesystem().reimport_files(to_reimport) + + for dir in DirAccess.get_directories_at(path): + if not dir.begins_with("."): + patch_uids(path.path_join(dir)) + await get_tree().process_frame + + +func remove_uids_from_file(file_path: String) -> bool: + var file := FileAccess.open(file_path, FileAccess.READ) + if file == null: + print("Failed to open file: ", file_path) + return false + + var original_content := file.get_as_text() + file.close() + + # Remove UIDs using regex + var regex := RegEx.new() + regex.compile("(\\[ext_resource[^\\]]*?)\\s+uid=\"uid://[^\"]*\"") + + var modified_content := regex.sub(original_content, "$1", true) + + # Check if any changes were made + if original_content != modified_content: + prints("Patched invalid uid's out in '%s'" % file_path) + # Write the modified content back + file = FileAccess.open(file_path, FileAccess.WRITE) + if file == null: + print("Failed to write to file: ", file_path) + return false + + file.store_string(modified_content) + file.close() + return true + + return false + + func restart_godot() -> void: prints("Force restart Godot") EditorInterface.restart_editor(true) @@ -128,37 +189,13 @@ func temp_dir() -> String: func create_temp_dir(folder_name :String) -> String: var new_folder := temp_dir() + "/" + folder_name - delete_directory(new_folder) + GdUnitFileAccess.delete_directory(new_folder) if not DirAccess.dir_exists_absolute(new_folder): @warning_ignore("return_value_discarded") DirAccess.make_dir_recursive_absolute(new_folder) return new_folder -func delete_directory(path: String, only_content := false) -> void: - var dir := DirAccess.open(path) - if dir != null: - @warning_ignore("return_value_discarded") - dir.list_dir_begin() - var file_name := "." - while file_name != "": - file_name = dir.get_next() - if file_name.is_empty() or file_name == "." or file_name == "..": - continue - var next := path + "/" +file_name - if dir.current_is_dir(): - delete_directory(next) - else: - # delete file - var err := dir.remove(next) - if err: - printerr("Delete %s failed: %s" % [next, error_string(err)]) - if not only_content: - var err := dir.remove(path) - if err: - printerr("Delete %s failed: %s" % [path, error_string(err)]) - - func copy_directory(from_dir: String, to_dir: String) -> bool: if not DirAccess.dir_exists_absolute(from_dir): printerr("Source directory not found '%s'" % from_dir) @@ -236,6 +273,26 @@ func download_release() -> void: await get_tree().create_timer(3).timeout +func rebuild_project() -> void: + # Check if this is a Godot .NET runtime instance + if not ClassDB.class_exists("CSharpScript"): + return + + update_progress("Rebuild the project ...") + await get_tree().process_frame + + var output := [] + var exit_code := OS.execute("dotnet", ["build"], output) + if exit_code == -1: + message_h4("Rebuild the project failed, check your project dependencies.", Color.INDIAN_RED) + await get_tree().create_timer(3).timeout + return + + for out: String in output: + print_rich("[color=DEEP_SKY_BLUE] %s" % out.strip_edges()) + await get_tree().process_frame + + func _on_confirmed() -> void: await run_update() diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid index 96491cda..2526f238 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitUpdate.gd.uid @@ -1 +1 @@ -uid://dkjqrrn41oprs +uid://dcjl2bygjmfxi diff --git a/addons/gdUnit4/src/update/GdUnitUpdate.tscn b/addons/gdUnit4/src/update/GdUnitUpdate.tscn index 4dd7a5c9..20d60b76 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdate.tscn +++ b/addons/gdUnit4/src/update/GdUnitUpdate.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=6 format=3 uid="uid://2eahgaw88y6q"] -[ext_resource type="Script" uid="uid://dkjqrrn41oprs" path="res://addons/gdUnit4/src/update/GdUnitUpdate.gd" id="1"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdate.gd" id="1"] [sub_resource type="Gradient" id="Gradient_wilsr"] colors = PackedColorArray(0.151276, 0.151276, 0.151276, 1, 1, 1, 1, 1) diff --git a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid index d2bf554e..0348691a 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitUpdateClient.gd.uid @@ -1 +1 @@ -uid://bjrq7poxyhrd5 +uid://6xqggom0yrfx diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid index c3ed3c4e..f585502c 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.gd.uid @@ -1 +1 @@ -uid://7xaqxcpu1c7j +uid://25cgn7r6fj5m diff --git a/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn index 8c00f244..46119f14 100644 --- a/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn +++ b/addons/gdUnit4/src/update/GdUnitUpdateNotify.tscn @@ -1,8 +1,8 @@ [gd_scene load_steps=4 format=3 uid="uid://0xyeci1tqebj"] -[ext_resource type="Script" uid="uid://7xaqxcpu1c7j" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.gd" id="1_112wo"] -[ext_resource type="Script" uid="uid://bjrq7poxyhrd5" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="2_18asx"] -[ext_resource type="PackedScene" uid="uid://2eahgaw88y6q" path="res://addons/gdUnit4/src/update/GdUnitUpdate.tscn" id="3_x87h6"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateNotify.gd" id="1_112wo"] +[ext_resource type="Script" path="res://addons/gdUnit4/src/update/GdUnitUpdateClient.gd" id="2_18asx"] +[ext_resource type="PackedScene" path="res://addons/gdUnit4/src/update/GdUnitUpdate.tscn" id="3_x87h6"] [node name="Control" type="MarginContainer"] anchors_preset = 15 diff --git a/addons/gdUnit4/src/update/assets/fonts/LICENSE.txt b/addons/gdUnit4/src/update/assets/fonts/LICENSE.txt deleted file mode 100644 index d6456956..00000000 --- a/addons/gdUnit4/src/update/assets/fonts/LICENSE.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/addons/gdUnit4/src/update/assets/fonts/README.txt b/addons/gdUnit4/src/update/assets/fonts/README.txt deleted file mode 100644 index b34e21b7..00000000 --- a/addons/gdUnit4/src/update/assets/fonts/README.txt +++ /dev/null @@ -1,77 +0,0 @@ -Roboto Mono Variable Font -========================= - -This download contains Roboto Mono as both variable fonts and static fonts. - -Roboto Mono is a variable font with this axis: - wght - -This means all the styles are contained in these files: - RobotoMono-VariableFont_wght.ttf - RobotoMono-Italic-VariableFont_wght.ttf - -If your app fully supports variable fonts, you can now pick intermediate styles -that aren’t available as static fonts. Not all apps support variable fonts, and -in those cases you can use the static font files for Roboto Mono: - static/RobotoMono-Thin.ttf - static/RobotoMono-ExtraLight.ttf - static/RobotoMono-Light.ttf - static/RobotoMono-Regular.ttf - static/RobotoMono-Medium.ttf - static/RobotoMono-SemiBold.ttf - static/RobotoMono-Bold.ttf - static/RobotoMono-ThinItalic.ttf - static/RobotoMono-ExtraLightItalic.ttf - static/RobotoMono-LightItalic.ttf - static/RobotoMono-Italic.ttf - static/RobotoMono-MediumItalic.ttf - static/RobotoMono-SemiBoldItalic.ttf - static/RobotoMono-BoldItalic.ttf - -Get started ------------ - -1. Install the font files you want to use - -2. Use your app's font picker to view the font family and all the -available styles - -Learn more about variable fonts -------------------------------- - - https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts - https://variablefonts.typenetwork.com - https://medium.com/variable-fonts - -In desktop apps - - https://theblog.adobe.com/can-variable-fonts-illustrator-cc - https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts - -Online - - https://developers.google.com/fonts/docs/getting_started - https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide - https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts - -Installing fonts - - MacOS: https://support.apple.com/en-us/HT201749 - Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux - Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows - -Android Apps - - https://developers.google.com/fonts/docs/android - https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts - -License -------- -Please read the full license text (LICENSE.txt) to understand the permissions, -restrictions and requirements for usage, redistribution, and modification. - -You can use them freely in your products & projects - print or digital, -commercial or otherwise. However, you can't sell the fonts on their own. - -This isn't legal advice, please consider consulting a lawyer and see the full -license for all details. diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf deleted file mode 100644 index 900fce68..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Bold.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf deleted file mode 100644 index 4bfe29ae..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-BoldItalic.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf deleted file mode 100644 index d5358845..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLight.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf deleted file mode 100644 index b28960a0..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ExtraLightItalic.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf deleted file mode 100644 index 4ee4dc49..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Italic.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf deleted file mode 100644 index 276af4c5..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Light.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf deleted file mode 100644 index a2801c21..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-LightItalic.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf deleted file mode 100644 index 8461be77..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Medium.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf deleted file mode 100644 index a3bfaa11..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-MediumItalic.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf deleted file mode 100644 index 7c4ce36a..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Regular.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf deleted file mode 100644 index 15ee6c6e..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBold.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf deleted file mode 100644 index 8e214977..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-SemiBoldItalic.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf deleted file mode 100644 index ee8a3fd4..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-Thin.ttf and /dev/null differ diff --git a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf b/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf deleted file mode 100644 index 40b01e40..00000000 Binary files a/addons/gdUnit4/src/update/assets/fonts/static/RobotoMono-ThinItalic.ttf and /dev/null differ diff --git a/icon.png.import b/icon.png.import index 7b89d800..f869ecee 100644 --- a/icon.png.import +++ b/icon.png.import @@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.cte compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -25,6 +27,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/icon.svg.import b/icon.svg.import index 123af4e7..8c1bf9f9 100644 --- a/icon.svg.import +++ b/icon.svg.import @@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -25,6 +27,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/project.godot b/project.godot index a22e6d10..c252e46e 100644 --- a/project.godot +++ b/project.godot @@ -12,7 +12,7 @@ config_version=5 config/name="Beehave" run/main_scene="res://examples/beehave_test_scene.tscn" -config/features=PackedStringArray("4.4") +config/features=PackedStringArray("4.5") boot_splash/fullsize=false boot_splash/image="res://splash.png" config/icon="res://icon.png" diff --git a/splash.png.import b/splash.png.import index eeb39986..588906c9 100644 --- a/splash.png.import +++ b/splash.png.import @@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/splash.png-929ed8a00b89ba36c51789452f874c77.c compress/mode=0 compress/high_quality=false compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 compress/hdr_compression=1 compress/normal_map=0 compress/channel_pack=0 @@ -25,6 +27,10 @@ mipmaps/generate=false mipmaps/limit=-1 roughness/mode=0 roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 process/fix_alpha_border=true process/premult_alpha=false process/normal_map_invert_y=false diff --git a/test/debug/debugger_test.gd b/test/debug/debugger_test.gd index 9c9d6936..866bb7ea 100644 --- a/test/debug/debugger_test.gd +++ b/test/debug/debugger_test.gd @@ -17,6 +17,6 @@ func test_debugger_renders_correctly(): var scene = create_scene() var runner = scene_runner(scene) await runner.simulate_frames(20) - runner.set_mouse_pos(Vector2(20, 20)) + runner.set_mouse_position(Vector2(20, 20)) runner.simulate_mouse_button_press(1) await runner.simulate_frames(10)