Added gdhexgrid addon

WorldChunkRefactoring
Martin Felis 2022-08-25 14:19:22 +02:00
parent a9c7e62d06
commit d9f68fc02f
43 changed files with 8066 additions and 0 deletions

1
addons/gdhexgrid/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.import

238
addons/gdhexgrid/HexCell.gd Normal file
View File

@ -0,0 +1,238 @@
"""
A single cell of a hexagonal grid.
There are many ways to orient a hex grid, this library was written
with the following assumptions:
* The hexes use a flat-topped orientation;
* Axial coordinates use +x => NE; +y => N;
* Offset coords have odd rows shifted up half a step.
Using x,y instead of the reference's preferred x,z for axial coords makes
following along with the reference a little more tricky, but is less confusing
when using Godot's Vector2(x, y) objects.
## Usage:
#### var cube_coords; var axial_coords; var offset_coords
Cube coordinates are used internally as the canonical representation, but
both axial and offset coordinates can be read and modified through these
properties.
#### func get_adjacent(direction)
Returns the neighbouring HexCell in the given direction.
The direction should be one of the DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, or
DIR_NW constants provided by the HexCell class.
#### func get_all_adjacent()
Returns an array of the six HexCell instances neighbouring this one.
#### func get_all_within(distance)
Returns an array of all the HexCells within the given number of steps,
including the current hex.
#### func get_ring(distance)
Returns an array of all the HexCells at the given distance from the current.
#### func distance_to(target)
Returns the number of hops needed to get from this hex to the given target.
The target can be supplied as either a HexCell instance, cube or axial
coordinates.
#### func line_to(target)
Returns an array of all the hexes crossed when drawing a straight line
between this hex and another.
The target can be supplied as either a HexCell instance, cube or axial
coordinates.
The items in the array will be in the order of traversal, and include both
the start (current) hex, as well as the final target.
"""
extends Resource
#warning-ignore-all:unused_class_variable
# We use unit-size flat-topped hexes
const size = Vector2(1, sqrt(3)/2)
# Directions of neighbouring cells
const DIR_N = Vector3(0, 1, -1)
const DIR_NE = Vector3(1, 0, -1)
const DIR_SE = Vector3(1, -1, 0)
const DIR_S = Vector3(0, -1, 1)
const DIR_SW = Vector3(-1, 0, 1)
const DIR_NW = Vector3(-1, 1, 0)
const DIR_ALL = [DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, DIR_NW]
# Cube coords are canonical
var cube_coords = Vector3(0, 0, 0) setget set_cube_coords, get_cube_coords
# but other coord systems can be used
var axial_coords setget set_axial_coords, get_axial_coords
var offset_coords setget set_offset_coords, get_offset_coords
func _init(coords=null):
# HexCells can be created with coordinates
if coords:
self.cube_coords = obj_to_coords(coords)
func new_hex(coords):
# Returns a new HexCell instance
return get_script().new(coords)
"""
Handle coordinate access and conversion
"""
func obj_to_coords(val):
# Returns suitable cube coordinates for the given object
# The given object can an be one of:
# * Vector3 of standard cube coords;
# * Vector2 of axial coords;
# * HexCell instance
# Any other type of value will return null
#
# NB that offset coords are NOT supported, as they are
# indistinguishable from axial coords.
if typeof(val) == TYPE_VECTOR3:
return val
elif typeof(val) == TYPE_VECTOR2:
return axial_to_cube_coords(val)
elif typeof(val) == TYPE_OBJECT and val.has_method("get_cube_coords"):
return val.get_cube_coords()
# Fall through to nothing
return
func axial_to_cube_coords(val):
# Returns the Vector3 cube coordinates for an axial Vector2
var x = val.x
var y = val.y
return Vector3(x, y, -x - y)
func round_coords(val):
# Rounds floaty coordinate to the nearest whole number cube coords
if typeof(val) == TYPE_VECTOR2:
val = axial_to_cube_coords(val)
# Straight round them
var rounded = Vector3(round(val.x), round(val.y), round(val.z))
# But recalculate the one with the largest diff so that x+y+z=0
var diffs = (rounded - val).abs()
if diffs.x > diffs.y and diffs.x > diffs.z:
rounded.x = -rounded.y - rounded.z
elif diffs.y > diffs.z:
rounded.y = -rounded.x - rounded.z
else:
rounded.z = -rounded.x - rounded.y
return rounded
func get_cube_coords():
# Returns a Vector3 of the cube coordinates
return cube_coords
func set_cube_coords(val):
# Sets the position from a Vector3 of cube coordinates
if abs(val.x + val.y + val.z) > 0.0001:
print("WARNING: Invalid cube coordinates for hex (x+y+z!=0): ", val)
return
cube_coords = round_coords(val)
func get_axial_coords():
# Returns a Vector2 of the axial coordinates
return Vector2(cube_coords.x, cube_coords.y)
func set_axial_coords(val):
# Sets position from a Vector2 of axial coordinates
set_cube_coords(axial_to_cube_coords(val))
func get_offset_coords():
# Returns a Vector2 of the offset coordinates
var x = int(cube_coords.x)
var y = int(cube_coords.y)
var off_y = y + (x - (x & 1)) / 2
return Vector2(x, off_y)
func set_offset_coords(val):
# Sets position from a Vector2 of offset coordinates
var x = int(val.x)
var y = int(val.y)
var cube_y = y - (x - (x & 1)) / 2
self.set_axial_coords(Vector2(x, cube_y))
"""
Finding our neighbours
"""
func get_adjacent(dir):
# Returns a HexCell instance for the given direction from this.
# Intended for one of the DIR_* consts, but really any Vector2 or x+y+z==0 Vector3 will do.
if typeof(dir) == TYPE_VECTOR2:
dir = axial_to_cube_coords(dir)
return new_hex(self.cube_coords + dir)
func get_all_adjacent():
# Returns an array of HexCell instances representing adjacent locations
var cells = Array()
for coord in DIR_ALL:
cells.append(new_hex(self.cube_coords + coord))
return cells
func get_all_within(distance):
# Returns an array of all HexCell instances within the given distance
var cells = Array()
for dx in range(-distance, distance+1):
for dy in range(max(-distance, -distance - dx), min(distance, distance - dx) + 1):
cells.append(new_hex(self.axial_coords + Vector2(dx, dy)))
return cells
func get_ring(distance):
# Returns an array of all HexCell instances at the given distance
if distance < 1:
return [new_hex(self.cube_coords)]
# Start at the top (+y) and walk in a clockwise circle
var cells = Array()
var current = new_hex(self.cube_coords + (DIR_N * distance))
for dir in [DIR_SE, DIR_S, DIR_SW, DIR_NW, DIR_N, DIR_NE]:
for _step in range(distance):
cells.append(current)
current = current.get_adjacent(dir)
return cells
func distance_to(target):
# Returns the number of hops from this hex to another
# Can be passed cube or axial coords, or another HexCell instance
target = obj_to_coords(target)
return int((
abs(cube_coords.x - target.x)
+ abs(cube_coords.y - target.y)
+ abs(cube_coords.z - target.z)
) / 2)
func line_to(target):
# Returns an array of HexCell instances representing
# a straight path from here to the target, including both ends
target = obj_to_coords(target)
# End of our lerp is nudged so it never lands exactly on an edge
var nudged_target = target + Vector3(1e-6, 2e-6, -3e-6)
var steps = distance_to(target)
var path = []
for dist in range(steps):
var lerped = cube_coords.linear_interpolate(nudged_target, float(dist) / steps)
path.append(new_hex(round_coords(lerped)))
path.append(new_hex(target))
return path

403
addons/gdhexgrid/HexGrid.gd Normal file
View File

@ -0,0 +1,403 @@
"""
A converter between hex and Godot-space coordinate systems.
The hex grid uses +x => NE and +y => N, whereas
the projection to Godot-space uses +x => E, +y => S.
We map hex coordinates to Godot-space with +y flipped to be the down vector
so that it maps neatly to both Godot's 2D coordinate system, and also to
x,z planes in 3D space.
## Usage:
#### var hex_scale = Vector2(...)
If you want your hexes to display larger than the default 1 x 0.866 units,
then you can customise the scale of the hexes using this property.
#### func get_hex_center(hex)
Returns the Godot-space Vector2 of the center of the given hex.
The coordinates can be given as either a HexCell instance; a Vector3 cube
coordinate, or a Vector2 axial coordinate.
#### func get_hex_center3(hex [, y])
Returns the Godot-space Vector3 of the center of the given hex.
The coordinates can be given as either a HexCell instance; a Vector3 cube
coordinate, or a Vector2 axial coordinate.
If a second parameter is given, it will be used for the y value in the
returned Vector3. Otherwise, the y value will be 0.
#### func get_hex_at(coords)
Returns HexCell whose grid position contains the given Godot-space coordinates.
The given value can either be a Vector2 on the grid's plane
or a Vector3, in which case its (x, z) coordinates will be used.
### Path-finding
HexGrid also includes an implementation of the A* pathfinding algorithm.
The class can be used to populate an internal representation of a game grid
with obstacles to traverse.
#### func set_bounds(min_coords, max_coords)
Sets the hard outer limits of the path-finding grid.
The coordinates given are the min and max corners *inside* a bounding
square (diamond in hex visualisation) region. Any hex outside that area
is considered an impassable obstacle.
The default bounds consider only the origin to be inside, so you're probably
going to want to do something about that.
#### func get_obstacles()
Returns a dict of all obstacles and their costs
The keys are Vector2s of the axial coordinates, the values will be the
cost value. Zero cost means an impassable obstacle.
#### func add_obstacles(coords, cost=0)
Adds one or more obstacles to the path-finding grid
The given coordinates (axial or cube), HexCell instance, or array thereof,
will be added as path-finding obstacles with the given cost. A zero cost
indicates an impassable obstacle.
#### func remove_obstacles(coords)
Removes one or more obstacles from the path-finding grid
The given coordinates (axial or cube), HexCell instance, or array thereof,
will be removed as obstacles from the path-finding grid.
#### func get_barriers()
Returns a dict of all barriers in the grid.
A barrier is an edge of a hex which is either impassable, or has a
non-zero cost to traverse. If two adjacent hexes both have barriers on
their shared edge, their costs are summed.
Barrier costs are in addition to the obstacle (or default) cost of
moving to a hex.
The outer dict is a mapping of axial coords to an inner barrier dict.
The inner dict maps between HexCell.DIR_* directions and the cost of
travel in that direction. A cost of zero indicates an impassable barrier.
#### func add_barriers(coords, dirs, cost=0)
Adds one or more barriers to locations on the grid.
The given coordinates (axial or cube), HexCell instance, or array thereof,
will have path-finding barriers added in the given HexCell.DIR_* directions
with the given cost. A zero cost indicates an impassable obstacle.
Existing barriers at given coordinates will not be removed, but will be
overridden if the direction is specified.
#### func remove_barriers(coords, dirs=null)
Remove one or more barriers from the path-finding grid.
The given coordinates (axial or cube), HexCell instance, or array thereof,
will have the path-finding barriers in the supplied HexCell.DIR_* directions
removed. If no direction is specified, all barriers for the given
coordinates will be removed.
#### func get_hex_cost(coords)
Returns the cost of moving into the specified grid position.
Will return 0 if the given grid position is inaccessible.
#### func get_move_cost(coords, direction)
Returns the cost of moving from one hex to an adjacent one.
This method takes into account any barriers defined between the two
hexes, as well as the cost of the target hex.
Will return 0 if the target hex is inaccessible, or if there is an
impassable barrier between the hexes.
The direction should be provided as one of the HexCell.DIR_* values.
#### func find_path(start, goal, exceptions=[])
Calculates an A* path from the start to the goal.
Returns a list of HexCell instances charting the path from the given start
coordinates to the goal, including both ends of the journey.
Exceptions can be specified as the third parameter, and will act as
impassable obstacles for the purposes of this call of the function.
This can be used for pathing around obstacles which may change position
(eg. enemy playing pieces), without having to update the grid's list of
obstacles every time something moves.
If the goal is an impassable location, the path will terminate at the nearest
adjacent coordinate. In this instance, the goal hex will not be included in
the returned array.
If there is no path possible to the goal, or any hex adjacent to it, an
empty array is returned. But the algorithm will only know that once it's
visited every tile it can reach, so try not to path to the impossible.
"""
extends Reference
var HexCell = preload("./HexCell.gd")
# Duplicate these from HexCell for ease of access
const DIR_N = Vector3(0, 1, -1)
const DIR_NE = Vector3(1, 0, -1)
const DIR_SE = Vector3(1, -1, 0)
const DIR_S = Vector3(0, -1, 1)
const DIR_SW = Vector3(-1, 0, 1)
const DIR_NW = Vector3(-1, 1, 0)
const DIR_ALL = [DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, DIR_NW]
# Allow the user to scale the hex for fake perspective or somesuch
export(Vector2) var hex_scale = Vector2(1, 1) setget set_hex_scale
var base_hex_size = Vector2(1, sqrt(3)/2)
var hex_size
var hex_transform
var hex_transform_inv
# Pathfinding obstacles {Vector2: cost}
# A zero cost means impassable
var path_obstacles = {}
# Barriers restrict traversing between edges (in either direction)
# costs for barriers and obstacles are cumulative, but impassable is impassable
# {Vector2: {DIR_VECTOR2: cost, ...}}
var path_barriers = {}
var path_bounds = Rect2()
var path_cost_default = 1.0
func _init():
set_hex_scale(hex_scale)
func set_hex_scale(scale):
# We need to recalculate some stuff when projection scale changes
hex_scale = scale
hex_size = base_hex_size * hex_scale
hex_transform = Transform2D(
Vector2(hex_size.x * 3/4, -hex_size.y / 2),
Vector2(0, -hex_size.y),
Vector2(0, 0)
)
hex_transform_inv = hex_transform.affine_inverse()
"""
Converting between hex-grid and 2D spatial coordinates
"""
func get_hex_center(hex):
# Returns hex's centre position on the projection plane
hex = HexCell.new(hex)
return hex_transform * hex.axial_coords
func get_hex_center_from_offset(offset):
# Returns hex's centre position at the given offset coordinates
var hex = HexCell.new()
hex.offset_coords = offset
return hex_transform * hex.axial_coords
func get_hex_at(coords):
# Returns a HexCell at the given Vector2/3 on the projection plane
# If the given value is a Vector3, its x,z coords will be used
if typeof(coords) == TYPE_VECTOR3:
coords = Vector2(coords.x, coords.z)
return HexCell.new(hex_transform_inv * coords)
func get_hex_center3(hex, y=0):
# Returns hex's centre position as a Vector3
var coords = get_hex_center(hex)
return Vector3(coords.x, y, coords.y)
"""
Pathfinding
Ref: https://www.redblobgames.com/pathfinding/a-star/introduction.html
We use axial coords for everything internally (to use Rect2.has_point),
but the methods accept cube or axial coords, or HexCell instances.
"""
func set_bounds(min_coords, max_coords):
# Set the absolute bounds of the pathfinding area in grid coords
# The given coords will be inside the boundary (hence the extra (1, 1))
min_coords = HexCell.new(min_coords).axial_coords
max_coords = HexCell.new(max_coords).axial_coords
path_bounds = Rect2(min_coords, (max_coords - min_coords) + Vector2(1, 1))
func get_obstacles():
return path_obstacles
func add_obstacles(vals, cost=0):
# Store the given coordinate/s as obstacles
if not typeof(vals) == TYPE_ARRAY:
vals = [vals]
for coords in vals:
coords = HexCell.new(coords).axial_coords
path_obstacles[coords] = cost
func remove_obstacles(vals):
# Remove the given obstacle/s from the grid
if not typeof(vals) == TYPE_ARRAY:
vals = [vals]
for coords in vals:
coords = HexCell.new(coords).axial_coords
path_obstacles.erase(coords)
func get_barriers():
return path_barriers
func add_barriers(vals, dirs, cost=0):
# Store the given directions of the given locations as barriers
if not typeof(vals) == TYPE_ARRAY:
vals = [vals]
if not typeof(dirs) == TYPE_ARRAY:
dirs = [dirs]
for coords in vals:
coords = HexCell.new(coords).axial_coords
var barriers = {}
if coords in path_barriers:
# Already something there
barriers = path_barriers[coords]
else:
path_barriers[coords] = barriers
# Set or override the given dirs
for dir in dirs:
barriers[dir] = cost
path_barriers[coords] = barriers
func remove_barriers(vals, dirs=null):
if not typeof(vals) == TYPE_ARRAY:
vals = [vals]
if dirs != null and not typeof(dirs) == TYPE_ARRAY:
dirs = [dirs]
for coords in vals:
coords = HexCell.new(coords).axial_coords
if dirs == null:
path_barriers.erase(coords)
else:
for dir in dirs:
path_barriers[coords].erase(dir)
func get_hex_cost(coords):
# Returns the cost of moving to the given hex
coords = HexCell.new(coords).axial_coords
if coords in path_obstacles:
return path_obstacles[coords]
if not path_bounds.has_point(coords):
# Out of bounds
return 0
return path_cost_default
func get_move_cost(coords, direction):
# Returns the cost of moving from one hex to a neighbour
direction = HexCell.new(direction).cube_coords
var start_hex = HexCell.new(coords)
var target_hex = HexCell.new(start_hex.cube_coords + direction)
coords = start_hex.axial_coords
# First check if either end is completely impassable
var cost = get_hex_cost(start_hex)
if cost == 0:
return 0
cost = get_hex_cost(target_hex)
if cost == 0:
return 0
# Check for barriers
var barrier_cost
if coords in path_barriers and direction in path_barriers[coords]:
barrier_cost = path_barriers[coords][direction]
if barrier_cost == 0:
return 0
cost += barrier_cost
var target_coords = target_hex.axial_coords
if target_coords in path_barriers and -direction in path_barriers[target_coords]:
barrier_cost = path_barriers[target_coords][-direction]
if barrier_cost == 0:
return 0
cost += barrier_cost
return cost
func get_path(start, goal, exceptions=[]):
# DEPRECATED!
# The function `get_path` is used by Godot for something completely different,
# so we renamed it here to `find_path`.
push_warning("HexGrid.get_path has been deprecated, use find_path instead.")
return find_path(start, goal, exceptions)
func find_path(start, goal, exceptions=[]):
# Light a starry path from the start to the goal, inclusive
start = HexCell.new(start).axial_coords
goal = HexCell.new(goal).axial_coords
# Make sure all the exceptions are axial coords
var exc = []
for ex in exceptions:
exc.append(HexCell.new(ex).axial_coords)
exceptions = exc
# Now we begin the A* search
var frontier = [make_priority_item(start, 0)]
var came_from = {start: null}
var cost_so_far = {start: 0}
while not frontier.empty():
var current = frontier.pop_front().v
if current == goal:
break
for next_hex in HexCell.new(current).get_all_adjacent():
var next = next_hex.axial_coords
var next_cost = get_move_cost(current, next - current)
if next == goal and (next in exceptions or get_hex_cost(next) == 0):
# Our goal is an obstacle, but we're next to it
# so our work here is done
came_from[next] = current
frontier.clear()
break
if not next_cost or next in exceptions:
# We shall not pass
continue
next_cost += cost_so_far[current]
if not next in cost_so_far or next_cost < cost_so_far[next]:
# New shortest path to that node
cost_so_far[next] = next_cost
var priority = next_cost + next_hex.distance_to(goal)
# Insert into the frontier
var item = make_priority_item(next, priority)
var idx = frontier.bsearch_custom(item, self, "comp_priority_item")
frontier.insert(idx, item)
came_from[next] = current
if not goal in came_from:
# Not found
return []
# Follow the path back where we came_from
var path = []
if not (get_hex_cost(goal) == 0 or goal in exceptions):
# We only include the goal if it's traversable
path.append(HexCell.new(goal))
var current = goal
while current != start:
current = came_from[current]
path.push_front(HexCell.new(current))
return path
# Used to make a priority queue out of an array
func make_priority_item(val, priority):
return {"v": val, "p": priority}
func comp_priority_item(a, b):
return a.p < b.p

View File

@ -0,0 +1,19 @@
tool
extends Polygon2D
export(Color) var OutLine = Color(0,0,0) setget set_color
export(float) var Width = 2.0 setget set_width
func _draw():
var poly = get_polygon()
for i in range(1 , poly.size()):
draw_line(poly[i-1] , poly[i], OutLine , Width)
draw_line(poly[poly.size() - 1] , poly[0], OutLine , Width)
func set_color(color):
OutLine = color
update()
func set_width(new_width):
Width = new_width
update()

View File

@ -0,0 +1,51 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://HexShape.gd" type="Script" id=1]
[node name="Highlight" type="Polygon2D"]
color = Color( 1, 1, 1, 0 )
polygon = PoolVector2Array( -12.5, 21.6506, 12.5, 21.6506, 25, 0, 12.5, -21.6506, -12.5, -21.6506, -25, 0 )
script = ExtResource( 1 )
OutLine = Color( 1, 1, 1, 0.133333 )
[node name="Label" type="Label" parent="."]
visible = false
margin_left = 5.0
margin_top = -39.0
margin_right = 52.0
margin_bottom = -25.0
text = "SCREEN"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="AreaCoords" type="Label" parent="."]
visible = false
margin_left = 55.0
margin_top = -39.0
margin_right = 105.0
margin_bottom = -25.0
text = "SCREEN"
[node name="Label2" type="Label" parent="."]
visible = false
margin_left = 25.0
margin_top = -19.0
margin_right = 56.0
margin_bottom = -5.0
text = "HEX"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="HexCoords" type="Label" parent="."]
margin_left = -26.0
margin_top = -8.0
margin_right = 24.0
margin_bottom = 6.0
text = "HEX"
align = 1
valign = 1
__meta__ = {
"_edit_use_anchors_": false
}

View File

@ -0,0 +1,7 @@
Copyright 2018 Mel Collins
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

229
addons/gdhexgrid/README.md Normal file
View File

@ -0,0 +1,229 @@
# GDHexGrid
Tools for using hexagonal grids in GDScript.
The reference used for creating this was the amazing guide:
https://www.redblobgames.com/grids/hexagons/
Copyright 2018 Mel Collins.
Distributed under the MIT license (see LICENSE.txt).
## Orientation
There are many ways to orient a hex grid, this library was written
using the following assumptions:
* The hexes use a flat-topped orientation;
* Axial coordinates use +x => NE; +y => N;
* Offset coords have odd rows shifted up half a step;
* Projections of the hex grid into Godot-space use +x => E, +y => S.
Using x,y instead of the reference's preferred x,z for axial coords makes
following along with the reference a little more tricky, but is less confusing
when using Godot's Vector2(x, y) objects.
We map hex coordinates to Godot-space with +y flipped to be the down vector
so that it maps neatly to both Godot's 2D coordinate system, and also to
x,z planes in 3D space.
## Usage
### HexGrid
HexGrid is used when you want to position hexes in a 2D or 3D scene.
It translates coordinates between the hex grid and conventional spaces.
#### var hex_scale = Vector2(...)
If you want your hexes to display larger than the default 1 x 0.866 units,
then you can customise the scale of the hexes using this property.
#### func get_hex_center(hex)
Returns the Godot-space coordinate of the center of the given hex coordinates.
The coordinates can be given as either a HexCell instance; a Vector3 cube
coordinate, or a Vector2 axial coordinate.
#### func get_hex_center3(hex [, y])
Returns the Godot-space Vector3 of the center of the given hex.
The coordinates can be given as either a HexCell instance; a Vector3 cube
coordinate, or a Vector2 axial coordinate.
If a second parameter is given, it will be used for the y value in the
returned Vector3. Otherwise, the y value will be 0.
#### func get_hex_at(coords)
Returns HexCell whose grid position contains the given Godot-space coordinates.
The given value can either be a Vector2 on the grid's plane
or a Vector3, in which case its (x, z) coordinates will be used.
### HexGrid pathfinding
HexGrid also includes an implementation of the A* pathfinding algorithm.
The class can be used to populate an internal representation of a game grid
with obstacles to traverse.
This was written with the aid of another amazing guide:
https://www.redblobgames.com/pathfinding/a-star/introduction.html
#### func set_bounds(min_coords, max_coords)
Sets the hard outer limits of the path-finding grid.
The coordinates given are the min and max corners *inside* a bounding
square (diamond in hex visualisation) region. Any hex outside that area
is considered an impassable obstacle.
The default bounds consider only the origin to be inside, so you're probably
going to want to do something about that.
#### func get_obstacles()
Returns a dict of all obstacles and their costs
The keys are Vector2s of the axial coordinates, the values will be the
cost value. Zero cost means an impassable obstacle.
#### func add_obstacles(coords, cost=0)
Adds one or more obstacles to the path-finding grid
The given coordinates (axial or cube), HexCell instance, or array thereof,
will be added as path-finding obstacles with the given cost. A zero cost
indicates an impassable obstacle.
#### func remove_obstacles(coords)
Removes one or more obstacles from the path-finding grid
The given coordinates (axial or cube), HexCell instance, or array thereof,
will be removed as obstacles from the path-finding grid.
#### func get_barriers()
Returns a dict of all barriers in the grid.
A barrier is an edge of a hex which is either impassable, or has a
non-zero cost to traverse. If two adjacent hexes both have barriers on
their shared edge, their costs are summed.
Barrier costs are in addition to the obstacle (or default) cost of
moving to a hex.
The outer dict is a mapping of axial coords to an inner barrier dict.
The inner dict maps between HexCell.DIR_* directions and the cost of
travel in that direction. A cost of zero indicates an impassable barrier.
#### func add_barriers(coords, dirs, cost=0)
Adds one or more barriers to locations on the grid.
The given coordinates (axial or cube), HexCell instance, or array thereof,
will have path-finding barriers added in the given HexCell.DIR_* directions
with the given cost. A zero cost indicates an impassable obstacle.
Existing barriers at given coordinates will not be removed, but will be
overridden if the direction is specified.
#### func remove_barriers(coords, dirs=null)
Remove one or more barriers from the path-finding grid.
The given coordinates (axial or cube), HexCell instance, or array thereof,
will have the path-finding barriers in the supplied HexCell.DIR_* directions
removed. If no direction is specified, all barriers for the given
coordinates will be removed.
#### func get_hex_cost(coords)
Returns the cost of moving into the specified grid position.
Will return 0 if the given grid position is inaccessible.
#### func get_move_cost(coords, direction)
Returns the cost of moving from one hex to an adjacent one.
This method takes into account any barriers defined between the two
hexes, as well as the cost of the target hex.
Will return 0 if the target hex is inaccessible, or if there is an
impassable barrier between the hexes.
The direction should be provided as one of the HexCell.DIR_* values.
#### func find_path(start, goal, exceptions=[])
Calculates an A* path from the start to the goal.
Returns a list of HexCell instances charting the path from the given start
coordinates to the goal, including both ends of the journey.
Exceptions can be specified as the third parameter, and will act as
impassable obstacles for the purposes of this call of the function.
This can be used for pathing around obstacles which may change position
(eg. enemy playing pieces), without having to update the grid's list of
obstacles every time something moves.
If the goal is an impassable location, the path will terminate at the nearest
adjacent coordinate. In this instance, the goal hex will not be included in
the returned array.
If there is no path possible to the goal, or any hex adjacent to it, an
empty array is returned. But the algorithm will only know that once it's
visited every tile it can reach, so try not to path to the impossible.
### HexCell
A HexCell represents a single hex in the grid, and is the meat of the library.
#### var cube_coords; var axial_coords; var offset_coords
Cube coordinates are used internally as the canonical representation, but
both axial and offset coordinates can be read and modified through these
properties.
#### func get_adjacent(direction)
Returns the neighbouring HexCell in the given direction.
The direction should be one of the DIR_N, DIR_NE, DIR_SE, DIR_S, DIR_SW, or
DIR_NW constants provided by the HexCell class.
#### func get_all_adjacent()
Returns an array of the six HexCell instances neighbouring this one.
#### func get_all_within(distance)
Returns an array of all the HexCells within the given number of steps,
including the current hex.
#### func get_ring(distance)
Returns an array of all the HexCells at the given distance from the current.
#### func distance_to(target)
Returns the number of hops needed to get from this hex to the given target.
The target can be supplied as either a HexCell instance, cube or axial
coordinates.
#### func line_to(target)
Returns an array of all the hexes crossed when drawing a straight line
between this hex and another.
The target can be supplied as either a HexCell instance, cube or axial
coordinates.
The items in the array will be in the order of traversal, and include both
the start (current) hex, as well as the final target.

View File

@ -0,0 +1,28 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=4
_global_script_classes=[ ]
_global_script_class_icons={
}
[application]
config/name="HexGrid"
run/main_scene="res://demo_2d.tscn"
config/icon="res://icon.png"
[editor_plugins]
enabled=PoolStringArray( "gut" )
[rendering]
environment/default_environment="res://default_env.tres"

View File

@ -0,0 +1,343 @@
extends Panel
onready var _script_list = $ScriptsList
onready var _nav = {
prev = $Navigation/Previous,
next = $Navigation/Next,
run = $Navigation/Run,
current_script = $Navigation/CurrentScript,
show_scripts = $Navigation/ShowScripts
}
onready var _progress = {
script = $ScriptProgress,
test = $TestProgress
}
onready var _summary = {
failing = $Summary/Failing,
passing = $Summary/Passing
}
onready var _extras = $ExtraOptions
onready var _ignore_pauses = $ExtraOptions/IgnorePause
onready var _continue_button = $Continue/Continue
onready var _text_box = $TextDisplay/RichTextLabel
onready var _titlebar = {
bar = $TitleBar,
time = $TitleBar/Time,
label = $TitleBar/Title
}
var _mouse = {
down = false,
in_title = false,
down_pos = null,
in_handle = false
}
var _is_running = false
var _time = 0
const DEFAULT_TITLE = 'Gut: The Godot Unit Testing tool.'
var _utils = load('res://addons/gut/utils.gd').new()
var _text_box_blocker_enabled = true
var _pre_maximize_size = null
signal end_pause
signal ignore_pause
signal log_level_changed
signal run_script
signal run_single_script
signal script_selected
func _ready():
_pre_maximize_size = rect_size
_hide_scripts()
_update_controls()
_nav.current_script.set_text("No scripts available")
set_title()
clear_summary()
$TitleBar/Time.set_text("")
$ExtraOptions/DisableBlocker.pressed = !_text_box_blocker_enabled
_extras.visible = false
update()
func _process(delta):
if(_is_running):
_time += delta
var disp_time = round(_time * 100)/100
$TitleBar/Time.set_text(str(disp_time))
func _draw(): # needs get_size()
# Draw the lines in the corner to show where you can
# drag to resize the dialog
var grab_margin = 3
var line_space = 3
var grab_line_color = Color(.4, .4, .4)
for i in range(1, 10):
var x = rect_size - Vector2(i * line_space, grab_margin)
var y = rect_size - Vector2(grab_margin, i * line_space)
draw_line(x, y, grab_line_color, 1, true)
func _on_Maximize_draw():
# draw the maximize square thing.
var btn = $TitleBar/Maximize
btn.set_text('')
var w = btn.get_size().x
var h = btn.get_size().y
btn.draw_rect(Rect2(0, 0, w, h), Color(0, 0, 0, 1))
btn.draw_rect(Rect2(2, 4, w - 4, h - 6), Color(1,1,1,1))
func _on_ShowExtras_draw():
var btn = $Continue/ShowExtras
btn.set_text('')
var start_x = 20
var start_y = 15
var pad = 5
var color = Color(.1, .1, .1, 1)
var width = 2
for i in range(3):
var y = start_y + pad * i
btn.draw_line(Vector2(start_x, y), Vector2(btn.get_size().x - start_x, y), color, width, true)
# ####################
# GUI Events
# ####################
func _on_Run_pressed():
_run_mode()
emit_signal('run_script', get_selected_index())
func _on_CurrentScript_pressed():
_run_mode()
emit_signal('run_single_script', get_selected_index())
func _on_Previous_pressed():
_select_script(get_selected_index() - 1)
func _on_Next_pressed():
_select_script(get_selected_index() + 1)
func _on_LogLevelSlider_value_changed(value):
emit_signal('log_level_changed', $LogLevelSlider.value)
func _on_Continue_pressed():
_continue_button.disabled = true
emit_signal('end_pause')
func _on_IgnorePause_pressed():
var checked = _ignore_pauses.is_pressed()
emit_signal('ignore_pause', checked)
if(checked):
emit_signal('end_pause')
_continue_button.disabled = true
func _on_ShowScripts_pressed():
_toggle_scripts()
func _on_ScriptsList_item_selected(index):
_select_script(index)
func _on_TitleBar_mouse_entered():
_mouse.in_title = true
func _on_TitleBar_mouse_exited():
_mouse.in_title = false
func _input(event):
if(event is InputEventMouseButton):
if(event.button_index == 1):
_mouse.down = event.pressed
if(_mouse.down):
_mouse.down_pos = event.position
if(_mouse.in_title):
if(event is InputEventMouseMotion and _mouse.down):
set_position(get_position() + (event.position - _mouse.down_pos))
_mouse.down_pos = event.position
if(_mouse.in_handle):
if(event is InputEventMouseMotion and _mouse.down):
var new_size = rect_size + event.position - _mouse.down_pos
var new_mouse_down_pos = event.position
rect_size = new_size
_mouse.down_pos = new_mouse_down_pos
_pre_maximize_size = rect_size
func _on_ResizeHandle_mouse_entered():
_mouse.in_handle = true
func _on_ResizeHandle_mouse_exited():
_mouse.in_handle = false
# Send scroll type events through to the text box
func _on_FocusBlocker_gui_input(ev):
if(_text_box_blocker_enabled):
if(ev is InputEventPanGesture):
get_text_box()._gui_input(ev)
# convert a drag into a pan gesture so it scrolls.
elif(ev is InputEventScreenDrag):
var converted = InputEventPanGesture.new()
converted.delta = Vector2(0, ev.relative.y)
converted.position = Vector2(0, 0)
get_text_box()._gui_input(converted)
elif(ev is InputEventMouseButton and (ev.button_index == BUTTON_WHEEL_DOWN or ev.button_index == BUTTON_WHEEL_UP)):
get_text_box()._gui_input(ev)
else:
get_text_box()._gui_input(ev)
print(ev)
func _on_RichTextLabel_gui_input(ev):
pass
# leaving this b/c it is wired up and might have to send
# more signals through
print(ev)
func _on_Copy_pressed():
_text_box.select_all()
_text_box.copy()
_text_box.deselect()
func _on_DisableBlocker_toggled(button_pressed):
_text_box_blocker_enabled = !button_pressed
func _on_ShowExtras_toggled(button_pressed):
_extras.visible = button_pressed
func _on_Maximize_pressed():
if(rect_size == _pre_maximize_size):
maximize()
else:
rect_size = _pre_maximize_size
# ####################
# Private
# ####################
func _run_mode(is_running=true):
if(is_running):
_time = 0
_summary.failing.set_text("0")
_summary.passing.set_text("0")
_is_running = is_running
_hide_scripts()
var ctrls = $Navigation.get_children()
for i in range(ctrls.size()):
ctrls[i].disabled = is_running
func _select_script(index):
$Navigation/CurrentScript.set_text(_script_list.get_item_text(index))
_script_list.select(index)
_update_controls()
func _toggle_scripts():
if(_script_list.visible):
_hide_scripts()
else:
_show_scripts()
func _show_scripts():
_script_list.show()
func _hide_scripts():
_script_list.hide()
func _update_controls():
var is_empty = _script_list.get_selected_items().size() == 0
if(is_empty):
_nav.next.disabled = true
_nav.prev.disabled = true
else:
var index = get_selected_index()
_nav.prev.disabled = index <= 0
_nav.next.disabled = index >= _script_list.get_item_count() - 1
_nav.run.disabled = is_empty
_nav.current_script.disabled = is_empty
_nav.show_scripts.disabled = is_empty
# ####################
# Public
# ####################
func run_mode(is_running=true):
_run_mode(is_running)
func set_scripts(scripts):
_script_list.clear()
for i in range(scripts.size()):
_script_list.add_item(scripts[i])
_select_script(0)
_update_controls()
func select_script(index):
_select_script(index)
func get_selected_index():
return _script_list.get_selected_items()[0]
func get_log_level():
return $LogLevelSlider.value
func set_log_level(value):
$LogLevelSlider.value = _utils.nvl(value, 0)
func set_ignore_pause(should):
_ignore_pauses.pressed = should
func get_ignore_pause():
return _ignore_pauses.pressed
func get_text_box():
return $TextDisplay/RichTextLabel
func end_run():
_run_mode(false)
_update_controls()
func set_progress_script_max(value):
_progress.script.set_max(value)
func set_progress_script_value(value):
_progress.script.set_value(value)
func set_progress_test_max(value):
_progress.test.set_max(value)
func set_progress_test_value(value):
_progress.test.set_value(value)
func clear_progress():
_progress.test.set_value(0)
_progress.script.set_value(0)
func pause():
print('we got here')
_continue_button.disabled = false
func set_title(title=null):
if(title == null):
$TitleBar/Title.set_text(DEFAULT_TITLE)
else:
$TitleBar/Title.set_text(title)
func get_run_duration():
return $TitleBar/Time.text.to_float()
func add_passing(amount=1):
if(!_summary):
return
_summary.passing.set_text(str(_summary.passing.get_text().to_int() + amount))
$Summary.show()
func add_failing(amount=1):
if(!_summary):
return
_summary.failing.set_text(str(_summary.failing.get_text().to_int() + amount))
$Summary.show()
func clear_summary():
_summary.passing.set_text("0")
_summary.failing.set_text("0")
$Summary.hide()
func maximize():
if(is_inside_tree()):
var vp_size_offset = get_viewport().size
rect_size = vp_size_offset / get_scale()

View File

@ -0,0 +1,842 @@
[gd_scene load_steps=5 format=2]
[ext_resource path="res://addons/gut/GutScene.gd" type="Script" id=1]
[sub_resource type="StyleBoxFlat" id=1]
content_margin_left = -1.0
content_margin_right = -1.0
content_margin_top = -1.0
content_margin_bottom = -1.0
bg_color = Color( 0.193863, 0.205501, 0.214844, 1 )
draw_center = true
border_width_left = 0
border_width_top = 0
border_width_right = 0
border_width_bottom = 0
border_color = Color( 0.8, 0.8, 0.8, 1 )
border_blend = false
corner_radius_top_left = 20
corner_radius_top_right = 20
corner_radius_bottom_right = 0
corner_radius_bottom_left = 0
corner_detail = 8
expand_margin_left = 0.0
expand_margin_right = 0.0
expand_margin_top = 0.0
expand_margin_bottom = 0.0
shadow_color = Color( 0, 0, 0, 0.6 )
shadow_size = 0
anti_aliasing = true
anti_aliasing_size = 1
_sections_unfolded = [ "Corner Radius" ]
[sub_resource type="StyleBoxFlat" id=2]
content_margin_left = -1.0
content_margin_right = -1.0
content_margin_top = -1.0
content_margin_bottom = -1.0
bg_color = Color( 1, 1, 1, 1 )
draw_center = true
border_width_left = 0
border_width_top = 0
border_width_right = 0
border_width_bottom = 0
border_color = Color( 0, 0, 0, 1 )
border_blend = false
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 0
corner_radius_bottom_left = 0
corner_detail = 8
expand_margin_left = 0.0
expand_margin_right = 0.0
expand_margin_top = 0.0
expand_margin_bottom = 0.0
shadow_color = Color( 0, 0, 0, 0.6 )
shadow_size = 0
anti_aliasing = true
anti_aliasing_size = 1
_sections_unfolded = [ "Border", "Corner Radius" ]
[sub_resource type="Theme" id=3]
resource_local_to_scene = true
Panel/styles/panel = SubResource( 2 )
Panel/styles/panelf = null
Panel/styles/panelnc = null
_sections_unfolded = [ "Panel", "Panel/colors", "Panel/styles", "Resource" ]
[node name="Gut" type="Panel" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 740.0
margin_bottom = 320.0
rect_min_size = Vector2( 740, 250 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
custom_styles/panel = SubResource( 1 )
script = ExtResource( 1 )
_sections_unfolded = [ "Theme", "Transform", "Z Index", "custom_styles" ]
[node name="TitleBar" type="Panel" parent="." index="0"]
editor/display_folded = true
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 0.0
margin_bottom = 40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
theme = SubResource( 3 )
_sections_unfolded = [ "Rect", "Theme" ]
[node name="Title" type="Label" parent="TitleBar" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 0.0
margin_bottom = 40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "Gut"
align = 1
valign = 1
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
_sections_unfolded = [ "Anchor", "custom_colors", "custom_fonts" ]
[node name="Time" type="Label" parent="TitleBar" index="1"]
anchor_left = 1.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 0.0
margin_left = -114.0
margin_right = -53.0
margin_bottom = 40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "9999.99"
valign = 1
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
_sections_unfolded = [ "Anchor", "custom_colors" ]
[node name="Maximize" type="Button" parent="TitleBar" index="2"]
anchor_left = 1.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 0.0
margin_left = -30.0
margin_top = 10.0
margin_right = -6.0
margin_bottom = 30.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
custom_colors/font_color = Color( 0, 0, 0, 1 )
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "M"
flat = true
align = 1
_sections_unfolded = [ "Anchor", "custom_colors" ]
[node name="ScriptProgress" type="ProgressBar" parent="." index="1"]
editor/display_folded = true
anchor_left = 0.0
anchor_top = 1.0
anchor_right = 0.0
anchor_bottom = 1.0
margin_left = 70.0
margin_top = -100.0
margin_right = 180.0
margin_bottom = -70.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 0
min_value = 0.0
max_value = 100.0
step = 1.0
page = 0.0
value = 0.0
exp_edit = false
rounded = false
percent_visible = true
[node name="Label" type="Label" parent="ScriptProgress" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = -70.0
margin_right = -10.0
margin_bottom = 24.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
text = "Script"
align = 1
valign = 1
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="TestProgress" type="ProgressBar" parent="." index="2"]
editor/display_folded = true
anchor_left = 0.0
anchor_top = 1.0
anchor_right = 0.0
anchor_bottom = 1.0
margin_left = 70.0
margin_top = -70.0
margin_right = 180.0
margin_bottom = -40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 0
min_value = 0.0
max_value = 100.0
step = 1.0
page = 0.0
value = 0.0
exp_edit = false
rounded = false
percent_visible = true
[node name="Label" type="Label" parent="TestProgress" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = -70.0
margin_right = -10.0
margin_bottom = 24.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
text = "Tests"
align = 1
valign = 1
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="TextDisplay" type="Panel" parent="." index="3"]
editor/display_folded = true
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_top = 40.0
margin_bottom = -107.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
_sections_unfolded = [ "Anchor", "Grow Direction", "Visibility" ]
__meta__ = {
"_edit_group_": true
}
[node name="RichTextLabel" type="TextEdit" parent="TextDisplay" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 1.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
text = ""
readonly = true
highlight_current_line = false
syntax_highlighting = true
show_line_numbers = false
highlight_all_occurrences = false
override_selected_font_color = false
context_menu_enabled = true
smooth_scrolling = true
v_scroll_speed = 80.0
hiding_enabled = 0
wrap_lines = false
caret_block_mode = false
caret_blink = false
caret_blink_speed = 0.65
caret_moving_by_right_click = true
_sections_unfolded = [ "Anchor", "Caret", "Grow Direction", "Margin", "Visibility", "custom_colors" ]
[node name="FocusBlocker" type="Panel" parent="TextDisplay" index="1"]
self_modulate = Color( 1, 1, 1, 0 )
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_right = -10.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
_sections_unfolded = [ "Anchor", "Visibility" ]
[node name="Navigation" type="Panel" parent="." index="4"]
editor/display_folded = true
self_modulate = Color( 1, 1, 1, 0 )
anchor_left = 0.0
anchor_top = 1.0
anchor_right = 0.0
anchor_bottom = 1.0
margin_left = 220.0
margin_top = -100.0
margin_right = 580.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
_sections_unfolded = [ "Visibility" ]
[node name="Previous" type="Button" parent="Navigation" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = -30.0
margin_right = 50.0
margin_bottom = 40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "<"
flat = false
align = 1
[node name="Next" type="Button" parent="Navigation" index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 230.0
margin_right = 310.0
margin_bottom = 40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = ">"
flat = false
align = 1
[node name="Run" type="Button" parent="Navigation" index="2"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 60.0
margin_right = 220.0
margin_bottom = 40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "Run"
flat = false
align = 1
[node name="CurrentScript" type="Button" parent="Navigation" index="3"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = -30.0
margin_top = 50.0
margin_right = 310.0
margin_bottom = 90.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "res://test/unit/test_gut.gd"
flat = false
clip_text = true
align = 1
[node name="ShowScripts" type="Button" parent="Navigation" index="4"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 320.0
margin_top = 50.0
margin_right = 360.0
margin_bottom = 90.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "..."
flat = false
align = 1
[node name="LogLevelSlider" type="HSlider" parent="." index="5"]
editor/display_folded = true
anchor_left = 0.0
anchor_top = 1.0
anchor_right = 0.0
anchor_bottom = 1.0
margin_left = 80.0
margin_top = -40.0
margin_right = 130.0
margin_bottom = -20.0
rect_scale = Vector2( 2, 2 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 0
min_value = 0.0
max_value = 2.0
step = 1.0
page = 0.0
value = 0.0
exp_edit = false
rounded = false
editable = true
tick_count = 3
ticks_on_borders = true
focus_mode = 2
_sections_unfolded = [ "Rect" ]
[node name="Label" type="Label" parent="LogLevelSlider" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = -35.0
margin_top = 5.0
margin_right = 25.0
margin_bottom = 25.0
rect_scale = Vector2( 0.5, 0.5 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
text = "Log Level"
align = 1
valign = 1
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
_sections_unfolded = [ "Rect" ]
[node name="ScriptsList" type="ItemList" parent="." index="6"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 1.0
margin_left = 180.0
margin_top = 40.0
margin_right = 620.0
margin_bottom = -108.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = true
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
items = [ ]
select_mode = 0
allow_reselect = true
icon_mode = 1
fixed_icon_size = Vector2( 0, 0 )
_sections_unfolded = [ "Anchor", "Columns", "Grow Direction", "Icon", "Margin", "Mouse", "Rect", "Size Flags", "Theme", "Visibility", "custom_colors", "custom_constants" ]
[node name="ExtraOptions" type="Panel" parent="." index="7"]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = -210.0
margin_top = -246.0
margin_bottom = -106.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
custom_styles/panel = SubResource( 1 )
_sections_unfolded = [ "Anchor", "Visibility" ]
[node name="IgnorePause" type="CheckBox" parent="ExtraOptions" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 10.0
margin_top = 10.0
margin_right = 128.0
margin_bottom = 34.0
rect_scale = Vector2( 1.5, 1.5 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = true
enabled_focus_mode = 2
shortcut = null
group = null
text = "Ignore Pauses"
flat = false
align = 0
_sections_unfolded = [ "Rect" ]
[node name="DisableBlocker" type="CheckBox" parent="ExtraOptions" index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 10.0
margin_top = 50.0
margin_right = 130.0
margin_bottom = 74.0
rect_scale = Vector2( 1.5, 1.5 )
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = true
enabled_focus_mode = 2
shortcut = null
group = null
text = "Selectable"
flat = false
align = 0
_sections_unfolded = [ "Rect", "Size Flags" ]
[node name="Copy" type="Button" parent="ExtraOptions" index="2"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 20.0
margin_top = 90.0
margin_right = 200.0
margin_bottom = 130.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "Copy"
flat = false
align = 1
[node name="ResizeHandle" type="Control" parent="." index="8"]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = -40.0
margin_top = -40.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
_sections_unfolded = [ "Anchor", "Grow Direction", "Material", "Visibility" ]
[node name="Continue" type="Panel" parent="." index="9"]
self_modulate = Color( 1, 1, 1, 0 )
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = -150.0
margin_top = -100.0
margin_right = -30.0
margin_bottom = -10.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
_sections_unfolded = [ "Anchor", "Visibility" ]
[node name="Continue" type="Button" parent="Continue" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 50.0
margin_right = 119.0
margin_bottom = 90.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
disabled = true
toggle_mode = false
enabled_focus_mode = 2
shortcut = null
group = null
text = "Continue"
flat = false
align = 1
[node name="ShowExtras" type="Button" parent="Continue" index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 50.0
margin_right = 120.0
margin_bottom = 40.0
rect_pivot_offset = Vector2( 35, 20 )
rect_clip_content = false
focus_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 1
toggle_mode = true
enabled_focus_mode = 2
shortcut = null
group = null
text = "_"
flat = false
align = 1
_sections_unfolded = [ "Rect" ]
[node name="Summary" type="Node2D" parent="." index="10"]
editor/display_folded = true
position = Vector2( 0, 3 )
_sections_unfolded = [ "Transform" ]
[node name="Passing" type="Label" parent="Summary" index="0"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 10.0
margin_right = 40.0
margin_bottom = 24.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "0"
align = 1
valign = 1
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[node name="Failing" type="Label" parent="Summary" index="1"]
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 40.0
margin_top = 10.0
margin_right = 80.0
margin_bottom = 24.0
rect_pivot_offset = Vector2( 0, 0 )
rect_clip_content = false
mouse_filter = 2
mouse_default_cursor_shape = 0
size_flags_horizontal = 1
size_flags_vertical = 4
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "0"
align = 1
valign = 1
percent_visible = 1.0
lines_skipped = 0
max_lines_visible = -1
[connection signal="mouse_entered" from="TitleBar" to="." method="_on_TitleBar_mouse_entered"]
[connection signal="mouse_exited" from="TitleBar" to="." method="_on_TitleBar_mouse_exited"]
[connection signal="draw" from="TitleBar/Maximize" to="." method="_on_Maximize_draw"]
[connection signal="pressed" from="TitleBar/Maximize" to="." method="_on_Maximize_pressed"]
[connection signal="gui_input" from="TextDisplay/RichTextLabel" to="." method="_on_RichTextLabel_gui_input"]
[connection signal="gui_input" from="TextDisplay/FocusBlocker" to="." method="_on_FocusBlocker_gui_input"]
[connection signal="pressed" from="Navigation/Previous" to="." method="_on_Previous_pressed"]
[connection signal="pressed" from="Navigation/Next" to="." method="_on_Next_pressed"]
[connection signal="pressed" from="Navigation/Run" to="." method="_on_Run_pressed"]
[connection signal="pressed" from="Navigation/CurrentScript" to="." method="_on_CurrentScript_pressed"]
[connection signal="pressed" from="Navigation/ShowScripts" to="." method="_on_ShowScripts_pressed"]
[connection signal="value_changed" from="LogLevelSlider" to="." method="_on_LogLevelSlider_value_changed"]
[connection signal="item_selected" from="ScriptsList" to="." method="_on_ScriptsList_item_selected"]
[connection signal="pressed" from="ExtraOptions/IgnorePause" to="." method="_on_IgnorePause_pressed"]
[connection signal="toggled" from="ExtraOptions/DisableBlocker" to="." method="_on_DisableBlocker_toggled"]
[connection signal="pressed" from="ExtraOptions/Copy" to="." method="_on_Copy_pressed"]
[connection signal="mouse_entered" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_entered"]
[connection signal="mouse_exited" from="ResizeHandle" to="." method="_on_ResizeHandle_mouse_exited"]
[connection signal="pressed" from="Continue/Continue" to="." method="_on_Continue_pressed"]
[connection signal="draw" from="Continue/ShowExtras" to="." method="_on_ShowExtras_draw"]
[connection signal="toggled" from="Continue/ShowExtras" to="." method="_on_ShowExtras_toggled"]

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
=====================
Copyright (c) 2018 Tom "Butch" Wesley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,385 @@
# ------------------------------------------------------------------------------
# Utility class to hold the local and built in methods seperately. Add all local
# methods FIRST, then add built ins.
# ------------------------------------------------------------------------------
class ScriptMethods:
# List of methods that should not be overloaded when they are not defined
# in the class being doubled. These either break things if they are
# overloaded or do not have a "super" equivalent so we can't just pass
# through.
var _blacklist = [
'has_method',
'get_script',
'get',
'_notification',
'get_path',
'_enter_tree',
'_exit_tree',
'_process',
'_draw',
'_physics_process',
'_input',
'_unhandled_input',
'_unhandled_key_input',
'_set',
'_get', # probably
'emit_signal', # can't handle extra parameters to be sent with signal.
]
var built_ins = []
var local_methods = []
var _method_names = []
func is_blacklisted(method_meta):
return _blacklist.find(method_meta.name) != -1
func _add_name_if_does_not_have(method_name):
var should_add = _method_names.find(method_name) == -1
if(should_add):
_method_names.append(method_name)
return should_add
func add_built_in_method(method_meta):
var did_add = _add_name_if_does_not_have(method_meta.name)
if(did_add and !is_blacklisted(method_meta)):
built_ins.append(method_meta)
func add_local_method(method_meta):
var did_add = _add_name_if_does_not_have(method_meta.name)
if(did_add):
local_methods.append(method_meta)
func to_s():
var text = "Locals\n"
for i in range(local_methods.size()):
text += str(" ", local_methods[i].name, "\n")
text += "Built-Ins\n"
for i in range(built_ins.size()):
text += str(" ", built_ins[i].name, "\n")
return text
# ------------------------------------------------------------------------------
# Helper class to deal with objects and inner classes.
# ------------------------------------------------------------------------------
class ObjectInfo:
var _path = null
var _subpaths = []
var _utils = load('res://addons/gut/utils.gd').new()
func _init(path, subpath=null):
_path = path
if(subpath != null):
_subpaths = _utils.split_string(subpath, '/')
# Returns an instance of the class/inner class
func instantiate():
return get_loaded_class().new()
# Can't call it get_class because that is reserved so it gets this ugly name.
# Loads up the class and then any inner classes to give back a reference to
# the desired Inner class (if there is any)
func get_loaded_class():
var LoadedClass = load(_path)
for i in range(_subpaths.size()):
LoadedClass = LoadedClass.get(_subpaths[i])
return LoadedClass
func to_s():
return str(_path, '[', get_subpath(), ']')
func get_path():
return _path
func get_subpath():
return _utils.join_array(_subpaths, '/')
func has_subpath():
return _subpaths.size() != 0
func get_extends_text():
var extend = str("extends '", get_path(), '\'')
if(has_subpath()):
extend += str('.', get_subpath().replace('/', '.'))
return extend
# ------------------------------------------------------------------------------
# START Doubler
# ------------------------------------------------------------------------------
var _output_dir = null
var _stubber = null
var _double_count = 0 # used in making files names unique
var _use_unique_names = true
var _spy = null
var _utils = load('res://addons/gut/utils.gd').new()
var _lgr = _utils.get_logger()
var _method_maker = _utils.MethodMaker.new()
var _strategy = null
var _swapped_out_strategy = null
func _temp_strategy(strat):
_swapped_out_strategy = _strategy
_strategy = strat
func _restore_strategy():
_strategy = _swapped_out_strategy
func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL):
# make sure _method_maker gets logger too
set_logger(_utils.get_logger())
_strategy = strategy
# ###############
# Private
# ###############
func _get_indented_line(indents, text):
var to_return = ''
for i in range(indents):
to_return += "\t"
return str(to_return, text, "\n")
func _write_file(obj_info, dest_path, override_path=null):
var script_methods = _get_methods(obj_info)
var metadata = _get_stubber_metadata_text(obj_info)
if(override_path):
metadata = _get_stubber_metadata_text(obj_info, override_path)
var f = File.new()
f.open(dest_path, f.WRITE)
f.store_string(str(obj_info.get_extends_text(), "\n"))
f.store_string(metadata)
for i in range(script_methods.local_methods.size()):
f.store_string(_get_func_text(script_methods.local_methods[i]))
for i in range(script_methods.built_ins.size()):
f.store_string(_get_super_func_text(script_methods.built_ins[i]))
f.close()
func _double_scene_and_script(target_path, dest_path):
var dir = Directory.new()
dir.copy(target_path, dest_path)
var inst = load(target_path).instance()
var script_path = null
if(inst.get_script()):
script_path = inst.get_script().get_path()
inst.free()
if(script_path):
var oi = ObjectInfo.new(script_path)
var double_path = _double(oi, target_path)
var dq = '"'
var f = File.new()
f.open(dest_path, f.READ)
var source = f.get_as_text()
f.close()
source = source.replace(dq + script_path + dq, dq + double_path + dq)
f.open(dest_path, f.WRITE)
f.store_string(source)
f.close()
return script_path
func _get_methods(object_info):
var obj = object_info.instantiate()
# any mehtod in the script or super script
var script_methods = ScriptMethods.new()
var methods = obj.get_method_list()
# first pass is for local mehtods only
for i in range(methods.size()):
# 65 is a magic number for methods in script, though documentation
# says 64. This picks up local overloads of base class methods too.
if(methods[i].flags == 65):
script_methods.add_local_method(methods[i])
if(_strategy == _utils.DOUBLE_STRATEGY.FULL):
if(_utils.is_version_30()):
# second pass is for anything not local
for i in range(methods.size()):
# 65 is a magic number for methods in script, though documentation
# says 64. This picks up local overloads of base class methods too.
if(methods[i].flags != 65):
script_methods.add_built_in_method(methods[i])
else:
_lgr.warn('Full doubling is disabled in 3.1')
return script_methods
func _get_inst_id_ref_str(inst):
var ref_str = 'null'
if(inst):
ref_str = str('instance_from_id(', inst.get_instance_id(),')')
return ref_str
func _get_stubber_metadata_text(obj_info, override_path = null):
var path = obj_info.get_path()
if(override_path != null):
path = override_path
return "var __gut_metadata_ = {\n" + \
"\tpath='" + path + "',\n" + \
"\tsubpath='" + obj_info.get_subpath() + "',\n" + \
"\tstubber=" + _get_inst_id_ref_str(_stubber) + ",\n" + \
"\tspy=" + _get_inst_id_ref_str(_spy) + "\n" + \
"}\n"
func _get_spy_text(method_hash):
var txt = ''
if(_spy):
var called_with = _method_maker.get_spy_call_parameters_text(method_hash)
txt += "\t__gut_metadata_.spy.add_call(self, '" + method_hash.name + "', " + called_with + ")\n"
return txt
func _get_func_text(method_hash):
var ftxt = _method_maker.get_decleration_text(method_hash) + "\n"
var called_with = _method_maker.get_spy_call_parameters_text(method_hash)
ftxt += _get_spy_text(method_hash)
if(_stubber and method_hash.name != '_init'):
ftxt += "\treturn __gut_metadata_.stubber.get_return(self, '" + method_hash.name + "', " + called_with + ")\n"
else:
ftxt += "\tpass\n"
return ftxt
func _get_super_func_text(method_hash):
var call_method = _method_maker.get_super_call_text(method_hash)
var call_super_text = str("return ", call_method, "\n")
var ftxt = _method_maker.get_decleration_text(method_hash) + "\n"
ftxt += _get_spy_text(method_hash)
ftxt += _get_indented_line(1, call_super_text)
return ftxt
# returns the path to write the double file to
func _get_temp_path(object_info):
var file_name = object_info.get_path().get_file().get_basename()
var extension = object_info.get_path().get_extension()
if(object_info.has_subpath()):
file_name += '__' + object_info.get_subpath().replace('/', '__')
if(_use_unique_names):
file_name += str('__dbl', _double_count, '__.', extension)
else:
file_name += '.' + extension
var to_return = _output_dir.plus_file(file_name)
return to_return
func _double(obj_info, override_path=null):
var temp_path = _get_temp_path(obj_info)
_write_file(obj_info, temp_path, override_path)
_double_count += 1
return temp_path
# ###############
# Public
# ###############
func get_output_dir():
return _output_dir
func set_output_dir(output_dir):
_output_dir = output_dir
var d = Directory.new()
d.make_dir_recursive(output_dir)
func get_spy():
return _spy
func set_spy(spy):
_spy = spy
func get_stubber():
return _stubber
func set_stubber(stubber):
_stubber = stubber
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
_method_maker.set_logger(logger)
func get_strategy():
return _strategy
func set_strategy(strategy):
_strategy = strategy
# double a scene
func double_scene(path, strategy=_strategy):
_temp_strategy(strategy)
var oi = ObjectInfo.new(path)
var temp_path = _get_temp_path(oi)
_double_scene_and_script(path, temp_path)
_restore_strategy()
return load(temp_path)
# double a script/object
func double(path, strategy=_strategy):
_temp_strategy(strategy)
var oi = ObjectInfo.new(path)
var to_return = load(_double(oi))
_restore_strategy()
return to_return
# double an inner class in a script
func double_inner(path, subpath, strategy=_strategy):
_temp_strategy(strategy)
var oi = ObjectInfo.new(path, subpath)
var to_return = load(_double(oi))
_restore_strategy()
return to_return
func clear_output_directory():
var did = false
if(_output_dir.find('user://') == 0):
var d = Directory.new()
var result = d.open(_output_dir)
# BIG GOTCHA HERE. If it cannot open the dir w/ erro 31, then the
# directory becomes res:// and things go on normally and gut clears out
# out res:// which is SUPER BAD.
if(result == OK):
d.list_dir_begin(true)
var files = []
var f = d.get_next()
while(f != ''):
d.remove(f)
f = d.get_next()
did = true
return did
func delete_output_directory():
var did = clear_output_directory()
if(did):
var d = Directory.new()
d.remove(_output_dir)
# When creating doubles a unique name is used that each double can be its own
# thing. Sometimes, for testing, we do not want to do this so this allows
# you to turn off creating unique names for each double class.
#
# THIS SHOULD NEVER BE USED OUTSIDE OF INTERNAL GUT TESTING. It can cause
# weird, hard to track down problems.
func set_use_unique_names(should):
_use_unique_names = should

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,332 @@
################################################################################
#(G)odot (U)nit (T)est class
#
################################################################################
#The MIT License (MIT)
#=====================
#
#Copyright (c) 2019 Tom "Butch" Wesley
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
#
################################################################################
# Description
# -----------
# Command line interface for the GUT unit testing tool. Allows you to run tests
# from the command line instead of running a scene. Place this script along with
# gut.gd into your scripts directory at the root of your project. Once there you
# can run this script (from the root of your project) using the following command:
# godot -s -d test/gut/gut_cmdln.gd
#
# See the readme for a list of options and examples. You can also use the -gh
# option to get more information about how to use the command line interface.
#
# Version 6.7.0
################################################################################
extends SceneTree
var Optparse = load('res://addons/gut/optparse.gd')
#-------------------------------------------------------------------------------
# Helper class to resolve the various different places where an option can
# be set. Using the get_value method will enforce the order of precedence of:
# 1. command line value
# 2. config file value
# 3. default value
#
# The idea is that you set the base_opts. That will get you a copies of the
# hash with null values for the other types of values. Lower precedented hashes
# will punch through null values of higher precedented hashes.
#-------------------------------------------------------------------------------
class OptionResolver:
var base_opts = null
var cmd_opts = null
var config_opts = null
func get_value(key):
return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key]))
func set_base_opts(opts):
base_opts = opts
cmd_opts = _null_copy(opts)
config_opts = _null_copy(opts)
# creates a copy of a hash with all values null.
func _null_copy(h):
var new_hash = {}
for key in h:
new_hash[key] = null
return new_hash
func _nvl(a, b):
if(a == null):
return b
else:
return a
func _string_it(h):
var to_return = ''
for key in h:
to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')')
return to_return
func to_s():
return str("base:\n", _string_it(base_opts), "\n", \
"config:\n", _string_it(config_opts), "\n", \
"cmd:\n", _string_it(cmd_opts), "\n", \
"resolved:\n", _string_it(get_resolved_values()))
func get_resolved_values():
var to_return = {}
for key in base_opts:
to_return[key] = get_value(key)
return to_return
func to_s_verbose():
var to_return = ''
var resolved = get_resolved_values()
for key in base_opts:
to_return += str(key, "\n")
to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n")
to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n")
to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n")
to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n")
return to_return
#-------------------------------------------------------------------------------
# Here starts the actual script that uses the Options class to kick off Gut
# and run your tests.
#-------------------------------------------------------------------------------
var _utils = load('res://addons/gut/utils.gd').new()
# instance of gut
var _tester = null
# array of command line options specified
var _opts = []
# Hash for easier access to the options in the code. Options will be
# extracted into this hash and then the hash will be used afterwards so
# that I don't make any dumb typos and get the neat code-sense when I
# type a dot.
var options = {
config_file = 'res://.gutconfig.json',
dirs = [],
double_strategy = 'partial',
ignore_pause = false,
include_subdirs = false,
inner_class = '',
log_level = 1,
opacity = 100,
prefix = 'test_',
selected = '',
should_exit = false,
should_maximize = false,
show_help = false,
suffix = '.gd',
tests = [],
unit_test_name = '',
}
# flag to indicate if only a single script should be run.
var _run_single = false
func setup_options():
var opts = Optparse.new()
opts.set_banner(('This is the command line interface for the unit testing tool Gut. With this ' +
'interface you can run one or more test scripts from the command line. In order ' +
'for the Gut options to not clash with any other godot options, each option starts ' +
'with a "g". Also, any option that requires a value will take the form of ' +
'"-g<name>=<value>". There cannot be any spaces between the option, the "=", or ' +
'inside a specified value or godot will think you are trying to run a scene.'))
opts.add('-gtest', [], 'Comma delimited list of full paths to test scripts to run.')
opts.add('-gdir', [], 'Comma delimited list of directories to add tests from.')
opts.add('-gprefix', 'test_', 'Prefix used to find tests when specifying -gdir. Default "[default]"')
opts.add('-gsuffix', '.gd', 'Suffix used to find tests when specifying -gdir. Default "[default]"')
opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.')
opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.')
opts.add('-glog', 1, 'Log level. Default [default]')
opts.add('-gignore_pause', false, 'Ignores any calls to gut.pause_before_teardown.')
opts.add('-gselect', '', ('Select a script to run initially. The first script that ' +
'was loaded using -gtest or -gdir that contains the specified ' +
'string will be executed. You may run others by interacting ' +
'with the GUI.'))
opts.add('-gunit_test_name', '', ('Name of a test to run. Any test that contains the specified ' +
'text will be run, all others will be skipped.'))
opts.add('-gh', false, 'Print this help, then quit')
opts.add('-gconfig', 'res://.gutconfig.json', 'A config file that contains configuration information. Default is res://.gutconfig.json')
opts.add('-ginner_class', '', 'Only run inner classes that contain this string')
opts.add('-gopacity', 100, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.')
opts.add('-gpo', false, 'Print option values from all sources and the value used, then quit.')
opts.add('-ginclude_subdirs', false, 'Include subdirectories of -gdir.')
opts.add('-gdouble_strategy', 'partial', 'Default strategy to use when doubling. Valid values are [partial, full]. Default "[default]"')
opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file then quit.')
return opts
# Parses options, applying them to the _tester or setting values
# in the options struct.
func extract_command_line_options(from, to):
to.tests = from.get_value('-gtest')
to.dirs = from.get_value('-gdir')
to.should_exit = from.get_value('-gexit')
to.should_maximize = from.get_value('-gmaximize')
to.log_level = from.get_value('-glog')
to.ignore_pause = from.get_value('-gignore_pause')
to.selected = from.get_value('-gselect')
to.prefix = from.get_value('-gprefix')
to.suffix = from.get_value('-gsuffix')
to.unit_test_name = from.get_value('-gunit_test_name')
to.config_file = from.get_value('-gconfig')
to.inner_class = from.get_value('-ginner_class')
to.opacity = from.get_value('-gopacity')
to.include_subdirs = from.get_value('-ginclude_subdirs')
to.double_strategy = from.get_value('-gdouble_strategy')
func load_options_from_config_file(file_path, into):
# SHORTCIRCUIT
var f = File.new()
if(!f.file_exists(file_path)):
if(file_path != 'res://.gutconfig.json'):
print('ERROR: Config File "', file_path, '" does not exist.')
return -1
else:
return 1
f.open(file_path, f.READ)
var json = f.get_as_text()
f.close()
var results = JSON.parse(json)
# SHORTCIRCUIT
if(results.error != OK):
print("\n\n",'!! ERROR parsing file: ', file_path)
print(' at line ', results.error_line, ':')
print(' ', results.error_string)
return -1
# Get all the options out of the config file using the option name. The
# options hash is now the default source of truth for the name of an option.
for key in into:
if(results.result.has(key)):
into[key] = results.result[key]
return 1
# Apply all the options specified to _tester. This is where the rubber meets
# the road.
func apply_options(opts):
_tester = load('res://addons/gut/gut.gd').new()
get_root().add_child(_tester)
_tester.connect('tests_finished', self, '_on_tests_finished', [opts.should_exit])
_tester.set_yield_between_tests(true)
_tester.set_modulate(Color(1.0, 1.0, 1.0, min(1.0, float(opts.opacity) / 100)))
_tester.show()
_tester.set_include_subdirectories(opts.include_subdirs)
if(opts.should_maximize):
_tester.maximize()
if(opts.inner_class != ''):
_tester.set_inner_class_name(opts.inner_class)
_tester.set_log_level(opts.log_level)
_tester.set_ignore_pause_before_teardown(opts.ignore_pause)
for i in range(opts.dirs.size()):
_tester.add_directory(opts.dirs[i], opts.prefix, opts.suffix)
for i in range(opts.tests.size()):
_tester.add_script(opts.tests[i])
if(opts.selected != ''):
_tester.select_script(opts.selected)
_run_single = true
if(opts.double_strategy == 'full'):
_tester.set_double_strategy(_utils.DOUBLE_STRATEGY.FULL)
elif(opts.double_strategy == 'partial'):
_tester.set_double_strategy(_utils.DOUBLE_STRATEGY.PARTIAL)
_tester.set_unit_test_name(opts.unit_test_name)
func _print_gutconfigs(values):
var header = """Here is a sample of a full .gutconfig.json file.
You do not need to specify all values in your own file. The values supplied in
this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample
option (the resolved values where default < .gutconfig < command line)."""
print("\n", header.replace("\n", ' '), "\n\n")
var resolved = values
# remove some options that don't make sense to be in config
resolved.erase("config_file")
resolved.erase("show_help")
print("Here's a config with all the properties set based off of your current command and config.")
var text = JSON.print(resolved)
print(text.replace(',', ",\n"))
for key in resolved:
resolved[key] = null
print("\n\nAnd here's an empty config for you fill in what you want.")
text = JSON.print(resolved)
print(text.replace(',', ",\n"))
# parse options and run Gut
func _init():
var opt_resolver = OptionResolver.new()
opt_resolver.set_base_opts(options)
print("\n\n", ' --- Gut ---')
var o = setup_options()
var all_options_valid = o.parse()
extract_command_line_options(o, opt_resolver.cmd_opts)
var load_result = \
load_options_from_config_file(opt_resolver.get_value('config_file'), opt_resolver.config_opts)
if(load_result == -1): # -1 indicates json parse error
quit()
else:
if(!all_options_valid):
quit()
elif(o.get_value('-gh')):
o.print_help()
quit()
elif(o.get_value('-gpo')):
print('All command line options and where they are specified. ' +
'The "final" value shows which value will actually be used ' +
'based on order of precedence (default < .gutconfig < cmd line).' + "\n")
print(opt_resolver.to_s_verbose())
quit()
elif(o.get_value('-gprint_gutconfig_sample')):
_print_gutconfigs(opt_resolver.get_resolved_values())
quit()
else:
apply_options(opt_resolver.get_resolved_values())
_tester.test_scripts(!_run_single)
# exit if option is set.
func _on_tests_finished(should_exit):
if(_tester.get_fail_count()):
OS.exit_code = 1
if(should_exit):
quit()

View File

@ -0,0 +1,353 @@
################################################################################
#The MIT License (MIT)
#=====================
#
#Copyright (c) 2017 Tom "Butch" Wesley
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
#
################################################################################
################################################################################
# This class contains all the GUI creation code for Gut. It was split out and
# hopefully can be moved to a scene in the future.
################################################################################
extends WindowDialog
# various counters. Most have been moved to the Summary object but not all.
var _summary = {
moved_methods = 0,
# these are used to display the tally in the top right corner. Since the
# implementation changed to summing things up at the end, the running
# update wasn't showing. Hack.
tally_passed = 0,
tally_failed = 0
}
var _is_running = false
var min_size = Vector2(650, 400)
#controls
var _ctrls = {
text_box = TextEdit.new(),
run_button = Button.new(),
copy_button = Button.new(),
clear_button = Button.new(),
continue_button = Button.new(),
log_level_slider = HSlider.new(),
scripts_drop_down = OptionButton.new(),
next_button = Button.new(),
previous_button = Button.new(),
stop_button = Button.new(),
script_progress = ProgressBar.new(),
test_progress = ProgressBar.new(),
runtime_label = Label.new(),
ignore_continue_checkbox = CheckBox.new(),
pass_count = Label.new(),
run_rest = Button.new()
}
var _mouse_down = false
var _mouse_down_pos = null
var _mouse_in = false
func _set_anchor_top_right(obj):
obj.set_anchor(MARGIN_RIGHT, ANCHOR_BEGIN)
obj.set_anchor(MARGIN_LEFT, ANCHOR_END)
obj.set_anchor(MARGIN_TOP, ANCHOR_BEGIN)
func _set_anchor_bottom_right(obj):
obj.set_anchor(MARGIN_LEFT, ANCHOR_END)
obj.set_anchor(MARGIN_RIGHT, ANCHOR_END)
obj.set_anchor(MARGIN_TOP, ANCHOR_END)
obj.set_anchor(MARGIN_BOTTOM, ANCHOR_END)
func _set_anchor_bottom_left(obj):
obj.set_anchor(MARGIN_LEFT, ANCHOR_BEGIN)
obj.set_anchor(MARGIN_TOP, ANCHOR_END)
obj.set_anchor(MARGIN_TOP, ANCHOR_END)
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
func setup_controls():
var button_size = Vector2(75, 35)
var button_spacing = Vector2(10, 0)
var pos = Vector2(0, 0)
add_child(_ctrls.text_box)
_ctrls.text_box.set_size(Vector2(get_size().x - 4, 300))
_ctrls.text_box.set_position(Vector2(2, 0))
_ctrls.text_box.set_readonly(true)
_ctrls.text_box.set_syntax_coloring(true)
_ctrls.text_box.set_anchor(MARGIN_LEFT, ANCHOR_BEGIN)
_ctrls.text_box.set_anchor(MARGIN_RIGHT, ANCHOR_END)
_ctrls.text_box.set_anchor(MARGIN_TOP, ANCHOR_BEGIN)
_ctrls.text_box.set_anchor(MARGIN_BOTTOM, ANCHOR_END)
add_child(_ctrls.copy_button)
_ctrls.copy_button.set_text("Copy")
_ctrls.copy_button.set_size(button_size)
_ctrls.copy_button.set_position(Vector2(get_size().x - 5 - button_size.x, _ctrls.text_box.get_size().y + 10))
_set_anchor_bottom_right(_ctrls.copy_button)
add_child(_ctrls.clear_button)
_ctrls.clear_button.set_text("Clear")
_ctrls.clear_button.set_size(button_size)
_ctrls.clear_button.set_position(_ctrls.copy_button.get_position() - Vector2(button_size.x, 0) - button_spacing)
_set_anchor_bottom_right(_ctrls.clear_button)
add_child(_ctrls.pass_count)
_ctrls.pass_count.set_text('0 - 0')
_ctrls.pass_count.set_size(Vector2(100, 30))
_ctrls.pass_count.set_position(Vector2(550, 0))
_ctrls.pass_count.set_align(HALIGN_RIGHT)
_set_anchor_top_right(_ctrls.pass_count)
add_child(_ctrls.continue_button)
_ctrls.continue_button.set_text("Continue")
_ctrls.continue_button.set_size(Vector2(100, 25))
_ctrls.continue_button.set_position(Vector2(_ctrls.clear_button.get_position().x, _ctrls.clear_button.get_position().y + _ctrls.clear_button.get_size().y + 10))
_ctrls.continue_button.set_disabled(true)
_set_anchor_bottom_right(_ctrls.continue_button)
add_child(_ctrls.ignore_continue_checkbox)
_ctrls.ignore_continue_checkbox.set_text("Ignore pauses")
#_ctrls.ignore_continue_checkbox.set_pressed(_ignore_pause_before_teardown)
_ctrls.ignore_continue_checkbox.set_size(Vector2(50, 30))
_ctrls.ignore_continue_checkbox.set_position(Vector2(_ctrls.continue_button.get_position().x, _ctrls.continue_button.get_position().y + _ctrls.continue_button.get_size().y - 5))
_set_anchor_bottom_right(_ctrls.ignore_continue_checkbox)
var log_label = Label.new()
add_child(log_label)
log_label.set_text("Log Level")
log_label.set_position(Vector2(10, _ctrls.text_box.get_size().y + 1))
_set_anchor_bottom_left(log_label)
add_child(_ctrls.log_level_slider)
_ctrls.log_level_slider.set_size(Vector2(75, 30))
_ctrls.log_level_slider.set_position(Vector2(10, log_label.get_position().y + 20))
_ctrls.log_level_slider.set_min(0)
_ctrls.log_level_slider.set_max(2)
_ctrls.log_level_slider.set_ticks(3)
_ctrls.log_level_slider.set_ticks_on_borders(true)
_ctrls.log_level_slider.set_step(1)
#_ctrls.log_level_slider.set_rounded_values(true)
#_ctrls.log_level_slider.set_value(_log_level)
_set_anchor_bottom_left(_ctrls.log_level_slider)
var script_prog_label = Label.new()
add_child(script_prog_label)
script_prog_label.set_position(Vector2(100, log_label.get_position().y))
script_prog_label.set_text('Scripts:')
_set_anchor_bottom_left(script_prog_label)
add_child(_ctrls.script_progress)
_ctrls.script_progress.set_size(Vector2(200, 10))
_ctrls.script_progress.set_position(script_prog_label.get_position() + Vector2(70, 0))
_ctrls.script_progress.set_min(0)
_ctrls.script_progress.set_max(1)
_ctrls.script_progress.set_step(1)
_set_anchor_bottom_left(_ctrls.script_progress)
var test_prog_label = Label.new()
add_child(test_prog_label)
test_prog_label.set_position(Vector2(100, log_label.get_position().y + 15))
test_prog_label.set_text('Tests:')
_set_anchor_bottom_left(test_prog_label)
add_child(_ctrls.test_progress)
_ctrls.test_progress.set_size(Vector2(200, 10))
_ctrls.test_progress.set_position(test_prog_label.get_position() + Vector2(70, 0))
_ctrls.test_progress.set_min(0)
_ctrls.test_progress.set_max(1)
_ctrls.test_progress.set_step(1)
_set_anchor_bottom_left(_ctrls.test_progress)
add_child(_ctrls.previous_button)
_ctrls.previous_button.set_size(Vector2(50, 25))
pos = _ctrls.test_progress.get_position() + Vector2(250, 25)
pos.x -= 300
_ctrls.previous_button.set_position(pos)
_ctrls.previous_button.set_text("<")
_set_anchor_bottom_left(_ctrls.previous_button)
add_child(_ctrls.stop_button)
_ctrls.stop_button.set_size(Vector2(50, 25))
pos.x += 60
_ctrls.stop_button.set_position(pos)
_ctrls.stop_button.set_text('stop')
_set_anchor_bottom_left(_ctrls.stop_button)
add_child(_ctrls.run_rest)
_ctrls.run_rest.set_text('run')
_ctrls.run_rest.set_size(Vector2(50, 25))
pos.x += 60
_ctrls.run_rest.set_position(pos)
_set_anchor_bottom_left(_ctrls.run_rest)
add_child(_ctrls.next_button)
_ctrls.next_button.set_size(Vector2(50, 25))
pos.x += 60
_ctrls.next_button.set_position(pos)
_ctrls.next_button.set_text(">")
_set_anchor_bottom_left(_ctrls.next_button)
add_child(_ctrls.runtime_label)
_ctrls.runtime_label.set_text('0.0')
_ctrls.runtime_label.set_size(Vector2(50, 30))
_ctrls.runtime_label.set_position(Vector2(_ctrls.clear_button.get_position().x - 90, _ctrls.next_button.get_position().y))
_set_anchor_bottom_right(_ctrls.runtime_label)
# the drop down has to be one of the last added so that when then list of
# scripts is displayed, other controls do not get in the way of selecting
# an item in the list.
add_child(_ctrls.scripts_drop_down)
_ctrls.scripts_drop_down.set_size(Vector2(375, 25))
_ctrls.scripts_drop_down.set_position(Vector2(10, _ctrls.log_level_slider.get_position().y + 50))
_set_anchor_bottom_left(_ctrls.scripts_drop_down)
_ctrls.scripts_drop_down.set_clip_text(true)
add_child(_ctrls.run_button)
_ctrls.run_button.set_text('<- run')
_ctrls.run_button.set_size(Vector2(50, 25))
_ctrls.run_button.set_position(_ctrls.scripts_drop_down.get_position() + Vector2(_ctrls.scripts_drop_down.get_size().x + 5, 0))
_set_anchor_bottom_left(_ctrls.run_button)
func set_it_up():
self.set_size(min_size)
setup_controls()
self.connect("mouse_entered", self, "_on_mouse_enter")
self.connect("mouse_exited", self, "_on_mouse_exit")
set_process(true)
set_pause_mode(PAUSE_MODE_PROCESS)
_update_controls()
#-------------------------------------------------------------------------------
# Updates the display
#-------------------------------------------------------------------------------
func _update_controls():
if(_is_running):
_ctrls.previous_button.set_disabled(true)
_ctrls.next_button.set_disabled(true)
_ctrls.pass_count.show()
else:
_ctrls.previous_button.set_disabled(_ctrls.scripts_drop_down.get_selected() <= 0)
_ctrls.next_button.set_disabled(_ctrls.scripts_drop_down.get_selected() != -1 and _ctrls.scripts_drop_down.get_selected() == _ctrls.scripts_drop_down.get_item_count() -1)
_ctrls.pass_count.hide()
# disabled during run
_ctrls.run_button.set_disabled(_is_running)
_ctrls.run_rest.set_disabled(_is_running)
_ctrls.scripts_drop_down.set_disabled(_is_running)
# enabled during run
_ctrls.stop_button.set_disabled(!_is_running)
_ctrls.pass_count.set_text(str( _summary.tally_passed, ' - ', _summary.tally_failed))
#-------------------------------------------------------------------------------
#detect mouse movement
#-------------------------------------------------------------------------------
func _on_mouse_enter():
_mouse_in = true
#-------------------------------------------------------------------------------
#detect mouse movement
#-------------------------------------------------------------------------------
func _on_mouse_exit():
_mouse_in = false
_mouse_down = false
#-------------------------------------------------------------------------------
#Send text box text to clipboard
#-------------------------------------------------------------------------------
func _copy_button_pressed():
_ctrls.text_box.select_all()
_ctrls.text_box.copy()
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
func _init_run():
_ctrls.text_box.clear_colors()
_ctrls.text_box.add_keyword_color("PASSED", Color(0, 1, 0))
_ctrls.text_box.add_keyword_color("FAILED", Color(1, 0, 0))
_ctrls.text_box.add_color_region('/#', '#/', Color(.9, .6, 0))
_ctrls.text_box.add_color_region('/-', '-/', Color(1, 1, 0))
_ctrls.text_box.add_color_region('/*', '*/', Color(.5, .5, 1))
#_ctrls.text_box.set_symbol_color(Color(.5, .5, .5))
_ctrls.runtime_label.set_text('0.0')
_ctrls.test_progress.set_max(1)
#-------------------------------------------------------------------------------
#-------------------------------------------------------------------------------
func _input(event):
#if the mouse is somewhere within the debug window
if(_mouse_in):
#Check for mouse click inside the resize handle
if(event is InputEventMouseButton):
if (event.button_index == 1):
#It's checking a square area for the bottom right corner, but that's close enough. I'm lazy
if(event.position.x > get_size().x + get_position().x - 10 and event.position.y > get_size().y + get_position().y - 10):
if event.pressed:
_mouse_down = true
_mouse_down_pos = event.position
else:
_mouse_down = false
#Reszie
if(event is InputEventMouseMotion):
if(_mouse_down):
if(get_size() >= min_size):
var new_size = get_size() + event.position - _mouse_down_pos
var new_mouse_down_pos = event.position
if(new_size.x < min_size.x):
new_size.x = min_size.x
new_mouse_down_pos.x = _mouse_down_pos.x
if(new_size.y < min_size.y):
new_size.y = min_size.y
new_mouse_down_pos.y = _mouse_down_pos.y
_mouse_down_pos = new_mouse_down_pos
set_size(new_size)
#-------------------------------------------------------------------------------
#Custom drawing to indicate results.
#-------------------------------------------------------------------------------
func _draw():
#Draw the lines in the corner to show where you can
#drag to resize the dialog
var grab_margin = 2
var line_space = 3
var grab_line_color = Color(.4, .4, .4)
for i in range(1, 6):
draw_line(get_size() - Vector2(i * line_space, grab_margin), get_size() - Vector2(grab_margin, i * line_space), grab_line_color)
return
var where = Vector2(430, 565)
var r = 25
if(_summary.tests > 0):
if(_summary.failed > 0):
draw_circle(where, r , Color(1, 0, 0, 1))
else:
draw_circle(where, r, Color(0, 1, 0, 1))

View File

@ -0,0 +1,12 @@
tool
extends EditorPlugin
func _enter_tree():
# Initialization of the plugin goes here
# Add the new type with a name, a parent type, a script and an icon
add_custom_type("Gut", "Control", preload("gut.gd"), preload("icon.png"))
func _exit_tree():
# Clean-up of the plugin goes here
# Always remember to remove it from the engine when deactivated
remove_custom_type("Gut")

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

View File

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/icon.png-94d6536ee4af028a7ca4b709cae95011.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdhexgrid/addons/gut/icon.png"
dest_files=[ "res://.import/icon.png-94d6536ee4af028a7ca4b709cae95011.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0

View File

@ -0,0 +1,105 @@
extends Node2D
var _gut = null
var types = {
warn = 'WARNING',
error = 'ERROR',
info = 'INFO',
debug = 'DEBUG',
deprecated = 'DEPRECATED'
}
var _logs = {
types.warn: [],
types.error: [],
types.info: [],
types.debug: [],
types.deprecated: []
}
var _suppress_output = false
func _gut_log_level_for_type(log_type):
if(log_type == types.warn or log_type == types.error or log_type == types.deprecated):
return 0
else:
return 2
func _log(type, text):
_logs[type].append(text)
var formatted = str('[', type, '] ', text)
if(!_suppress_output):
if(_gut):
# this will keep the text indented under test for readability
_gut.p(formatted, _gut_log_level_for_type(type))
# IDEA! We could store the current script and test that generated
# this output, which could be useful later if we printed out a summary.
else:
print(formatted)
return formatted
# ---------------
# Get Methods
# ---------------
func get_warnings():
return get_log_entries(types.warn)
func get_errors():
return get_log_entries(types.error)
func get_infos():
return get_log_entries(types.info)
func get_debugs():
return get_log_entries(types.debug)
func get_deprecated():
return get_log_entries(types.deprecated)
func get_count(log_type=null):
var count = 0
if(log_type == null):
for key in _logs:
count += _logs[key].size()
else:
count = _logs[log_type].size()
return count
func get_log_entries(log_type):
return _logs[log_type]
# ---------------
# Log methods
# ---------------
func warn(text):
return _log(types.warn, text)
func error(text):
return _log(types.error, text)
func info(text):
return _log(types.info, text)
func debug(text):
return _log(types.debug, text)
# supply some text or the name of the deprecated method and the replacement.
func deprecated(text, alt_method=null):
var msg = text
if(alt_method):
msg = str('The method ', text, ' is deprecated, use ', alt_method , ' instead.')
return _log(types.deprecated, msg)
# ---------------
# Misc
# ---------------
func get_gut():
return _gut
func set_gut(gut):
_gut = gut
func clear():
for key in _logs:
_logs[key].clear()

View File

@ -0,0 +1,194 @@
# This class will generate method decleration lines based on method meta
# data. It will create defaults that match the method data.
#
# --------------------
# function meta data
# --------------------
# name:
# flags:
# args: [{
# (class_name:),
# (hint:0),
# (hint_string:),
# (name:),
# (type:4),
# (usage:7)
# }]
# default_args []
var _utils = load('res://addons/gut/utils.gd').new()
var _lgr = _utils.get_logger()
const PARAM_PREFIX = 'p_'
# ------------------------------------------------------
# _supported_defaults
#
# This array contains all the data types that are supported for default values.
# If a value is supported it will contain either an empty string or a prefix
# that should be used when setting the parameter default value.
# For example int, real, bool do not need anything func(p1=1, p2=2.2, p3=false)
# but things like Vectors and Colors do since only the parameters to create a
# new Vecotr or Color are included in the metadata.
# ------------------------------------------------------
# TYPE_NIL = 0 — Variable is of type nil (only applied for null).
# TYPE_BOOL = 1 — Variable is of type bool.
# TYPE_INT = 2 — Variable is of type int.
# TYPE_REAL = 3 — Variable is of type float/real.
# TYPE_STRING = 4 — Variable is of type String.
# TYPE_VECTOR2 = 5 — Variable is of type Vector2.
# TYPE_RECT2 = 6 — Variable is of type Rect2.
# TYPE_VECTOR3 = 7 — Variable is of type Vector3.
# TYPE_COLOR = 14 — Variable is of type Color.
# TYPE_OBJECT = 17 — Variable is of type Object.
# TYPE_DICTIONARY = 18 — Variable is of type Dictionary.
# TYPE_ARRAY = 19 — Variable is of type Array.
# TYPE_VECTOR2_ARRAY = 24 — Variable is of type PoolVector2Array.
# TYPE_TRANSFORM2D = 8 — Variable is of type Transform2D.
# TYPE_PLANE = 9 — Variable is of type Plane.
# TYPE_QUAT = 10 — Variable is of type Quat.
# TYPE_AABB = 11 — Variable is of type AABB.
# TYPE_BASIS = 12 — Variable is of type Basis.
# TYPE_TRANSFORM = 13 — Variable is of type Transform.
# TYPE_NODE_PATH = 15 — Variable is of type NodePath.
# TYPE_RID = 16 — Variable is of type RID.
# TYPE_RAW_ARRAY = 20 — Variable is of type PoolByteArray.
# TYPE_INT_ARRAY = 21 — Variable is of type PoolIntArray.
# TYPE_REAL_ARRAY = 22 — Variable is of type PoolRealArray.
# TYPE_STRING_ARRAY = 23 — Variable is of type PoolStringArray.
# TYPE_VECTOR3_ARRAY = 25 — Variable is of type PoolVector3Array.
# TYPE_COLOR_ARRAY = 26 — Variable is of type PoolColorArray.
# TYPE_MAX = 27 — Marker for end of type constants.
# ------------------------------------------------------
var _supported_defaults = []
func _init():
for i in range(TYPE_MAX):
_supported_defaults.append(null)
# These types do not require a prefix for defaults
_supported_defaults[TYPE_NIL] = ''
_supported_defaults[TYPE_BOOL] = ''
_supported_defaults[TYPE_INT] = ''
_supported_defaults[TYPE_REAL] = ''
_supported_defaults[TYPE_OBJECT] = ''
_supported_defaults[TYPE_ARRAY] = ''
_supported_defaults[TYPE_STRING] = ''
_supported_defaults[TYPE_DICTIONARY] = ''
_supported_defaults[TYPE_VECTOR2_ARRAY] = ''
# These require a prefix for whatever default is provided
_supported_defaults[TYPE_VECTOR2] = 'Vector2'
_supported_defaults[TYPE_RECT2] = 'Rect2'
_supported_defaults[TYPE_VECTOR3] = 'Vector3'
_supported_defaults[TYPE_COLOR] = 'Color'
# ###############
# Private
# ###############
func _is_supported_default(type_flag):
return type_flag >= 0 and type_flag < _supported_defaults.size() and [type_flag] != null
# Creates a list of paramters with defaults of null unless a default value is
# found in the metadata. If a default is found in the meta then it is used if
# it is one we know how support.
#
# If a default is found that we don't know how to handle then this method will
# return null.
func _get_arg_text(method_meta):
var text = ''
var args = method_meta.args
var defaults = []
var has_unsupported_defaults = false
# fill up the defaults with null defaults for everything that doesn't have
# a default in the meta data. default_args is an array of default values
# for the last n parameters where n is the size of default_args so we only
# add nulls for everything up to the first parameter with a default.
for i in range(args.size() - method_meta.default_args.size()):
defaults.append('null')
# Add meta-data defaults.
for i in range(method_meta.default_args.size()):
var t = args[defaults.size()]['type']
var value = ''
if(_is_supported_default(t)):
# strings are special, they need quotes around the value
if(t == TYPE_STRING):
value = str("'", str(method_meta.default_args[i]), "'")
# Colors need the parens but things like Vector2 and Rect2 don't
elif(t == TYPE_COLOR):
value = str(_supported_defaults[t], '(', str(method_meta.default_args[i]), ')')
# Everything else puts the prefix (if one is there) fomr _supported_defaults
# in front. The to_lower is used b/c for some reason the defaults for
# null, true, false are all "Null", "True", "False".
else:
value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower())
else:
_lgr.warn(str(
'Unsupported default param type: ',method_meta.name, '-', args[defaults.size()].name, ' ', t, ' = ', method_meta.default_args[i]))
value = str('unsupported=',t)
has_unsupported_defaults = true
defaults.append(value)
# construct the string of parameters
for i in range(args.size()):
text += str(PARAM_PREFIX, args[i].name, '=', defaults[i])
if(i != args.size() -1):
text += ', '
# if we don't know how to make a default then we have to return null b/c
# it will cause a runtime error and it's one thing we could return to let
# callers know it didn't work.
if(has_unsupported_defaults):
text = null
return text
# ###############
# Public
# ###############
# Creates a delceration for a function based off of function metadata. All
# types whose defaults are supported will have their values. If a datatype
# is not supported and the paramter has a default, a warning message will be
# printed and the decleration will return null.
func get_decleration_text(meta):
var param_text = _get_arg_text(meta)
var text = null
if(param_text != null):
text = str('func ', meta.name, '(', param_text, '):')
return text
# creates a call to the function in meta in the super's class.
func get_super_call_text(meta):
var params = ''
var all_supported = true
for i in range(meta.args.size()):
params += PARAM_PREFIX + meta.args[i].name
if(meta.args.size() > 1 and i != meta.args.size() -1):
params += ', '
return str('.', meta.name, '(', params, ')')
func get_spy_call_parameters_text(meta):
var called_with = 'null'
if(meta.args.size() > 0):
called_with = '['
for i in range(meta.args.size()):
called_with += str(PARAM_PREFIX, meta.args[i].name)
if(i < meta.args.size() - 1):
called_with += ', '
called_with += ']'
return called_with
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger

View File

@ -0,0 +1,246 @@
################################################################################
#(G)odot (U)nit (T)est class
#
################################################################################
#The MIT License (MIT)
#=====================
#
#Copyright (c) 2019 Tom "Butch" Wesley
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
#
################################################################################
# Description
# -----------
# Command line interface for the GUT unit testing tool. Allows you to run tests
# from the command line instead of running a scene. Place this script along with
# gut.gd into your scripts directory at the root of your project. Once there you
# can run this script (from the root of your project) using the following command:
# godot -s -d test/gut/gut_cmdln.gd
#
# See the readme for a list of options and examples. You can also use the -gh
# option to get more information about how to use the command line interface.
#
# Version 6.7.0
################################################################################
#-------------------------------------------------------------------------------
# Parses the command line arguments supplied into an array that can then be
# examined and parsed based on how the gut options work.
#-------------------------------------------------------------------------------
class CmdLineParser:
var _used_options = []
var _opts = []
func _init():
for i in range(OS.get_cmdline_args().size()):
_opts.append(OS.get_cmdline_args()[i])
# Parse out multiple comma delimited values from a command line
# option. Values are separated from option name with "=" and
# additional values are comma separated.
func _parse_array_value(full_option):
var value = _parse_option_value(full_option)
var split = value.split(',')
return split
# Parse out the value of an option. Values are separated from
# the option name with "="
func _parse_option_value(full_option):
var split = full_option.split('=')
if(split.size() > 1):
return split[1]
else:
return null
# Search _opts for an element that starts with the option name
# specified.
func find_option(name):
var found = false
var idx = 0
while(idx < _opts.size() and !found):
if(_opts[idx].find(name) == 0):
found = true
else:
idx += 1
if(found):
return idx
else:
return -1
func get_array_value(option):
_used_options.append(option)
var to_return = []
var opt_loc = find_option(option)
if(opt_loc != -1):
to_return = _parse_array_value(_opts[opt_loc])
_opts.remove(opt_loc)
return to_return
# returns the value of an option if it was specfied, null otherwise. This
# used to return the default but that became problemnatic when trying to
# punch through the different places where values could be specified.
func get_value(option):
_used_options.append(option)
var to_return = null
var opt_loc = find_option(option)
if(opt_loc != -1):
to_return = _parse_option_value(_opts[opt_loc])
_opts.remove(opt_loc)
return to_return
# returns true if it finds the option, false if not.
func was_specified(option):
_used_options.append(option)
return find_option(option) != -1
# Returns any unused command line options. I found that only the -s and
# script name come through from godot, all other options that godot uses
# are not sent through OS.get_cmdline_args().
#
# This is a onetime thing b/c i kill all items in _used_options
func get_unused_options():
var to_return = []
for i in range(_opts.size()):
to_return.append(_opts[i].split('=')[0])
var script_option = to_return.find('-s')
to_return.remove(script_option + 1)
to_return.remove(script_option)
while(_used_options.size() > 0):
var index = to_return.find(_used_options[0].split("=")[0])
if(index != -1):
to_return.remove(index)
_used_options.remove(0)
return to_return
#-------------------------------------------------------------------------------
# Simple class to hold a command line option
#-------------------------------------------------------------------------------
class Option:
var value = null
var option_name = ''
var default = null
var description = ''
func _init(name, default_value, desc=''):
option_name = name
default = default_value
description = desc
value = null#default_value
func pad(value, size, pad_with=' '):
var to_return = value
for i in range(value.length(), size):
to_return += pad_with
return to_return
func to_s(min_space=0):
var subbed_desc = description
if(subbed_desc.find('[default]') != -1):
subbed_desc = subbed_desc.replace('[default]', str(default))
return pad(option_name, min_space) + subbed_desc
#-------------------------------------------------------------------------------
# The high level interface between this script and the command line options
# supplied. Uses Option class and CmdLineParser to extract information from
# the command line and make it easily accessible.
#-------------------------------------------------------------------------------
var options = []
var _opts = []
var _banner = ''
func add(name, default, desc):
options.append(Option.new(name, default, desc))
func get_value(name):
var found = false
var idx = 0
while(idx < options.size() and !found):
if(options[idx].option_name == name):
found = true
else:
idx += 1
if(found):
return options[idx].value
else:
print("COULD NOT FIND OPTION " + name)
return null
func set_banner(banner):
_banner = banner
func print_help():
var longest = 0
for i in range(options.size()):
if(options[i].option_name.length() > longest):
longest = options[i].option_name.length()
print('---------------------------------------------------------')
print(_banner)
print("\nOptions\n-------")
for i in range(options.size()):
print(' ' + options[i].to_s(longest + 2))
print('---------------------------------------------------------')
func print_options():
for i in range(options.size()):
print(options[i].option_name + '=' + str(options[i].value))
func parse():
var parser = CmdLineParser.new()
for i in range(options.size()):
var t = typeof(options[i].default)
# only set values that were specified at the command line so that
# we can punch through default and config values correctly later.
# Without this check, you can't tell the difference between the
# defaults and what was specified, so you can't punch through
# higher level options.
if(parser.was_specified(options[i].option_name)):
if(t == TYPE_INT):
options[i].value = int(parser.get_value(options[i].option_name))
elif(t == TYPE_STRING):
options[i].value = parser.get_value(options[i].option_name)
elif(t == TYPE_ARRAY):
options[i].value = parser.get_array_value(options[i].option_name)
elif(t == TYPE_BOOL):
options[i].value = parser.was_specified(options[i].option_name)
elif(t == TYPE_NIL):
print(options[i].option_name + ' cannot be processed, it has a nil datatype')
else:
print(options[i].option_name + ' cannot be processsed, it has unknown datatype:' + str(t))
var unused = parser.get_unused_options()
if(unused.size() > 0):
print("Unrecognized options: ", unused)
return false
return true

View File

@ -0,0 +1,7 @@
[plugin]
name="Gut"
description="Unit Testing tool for Godot."
author="Butch Wesley"
version="6.7.0"
script="gut_plugin.gd"

View File

@ -0,0 +1,166 @@
################################################################################
#The MIT License (MIT)
#=====================
#
#Copyright (c) 2019 Tom "Butch" Wesley
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
#
################################################################################
# Some arbitrary string that should never show up by accident. If it does, then
# shame on you.
const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_'
# This hash holds the objects that are being watched, the signals that are being
# watched, and an array of arrays that contains arguments that were passed
# each time the signal was emitted.
#
# For example:
# _watched_signals => {
# ref1 => {
# 'signal1' => [[], [], []],
# 'signal2' => [[p1, p2]],
# 'signal3' => [[p1]]
# },
# ref2 => {
# 'some_signal' => [],
# 'other_signal' => [[p1, p2, p3], [p1, p2, p3], [p1, p2, p3]]
# }
# }
#
# In this sample:
# - signal1 on the ref1 object was emitted 3 times and each time, zero
# parameters were passed.
# - signal3 on ref1 was emitted once and passed a single parameter
# - some_signal on ref2 was never emitted.
# - other_signal on ref2 was emitted 3 times, each time with 3 parameters.
var _watched_signals = {}
var _utils = load('res://addons/gut/utils.gd').new()
func _add_watched_signal(obj, name):
# SHORTCIRCUIT - ignore dupes
if(_watched_signals.has(obj) and _watched_signals[obj].has(name)):
return
if(!_watched_signals.has(obj)):
_watched_signals[obj] = {name:[]}
else:
_watched_signals[obj][name] = []
obj.connect(name, self, '_on_watched_signal', [obj, name])
# This handles all the signals that are watched. It supports up to 9 parameters
# which could be emitted by the signal and the two parameters used when it is
# connected via watch_signal. I chose 9 since you can only specify up to 9
# parameters when dynamically calling a method via call (per the Godot
# documentation, i.e. some_object.call('some_method', 1, 2, 3...)).
#
# Based on the documentation of emit_signal, it appears you can only pass up
# to 4 parameters when firing a signal. I haven't verified this, but this should
# future proof this some if the value ever grows.
func _on_watched_signal(arg1=ARG_NOT_SET, arg2=ARG_NOT_SET, arg3=ARG_NOT_SET, \
arg4=ARG_NOT_SET, arg5=ARG_NOT_SET, arg6=ARG_NOT_SET, \
arg7=ARG_NOT_SET, arg8=ARG_NOT_SET, arg9=ARG_NOT_SET, \
arg10=ARG_NOT_SET, arg11=ARG_NOT_SET):
var args = [arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11]
# strip off any unused vars.
var idx = args.size() -1
while(str(args[idx]) == ARG_NOT_SET):
args.remove(idx)
idx -= 1
# retrieve object and signal name from the array and remove them. These
# will always be at the end since they are added when the connect happens.
var signal_name = args[args.size() -1]
args.pop_back()
var object = args[args.size() -1]
args.pop_back()
_watched_signals[object][signal_name].append(args)
func does_object_have_signal(object, signal_name):
var signals = object.get_signal_list()
for i in range(signals.size()):
if(signals[i]['name'] == signal_name):
return true
return false
func watch_signals(object):
var signals = object.get_signal_list()
for i in range(signals.size()):
_add_watched_signal(object, signals[i]['name'])
func watch_signal(object, signal_name):
var did = false
if(does_object_have_signal(object, signal_name)):
_add_watched_signal(object, signal_name)
did = true
return did
func get_emit_count(object, signal_name):
var to_return = -1
if(is_watching(object, signal_name)):
to_return = _watched_signals[object][signal_name].size()
return to_return
func did_emit(object, signal_name):
var did = false
if(is_watching(object, signal_name)):
did = get_emit_count(object, signal_name) != 0
return did
func print_object_signals(object):
var list = object.get_signal_list()
for i in range(list.size()):
print(list[i].name, "\n ", list[i])
func get_signal_parameters(object, signal_name, index=-1):
var params = null
if(is_watching(object, signal_name)):
var all_params = _watched_signals[object][signal_name]
if(all_params.size() > 0):
if(index == -1):
index = all_params.size() -1
params = all_params[index]
return params
func is_watching_object(object):
return _watched_signals.has(object)
func is_watching(object, signal_name):
return _watched_signals.has(object) and _watched_signals[object].has(signal_name)
func clear():
for obj in _watched_signals:
for signal_name in _watched_signals[obj]:
if(_utils.is_not_freed(obj)):
obj.disconnect(signal_name, self, '_on_watched_signal')
_watched_signals.clear()
# Returns a list of all the signal names that were emitted by the object.
# If the object is not being watched then an empty list is returned.
func get_signals_emitted(obj):
var emitted = []
if(is_watching_object(obj)):
for signal_name in _watched_signals[obj]:
if(_watched_signals[obj][signal_name].size() > 0):
emitted.append(signal_name)
return emitted

Binary file not shown.

View File

@ -0,0 +1,96 @@
# {
# instance_id_or_path1:{
# method1:[ [p1, p2], [p1, p2] ],
# method2:[ [p1, p2], [p1, p2] ]
# },
# instance_id_or_path1:{
# method1:[ [p1, p2], [p1, p2] ],
# method2:[ [p1, p2], [p1, p2] ]
# },
# }
var _calls = {}
var _utils = load('res://addons/gut/utils.gd').new()
var _lgr = _utils.get_logger()
func _get_params_as_string(params):
var to_return = ''
if(params == null):
return ''
for i in range(params.size()):
if(params[i] == null):
to_return += 'null'
else:
if(typeof(params[i]) == TYPE_STRING):
to_return += str('"', params[i], '"')
else:
to_return += str(params[i])
if(i != params.size() -1):
to_return += ', '
return to_return
func add_call(variant, method_name, parameters=null):
if(!_calls.has(variant)):
_calls[variant] = {}
if(!_calls[variant].has(method_name)):
_calls[variant][method_name] = []
_calls[variant][method_name].append(parameters)
func was_called(variant, method_name, parameters=null):
var to_return = false
if(_calls.has(variant) and _calls[variant].has(method_name)):
if(parameters):
to_return = _calls[variant][method_name].has(parameters)
else:
to_return = true
return to_return
func get_call_parameters(variant, method_name, index=-1):
var to_return = null
var get_index = -1
if(_calls.has(variant) and _calls[variant].has(method_name)):
var call_size = _calls[variant][method_name].size()
if(index == -1):
# get the most recent call by default
get_index = call_size -1
else:
get_index = index
if(get_index < call_size):
to_return = _calls[variant][method_name][get_index]
else:
_lgr.error(str('Specified index ', index, ' is outside range of the number of registered calls: ', call_size))
return to_return
func call_count(instance, method_name, parameters=null):
var to_return = 0
if(was_called(instance, method_name)):
if(parameters):
for i in range(_calls[instance][method_name].size()):
if(_calls[instance][method_name][i] == parameters):
to_return += 1
else:
to_return = _calls[instance][method_name].size()
return to_return
func clear():
_calls = {}
func get_call_list_as_string(instance):
var to_return = ''
if(_calls.has(instance)):
for method in _calls[instance]:
for i in range(_calls[instance][method].size()):
to_return += str(method, '(', _get_params_as_string(_calls[instance][method][i]), ")\n")
return to_return
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger

View File

@ -0,0 +1,28 @@
var return_val = null
var stub_target = null
var target_subpath = null
var parameters = null
var stub_method = null
const NOT_SET = '|_1_this_is_not_set_1_|'
func _init(target=null, method=null, subpath=null):
stub_target = target
stub_method = method
target_subpath = subpath
func to_return(val):
return_val = val
return self
func when_passed(p1=NOT_SET,p2=NOT_SET,p3=NOT_SET,p4=NOT_SET,p5=NOT_SET,p6=NOT_SET,p7=NOT_SET,p8=NOT_SET,p9=NOT_SET,p10=NOT_SET):
parameters = [p1,p2,p3,p4,p5,p6,p7,p8,p9,p10]
var idx = 0
while(idx < parameters.size()):
if(str(parameters[idx]) == NOT_SET):
parameters.remove(idx)
else:
idx += 1
return self
func to_s():
return str(stub_target, '(', target_subpath, ').', stub_method, ' with (', parameters, ') = ', return_val)

View File

@ -0,0 +1,139 @@
# {
# inst_id_or_path1:{
# method_name1: [StubParams, StubParams],
# method_name2: [StubParams, StubParams]
# },
# inst_id_or_path2:{
# method_name1: [StubParams, StubParams],
# method_name2: [StubParams, StubParams]
# }
# }
var returns = {}
var _utils = load('res://addons/gut/utils.gd').new()
var _lgr = _utils.get_logger()
func _is_instance(obj):
return typeof(obj) == TYPE_OBJECT and !obj.has_method('new')
func _make_key_from_metadata(doubled):
var to_return = doubled.__gut_metadata_.path
if(doubled.__gut_metadata_.subpath != ''):
to_return += str('-', doubled.__gut_metadata_.subpath)
return to_return
# Creates they key for the returns hash based on the type of object passed in
# obj could be a string of a path to a script with an optional subpath or
# it could be an instance of a doubled object.
func _make_key_from_variant(obj, subpath=null):
var to_return = null
match typeof(obj):
TYPE_STRING:
# this has to match what is done in _make_key_from_metadata
to_return = obj
if(subpath != null):
to_return += str('-', subpath)
TYPE_OBJECT:
if(_is_instance(obj)):
to_return = _make_key_from_metadata(obj)
else:
to_return = obj.resource_path
return to_return
func _add_obj_method(obj, method, subpath=null):
var key = _make_key_from_variant(obj, subpath)
if(_is_instance(obj)):
key = obj
if(!returns.has(key)):
returns[key] = {}
if(!returns[key].has(method)):
returns[key][method] = []
return key
# ##############
# Public
# ##############
# TODO: This method is only used in tests and should be refactored out. It
# does not support inner classes and isn't helpful.
func set_return(obj, method, value, parameters=null):
var key = _add_obj_method(obj, method)
var sp = _utils.StubParams.new(key, method)
sp.parameters = parameters
sp.return_val = value
returns[key][method].append(sp)
func add_stub(stub_params):
var key = _add_obj_method(stub_params.stub_target, stub_params.stub_method, stub_params.target_subpath)
returns[key][stub_params.stub_method].append(stub_params)
# Gets a stubbed return value for the object and method passed in. If the
# instance was stubbed it will use that, otherwise it will use the path and
# subpath of the object to try to find a value.
#
# It will also use the optional list of parameter values to find a value. If
# the objet was stubbed with no parameters then any parameters will match.
# If it was stubbed with specific paramter values then it will try to match.
# If the parameters do not match BUT there was also an empty paramter list stub
# then it will return those.
# If it cannot find anything that matches then null is returned.for
#
# Parameters
# obj: this should be an instance of a doubled object.
# method: the method called
# paramters: optional array of paramter vales to find a return value for.
func get_return(obj, method, parameters=null):
var key = _make_key_from_variant(obj)
var to_return = null
if(_is_instance(obj)):
if(returns.has(obj) and returns[obj].has(method)):
key = obj
elif(obj.get('__gut_metadata_')):
key = _make_key_from_metadata(obj)
if(returns.has(key) and returns[key].has(method)):
var param_idx = -1
var null_idx = -1
for i in range(returns[key][method].size()):
if(returns[key][method][i].parameters == parameters):
param_idx = i
if(returns[key][method][i].parameters == null):
null_idx = i
# We have matching parameter values so return the stub value for that
if(param_idx != -1):
to_return = returns[key][method][param_idx].return_val
# We found a case where the parameters were not specified so return
# parameters for that
elif(null_idx != -1):
to_return = returns[key][method][null_idx].return_val
else:
_lgr.warn(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. Null was returned.'))
else:
_lgr.info('Unstubbed call to ' + method)
return to_return
func clear():
returns.clear()
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
func to_s():
var text = ''
for thing in returns:
text += str(thing) + "\n"
for method in returns[thing]:
text += str("\t", method, "\n")
for i in range(returns[thing][method].size()):
text += "\t\t" + returns[thing][method][i].to_s() + "\n"
return text

View File

@ -0,0 +1,145 @@
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class Test:
var pass_texts = []
var fail_texts = []
var pending_texts = []
func to_s():
var pad = ' '
var to_return = ''
for i in range(fail_texts.size()):
to_return += str(pad, 'FAILED: ', fail_texts[i], "\n")
for i in range(pending_texts.size()):
to_return += str(pad, 'Pending: ', pending_texts[i], "\n")
return to_return
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class TestScript:
var name = 'NOT_SET'
var _tests = {}
var _test_order = []
func _init(script_name):
name = script_name
func get_pass_count():
var count = 0
for key in _tests:
count += _tests[key].pass_texts.size()
return count
func get_fail_count():
var count = 0
for key in _tests:
count += _tests[key].fail_texts.size()
return count
func get_pending_count():
var count = 0
for key in _tests:
count += _tests[key].pending_texts.size()
return count
func get_test_obj(name):
if(!_tests.has(name)):
_tests[name] = Test.new()
_test_order.append(name)
return _tests[name]
func add_pass(test_name, reason):
var t = get_test_obj(test_name)
t.pass_texts.append(reason)
func add_fail(test_name, reason):
var t = get_test_obj(test_name)
t.fail_texts.append(reason)
func add_pending(test_name, reason):
var t = get_test_obj(test_name)
t.pending_texts.append(reason)
# ------------------------------------------------------------------------------
# Main class
# ------------------------------------------------------------------------------
var _scripts = []
func add_script(name):
_scripts.append(TestScript.new(name))
func get_scripts():
return _scripts
func get_current_script():
return _scripts[_scripts.size() - 1]
func add_test(test_name):
get_current_script().get_test_obj(test_name)
func add_pass(test_name, reason = ''):
get_current_script().add_pass(test_name, reason)
func add_fail(test_name, reason = ''):
get_current_script().add_fail(test_name, reason)
func add_pending(test_name, reason = ''):
get_current_script().add_pending(test_name, reason)
func get_test_text(test_name):
return test_name + "\n" + get_current_script().get_test_obj(test_name).to_s()
# Gets the count of unique script names minus the .<Inner Class Name> at the
# end. Used for displaying the number of scripts without including all the
# Inner Classes.
func get_non_inner_class_script_count():
var count = 0
var unique_scripts = {}
for i in range(_scripts.size()):
var ext_loc = _scripts[i].name.find_last('.gd.')
if(ext_loc == -1):
unique_scripts[_scripts[i].name] = 1
else:
unique_scripts[_scripts[i].name.substr(0, ext_loc + 3)] = 1
return unique_scripts.keys().size()
func get_totals():
var totals = {
passing = 0,
pending = 0,
failing = 0,
tests = 0,
scripts = 0
}
for s in range(_scripts.size()):
totals.passing += _scripts[s].get_pass_count()
totals.pending += _scripts[s].get_pending_count()
totals.failing += _scripts[s].get_fail_count()
totals.tests += _scripts[s]._test_order.size()
totals.scripts = get_non_inner_class_script_count()
return totals
func get_summary_text():
var _totals = get_totals()
var to_return = ''
for s in range(_scripts.size()):
if(_scripts[s].get_fail_count() > 0 or _scripts[s].get_pending_count() > 0):
to_return += _scripts[s].name + "\n"
for t in range(_scripts[s]._test_order.size()):
var tname = _scripts[s]._test_order[t]
var test = _scripts[s].get_test_obj(tname)
if(test.fail_texts.size() > 0 or test.pending_texts.size() > 0):
to_return += str(' - ', tname, "\n", test.to_s())
var header = "*** Totals ***\n"
header += str(' scripts: ', get_non_inner_class_script_count(), "\n")
header += str(' tests: ', _totals.tests, "\n")
header += str(' passing asserts: ', _totals.passing, "\n")
header += str(' failing asserts: ',_totals.failing, "\n")
header += str(' pending: ', _totals.pending, "\n")
return to_return + "\n" + header

View File

@ -0,0 +1,964 @@
################################################################################
#(G)odot (U)nit (T)est class
#
################################################################################
#The MIT License (MIT)
#=====================
#
#Copyright (c) 2019 Tom "Butch" Wesley
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
#
################################################################################
# View readme for usage details.
#
# Version - see gut.gd
################################################################################
# Class that all test scripts must extend.
#
# This provides all the asserts and other testing features. Test scripts are
# run by the Gut class in gut.gd
################################################################################
extends Node
# constant for signal when calling yield_for
const YIELD = 'timeout'
# Need a reference to the instance that is running the tests. This
# is set by the gut class when it runs the tests. This gets you
# access to the asserts in the tests you write.
var gut = null
var passed = false
var failed = false
var _disable_strict_datatype_checks = false
# Holds all the text for a test's fail/pass. This is used for testing purposes
# to see the text of a failed sub-test in test_test.gd
var _fail_pass_text = []
# Hash containing all the built in types in Godot. This provides an English
# name for the types that corosponds with the type constants defined in the
# engine. This is used for priting out messages when comparing types fails.
var types = {}
func _init_types_dictionary():
types[TYPE_NIL] = 'TYPE_NIL'
types[TYPE_BOOL] = 'Bool'
types[TYPE_INT] = 'Int'
types[TYPE_REAL] = 'Float/Real'
types[TYPE_STRING] = 'String'
types[TYPE_VECTOR2] = 'Vector2'
types[TYPE_RECT2] = 'Rect2'
types[TYPE_VECTOR3] = 'Vector3'
#types[8] = 'Matrix32'
types[TYPE_PLANE] = 'Plane'
types[TYPE_QUAT] = 'QUAT'
types[TYPE_AABB] = 'AABB'
#types[12] = 'Matrix3'
types[TYPE_TRANSFORM] = 'Transform'
types[TYPE_COLOR] = 'Color'
#types[15] = 'Image'
types[TYPE_NODE_PATH] = 'Node Path'
types[TYPE_RID] = 'RID'
types[TYPE_OBJECT] = 'TYPE_OBJECT'
#types[19] = 'TYPE_INPUT_EVENT'
types[TYPE_DICTIONARY] = 'Dictionary'
types[TYPE_ARRAY] = 'Array'
types[TYPE_RAW_ARRAY] = 'TYPE_RAW_ARRAY'
types[TYPE_INT_ARRAY] = 'TYPE_INT_ARRAY'
types[TYPE_REAL_ARRAY] = 'TYPE_REAL_ARRAY'
types[TYPE_STRING_ARRAY] = 'TYPE_STRING_ARRAY'
types[TYPE_VECTOR2_ARRAY] = 'TYPE_VECTOR2_ARRAY'
types[TYPE_VECTOR3_ARRAY] = 'TYPE_VECTOR3_ARRAY'
types[TYPE_COLOR_ARRAY] = 'TYPE_COLOR_ARRAY'
types[TYPE_MAX] = 'TYPE_MAX'
const EDITOR_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE | PROPERTY_USAGE_DEFAULT
const VARIABLE_PROPERTY = PROPERTY_USAGE_SCRIPT_VARIABLE
# Summary counts for the test.
var _summary = {
asserts = 0,
passed = 0,
failed = 0,
tests = 0,
pending = 0
}
# This is used to watch signals so we can make assertions about them.
var _signal_watcher = load('res://addons/gut/signal_watcher.gd').new()
# Convenience copy of _utils.DOUBLE_STRATEGY
var DOUBLE_STRATEGY = null
var _utils = load('res://addons/gut/utils.gd').new()
var _lgr = _utils.get_logger()
func _init():
_init_types_dictionary()
DOUBLE_STRATEGY = _utils.DOUBLE_STRATEGY # yes, this is right
# ------------------------------------------------------------------------------
# Fail an assertion. Causes test and script to fail as well.
# ------------------------------------------------------------------------------
func _fail(text):
_summary.asserts += 1
_summary.failed += 1
var msg = 'FAILED: ' + text
_fail_pass_text.append(msg)
if(gut):
gut.p(msg, gut.LOG_LEVEL_FAIL_ONLY)
gut._fail(text)
# ------------------------------------------------------------------------------
# Pass an assertion.
# ------------------------------------------------------------------------------
func _pass(text):
_summary.asserts += 1
_summary.passed += 1
var msg = "PASSED: " + text
_fail_pass_text.append(msg)
if(gut):
gut.p(msg, gut.LOG_LEVEL_ALL_ASSERTS)
gut._pass(text)
# ------------------------------------------------------------------------------
# Checks if the datatypes passed in match. If they do not then this will cause
# a fail to occur. If they match then TRUE is returned, FALSE if not. This is
# used in all the assertions that compare values.
# ------------------------------------------------------------------------------
func _do_datatypes_match__fail_if_not(got, expected, text):
var passed = true
if(!_disable_strict_datatype_checks):
var got_type = typeof(got)
var expect_type = typeof(expected)
if(got_type != expect_type and got != null and expected != null):
# If we have a mismatch between float and int (types 2 and 3) then
# print out a warning but do not fail.
if([2, 3].has(got_type) and [2, 3].has(expect_type)):
_lgr.warn(str('Warn: Float/Int comparison. Got ', types[got_type], ' but expected ', types[expect_type]))
else:
_fail('Cannot compare ' + types[got_type] + '[' + str(got) + '] to ' + types[expect_type] + '[' + str(expected) + ']. ' + text)
passed = false
return passed
# ------------------------------------------------------------------------------
# Create a string that lists all the methods that were called on an spied
# instance.
# ------------------------------------------------------------------------------
func _get_desc_of_calls_to_instance(inst):
var BULLET = ' * '
var calls = gut.get_spy().get_call_list_as_string(inst)
# indent all the calls
calls = BULLET + calls.replace("\n", "\n" + BULLET)
# remove trailing newline and bullet
calls = calls.substr(0, calls.length() - BULLET.length() - 1)
return "Calls made on " + str(inst) + "\n" + calls
# ------------------------------------------------------------------------------
# Signal assertion helper. Do not call directly, use _can_make_signal_assertions
# ------------------------------------------------------------------------------
func _fail_if_does_not_have_signal(object, signal_name):
var did_fail = false
if(!_signal_watcher.does_object_have_signal(object, signal_name)):
_fail(str('Object ', object, ' does not have the signal [', signal_name, ']'))
did_fail = true
return did_fail
# ------------------------------------------------------------------------------
# Signal assertion helper. Do not call directly, use _can_make_signal_assertions
# ------------------------------------------------------------------------------
func _fail_if_not_watching(object):
var did_fail = false
if(!_signal_watcher.is_watching_object(object)):
_fail(str('Cannot make signal assertions because the object ', object, \
' is not being watched. Call watch_signals(some_object) to be able to make assertions about signals.'))
did_fail = true
return did_fail
# ------------------------------------------------------------------------------
# Returns text that contains original text and a list of all the signals that
# were emitted for the passed in object.
# ------------------------------------------------------------------------------
func _get_fail_msg_including_emitted_signals(text, object):
return str(text," (Signals emitted: ", _signal_watcher.get_signals_emitted(object), ")")
# #######################
# Virtual Methods
# #######################
# alias for prerun_setup
func before_all():
pass
# alias for setup
func before_each():
pass
# alias for postrun_teardown
func after_all():
pass
# alias for teardown
func after_each():
pass
# #######################
# Public
# #######################
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
# #######################
# Asserts
# #######################
# ------------------------------------------------------------------------------
# Asserts that the expected value equals the value got.
# ------------------------------------------------------------------------------
func assert_eq(got, expected, text=""):
var disp = "[" + str(got) + "] expected to equal [" + str(expected) + "]: " + text
if(_do_datatypes_match__fail_if_not(got, expected, text)):
if(expected != got):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Asserts that the value got does not equal the "not expected" value.
# ------------------------------------------------------------------------------
func assert_ne(got, not_expected, text=""):
var disp = "[" + str(got) + "] expected to be anything except [" + str(not_expected) + "]: " + text
if(_do_datatypes_match__fail_if_not(got, not_expected, text)):
if(got == not_expected):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Asserts that the expected value almost equals the value got.
# ------------------------------------------------------------------------------
func assert_almost_eq(got, expected, error_interval, text=''):
var disp = "[" + str(got) + "] expected to equal [" + str(expected) + "] +/- [" + str(error_interval) + "]: " + text
if(_do_datatypes_match__fail_if_not(got, expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)):
if(got < (expected - error_interval) or got > (expected + error_interval)):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Asserts that the expected value does not almost equal the value got.
# ------------------------------------------------------------------------------
func assert_almost_ne(got, not_expected, error_interval, text=''):
var disp = "[" + str(got) + "] expected to not equal [" + str(not_expected) + "] +/- [" + str(error_interval) + "]: " + text
if(_do_datatypes_match__fail_if_not(got, not_expected, text) and _do_datatypes_match__fail_if_not(got, error_interval, text)):
if(got < (not_expected - error_interval) or got > (not_expected + error_interval)):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Asserts got is greater than expected
# ------------------------------------------------------------------------------
func assert_gt(got, expected, text=""):
var disp = "[" + str(got) + "] expected to be > than [" + str(expected) + "]: " + text
if(_do_datatypes_match__fail_if_not(got, expected, text)):
if(got > expected):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Asserts got is less than expected
# ------------------------------------------------------------------------------
func assert_lt(got, expected, text=""):
var disp = "[" + str(got) + "] expected to be < than [" + str(expected) + "]: " + text
if(_do_datatypes_match__fail_if_not(got, expected, text)):
if(got < expected):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# asserts that got is true
# ------------------------------------------------------------------------------
func assert_true(got, text=""):
if(!got):
_fail(text)
else:
_pass(text)
# ------------------------------------------------------------------------------
# Asserts that got is false
# ------------------------------------------------------------------------------
func assert_false(got, text=""):
if(got):
_fail(text)
else:
_pass(text)
# ------------------------------------------------------------------------------
# Asserts value is between (inclusive) the two expected values.
# ------------------------------------------------------------------------------
func assert_between(got, expect_low, expect_high, text=""):
var disp = "[" + str(got) + "] expected to be between [" + str(expect_low) + "] and [" + str(expect_high) + "]: " + text
if(_do_datatypes_match__fail_if_not(got, expect_low, text) and _do_datatypes_match__fail_if_not(got, expect_high, text)):
if(expect_low > expect_high):
disp = "INVALID range. [" + str(expect_low) + "] is not less than [" + str(expect_high) + "]"
_fail(disp)
else:
if(got < expect_low or got > expect_high):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Uses the 'has' method of the object passed in to determine if it contains
# the passed in element.
# ------------------------------------------------------------------------------
func assert_has(obj, element, text=""):
var disp = str('Expected [', obj, '] to contain value: [', element, ']: ', text)
if(obj.has(element)):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func assert_does_not_have(obj, element, text=""):
var disp = str('Expected [', obj, '] to NOT contain value: [', element, ']: ', text)
if(obj.has(element)):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Asserts that a file exists
# ------------------------------------------------------------------------------
func assert_file_exists(file_path):
var disp = 'expected [' + file_path + '] to exist.'
var f = File.new()
if(f.file_exists(file_path)):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Asserts that a file should not exist
# ------------------------------------------------------------------------------
func assert_file_does_not_exist(file_path):
var disp = 'expected [' + file_path + '] to NOT exist'
var f = File.new()
if(!f.file_exists(file_path)):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Asserts the specified file is empty
# ------------------------------------------------------------------------------
func assert_file_empty(file_path):
var disp = 'expected [' + file_path + '] to be empty'
var f = File.new()
if(f.file_exists(file_path) and gut.is_file_empty(file_path)):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Asserts the specified file is not empty
# ------------------------------------------------------------------------------
func assert_file_not_empty(file_path):
var disp = 'expected [' + file_path + '] to contain data'
if(!gut.is_file_empty(file_path)):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Asserts the object has the specified method
# ------------------------------------------------------------------------------
func assert_has_method(obj, method):
assert_true(obj.has_method(method), 'Should have method: ' + method)
# Old deprecated method name
func assert_get_set_methods(obj, property, default, set_to):
_lgr.deprecated('assert_get_set_methods', 'assert_accessors')
assert_accessors(obj, property, default, set_to)
# ------------------------------------------------------------------------------
# Verifies the object has get and set methods for the property passed in. The
# property isn't tied to anything, just a name to be appended to the end of
# get_ and set_. Asserts the get_ and set_ methods exist, if not, it stops there.
# If they exist then it asserts get_ returns the expected default then calls
# set_ and asserts get_ has the value it was set to.
# ------------------------------------------------------------------------------
func assert_accessors(obj, property, default, set_to):
var fail_count = _summary.failed
var get = 'get_' + property
var set = 'set_' + property
assert_has_method(obj, get)
assert_has_method(obj, set)
# SHORT CIRCUIT
if(_summary.failed > fail_count):
return
assert_eq(obj.call(get), default, 'It should have the expected default value.')
obj.call(set, set_to)
assert_eq(obj.call(get), set_to, 'The set value should have been returned.')
# ---------------------------------------------------------------------------
# Property search helper. Used to retrieve Dictionary of specified property
# from passed object. Returns null if not found.
# If provided, property_usage constrains the type of property returned by
# passing either:
# EDITOR_PROPERTY for properties defined as: export(int) var some_value
# VARIABLE_PROPERTY for properties definded as: var another_value
# ---------------------------------------------------------------------------
func _find_object_property(obj, property_name, property_usage=null):
var result = null
var found = false
var properties = obj.get_property_list()
while !found and !properties.empty():
var property = properties.pop_back()
if property['name'] == property_name:
if property_usage == null or property['usage'] == property_usage:
result = property
found = true
return result
# ------------------------------------------------------------------------------
# Asserts a class exports a variable.
# ------------------------------------------------------------------------------
func assert_exports(obj, property_name, type):
var disp = 'expected %s to have editor property [%s]' % [obj, property_name]
var property = _find_object_property(obj, property_name, EDITOR_PROPERTY)
if property != null:
disp += ' of type [%s]. Got type [%s].' % [types[type], types[property['type']]]
if property['type'] == type:
_pass(disp)
else:
_fail(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Signal assertion helper.
#
# Verifies that the object and signal are valid for making signal assertions.
# This will fail with specific messages that indicate why they are not valid.
# This returns true/false to indicate if the object and signal are valid.
# ------------------------------------------------------------------------------
func _can_make_signal_assertions(object, signal_name):
return !(_fail_if_not_watching(object) or _fail_if_does_not_have_signal(object, signal_name))
# ------------------------------------------------------------------------------
# Watch the signals for an object. This must be called before you can make
# any assertions about the signals themselves.
# ------------------------------------------------------------------------------
func watch_signals(object):
_signal_watcher.watch_signals(object)
# ------------------------------------------------------------------------------
# Asserts that a signal has been emitted at least once.
#
# This will fail with specific messages if the object is not being watched or
# the object does not have the specified signal
# ------------------------------------------------------------------------------
func assert_signal_emitted(object, signal_name, text=""):
var disp = str('Expected object ', object, ' to have emitted signal [', signal_name, ']: ', text)
if(_can_make_signal_assertions(object, signal_name)):
if(_signal_watcher.did_emit(object, signal_name)):
_pass(disp)
else:
_fail(_get_fail_msg_including_emitted_signals(disp, object))
# ------------------------------------------------------------------------------
# Asserts that a signal has not been emitted.
#
# This will fail with specific messages if the object is not being watched or
# the object does not have the specified signal
# ------------------------------------------------------------------------------
func assert_signal_not_emitted(object, signal_name, text=""):
var disp = str('Expected object ', object, ' to NOT emit signal [', signal_name, ']: ', text)
if(_can_make_signal_assertions(object, signal_name)):
if(_signal_watcher.did_emit(object, signal_name)):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Asserts that a signal was fired with the specified parameters. The expected
# parameters should be passed in as an array. An optional index can be passed
# when a signal has fired more than once. The default is to retrieve the most
# recent emission of the signal.
#
# This will fail with specific messages if the object is not being watched or
# the object does not have the specified signal
# ------------------------------------------------------------------------------
func assert_signal_emitted_with_parameters(object, signal_name, parameters, index=-1):
var disp = str('Expected object ', object, ' to emit signal [', signal_name, '] with parameters ', parameters, ', got ')
if(_can_make_signal_assertions(object, signal_name)):
if(_signal_watcher.did_emit(object, signal_name)):
var parms_got = _signal_watcher.get_signal_parameters(object, signal_name, index)
if(parameters == parms_got):
_pass(str(disp, parms_got))
else:
_fail(str(disp, parms_got))
else:
var text = str('Object ', object, ' did not emit signal [', signal_name, ']')
_fail(_get_fail_msg_including_emitted_signals(text, object))
# ------------------------------------------------------------------------------
# Assert that a signal has been emitted a specific number of times.
#
# This will fail with specific messages if the object is not being watched or
# the object does not have the specified signal
# ------------------------------------------------------------------------------
func assert_signal_emit_count(object, signal_name, times, text=""):
if(_can_make_signal_assertions(object, signal_name)):
var count = _signal_watcher.get_emit_count(object, signal_name)
var disp = str('Expected the signal [', signal_name, '] emit count of [', count, '] to equal [', times, ']: ', text)
if(count== times):
_pass(disp)
else:
_fail(_get_fail_msg_including_emitted_signals(disp, object))
# ------------------------------------------------------------------------------
# Assert that the passed in object has the specfied signal
# ------------------------------------------------------------------------------
func assert_has_signal(object, signal_name, text=""):
var disp = str('Expected object ', object, ' to have signal [', signal_name, ']: ', text)
if(_signal_watcher.does_object_have_signal(object, signal_name)):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Returns the number of times a signal was emitted. -1 returned if the object
# is not being watched.
# ------------------------------------------------------------------------------
func get_signal_emit_count(object, signal_name):
return _signal_watcher.get_emit_count(object, signal_name)
# ------------------------------------------------------------------------------
# Get the parmaters of a fired signal. If the signal was not fired null is
# returned. You can specify an optional index (use get_signal_emit_count to
# determine the number of times it was emitted). The default index is the
# latest time the signal was fired (size() -1 insetead of 0). The parameters
# returned are in an array.
# ------------------------------------------------------------------------------
func get_signal_parameters(object, signal_name, index=-1):
return _signal_watcher.get_signal_parameters(object, signal_name, index)
# ------------------------------------------------------------------------------
# Get the parameters for a method call to a doubled object. By default it will
# return the most recent call. You can optionally specify an index.
#
# Returns:
# * an array of parameter values if a call the method was found
# * null when a call to the method was not found or the index specified was
# invalid.
# ------------------------------------------------------------------------------
func get_call_parameters(object, method_name, index=-1):
var to_return = null
if(_utils.is_double(object)):
to_return = gut.get_spy().get_call_parameters(object, method_name, index)
else:
_lgr.error('You must pass a doulbed object to get_call_parameters.')
return to_return
# ------------------------------------------------------------------------------
# Assert that object is an instance of a_class
# ------------------------------------------------------------------------------
func assert_extends(object, a_class, text=''):
_lgr.deprecated('assert_extends', 'assert_is')
assert_is(object, a_class, text)
# Alias for assert_extends
func assert_is(object, a_class, text=''):
var disp = str('Expected [', object, '] to be type of [', a_class, ']: ', text)
var NATIVE_CLASS = 'GDScriptNativeClass'
var GDSCRIPT_CLASS = 'GDScript'
var bad_param_2 = 'Parameter 2 must be a Class (like Node2D or Label). You passed '
if(typeof(object) != TYPE_OBJECT):
_fail(str('Parameter 1 must be an instance of an object. You passed: ', types[typeof(object)]))
elif(typeof(a_class) != TYPE_OBJECT):
_fail(str(bad_param_2, types[typeof(a_class)]))
else:
disp = str('Expected [', object.get_class(), '] to extend [', a_class.get_class(), ']: ', text)
if(a_class.get_class() != NATIVE_CLASS and a_class.get_class() != GDSCRIPT_CLASS):
_fail(str(bad_param_2, a_class.get_class(), ' ', types[typeof(a_class)]))
else:
if(object is a_class):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Assert that text contains given search string.
# The match_case flag determines case sensitivity.
# ------------------------------------------------------------------------------
func assert_string_contains(text, search, match_case=true):
var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.'
var disp = 'Expected \'%s\' to contain \'%s\', match_case=%s' % [text, search, match_case]
if(text == '' or search == ''):
_fail(empty_search % [text, search])
elif(match_case):
if(text.find(search) == -1):
_fail(disp)
else:
_pass(disp)
else:
if(text.to_lower().find(search.to_lower()) == -1):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Assert that text starts with given search string.
# match_case flag determines case sensitivity.
# ------------------------------------------------------------------------------
func assert_string_starts_with(text, search, match_case=true):
var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.'
var disp = 'Expected \'%s\' to start with \'%s\', match_case=%s' % [text, search, match_case]
if(text == '' or search == ''):
_fail(empty_search % [text, search])
elif(match_case):
if(text.find(search) == 0):
_pass(disp)
else:
_fail(disp)
else:
if(text.to_lower().find(search.to_lower()) == 0):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Assert that text ends with given search string.
# match_case flag determines case sensitivity.
# ------------------------------------------------------------------------------
func assert_string_ends_with(text, search, match_case=true):
var empty_search = 'Expected text and search strings to be non-empty. You passed \'%s\' and \'%s\'.'
var disp = 'Expected \'%s\' to end with \'%s\', match_case=%s' % [text, search, match_case]
var required_index = len(text) - len(search)
if(text == '' or search == ''):
_fail(empty_search % [text, search])
elif(match_case):
if(text.find(search) == required_index):
_pass(disp)
else:
_fail(disp)
else:
if(text.to_lower().find(search.to_lower()) == required_index):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Assert that a method was called on an instance of a doubled class. If
# parameters are supplied then the params passed in when called must match.
# TODO make 3rd paramter "param_or_text" and add fourth parameter of "text" and
# then work some magic so this can have a "text" parameter without being
# annoying.
# ------------------------------------------------------------------------------
func assert_called(inst, method_name, parameters=null):
var disp = str('Expected [',method_name,'] to have been called on ',inst)
if(!_utils.is_double(inst)):
_fail('You must pass a doubled instance to assert_called. Check the wiki for info on using double.')
else:
if(gut.get_spy().was_called(inst, method_name, parameters)):
_pass(disp)
else:
if(parameters != null):
disp += str(' with parameters ', parameters)
_fail(str(disp, "\n", _get_desc_of_calls_to_instance(inst)))
# ------------------------------------------------------------------------------
# Assert that a method was not called on an instance of a doubled class. If
# parameters are specified then this will only fail if it finds a call that was
# sent matching parameters.
# ------------------------------------------------------------------------------
func assert_not_called(inst, method_name, parameters=null):
var disp = str('Expected [', method_name, '] to NOT have been called on ', inst)
if(!_utils.is_double(inst)):
_fail('You must pass a doubled instance to assert_not_called. Check the wiki for info on using double.')
else:
if(gut.get_spy().was_called(inst, method_name, parameters)):
if(parameters != null):
disp += str(' with parameters ', parameters)
_fail(str(disp, "\n", _get_desc_of_calls_to_instance(inst)))
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Assert that a method on an instance of a doubled class was called a number
# of times. If parameters are specified then only calls with matching
# parameter values will be counted.
# ------------------------------------------------------------------------------
func assert_call_count(inst, method_name, expected_count, parameters=null):
var count = gut.get_spy().call_count(inst, method_name, parameters)
var param_text = ''
if(parameters):
param_text = ' with parameters ' + str(parameters)
var disp = 'Expected [%s] on %s to be called [%s] times%s. It was called [%s] times.'
disp = disp % [method_name, inst, expected_count, param_text, count]
if(!_utils.is_double(inst)):
_fail('You must pass a doubled instance to assert_call_count. Check the wiki for info on using double.')
else:
if(count == expected_count):
_pass(disp)
else:
_fail(str(disp, "\n", _get_desc_of_calls_to_instance(inst)))
# ------------------------------------------------------------------------------
# Asserts the passed in value is null
# ------------------------------------------------------------------------------
func assert_null(got, text=''):
var disp = str('Expected [', got, '] to be NULL: ', text)
if(got == null):
_pass(disp)
else:
_fail(disp)
# ------------------------------------------------------------------------------
# Asserts the passed in value is null
# ------------------------------------------------------------------------------
func assert_not_null(got, text=''):
var disp = str('Expected [', got, '] to be anything but NULL: ', text)
if(got == null):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------
# Mark the current test as pending.
# ------------------------------------------------------------------------------
func pending(text=""):
_summary.pending += 1
if(gut):
if(text == ""):
gut.p("Pending")
else:
gut.p("Pending: " + text)
gut._pending(text)
# ------------------------------------------------------------------------------
# Returns the number of times a signal was emitted. -1 returned if the object
# is not being watched.
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
# Yield for the time sent in. The optional message will be printed when
# Gut detects the yeild. When the time expires the YIELD signal will be
# emitted.
# ------------------------------------------------------------------------------
func yield_for(time, msg=''):
return gut.set_yield_time(time, msg)
# ------------------------------------------------------------------------------
# Yield to a signal or a maximum amount of time, whichever comes first. When
# the conditions are met the YIELD signal will be emitted.
# ------------------------------------------------------------------------------
func yield_to(obj, signal_name, max_wait, msg=''):
watch_signals(obj)
gut.set_yield_signal_or_time(obj, signal_name, max_wait, msg)
return gut
# ------------------------------------------------------------------------------
# Ends a test that had a yield in it. You only need to use this if you do
# not make assertions after a yield.
# ------------------------------------------------------------------------------
func end_test():
_lgr.deprecated('end_test is no longer necessary, you can remove it.')
#gut.end_yielded_test()
func get_summary():
return _summary
func get_fail_count():
return _summary.failed
func get_pass_count():
return _summary.passed
func get_pending_count():
return _summary.pending
func get_assert_count():
return _summary.asserts
func clear_signal_watcher():
_signal_watcher.clear()
func get_double_strategy():
return gut.get_doubler().get_strategy()
func set_double_strategy(double_strategy):
gut.get_doubler().set_strategy(double_strategy)
func pause_before_teardown():
gut.pause_before_teardown()
# ------------------------------------------------------------------------------
# Convert the _summary dictionary into text
# ------------------------------------------------------------------------------
func get_summary_text():
var to_return = get_script().get_path() + "\n"
to_return += str(' ', _summary.passed, ' of ', _summary.asserts, ' passed.')
if(_summary.pending > 0):
to_return += str("\n ", _summary.pending, ' pending')
if(_summary.failed > 0):
to_return += str("\n ", _summary.failed, ' failed.')
return to_return
# ------------------------------------------------------------------------------
# Double a script, inner class, or scene using a path or a loaded script/scene.
#
#
# ------------------------------------------------------------------------------
func double(thing, p2=null, p3=null):
var strategy = p2
var subpath = null
if(typeof(p2) == TYPE_STRING):
strategy = p3
subpath = p2
var path = null
if(typeof(thing) == TYPE_OBJECT):
path = thing.resource_path
else:
path = thing
var extension = path.get_extension()
var to_return = null
if(extension == 'tscn'):
to_return = double_scene(path, strategy)
elif(extension == 'gd'):
if(subpath == null):
to_return = double_script(path, strategy)
else:
to_return = double_inner(path, subpath, strategy)
return to_return
# ------------------------------------------------------------------------------
# Specifically double a scene
# ------------------------------------------------------------------------------
func double_scene(path, strategy=null):
var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy())
return gut.get_doubler().double_scene(path, override_strat)
# ------------------------------------------------------------------------------
# Specifically double a script
# ------------------------------------------------------------------------------
func double_script(path, strategy=null):
var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy())
return gut.get_doubler().double(path, override_strat)
# ------------------------------------------------------------------------------
# Specifically double an Inner class in a a script
# ------------------------------------------------------------------------------
func double_inner(path, subpath, strategy=null):
var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy())
return gut.get_doubler().double_inner(path, subpath, override_strat)
# ------------------------------------------------------------------------------
# Stub something.
#
# Parameters
# 1: the thing to stub, a file path or a instance or a class
# 2: either an inner class subpath or the method name
# 3: the method name if an inner class subpath was specified
# NOTE: right now we cannot stub inner classes at the path level so this should
# only be called with two parameters. I did the work though so I'm going
# to leave it but not update the wiki.
# ------------------------------------------------------------------------------
func stub(thing, p2, p3=null):
var method_name = p2
var subpath = null
if(p3 != null):
subpath = p2
method_name = p3
var sp = _utils.StubParams.new(thing, method_name, subpath)
gut.get_stubber().add_stub(sp)
return sp
# ------------------------------------------------------------------------------
# convenience wrapper.
# ------------------------------------------------------------------------------
func simulate(obj, times, delta):
gut.simulate(obj, times, delta)
# ------------------------------------------------------------------------------
# Replace the node at base_node.get_node(path) with with_this. All refrences
# to the node via $ and get_node(...) will now return with_this. with_this will
# get all the groups that the node that was replaced had.
#
# The node that was replaced is queued to be freed.
# ------------------------------------------------------------------------------
func replace_node(base_node, path_or_node, with_this):
var path = path_or_node
if(typeof(path_or_node) != TYPE_STRING):
# This will cause an engine error if it fails. It always returns a
# NodePath, even if it fails. Checking the name count is the only way
# I found to check if it found something or not (after it worked I
# didn't look any farther).
path = base_node.get_path_to(path_or_node)
if(path.get_name_count() == 0):
_lgr.error('You passed an objet that base_node does not have. Cannot replace node.')
return
if(!base_node.has_node(path)):
_lgr.error(str('Could not find node at path [', path, ']'))
return
var to_replace = base_node.get_node(path)
var parent = to_replace.get_parent()
var replace_name = to_replace.get_name()
parent.remove_child(to_replace)
parent.add_child(with_this)
with_this.set_name(replace_name)
with_this.set_owner(parent)
var groups = to_replace.get_groups()
for i in range(groups.size()):
with_this.add_to_group(groups[i])
to_replace.queue_free()

View File

@ -0,0 +1,248 @@
# ------------------------------------------------------------------------------
# Used to keep track of info about each test ran.
# ------------------------------------------------------------------------------
class Test:
# indicator if it passed or not. defaults to true since it takes only
# one failure to make it not pass. _fail in gut will set this.
var passed = true
# the name of the function
var name = ""
# flag to know if the name has been printed yet.
var has_printed_name = false
# the line number the test is on
var line_number = -1
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class TestScript:
var inner_class_name = null
var tests = []
var path = null
var _utils = null
var _lgr = null
func _init(utils=null, logger=null):
_utils = utils
_lgr = logger
func to_s():
var to_return = path
if(inner_class_name != null):
to_return += str('.', inner_class_name)
to_return += "\n"
for i in range(tests.size()):
to_return += str(' ', tests[i].name, "\n")
return to_return
func get_new():
var TheScript = load(path)
var inst = null
if(inner_class_name != null):
inst = TheScript.get(inner_class_name).new()
else:
inst = TheScript.new()
return inst
func get_full_name():
var to_return = path
if(inner_class_name != null):
to_return += '.' + inner_class_name
return to_return
func get_filename():
return path.get_file()
func has_inner_class():
return inner_class_name != null
func export_to(config_file, section):
config_file.set_value(section, 'path', path)
config_file.set_value(section, 'inner_class', inner_class_name)
var names = []
for i in range(tests.size()):
names.append(tests[i].name)
config_file.set_value(section, 'tests', names)
func _remap_path(path):
var to_return = path
if(!_utils.file_exists(path)):
_lgr.debug('Checking for remap for: ' + path)
var remap_path = path.get_basename() + '.gd.remap'
if(_utils.file_exists(remap_path)):
var cf = ConfigFile.new()
cf.load(remap_path)
to_return = cf.get_value('remap', 'path')
else:
_lgr.warn('Could not find remap file ' + remap_path)
return to_return
func import_from(config_file, section):
path = config_file.get_value(section, 'path')
path = _remap_path(path)
var test_names = config_file.get_value(section, 'tests')
for i in range(test_names.size()):
var t = Test.new()
t.name = test_names[i]
tests.append(t)
# Null is an acceptable value, but you can't pass null as a default to
# get_value since it thinks you didn't send a default...then it spits
# out red text. This works around that.
var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder')
if(inner_name != 'Placeholder'):
inner_class_name = inner_name
else: # just being explicit
inner_class_name = null
# ------------------------------------------------------------------------------
# start test_collector, I don't think I like the name.
# ------------------------------------------------------------------------------
var scripts = []
var _test_prefix = 'test_'
var _test_class_prefix = 'Test'
var _utils = load('res://addons/gut/utils.gd').new()
var _lgr = _utils.get_logger()
func _parse_script(script):
var file = File.new()
var line = ""
var line_count = 0
var inner_classes = []
var scripts_found = []
file.open(script.path, 1)
while(!file.eof_reached()):
line_count += 1
line = file.get_line()
#Add a test
if(line.begins_with("func " + _test_prefix)):
var from = line.find(_test_prefix)
var line_len = line.find("(") - from
var new_test = Test.new()
new_test.name = line.substr(from, line_len)
new_test.line_number = line_count
script.tests.append(new_test)
if(line.begins_with('class ')):
var iclass_name = line.replace('class ', '')
iclass_name = iclass_name.replace(':', '')
if(iclass_name.begins_with(_test_class_prefix)):
inner_classes.append(iclass_name)
scripts_found.append(script.path)
for i in range(inner_classes.size()):
var ts = TestScript.new(_utils, _lgr)
ts.path = script.path
ts.inner_class_name = inner_classes[i]
if(_parse_inner_class_tests(ts)):
scripts.append(ts)
scripts_found.append(script.path + '[' + inner_classes[i] +']')
file.close()
return scripts_found
func _parse_inner_class_tests(script):
var inst = script.get_new()
if(!inst is _utils.Test):
_lgr.warn('Ignoring ' + script.inner_class_name + ' because it starts with "' + _test_class_prefix + '" but does not extend addons/gut/test.gd')
return false
var methods = inst.get_method_list()
for i in range(methods.size()):
var name = methods[i]['name']
if(name.begins_with(_test_prefix) and methods[i]['flags'] == 65):
var t = Test.new()
t.name = name
script.tests.append(t)
return true
# -----------------
# Public
# -----------------
func add_script(path):
# SHORTCIRCUIT
if(has_script(path)):
return []
var f = File.new()
# SHORTCIRCUIT
if(!f.file_exists(path)):
_lgr.error('Could not find script: ' + path)
return
var ts = TestScript.new(_utils, _lgr)
ts.path = path
scripts.append(ts)
return _parse_script(ts)
func to_s():
var to_return = ''
for i in range(scripts.size()):
to_return += scripts[i].to_s() + "\n"
return to_return
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
func get_test_prefix():
return _test_prefix
func set_test_prefix(test_prefix):
_test_prefix = test_prefix
func get_test_class_prefix():
return _test_class_prefix
func set_test_class_prefix(test_class_prefix):
_test_class_prefix = test_class_prefix
func clear():
scripts.clear()
func has_script(path):
var found = false
var idx = 0
while(idx < scripts.size() and !found):
if(scripts[idx].path == path):
found = true
else:
idx += 1
return found
func export_tests(path):
if(_utils.is_version_31()):
_lgr.error("Exporting and importing not supported in 3.1 yet. There is a workaround, check the wiki.")
return false
var success = true
var f = ConfigFile.new()
for i in range(scripts.size()):
scripts[i].export_to(f, str('TestScript-', i))
var result = f.save(path)
if(result != OK):
_lgr.error(str('Could not save exported tests to [', path, ']. Error code: ', result))
success = false
return success
func import_tests(path):
if(_utils.is_version_31()):
_lgr.error("Exporting and importing not supported in 3.1 yet. There is a workaround, check the wiki.")
return false
var success = false
var f = ConfigFile.new()
var result = f.load(path)
if(result != OK):
_lgr.error(str('Could not load exported tests from [', path, ']. Error code: ', result))
else:
var sections = f.get_sections()
for key in sections:
var ts = TestScript.new(_utils, _lgr)
ts.import_from(f, key)
scripts.append(ts)
success = true
return success

View File

@ -0,0 +1,43 @@
var things = {}
func get_unique_count():
return things.size()
func add(thing):
if(things.has(thing)):
things[thing] += 1
else:
things[thing] = 1
func has(thing):
return things.has(thing)
func get(thing):
var to_return = 0
if(things.has(thing)):
to_return = things[thing]
return to_return
func sum():
var count = 0
for key in things:
count += things[key]
return count
func to_s():
var to_return = ""
for key in things:
to_return += str(key, ": ", things[key], "\n")
to_return += str("sum: ", sum())
return to_return
func get_max_count():
var max_val = null
for key in things:
if(max_val == null or things[key] > max_val):
max_val = things[key]
return max_val
func add_array_items(array):
for i in range(array.size()):
add(array[i])

View File

@ -0,0 +1,108 @@
var _Logger = load('res://addons/gut/logger.gd') # everything should use get_logger
var Doubler = load('res://addons/gut/doubler.gd')
var MethodMaker = load('res://addons/gut/method_maker.gd')
var Spy = load('res://addons/gut/spy.gd')
var Stubber = load('res://addons/gut/stubber.gd')
var StubParams = load('res://addons/gut/stub_params.gd')
var Summary = load('res://addons/gut/summary.gd')
var Test = load('res://addons/gut/test.gd')
var TestCollector = load('res://addons/gut/test_collector.gd')
var ThingCounter = load('res://addons/gut/thing_counter.gd')
const GUT_METADATA = '__gut_metadata_'
enum DOUBLE_STRATEGY{
FULL,
PARTIAL
}
var _file_checker = File.new()
func is_version_30():
var info = Engine.get_version_info()
return info.major == 3 and info.minor == 0
func is_version_31():
var info = Engine.get_version_info()
return info.major == 3 and info.minor == 1
# ------------------------------------------------------------------------------
# Everything should get a logger through this.
#
# Eventually I want to make this get a single instance of a logger but I'm not
# sure how to do that without everything having to be in the tree which I
# DO NOT want to to do. I'm thinking of writings some instance ids to a file
# and loading them in the _init for this.
# ------------------------------------------------------------------------------
func get_logger():
return _Logger.new()
# ------------------------------------------------------------------------------
# Returns an array created by splitting the string by the delimiter
# ------------------------------------------------------------------------------
func split_string(to_split, delim):
var to_return = []
var loc = to_split.find(delim)
while(loc != -1):
to_return.append(to_split.substr(0, loc))
to_split = to_split.substr(loc + 1, to_split.length() - loc)
loc = to_split.find(delim)
to_return.append(to_split)
return to_return
# ------------------------------------------------------------------------------
# Returns a string containing all the elements in the array seperated by delim
# ------------------------------------------------------------------------------
func join_array(a, delim):
var to_return = ''
for i in range(a.size()):
to_return += str(a[i])
if(i != a.size() -1):
to_return += str(delim)
return to_return
# ------------------------------------------------------------------------------
# return if_null if value is null otherwise return value
# ------------------------------------------------------------------------------
func nvl(value, if_null):
if(value == null):
return if_null
else:
return value
# ------------------------------------------------------------------------------
# returns true if the object has been freed, false if not
#
# From what i've read, the weakref approach should work. It seems to work most
# of the time but sometimes it does not catch it. The str comparison seems to
# fill in the gaps. I've not seen any errors after adding that check.
# ------------------------------------------------------------------------------
func is_freed(obj):
var wr = weakref(obj)
return !(wr.get_ref() and str(obj) != '[Deleted Object]')
func is_not_freed(obj):
return !is_freed(obj)
func is_double(obj):
return obj.get(GUT_METADATA) != null
func extract_property_from_array(source, property):
var to_return = []
for i in (source.size()):
to_return.append(source[i].get(property))
return to_return
func file_exists(path):
return _file_checker.file_exists(path)
func write_file(path, content):
var f = File.new()
f.open(path, f.WRITE)
f.store_string(content)
f.close()
func is_null_or_empty(text):
return text == null or text == ''

View File

@ -0,0 +1,13 @@
[gd_resource type="Environment" load_steps=2 format=2]
[sub_resource type="ProceduralSky" id=1]
sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 )
sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 )
sky_curve = 0.25
ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 )
ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 )
ground_curve = 0.01
[resource]
background_mode = 2
background_sky = SubResource( 1 )

View File

@ -0,0 +1,78 @@
# Script to attach to a node which represents a hex grid
extends Node2D
var HexGrid = preload("./HexGrid.gd").new()
var HexShape = preload("./HexShape.tscn")
onready var highlight = get_node("Highlight")
onready var area_coords = get_node("Highlight/AreaCoords")
onready var hex_coords = get_node("Highlight/HexCoords")
var rect_min = Vector2(-200, 50)
var rect_max = Vector2(400, 200)
var drag_start = null
func _ready():
HexGrid.hex_scale = Vector2(50, 50)
func _draw():
var hex_cell_bb_min = HexGrid.get_hex_at(rect_min).offset_coords
var hex_cell_bb_max = HexGrid.get_hex_at(rect_max).offset_coords
var hex_cell_offset_min = Vector2(
min(hex_cell_bb_min.x, hex_cell_bb_max.x),
min(hex_cell_bb_min.y, hex_cell_bb_max.y)
)
var hex_cell_offset_max = Vector2(
max(hex_cell_bb_min.x, hex_cell_bb_max.x),
max(hex_cell_bb_min.y, hex_cell_bb_max.y)
)
var rect_highlights = get_node ("rect_highlights")
if rect_highlights:
remove_child(rect_highlights)
rect_highlights.queue_free()
rect_highlights = Node2D.new()
rect_highlights.name = "rect_highlights"
add_child(rect_highlights)
for xi in range (hex_cell_offset_min.x, hex_cell_offset_max.x + 1):
for yi in range (hex_cell_offset_min.y, hex_cell_offset_max.y + 1):
var offset_coords = Vector2(xi, yi)
var hex_shape = HexShape.instance()
hex_shape.position = HexGrid.get_hex_center_from_offset(offset_coords)
var hex_shape_coords = hex_shape.get_node("HexCoords")
hex_shape_coords.text = str(offset_coords)
rect_highlights.add_child (hex_shape)
var rect = Rect2(rect_min, rect_max - rect_min)
draw_rect (rect, Color("55FFFFFF"))
func _unhandled_input(event):
if 'position' in event:
var relative_pos = self.transform.affine_inverse() * event.position
# Display the coords used
if area_coords != null:
area_coords.text = str(relative_pos)
if hex_coords != null:
hex_coords.text = str(HexGrid.get_hex_at(relative_pos).axial_coords)
# Snap the highlight to the nearest grid cell
if highlight != null:
highlight.position = HexGrid.get_hex_center(HexGrid.get_hex_at(relative_pos))
if event is InputEventMouseButton:
if event.button_index == BUTTON_LEFT:
if event.pressed:
drag_start = relative_pos
else:
drag_start = null
if drag_start != null:
rect_min = Vector2(min(drag_start.x, relative_pos.x), min(drag_start.y, relative_pos.y))
rect_max = Vector2(max(drag_start.x, relative_pos.x), max(drag_start.y, relative_pos.y))
update()

View File

@ -0,0 +1,47 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://demo_2d.gd" type="Script" id=1]
[sub_resource type="RectangleShape2D" id=1]
extents = Vector2( 512, 300 )
[node name="2D Demo" type="Node"]
[node name="Area2D" type="Area2D" parent="."]
position = Vector2( 512, 300 )
script = ExtResource( 1 )
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
position = Vector2( 0.237854, 0 )
shape = SubResource( 1 )
[node name="Highlight" type="Polygon2D" parent="Area2D"]
polygon = PoolVector2Array( -12.5, 21.6506, 12.5, 21.6506, 25, 0, 12.5, -21.6506, -12.5, -21.6506, -25, 0 )
[node name="Label" type="Label" parent="Area2D/Highlight"]
margin_left = 5.0
margin_top = -39.0
margin_right = 52.0
margin_bottom = -25.0
text = "SCREEN"
[node name="AreaCoords" type="Label" parent="Area2D/Highlight"]
margin_left = 55.0
margin_top = -39.0
margin_right = 105.0
margin_bottom = -25.0
text = "SCREEN"
[node name="Label2" type="Label" parent="Area2D/Highlight"]
margin_left = 25.0
margin_top = -19.0
margin_right = 56.0
margin_bottom = -5.0
text = "HEX"
[node name="HexCoords" type="Label" parent="Area2D/Highlight"]
margin_left = 55.0
margin_top = -19.0
margin_right = 105.0
margin_bottom = -5.0
text = "HEX"

View File

@ -0,0 +1,25 @@
# Script to attach to a node which represents a hex grid
extends Spatial
var HexGrid = preload("./HexGrid.gd").new()
onready var highlight = get_node("Highlight")
onready var plane_coords_label = get_node("Highlight/Viewport/PlaneCoords")
onready var hex_coords_label = get_node("Highlight/Viewport/HexCoords")
func _on_HexGrid_input_event(_camera, _event, click_position, _click_normal, _shape_idx):
# It's called click_position, but you don't need to click
var plane_coords = self.transform.affine_inverse() * click_position
plane_coords = Vector2(plane_coords.x, plane_coords.z)
# Display the coords used
if plane_coords_label != null:
plane_coords_label.text = str(plane_coords)
if hex_coords_label != null:
hex_coords_label.text = str(HexGrid.get_hex_at(plane_coords).axial_coords)
# Snap the highlight to the nearest grid cell
if highlight != null:
var plane_pos = HexGrid.get_hex_center(HexGrid.get_hex_at(plane_coords))
highlight.translation.x = plane_pos.x
highlight.translation.z = plane_pos.y

View File

@ -0,0 +1,166 @@
[gd_scene load_steps=13 format=2]
[ext_resource path="res://demo_3d.gd" type="Script" id=1]
[sub_resource type="BoxShape" id=1]
extents = Vector3( 4, 0.1, 4 )
[sub_resource type="CubeMesh" id=2]
size = Vector3( 8, 0.2, 8 )
[sub_resource type="CylinderMesh" id=3]
top_radius = 0.5
bottom_radius = 0.5
height = 0.1
radial_segments = 6
rings = 1
[sub_resource type="SpatialMaterial" id=4]
roughness = 0.0
[sub_resource type="QuadMesh" id=5]
size = Vector2( 2, 1 )
[sub_resource type="ViewportTexture" id=6]
viewport_path = NodePath("HexGrid/Highlight/Viewport")
[sub_resource type="SpatialMaterial" id=7]
resource_local_to_scene = true
flags_transparent = true
albedo_texture = SubResource( 6 )
[sub_resource type="BoxShape" id=8]
extents = Vector3( 4, 0.1, 2 )
[sub_resource type="CubeMesh" id=9]
size = Vector3( 8, 0.2, 4 )
[sub_resource type="ViewportTexture" id=10]
viewport_path = NodePath("HexGrid2/Highlight/Viewport")
[sub_resource type="SpatialMaterial" id=11]
resource_local_to_scene = true
flags_transparent = true
albedo_texture = SubResource( 10 )
roughness = 0.0
[node name="Spatial" type="Spatial"]
[node name="Camera" type="Camera" parent="."]
transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 5 )
[node name="HexGrid" type="StaticBody" parent="."]
script = ExtResource( 1 )
[node name="CollisionShape" type="CollisionShape" parent="HexGrid"]
shape = SubResource( 1 )
[node name="MeshInstance" type="MeshInstance" parent="HexGrid"]
mesh = SubResource( 2 )
material/0 = null
[node name="Highlight" type="MeshInstance" parent="HexGrid"]
transform = Transform( -1.62921e-07, 0, 1, 0, 1, 0, -1, 0, -1.62921e-07, 0, 0.2, 0 )
mesh = SubResource( 3 )
material/0 = SubResource( 4 )
[node name="Viewport" type="Viewport" parent="HexGrid/Highlight"]
size = Vector2( 200, 100 )
transparent_bg = true
hdr = false
usage = 0
render_target_v_flip = true
[node name="Label" type="Label" parent="HexGrid/Highlight/Viewport"]
margin_right = 41.0
margin_bottom = 14.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "PLANE"
[node name="PlaneCoords" type="Label" parent="HexGrid/Highlight/Viewport"]
margin_left = 50.0
margin_right = 97.0
margin_bottom = 14.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "PLANE"
[node name="Label2" type="Label" parent="HexGrid/Highlight/Viewport"]
margin_left = 18.0
margin_top = 20.0
margin_right = 58.0
margin_bottom = 34.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "HEX"
[node name="HexCoords" type="Label" parent="HexGrid/Highlight/Viewport"]
margin_left = 50.0
margin_top = 20.0
margin_right = 90.0
margin_bottom = 34.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "HEX"
[node name="LabelQuad" type="MeshInstance" parent="HexGrid/Highlight"]
transform = Transform( -2.8213e-07, 0, -1, 0, 1, 0, 1, 0, -2.8213e-07, 0, 0.3, 0.5 )
mesh = SubResource( 5 )
material/0 = SubResource( 7 )
[node name="HexGrid2" type="StaticBody" parent="."]
transform = Transform( 1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 3, -2 )
script = ExtResource( 1 )
[node name="CollisionShape" type="CollisionShape" parent="HexGrid2"]
shape = SubResource( 8 )
[node name="MeshInstance" type="MeshInstance" parent="HexGrid2"]
mesh = SubResource( 9 )
material/0 = null
[node name="Highlight" type="MeshInstance" parent="HexGrid2"]
transform = Transform( -1.62921e-07, 0, 1, 0, 1, 0, -1, 0, -1.62921e-07, 0, 0.2, 0 )
mesh = SubResource( 3 )
material/0 = SubResource( 4 )
[node name="Viewport" type="Viewport" parent="HexGrid2/Highlight"]
size = Vector2( 200, 100 )
transparent_bg = true
hdr = false
usage = 0
render_target_v_flip = true
[node name="Label" type="Label" parent="HexGrid2/Highlight/Viewport"]
margin_right = 40.0
margin_bottom = 14.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "PLANE"
[node name="PlaneCoords" type="Label" parent="HexGrid2/Highlight/Viewport"]
margin_left = 50.0
margin_right = 97.0
margin_bottom = 14.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "PLANE"
[node name="Label2" type="Label" parent="HexGrid2/Highlight/Viewport"]
margin_left = 18.0
margin_top = 20.0
margin_right = 58.0
margin_bottom = 34.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "HEX"
[node name="HexCoords" type="Label" parent="HexGrid2/Highlight/Viewport"]
margin_left = 50.0
margin_top = 20.0
margin_right = 90.0
margin_bottom = 34.0
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "HEX"
[node name="LabelQuad" type="MeshInstance" parent="HexGrid2/Highlight"]
transform = Transform( -1.62921e-07, 1, 4.37114e-08, 0, -4.37114e-08, 1, 1, 1.62921e-07, 7.1215e-15, 0, 0.3, 0.5 )
mesh = SubResource( 5 )
material/0 = SubResource( 11 )
[connection signal="input_event" from="HexGrid" to="HexGrid" method="_on_HexGrid_input_event"]
[connection signal="input_event" from="HexGrid2" to="HexGrid2" method="_on_HexGrid_input_event"]

BIN
addons/gdhexgrid/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/icon.png-9e2c68854eff2ba15efab32e39b37c44.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gdhexgrid/icon.png"
dest_files=[ "res://.import/icon.png-9e2c68854eff2ba15efab32e39b37c44.stex" ]
[params]
compress/mode=0
compress/lossy_quality=0.7
compress/hdr_mode=0
compress/bptc_ldr=0
compress/normal_map=0
flags/repeat=0
flags/filter=true
flags/mipmaps=false
flags/anisotropic=false
flags/srgb=2
process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true
svg/scale=1.0

View File

@ -0,0 +1,25 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://addons/gut/gut.gd" type="Script" id=1]
[ext_resource path="res://addons/gut/icon.png" type="Texture" id=2]
[node name="Node" type="Node"]
[node name="Gut" type="WindowDialog" parent="."]
visible = true
self_modulate = Color( 1, 1, 1, 0 )
margin_right = 851.0
margin_bottom = 496.0
rect_min_size = Vector2( 740, 250 )
script = ExtResource( 1 )
__meta__ = {
"_editor_icon": ExtResource( 2 )
}
_select_script = null
_tests_like = null
_run_on_load = true
_yield_between_tests = false
_directory1 = "res://test/unit"
_directory2 = "res://test/integration"
_double_strategy = 1

View File

@ -0,0 +1,266 @@
extends "res://addons/gut/test.gd"
class TestNew:
extends "res://addons/gut/test.gd"
var HexCell = load("res://HexCell.gd")
var cell
func setup():
cell = null
func test_null():
cell = HexCell.new()
assert_eq(cell.axial_coords, Vector2(0, 0))
func test_cube():
cell = HexCell.new(Vector3(1, 1, -2))
assert_eq(cell.axial_coords, Vector2(1, 1))
func test_axial():
cell = HexCell.new(Vector2(1, -1))
assert_eq(cell.axial_coords, Vector2(1, -1))
func test_instance():
var test_cell = HexCell.new(Vector3(-1, 2, -1))
cell = HexCell.new(test_cell)
assert_eq(cell.axial_coords, Vector2(-1, 2))
class TestConversions:
extends "res://addons/gut/test.gd"
var HexCell = load("res://HexCell.gd")
var cell
func setup():
cell = HexCell.new()
func test_axial_to_cube():
assert_eq(cell.axial_to_cube_coords(Vector2(2, 1)), Vector3(2, 1, -3))
assert_eq(cell.axial_to_cube_coords(Vector2(-1, -1)), Vector3(-1, -1, 2))
func test_rounding():
assert_eq(cell.round_coords(Vector3(0.1, 0.5, -0.6)), Vector3(0, 1, -1))
assert_eq(cell.round_coords(Vector3(-0.4, -1.3, 1.7)), Vector3(-1, -1, 2))
assert_eq(cell.round_coords(Vector2(-0.1, 0.6)), Vector3(0, 1, -1))
assert_eq(cell.round_coords(Vector2(4.2, -5.5)), Vector3(4, -5, 1))
class TestCoords:
extends "res://addons/gut/test.gd"
var HexCell = load("res://HexCell.gd")
var cell
func setup():
cell = HexCell.new()
func test_from_cubic_positive():
cell.cube_coords = Vector3(2, 1, -3)
assert_eq(cell.cube_coords, Vector3(2, 1, -3))
assert_eq(cell.axial_coords, Vector2(2, 1))
assert_eq(cell.offset_coords, Vector2(2, 2))
func test_from_cubic_negative():
cell.cube_coords = Vector3(-1, -1, 2)
assert_eq(cell.cube_coords, Vector3(-1, -1, 2))
assert_eq(cell.axial_coords, Vector2(-1, -1))
assert_eq(cell.offset_coords, Vector2(-1, -2))
func test_from_cubic_invalid():
cell.cube_coords = Vector3(1, 2, 3)
assert_eq(cell.cube_coords, Vector3(0, 0, 0))
func test_from_axial_positive():
cell.axial_coords = Vector2(2, 1)
assert_eq(cell.cube_coords, Vector3(2, 1, -3))
assert_eq(cell.axial_coords, Vector2(2, 1))
assert_eq(cell.offset_coords, Vector2(2, 2))
func test_from_axial_negative():
cell.axial_coords = Vector2(-1, -1)
assert_eq(cell.cube_coords, Vector3(-1, -1, 2))
assert_eq(cell.axial_coords, Vector2(-1, -1))
assert_eq(cell.offset_coords, Vector2(-1, -2))
func test_from_offset_positive():
cell.offset_coords = Vector2(2, 2)
assert_eq(cell.cube_coords, Vector3(2, 1, -3))
assert_eq(cell.axial_coords, Vector2(2, 1))
assert_eq(cell.offset_coords, Vector2(2, 2))
func test_from_offset_negative():
cell.offset_coords = Vector2(-1, -2)
assert_eq(cell.cube_coords, Vector3(-1, -1, 2))
assert_eq(cell.axial_coords, Vector2(-1, -1))
assert_eq(cell.offset_coords, Vector2(-1, -2))
class TestNearby:
extends "res://addons/gut/test.gd"
var HexCell = load("res://HexCell.gd")
var cell
func setup():
cell = HexCell.new(Vector2(1, 2))
func check_expected(cells, expected):
# Check that a bunch of cells are what were expected
assert_eq(cells.size(), expected.size())
for hex in cells:
assert_has(expected, hex.axial_coords)
func test_adjacent():
var foo = cell.get_adjacent(HexCell.DIR_N)
assert_eq(foo.axial_coords, Vector2(1, 3))
foo = cell.get_adjacent(HexCell.DIR_NE)
assert_eq(foo.axial_coords, Vector2(2, 2))
foo = cell.get_adjacent(HexCell.DIR_SE)
assert_eq(foo.axial_coords, Vector2(2, 1))
foo = cell.get_adjacent(HexCell.DIR_S)
assert_eq(foo.axial_coords, Vector2(1, 1))
foo = cell.get_adjacent(HexCell.DIR_SW)
assert_eq(foo.axial_coords, Vector2(0, 2))
foo = cell.get_adjacent(HexCell.DIR_NW)
assert_eq(foo.axial_coords, Vector2(0, 3))
func test_not_really_adjacent():
var foo = cell.get_adjacent(Vector3(-3, -3, 6))
assert_eq(foo.axial_coords, Vector2(-2, -1))
func test_adjacent_axial():
var foo = cell.get_adjacent(Vector2(1, 1))
assert_eq(foo.axial_coords, Vector2(2, 3))
func test_all_adjacent():
var coords = []
for foo in cell.get_all_adjacent():
coords.append(foo.axial_coords)
assert_has(coords, Vector2(1, 3))
assert_has(coords, Vector2(2, 2))
assert_has(coords, Vector2(2, 1))
assert_has(coords, Vector2(1, 1))
assert_has(coords, Vector2(0, 2))
assert_has(coords, Vector2(0, 3))
func test_all_within_0():
var expected = [
Vector2(1, 2),
]
var cells = cell.get_all_within(0)
check_expected(cells, expected)
func test_all_within_1():
var expected = [
Vector2(1, 2),
Vector2(1, 3),
Vector2(2, 2),
Vector2(2, 1),
Vector2(1, 1),
Vector2(0, 2),
Vector2(0, 3),
]
var cells = cell.get_all_within(1)
check_expected(cells, expected)
func test_all_within_2():
var expected = [
Vector2(-1, 4), Vector2(0, 4), Vector2(1, 4),
Vector2(-1, 3), Vector2(0, 3), Vector2(1, 3), Vector2(2, 3),
Vector2(-1, 2), Vector2(0, 2),
Vector2(1, 2),
Vector2(2, 2), Vector2(3, 2),
Vector2(0, 1), Vector2(1, 1), Vector2(2, 1), Vector2(3, 1),
Vector2(1, 0), Vector2(2, 0), Vector2(3, 0),
]
var cells = cell.get_all_within(2)
check_expected(cells, expected)
func test_ring_0():
var expected = [
Vector2(1, 2),
]
var cells = cell.get_ring(0)
check_expected(cells, expected)
func test_ring_1():
var expected = [
Vector2(1, 3),
Vector2(2, 2),
Vector2(2, 1),
Vector2(1, 1),
Vector2(0, 2),
Vector2(0, 3),
]
var cells = cell.get_ring(1)
check_expected(cells, expected)
func test_ring_2():
var expected = [
Vector2(1, 4), # Start at +2y
Vector2(2, 3), Vector2(3, 2), # SE
Vector2(3, 1), Vector2(3, 0), # S
Vector2(2, 0), Vector2(1, 0), # SW
Vector2(0, 1), Vector2(-1, 2), # NW
Vector2(-1, 3), Vector2(-1, 4), # N
Vector2(0, 4), # NE
]
var cells = cell.get_ring(2)
check_expected(cells, expected)
class TestBetweenTwo:
extends "res://addons/gut/test.gd"
var HexCell = load("res://HexCell.gd")
var cell
func setup():
cell = HexCell.new(Vector2(1, 2))
func test_distance():
assert_eq(cell.distance_to(Vector2(0, 0)), 3)
assert_eq(cell.distance_to(Vector2(3, 4)), 4)
assert_eq(cell.distance_to(Vector2(-1, -1)), 5)
func test_line_straight():
# Straight line, nice and simple
var expected = [
Vector2(1, 2),
Vector2(2, 2),
Vector2(3, 2),
Vector2(4, 2),
Vector2(5, 2),
]
var path = cell.line_to(Vector2(5, 2))
assert_eq(path.size(), expected.size())
for idx in range(expected.size()):
assert_eq(path[idx].axial_coords, expected[idx])
func test_line_angled():
# It's gone all wibbly-wobbly
var expected = [
Vector2(1, 2),
Vector2(2, 2),
Vector2(2, 3),
Vector2(3, 3),
Vector2(4, 3),
Vector2(4, 4),
Vector2(5, 4),
]
var path = cell.line_to(Vector2(5, 4))
assert_eq(path.size(), expected.size())
for idx in range(expected.size()):
assert_eq(path[idx].axial_coords, expected[idx])
func test_line_edge():
# Living on the edge between two hexes
var expected = [
Vector2(1, 2),
Vector2(1, 3),
Vector2(2, 3),
Vector2(2, 4),
Vector2(3, 4),
]
var path = cell.line_to(Vector2(3, 4))
assert_eq(path.size(), expected.size())
for idx in range(expected.size()):
assert_eq(path[idx].axial_coords, expected[idx])

View File

@ -0,0 +1,97 @@
extends "res://addons/gut/test.gd"
var HexCell = load("res://HexCell.gd")
var HexGrid = load("res://HexGrid.gd")
var cell
var grid
var w
var h
func setup():
cell = HexCell.new()
grid = HexGrid.new()
w = grid.hex_size.x
h = grid.hex_size.y
func test_hex_to_projection():
var tests = {
# Remember, projection +y => S
Vector2(0, 0): Vector2(0, 0),
Vector2(0, 1): Vector2(0, -h),
Vector2(1, 0): Vector2(w*0.75, -h/2),
Vector2(-4, -3): Vector2(4 * (-w*0.75), (3 * h) + (4 * h / 2)),
}
for hex in tests:
assert_eq(tests[hex], grid.get_hex_center(hex))
func test_hex_to_projection_scaled():
grid.set_hex_scale(Vector2(2, 2))
var tests = {
Vector2(0, 0): Vector2(0, 0),
Vector2(0, 1): 2 * Vector2(0, -h),
Vector2(1, 0): 2 * Vector2(w * 0.75, -h/2),
Vector2(-4, -3): 2 * Vector2(4 * (-w * 0.75), (3 * h) + (4 * h / 2)),
}
for hex in tests:
assert_eq(tests[hex], grid.get_hex_center(hex))
func test_hex_to_projection_squished():
grid.set_hex_scale(Vector2(2, 1))
var tests = {
Vector2(0, 0): Vector2(0, 0),
Vector2(0, 1): Vector2(0, -h),
Vector2(1, 0): Vector2(2 * w * 0.75, -h/2),
Vector2(-4, -3): Vector2(2 * 4 * (-w * 0.75), (3 * h) + (4 * h / 2)),
}
for hex in tests:
assert_eq(tests[hex], grid.get_hex_center(hex))
func test_hex_to_3d_projection():
var tests = {
Vector2(0, 0): Vector3(0, 0, 0),
Vector2(0, 1): Vector3(0, 0, -h),
Vector2(1, 0): Vector3(w*0.75, 0, -h/2),
Vector2(-4, -3): Vector3(4 * (-w*0.75), 0, (3 * h) + (4 * h / 2)),
}
for hex in tests:
assert_eq(tests[hex], grid.get_hex_center3(hex))
# Also test the second parameter
assert_eq(
Vector3(0, 1.2, 0),
grid.get_hex_center3(Vector2(0, 0), 1.2)
)
func test_projection_to_hex():
var tests = {
Vector2(0, 0): Vector2(0, 0),
Vector2(w / 2 - 0.01, 0): Vector2(0, 0),
Vector2(w / 2 - 0.01, h / 2): Vector2(1, -1),
Vector2(w / 2 - 0.01, -h / 2): Vector2(1, 0),
Vector2(0, h): Vector2(0, -1),
Vector2(-w - 0.01, 0): Vector2(-2, 1),
Vector2(-w, 0.01): Vector2(-1, 0),
Vector2(-w, -0.01): Vector2(-1, 1),
# Also Vector3s are valid input
Vector3(0, 0, 0): Vector2(0, 0),
Vector3(w / 2 - 0.01, 12, h / 2): Vector2(1, -1),
}
for coords in tests:
assert_eq(tests[coords], grid.get_hex_at(coords).axial_coords)
func test_projection_to_hex_doublesquished():
grid.set_hex_scale(Vector2(4, 2))
var tests = {
Vector2(0, 0): Vector2(0, 0),
Vector2(4 * w / 2 - 0.01, 0): Vector2(0, 0),
Vector2(4 * w / 2 - 0.01, h / 2): Vector2(1, -1),
Vector2(4 * w / 2 - 0.01, -h / 2): Vector2(1, 0),
Vector2(0, 2 * h): Vector2(0, -1),
Vector2(4 * -w - 0.01, 0): Vector2(-2, 1),
Vector2(4 * -w, 0.01): Vector2(-1, 0),
Vector2(4 * -w, -0.01): Vector2(-1, 1),
}
for coords in tests:
assert_eq(tests[coords], grid.get_hex_at(coords).axial_coords)

View File

@ -0,0 +1,331 @@
extends "res://addons/gut/test.gd"
var HexCell = load("res://HexCell.gd")
var HexGrid = load("res://HexGrid.gd")
var grid
var map
# This is the hex map we'll test with:
# remember: +y is N, +x is NE
"""
.
.
. .
. .
. . .
. . .
. . . .
. O B .
OF O . C
. E O .
O O D
. O .
. .
. A
G
. <- (0, 0)
"""
var a_pos = Vector2(2, 0)
var b_pos = Vector2(4, 2)
var c_pos = Vector2(7, 0)
var d_pos = Vector2(5, 0)
var e_pos = Vector2(2, 2)
var f_pos = Vector2(1, 3)
var g_pos = Vector2(1, 0)
var obstacles = [
Vector2(2, 1),
Vector2(3, 1),
Vector2(4, 1),
Vector2(1, 2),
Vector2(3, 2),
Vector2(1, 3),
Vector2(2, 3),
]
func setup():
grid = HexGrid.new()
grid.set_bounds(Vector2(0, 0), Vector2(7, 4))
grid.add_obstacles(obstacles)
func test_bounds():
# Push the boundaries
# Check that the test boundary works properly
assert_eq(grid.get_hex_cost(Vector2(0, 0)), grid.path_cost_default, "SW is open")
assert_eq(grid.get_hex_cost(Vector2(0, 4)), grid.path_cost_default, "W is open")
assert_eq(grid.get_hex_cost(Vector2(7, 0)), grid.path_cost_default, "E is open")
assert_eq(grid.get_hex_cost(Vector2(7, 4)), grid.path_cost_default, "NE is open")
assert_eq(grid.get_hex_cost(Vector2(8, 2)), 0, "Too much X is blocked")
assert_eq(grid.get_hex_cost(Vector2(6, 5)), 0, "Too much Y is blocked")
assert_eq(grid.get_hex_cost(Vector2(-1, 2)), 0, "Too little X is blocked")
assert_eq(grid.get_hex_cost(Vector2(6, -1)), 0, "Too little Y is blocked")
func test_negative_bounds():
# Test negative space
grid = HexGrid.new()
grid.set_bounds(Vector2(-5, -5), Vector2(-2, -2))
assert_eq(grid.get_hex_cost(Vector2(-2, -2)), grid.path_cost_default)
assert_eq(grid.get_hex_cost(Vector2(-5, -5)), grid.path_cost_default)
assert_eq(grid.get_hex_cost(Vector2(0, 0)), 0)
assert_eq(grid.get_hex_cost(Vector2(-6, -3)), 0)
assert_eq(grid.get_hex_cost(Vector2(-3, -1)), 0)
func test_roundabounds():
# We can also go both ways
grid.set_bounds(Vector2(-3, -3), Vector2(2, 2))
assert_eq(grid.get_hex_cost(Vector2(-3, -3)), grid.path_cost_default)
assert_eq(grid.get_hex_cost(Vector2(2, 2)), grid.path_cost_default)
assert_eq(grid.get_hex_cost(Vector2(0, 0)), grid.path_cost_default)
assert_eq(grid.get_hex_cost(Vector2(-4, 0)), 0)
assert_eq(grid.get_hex_cost(Vector2(0, 3)), 0)
func test_grid_obstacles():
# Make sure we can obstacleize the grid
assert_eq(grid.get_obstacles().size(), obstacles.size())
# Test adding via a HexCell instance
grid.add_obstacles(HexCell.new(Vector2(0, 0)))
assert_eq(grid.get_obstacles()[Vector2(0, 0)], 0)
# Test replacing an obstacle
grid.add_obstacles(Vector2(0, 0), 2)
assert_eq(grid.get_obstacles()[Vector2(0, 0)], 2)
# Test removing an obstacle
grid.remove_obstacles(Vector2(0, 0))
assert_does_not_have(grid.get_obstacles(), Vector2(0, 0))
# Make sure removing a non-obstacle doesn't error
grid.remove_obstacles(Vector2(0, 0))
func test_grid_barriers():
# Make sure we can barrier things on the grid
assert_eq(grid.get_barriers().size(), 0)
# Add a barrier
var coords = Vector2(0, 0)
var barriers = grid.get_barriers()
grid.add_barriers(coords, HexCell.DIR_N)
assert_eq(barriers.size(), 1)
assert_has(barriers, coords)
assert_eq(barriers[coords].size(), 1)
assert_has(barriers[coords], HexCell.DIR_N)
# Overwrite the barrier
grid.add_barriers(coords, HexCell.DIR_N, 1337)
assert_eq(barriers[coords][HexCell.DIR_N], 1337)
# Add more barrier to the hex
grid.add_barriers(coords, [HexCell.DIR_S, HexCell.DIR_NE])
assert_eq(barriers[coords].size(), 3)
assert_has(barriers[coords], HexCell.DIR_N)
# Remove part of the hex's barrier
grid.remove_barriers(coords, [HexCell.DIR_N])
assert_eq(barriers[coords].size(), 2)
assert_does_not_have(barriers[coords], HexCell.DIR_N)
assert_has(barriers[coords], HexCell.DIR_S)
assert_has(barriers[coords], HexCell.DIR_NE)
# Remove all the hex's barriers
grid.remove_barriers(coords)
assert_eq(barriers.size(), 0)
# Remove no barrier with no error
grid.remove_barriers([Vector2(1, 1), Vector2(2, 2)])
func test_hex_costs():
# Test that the price is right
assert_eq(grid.get_hex_cost(HexCell.new(Vector2(1, 1))), grid.path_cost_default, "Open hex is open")
assert_eq(grid.get_hex_cost(Vector3(2, 1, -3)), 0, "Obstacle being obstructive")
# Test partial obstacle
grid.add_obstacles(Vector2(1, 1), 1.337)
assert_eq(grid.get_hex_cost(Vector2(1, 1)), 1.337, "9")
func test_move_costs():
# Test that more than just hex costs are at work
assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), grid.path_cost_default)
func test_move_cost_barrier():
# Put up a barrier
grid.add_barriers(Vector2(0, 0), HexCell.DIR_N)
assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), 0)
func test_move_cost_barrier_backside():
# The destination has a barrier
grid.add_barriers(Vector2(0, 1), HexCell.DIR_S)
assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), 0)
func test_move_cost_cumulative():
# Test that moving adds up hex and barrier values
# But NOT from the *starting* hex!
grid.add_obstacles(Vector2(0, 0), 1)
grid.add_obstacles(Vector2(0, 1), 2)
grid.add_barriers(Vector2(0, 0), HexCell.DIR_N, 4)
grid.add_barriers(Vector2(0, 1), HexCell.DIR_S, 8)
assert_eq(grid.get_move_cost(Vector2(0, 0), HexCell.DIR_N), 14)
func check_path(got, expected):
# Assert that the gotten path was the expected route
assert_eq(got.size(), expected.size(), "Path should be as long as expected")
for idx in range(min(got.size(), expected.size())):
var hex = got[idx]
var check = expected[idx]
if typeof(check) == TYPE_ARRAY:
# In case of multiple valid paths
assert_has(check, hex.axial_coords)
else:
assert_eq(check, hex.axial_coords)
func test_straight_line():
# Path between A and C is straight
var path = [
a_pos,
Vector2(3, 0),
Vector2(4, 0),
Vector2(5, 0),
Vector2(6, 0),
c_pos,
]
check_path(grid.find_path(a_pos, c_pos), path)
func test_wonky_line():
# Path between B and C is a bit wonky
var path = [
b_pos,
[Vector2(5, 1), Vector2(5, 2)],
[Vector2(6, 0), Vector2(6, 1)],
c_pos,
]
check_path(grid.find_path(HexCell.new(b_pos), HexCell.new(c_pos)), path)
func test_obstacle():
# Path between A and B should go around the bottom
var path = [
a_pos,
Vector2(3, 0),
Vector2(4, 0),
Vector2(5, 0),
Vector2(5, 1),
b_pos,
]
check_path(grid.find_path(a_pos, b_pos), path)
func test_walls():
# Test that we can't walk through walls
var walls = [
HexCell.DIR_N,
HexCell.DIR_NE,
HexCell.DIR_SE,
HexCell.DIR_S,
# DIR_SE is the only opening
HexCell.DIR_NW,
]
grid.add_barriers(g_pos, walls)
var path = [
a_pos,
Vector2(1, 1),
Vector2(0, 1),
Vector2(0, 0),
g_pos,
]
check_path(grid.find_path(a_pos, g_pos), path)
func test_slopes():
# Test that we *can* walk through *some* walls
# A barrier which is passable, but not worth our hex
grid.add_barriers(g_pos, HexCell.DIR_NE, 3)
# A barrier which is marginally better than moving that extra hex
grid.add_barriers(g_pos, HexCell.DIR_N, grid.path_cost_default - 0.1)
var path = [
a_pos,
Vector2(1, 1),
g_pos,
]
check_path(grid.find_path(a_pos, g_pos), path)
func test_rough_terrain():
# Path between A and B depends on the toughness of D
var short_path = [
a_pos,
Vector2(3, 0),
Vector2(4, 0),
d_pos,
Vector2(5, 1),
b_pos,
]
var long_path = [
a_pos,
Vector2(1, 1),
Vector2(0, 2),
Vector2(0, 3),
Vector2(0, 4),
Vector2(1, 4),
Vector2(2, 4),
Vector2(3, 3),
b_pos,
]
# The long path is 9 long, the short 6,
# so it should take the long path once d_pos costs more than 3 over default
var tests = {
grid.path_cost_default: short_path,
grid.path_cost_default + 1: short_path,
grid.path_cost_default + 2.9: short_path,
grid.path_cost_default + 3.1: long_path,
grid.path_cost_default + 50: long_path,
0: long_path,
}
for cost in tests:
grid.add_obstacles(d_pos, cost)
check_path(grid.find_path(a_pos, b_pos), tests[cost])
func test_exception():
# D is impassable, so path between A and B should go around the top as well
var path = [
a_pos,
Vector2(1, 1),
Vector2(0, 2),
Vector2(0, 3),
Vector2(0, 4),
Vector2(1, 4),
Vector2(2, 4),
Vector2(3, 3),
b_pos,
]
check_path(grid.find_path(a_pos, b_pos, [d_pos]), path)
func test_exception_hex():
# Same as the above, but providing an exceptional HexCell instance
var path = [
a_pos,
Vector2(1, 1),
Vector2(0, 2),
Vector2(0, 3),
Vector2(0, 4),
Vector2(1, 4),
Vector2(2, 4),
Vector2(3, 3),
b_pos,
]
check_path(grid.find_path(a_pos, b_pos, [HexCell.new(d_pos)]), path)
func test_exceptional_goal():
# If D is impassable, we should path to its neighbour
var path = [
a_pos,
Vector2(3, 0),
Vector2(4, 0),
]
check_path(grid.find_path(a_pos, d_pos, [d_pos]), path)
func test_inaccessible():
# E is inaccessible!
var path = grid.find_path(a_pos, e_pos)
assert_eq(path.size(), 0)
func test_obstacle_neighbour():
# Sometimes we can't get to something, but we can get next to it.
var path = [
a_pos,
Vector2(1, 1),
Vector2(0, 2),
Vector2(0, 3),
]
check_path(grid.find_path(a_pos, f_pos), path)
func test_difficult_goal():
# We should be able to path to a goal, no matter how difficult the final step
grid.add_obstacles(f_pos, 1337)
var path = [
a_pos,
Vector2(1, 1),
Vector2(0, 2),
Vector2(0, 3),
f_pos,
]
check_path(grid.find_path(a_pos, f_pos), path)