extends Object class_name VestDefs ## Grouping class for test definition primitives. ## ## See [VestDefs.Suite][br] ## See [VestDefs.Case][br] ## See [VestDefs.Benchmark][br] ## Test suite definition. ## ## A test suite consists of test cases, and optionally other, nested test ## suites. ## ## @tutorial(Writing tests): https://foxssake.github.io/vest/latest/user-guide/writing-tests/ class Suite: ## Test suite name, displayed in reports var name: String = "" ## Test cases contained in the suite var cases: Array[VestDefs.Case] = [] ## Nested test suites contained in the suite var suites: Array[VestDefs.Suite] = [] ## If true, only this group of tests should run var is_only: bool = false ## The resource path to the script that defined the suite var definition_file: String = "" ## The line number of the suite definition. [br] ## Set to -1 for undetermined. var definition_line: int = -1 ## Return true if either this suite, one of its cases, or one of its ## subsuites is marked as only. func has_only() -> bool: if is_only: return true return cases.any(func(it): return it.is_only) or\ suites.any(func(it): return it.has_only()) ## Get the number of test cases in the suite.[br] ## Includes the number of test cases in the suite, and recursively sums up ## the test cases in any of the nested suites. func size() -> int: return cases.size() + suites.reduce(func(acc, it): return acc + it.size(), 0) func _to_string() -> String: return "VestDefs.Suite(name=\"%s\", cases=%s, suites=%s)" % [name, cases, suites] func _to_wire() -> Dictionary: return { "name": name } static func _from_wire(data: Dictionary) -> Suite: var result := Suite.new() result.name = data["name"] return result ## Test case definition. ## ## @tutorial(Writing tests): https://foxssake.github.io/vest/latest/user-guide/writing-tests/ class Case: ## Test case description, displayed in reports var description: String = "" ## The method defining the test case - empty if defined via [method VestTest.test] var method_name: String = "" ## If true, only this test should run var is_only: bool = false ## The method called to run the test case var callback: Callable ## The resource path to the script that defined the suite var definition_file: String = "" ## The line number of the suite definition.[br] ## Set to -1 for undetermined. var definition_line: int = -1 func _to_string() -> String: return "VestDefs.Case(\"%s\", \"%s\":%d)" % [description, definition_file, definition_line] func _to_wire() -> Dictionary: return { "description": description, "method_name": method_name, "definition_file": definition_file, "definition_line": definition_line } static func _from_wire(data: Dictionary) -> Case: var result := Case.new() result.description = data["description"] result.method_name = data["method_name"] result.definition_file = data["definition_file"] result.definition_line = data["definition_line"] return result ## Benchmark definition. ## ## Benchmarks run a piece of code multiple times, measuring how much time each ## iteration took. ## ## @tutorial(Benchmarking): https://foxssake.github.io/vest/latest/user-guide/benchmarking/ class Benchmark: ## Method to measure during the benchmark var callback: Callable ## Benchmark name, displayed in the reports var name: String = "" var _iterations: int = 0 var _duration: float = 0.0 var _batch_size: int = 1 var _max_iterations: int = -1 var _max_duration: float = -1.0 var _enable_builtin_measures: bool = true var _measures: Array[VestMeasure] = [] var _metric_signals: Dictionary = {} # metric name to signal var _emit_buffer: Array = [] signal _on_emit_template(value: Variant) var _test: VestTest ## Set a limit on the number of iterations to run. func with_iterations(p_iterations: int) -> Benchmark: _max_iterations = p_iterations return self ## Set a limit on the duration for running the benchmark. ## [br][br] ## The duration limit is in [i]seconds[/i]. func with_duration(p_duration: float) -> Benchmark: _max_duration = p_duration return self ## Set the batch size for running the benchmark. ## [br][br] ## When the batch size is greater than one, benchmark runs won't be measured ## one by one, but in batches. ## [br][br] ## This can be useful when running microbenchmarks, where an individual test ## run is too fast to accurately measure. func with_batch_size(p_batch_size: int) -> Benchmark: if p_batch_size >= 1: _batch_size = p_batch_size else: push_error("Invalid batch size set for benchmark %s: %d! Ignoring." % [name, p_batch_size]) return self ## Add a custom measurement. ## [br][br] ## There are many built-in measurements, see [method measure_value], ## [method measure_average], [method measure_sum], [method measure_min], ## and [method measure_max]. func with_measure(measure: VestMeasure) -> Benchmark: # Append measure _measures.append(measure) # Connect to appropriate signal var metric := measure.get_metric_name() if not _metric_signals.has(metric): _metric_signals[metric] = Signal(_on_emit_template) (_metric_signals[metric] as Signal).connect(measure.ingest) return self ## Measure the value of [param metric]. ## [br][br] ## The last value emitted will be included in the report. func measure_value(metric: StringName) -> Benchmark: with_measure(Vest.ValueMeasure.new(metric)) return self ## Measure the average of [param metric]. func measure_average(metric: StringName) -> Benchmark: with_measure(Vest.AverageMeasure.new(metric)) return self ## Measure the maximum value of [param metric]. func measure_max(metric: StringName) -> Benchmark: with_measure(Vest.MaxMeasure.new(metric)) return self ## Measure the minimum value of [param metric]. func measure_min(metric: StringName) -> Benchmark: with_measure(Vest.MinMeasure.new(metric)) return self ## Measure the sum of all emissions of [param metric]. func measure_sum(metric: StringName) -> Benchmark: with_measure(Vest.SumMeasure.new(metric)) return self ## Disable the builtin measurements. ## [br][br] ## This will stop iterations, durations, iters/sec, and average iteration ## time from being reported. They're enabled by default. func without_builtin_measures() -> Benchmark: _enable_builtin_measures = false return self ## Run the benchmark only once. func once() -> Benchmark: _max_iterations = 1 _max_duration = 1.0 return run() ## Run the benchmark with the configured limits. func run() -> Benchmark: # Run benchmark while _is_within_limits(): var batch_runs := mini(_batch_size, _get_remaining_iterations()) var t_start := Vest.time() for __ in batch_runs: callback.call(_emit) var duration := Vest.time() - t_start _duration += duration _iterations += batch_runs # Emit runtime _emit(&"duration", duration) # Metric emits are buffered, so they don't influence runtime measure # much # This call flushes the buffered metric emissions _flush_emits() # Report var result_data := _test._get_result().data var benchmarks := result_data.get("benchmarks", []) as Array benchmarks.append(_to_data()) result_data["benchmarks"] = benchmarks # Set test to pass by default if no asserts are added _test.ok("", result_data) # Warn user when running benchmarks in too small batches var avg_batch_time := get_avg_iteration_time() * _batch_size var batch_threshold := 0.001 # 1ms if avg_batch_time <= batch_threshold and _enable_builtin_measures: Vest.message(( "Benchmark \"%s\" has run with an average of %s per batch. " + "This is probably faster than what can be reliably measured. " + "To avoid this warning, increase the batch size to at least %s.") % [name, VestUI.format_duration(avg_batch_time), ceil(_batch_size * batch_threshold / avg_batch_time * 1.1)] ) return self ## Get the number of iterations ran. func get_iterations() -> int: return _iterations ## Get the total time it took to run the benchmark. func get_duration() -> float: return _duration ## Get the average number of iterations ran per second. func get_iters_per_sec() -> float: return _iterations / _duration ## Get the average time it took to run an iteration, in seconds. func get_avg_iteration_time() -> float: return _duration / _iterations ## Get the value of a measurement. ## [br][br] ## Measurements can be taken from the benchmark report, e.g. "Size - Value" ## corresponds to [code]get_measurement(&"Size", &"value")[/code]. ## [br][br] ## The returned value can be used for assertions. func get_measurement(metric: StringName, measurement: StringName) -> Variant: for measure in _measures: if measure.get_metric_name() == metric and measure.get_measure_name() == measurement: return measure.get_value() assert(false, "Measurement not found!") return null func _is_within_limits() -> bool: return _get_remaining_iterations() > 0 and _get_remaining_time() > 0. func _get_remaining_time() -> float: if _max_duration >= 0.0: return _max_duration - _duration return INF func _get_remaining_iterations() -> int: if _max_iterations >= 0: return _max_iterations - _iterations return _batch_size + 1 func _emit(metric: StringName, value: Variant) -> void: _emit_buffer.push_back([metric, value]) func _flush_emits() -> void: for emit in _emit_buffer: var metric := emit[0] as StringName var value = emit[1] if not _metric_signals.has(metric): continue (_metric_signals.get(metric) as Signal).emit(value) _emit_buffer.clear() func _to_data() -> Dictionary: var result := {} # Add custom measures for measure in _measures: var measure_name := "%s - %s" % [measure.get_metric_name(), measure.get_measure_name().capitalize()] result[measure_name] = str(measure.get_value()) # Add builtin measures if _enable_builtin_measures: result["iterations"] = _iterations result["duration"] = VestUI.format_duration(_duration) result["iters/sec"] = get_iters_per_sec() result["average iteration time"] = VestUI.format_duration(get_avg_iteration_time()) # Add benchmark data result["name"] = name return result