Skip to content

Commit

Permalink
Add integration tests from godot physics tests. Fix inertia issue aga…
Browse files Browse the repository at this point in the history
…in. (#259)

- Fixes #255
- Fixes #244
  • Loading branch information
Ughuuu authored Sep 25, 2024
1 parent bc51f65 commit 4e550c1
Show file tree
Hide file tree
Showing 132 changed files with 7,101 additions and 43 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/godot_builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ jobs:
path: |
bin${{ matrix.dimensions }}
!bin${{ matrix.dimensions }}/test/*
!bin${{ matrix.dimensions }}/tests/*
!bin${{ matrix.dimensions }}/base/*
!bin${{ matrix.dimensions }}/export_presets.cfg
!bin${{ matrix.dimensions }}/project.godot
!bin${{ matrix.dimensions }}/test.gd
!bin${{ matrix.dimensions }}/start.gd
!bin${{ matrix.dimensions }}/start.tscn
!bin${{ matrix.dimensions }}/test.tscn
if-no-files-found: error
94 changes: 94 additions & 0 deletions bin2d/base/Global.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
extends Node

enum TEST_MODE {
REGRESSION,
QUALITY,
PERFORMANCE
}

# View
var WINDOW_SIZE := Vector2(1152,648)
var NUMBER_TEST_PER_ROW := 2
var MAXIMUM_PARALLEL_TESTS := NUMBER_TEST_PER_ROW * NUMBER_TEST_PER_ROW

# Output
var DEBUG := false
var VERBOSE := true
var NB_TESTS_COMPLETED := 0
var MONITOR_PASSED := 0
var MONITOR_FAILED := 0
var MONITOR_EXPECTED_TO_FAIL: Array[String] = []
var MONITOR_REGRESSION: Array[String] = []
var MONITOR_IMRPOVEMENT: Array[String] = []
var TEST_PASSED := 0

var RUN_2D_TEST := true
var RUN_3D_TEST := false

var engine_2d = "GodotPhysics2D"
var engine_3d = "GodotPhysics3D"

var PERFORMANCE_RESULT := {}

func _process(_delta: float) -> void:
if Input.is_action_just_pressed("ui_cancel"):
exit()

func _ready() -> void:
get_tree().debug_collisions_hint = true

var setting_2d_engine = ProjectSettings.get("physics/2d/physics_engine")
if setting_2d_engine != "DEFAULT" and setting_2d_engine != "GodotPhysics2D":
engine_2d = setting_2d_engine

var setting_3d_engine = ProjectSettings.get("physics/3d/physics_engine")
if setting_3d_engine != "DEFAULT" and setting_3d_engine != "GodotPhysics3D":
engine_3d = setting_3d_engine

func exit(p_code := 0) -> void:
await get_tree().create_timer(1).timeout # sometimes the application quits before printing everything in the output
get_tree().quit(p_code)

func print_summary(duration: float) -> void:
var status = "FAILED" if Global.MONITOR_REGRESSION.size() != 0 else "SUCCESS"
var color = "red" if Global.MONITOR_REGRESSION.size() != 0 else "green"

if Global.VERBOSE and Global.MONITOR_EXPECTED_TO_FAIL.size() != 0:
print_rich("\n[indent]%d Monitor(s) expected to fail:[/indent]" % [Global.MONITOR_EXPECTED_TO_FAIL.size()])
var cpt := 0
for expected in Global.MONITOR_EXPECTED_TO_FAIL:
cpt += 1
print_rich("[indent][indent]%d. %s[/indent][/indent]" % [cpt, expected])

if Global.MONITOR_REGRESSION.size() != 0:
print_rich("\n[indent]%d Regression(s):[/indent]" % [Global.MONITOR_REGRESSION.size()])
var cpt := 0
for regression in Global.MONITOR_REGRESSION:
cpt += 1
print_rich("[indent][indent][color=red]%d. %s[/color][/indent][/indent]" % [cpt, regression])
if Global.MONITOR_IMRPOVEMENT.size() != 0:
print_rich("\n[indent]%d Improvement(s):[/indent]" % [Global.MONITOR_IMRPOVEMENT.size()])
var cpt := 0
for improvement in Global.MONITOR_IMRPOVEMENT:
cpt += 1
print_rich("[indent][indent][color=green]%d. %s[/color][/indent][/indent]" % [cpt, improvement])

var extra = ""
if Global.MONITOR_REGRESSION.size() != 0 or Global.MONITOR_IMRPOVEMENT.size() != 0:
extra = " | "
if Global.MONITOR_REGRESSION.size() != 0:
extra += "☹ %d regression(s) " % Global.MONITOR_REGRESSION.size()
if Global.MONITOR_IMRPOVEMENT.size() != 0:
extra += "❤️ %d improvement(s)" % Global.MONITOR_IMRPOVEMENT.size()

print_rich("\n[color=%s] > COMPLETED IN %.2fs | STATUS: %s (PASSED MONITORS: %d/%d)%s[/color]" % [color, duration, status, Global.MONITOR_PASSED, Global.MONITOR_PASSED + Global.MONITOR_FAILED, extra])

func print_engine() -> void:
if Global.VERBOSE:
var engine_txt := ""
if Global.RUN_2D_TEST:
engine_txt += " | 2D → %s" % [Global.engine_2d]
if Global.RUN_3D_TEST:
engine_txt += " | 3D → %s" % [Global.engine_3d]

print_rich("[color=orange] > ENGINE:%s[/color]\n" % engine_txt)
10 changes: 10 additions & 0 deletions bin2d/base/Utils.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extends Node

func vec2_equals(v1: Vector2, v2: Vector2, tolerance := 0.00001) -> bool:
return f_equals(v1.x, v2.x, tolerance) and f_equals(v1.y, v2.y, tolerance)

func vec3_equals(v1: Vector3, v2: Vector3, tolerance := 0.00001) -> bool:
return f_equals(v1.x, v2.x, tolerance) and f_equals(v1.y, v2.y, tolerance) and f_equals(v1.z, v2.z, tolerance)

func f_equals(f1: float, f2: float, tolerance := 0.00001) -> bool:
return abs(f1 - f2) <= tolerance
88 changes: 88 additions & 0 deletions bin2d/base/class/monitor.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
extends Node
class_name Monitor
signal completed

var multi_test_list: Array[Dictionary] = []
var multi_test_current := 0
var monitor_duration := 0.0
var monitor_maximum_duration := 10.0
var error_message := ""
var success := false
var started := false
var frame := 0 # physics frame
var expected_to_fail := false
var engine_expected_to_fail: Array[String] = []

var text: Dictionary:
set(value):
text = value

func _init() -> void:
process_mode = Node.PROCESS_MODE_DISABLED

func test_start() -> void:
process_mode = Node.PROCESS_MODE_INHERIT
started = true

func is_test_passed() -> bool:
return success

func is_expected_to_fail() -> bool:
if engine_expected_to_fail.size() > 0:
if Global.engine_2d in engine_expected_to_fail or Global.engine_3d in engine_expected_to_fail:
return true
return expected_to_fail

func is_sub_test_expected_to_fail(p_test:Dictionary) -> bool:
if p_test["engine_expected_to_fail"].size() > 0:
if Global.engine_2d in p_test["engine_expected_to_fail"] or Global.engine_3d in p_test["engine_expected_to_fail"]:
return true
return p_test["expected_to_fail"]

func _process(delta: float) -> void:
# Maximum duration
monitor_duration += delta
if monitor_duration > monitor_maximum_duration:
error_message = "The maximum duration has been exceeded (> %.1f s)" % [monitor_maximum_duration]
monitor_completed()
return

func monitor_name() -> String:
@warning_ignore("assert_always_false")
assert(false, "ERROR: You must implement monitor_name()")
return ""

func monitor_completed() -> void:
completed.emit()
process_mode = PROCESS_MODE_DISABLED

func failed(p_message = ""):
error_message = p_message
success = false
monitor_completed()

func passed():
success = true
monitor_completed()

func add_sub_test(p_name: String, p_expected_to_fail := false) -> void:
multi_test_list.append({
"name": p_name,
"result": false,
"errors": [],
"expected_to_fail": p_expected_to_fail,
"engine_expected_to_fail": []
})

func add_test_expected_to_fail():
multi_test_list[multi_test_current].expected_to_fail = true

func add_test_engine_expected_to_fail(p_engine: Array[String]):
multi_test_list[multi_test_current].engine_expected_to_fail.append_array(p_engine)

func add_test_error(p_error: String):
multi_test_list[multi_test_current].errors.append(p_error)

func add_test_result(p_result: bool):
multi_test_list[multi_test_current].result = p_result
multi_test_current += 1
110 changes: 110 additions & 0 deletions bin2d/base/class/physics_performance_2d.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
class_name PhysicsPerformanceTest2D
extends PhysicsTest2D

var NB_FRAME_SMOOTHING = 5
var WARMING_SKIPPED_FRAMES = 20

var _total_frame := 0
var _fps_label : Label

var _current_fps := 60.0
var _prev_tick_ms := -1.0

var _max_fps := 0.0
var _min_fps := 9999.0
var _average_fps := 0.0
var _average_record := 0
var _smoothed_fps := 60.0

var _frame_cpt := 0
var _fps_buffer := 0.0

var extra_text := []

var _warming := true # wait few frame before start monitoring

func _init() -> void:
process_mode = PROCESS_MODE_DISABLED

func _process(_delta: float) -> void:

_frame_cpt += 1
_total_frame += 1

# Skip Frames
if _warming and _frame_cpt == WARMING_SKIPPED_FRAMES:
_warming = false

if _warming:
return

# Start Computing FPS
if _prev_tick_ms == -1.0:
_prev_tick_ms = Time.get_ticks_usec()
_frame_cpt = 0
return

var new_tick: float = Time.get_ticks_usec()
_current_fps = 1000000.0 / (new_tick - _prev_tick_ms)
_prev_tick_ms = new_tick

# Smooth FPS and compute average
_fps_buffer += _current_fps

if _frame_cpt == NB_FRAME_SMOOTHING:
_smoothed_fps = _fps_buffer / _frame_cpt
_frame_cpt = 0
_fps_buffer = 0.0
_average_record += 1
_average_fps += _smoothed_fps

if _smoothed_fps > _max_fps:
_max_fps = _smoothed_fps
if _smoothed_fps < _min_fps:
_min_fps = _smoothed_fps

if _total_frame % 30 ==0 and _fps_label:
_fps_label.text = "%d FPS" % [_smoothed_fps]

func get_smoothed_fps():
return _smoothed_fps

func get_fps():
return _current_fps

func test_start() -> void:
super()

_fps_label = Label.new()
_fps_label.position = Vector2(20,40)
_fps_label.set("theme_override_font_sizes/font_size", 18)
add_child(_fps_label)

process_mode = Node.PROCESS_MODE_INHERIT

func register_result(p_name: String, result: String):
if not Global.PERFORMANCE_RESULT.has(get_name()):
Global.PERFORMANCE_RESULT[get_name()] = []
Global.PERFORMANCE_RESULT[get_name()].append([p_name, _min_fps, _max_fps, _average_fps / _average_record, result])

func test_completed(delay := 0) -> void:
super()
if not extra_text.is_empty():
for s in extra_text:
output += "[indent][indent][color=green]%s[/color][/indent][/indent]\n" % [s]
if Global.PERFORMANCE_RESULT.has(get_name()):
for result in Global.PERFORMANCE_RESULT[get_name()]:
output += "[indent][indent][color=orange] → %s : [b]%s[/b][/color] - [color=purple][b]FPS[/b] | min: [b]%d[/b] max: [b]%d[/b] avg: [b]%d[/b][/color][/indent][/indent]\n" % [result[0], result[4], result[1], result[2], result[3]]
else:
output += "[indent][indent][color=orange] Simulation completed[/color][/indent][/indent]\n"
print_rich(output)
process_mode = PROCESS_MODE_DISABLED
if delay != 0:
await get_tree().create_timer(delay).timeout

if has_method("clean"):
call("clean")

completed.emit()
queue_free()

Loading

0 comments on commit 4e550c1

Please sign in to comment.