mirror of
https://github.com/lihop/godot-xterm.git
synced 2025-05-04 04:14:22 +02:00
Add all the files
This commit is contained in:
parent
d7db117f8b
commit
96e9ddcf79
68 changed files with 9064 additions and 7 deletions
23
addons/godot_xterm/Constants.gd
Normal file
23
addons/godot_xterm/Constants.gd
Normal file
|
@ -0,0 +1,23 @@
|
|||
extends Reference
|
||||
|
||||
# font flags
|
||||
enum {
|
||||
FONT_NORMAL = 0,
|
||||
FONT_BOLD = 1 << 0
|
||||
FONT_ITALIC = 1 << 1,
|
||||
FONT_UNDERLINED = 1 << 2
|
||||
FONT_BLINK = 1 << 3
|
||||
FONT_INVERSE = 1 << 4
|
||||
FONT_IVSIBILE = 1 << 5
|
||||
FONT_STRIKETHROUGH = 1 << 6
|
||||
}
|
||||
|
||||
# colors
|
||||
const COLOR_BLACK = Color(0.0, 0.0, 0.0) # 0
|
||||
const COLOR_RED = Color(1.0, 0.0, 0.0) # 1
|
||||
const COLOR_GREEN = Color(0.0, 1.0, 0.0) # 2
|
||||
const COLOR_YELLOW = Color(1.0, 1.0, 0.0) # 3
|
||||
const COLOR_BLUE = Color(0.0, 0.0, 1.0) # 4
|
||||
const COLOR_MAGENTA = Color(1.0, 0.0, 1.0) # 5
|
||||
const COLOR_CYAN = Color(0.0, 1.0, 1.0) # 6
|
||||
const COLOR_WHITE = Color(1.0, 1.0, 1.0) # 7
|
271
addons/godot_xterm/buffer.gd
Normal file
271
addons/godot_xterm/buffer.gd
Normal file
|
@ -0,0 +1,271 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const CharData = preload("res://addons/godot_xterm/char_data.gd")
|
||||
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
||||
|
||||
const MAX_BUFFER_SIZE = 32768 # 32 KiB
|
||||
|
||||
|
||||
|
||||
# Erase in Line (EL)
|
||||
enum {EL_RIGHT, EL_LEFT, EL_ALL}
|
||||
enum {FONT_NORMAL, FONT_BOLD, FONT_BLINK}
|
||||
|
||||
# Places a tab stop after every 8 columns.
|
||||
var tabWidth = 8
|
||||
|
||||
var rows = [[]] # array of CharData
|
||||
|
||||
var fg = Color(1.0, 1.0, 1.0) # foreground color
|
||||
var bg = Color(0.0, 0.0, 0.0) # background color
|
||||
var font # font
|
||||
var font_flags = FONT_NORMAL
|
||||
|
||||
var crow = 0 setget _set_crow # cursor's row
|
||||
var ccol = 0 setget _set_ccol # cursor's column
|
||||
|
||||
var ccol_saved: int
|
||||
var crow_saved: int
|
||||
|
||||
var num_rows = 20
|
||||
var num_cols = 70
|
||||
|
||||
var savedBuffer
|
||||
var savedCursorRow
|
||||
var savedCursorCol
|
||||
|
||||
func _init(num_rows: int, num_columns: int, alternate: bool = false):
|
||||
rows = []
|
||||
rows.resize(num_rows)
|
||||
for i in range(rows.size()):
|
||||
var cols = []
|
||||
cols.resize(num_columns)
|
||||
for j in range(cols.size()):
|
||||
cols[j] = CharData.new(" ", bg, fg, font_flags)
|
||||
rows[i] = cols
|
||||
|
||||
func _get_buffer_size():
|
||||
# Get the size of the (virtual) buffer.
|
||||
# Count each CharData as one byte even though it might be multiple bytes
|
||||
# in the case of unicode characters.
|
||||
var size = 0
|
||||
for row in rows:
|
||||
size += row.size()
|
||||
return size
|
||||
|
||||
func _set_rows(rows):
|
||||
print("rows: ", rows)
|
||||
|
||||
func _set_crow(row: int):
|
||||
print("setting crow")
|
||||
# Ensure there are enoungh rows in the
|
||||
# buffer for the new cursor position.
|
||||
if row >= rows.size():
|
||||
rows.resize(row + 1)
|
||||
|
||||
# resize() uses null for new elements.
|
||||
# but a row should be an array so we
|
||||
# need to replace null values.
|
||||
for i in range(rows.size()):
|
||||
if rows[i] == null:
|
||||
rows[i] = []
|
||||
|
||||
crow = row
|
||||
|
||||
func _set_ccol(col: int):
|
||||
# Ensure there are enough columns in the
|
||||
# row for the new cursor position.
|
||||
print("da size: ", rows[crow].size())
|
||||
if col >= rows[crow].size():
|
||||
rows[crow].resize(col + 1)
|
||||
|
||||
print("da new size: ", rows[crow].size())
|
||||
|
||||
for i in range(rows[crow].size()):
|
||||
if rows[crow][i] == null:
|
||||
rows[crow][i] = CharData.new(' ', bg, fg)
|
||||
|
||||
ccol = col
|
||||
|
||||
func save_cursor():
|
||||
ccol_saved = ccol
|
||||
crow_saved = crow
|
||||
|
||||
func restore_cursor():
|
||||
ccol = ccol_saved
|
||||
crow = crow_saved
|
||||
|
||||
func insert_at_cursor(d, start: int = 0, end: int = 1):
|
||||
var string
|
||||
if typeof(d) == TYPE_ARRAY:
|
||||
string = Decoder.utf32_to_string(d.slice(start, end - 1))
|
||||
else:
|
||||
string = d
|
||||
|
||||
var row = rows[crow]
|
||||
|
||||
for i in range(string.length()):
|
||||
var data = CharData.new(string[i], bg, fg, font_flags)
|
||||
|
||||
if ccol < row.size():
|
||||
row[ccol] = data
|
||||
else:
|
||||
row.resize(ccol)
|
||||
|
||||
for i in range(row.size()):
|
||||
if row[i] == null:
|
||||
row[i] = CharData.new(' ', bg, fg, font_flags)
|
||||
|
||||
row.append(data)
|
||||
|
||||
# Update the cursor position.
|
||||
ccol += 1
|
||||
|
||||
func insert_tab():
|
||||
print("Insert a tab!")
|
||||
# Insert a space.
|
||||
insert_at_cursor(' ')
|
||||
|
||||
# Keep inserting spaces until cursor is at next Tab stop.
|
||||
while ccol % tabWidth != 0:
|
||||
insert_at_cursor(' ')
|
||||
|
||||
# cr
|
||||
func carriage_return():
|
||||
ccol = 0
|
||||
|
||||
# lf
|
||||
func line_feed():
|
||||
rows.resize(rows.size() + 1)
|
||||
rows[-1] = []
|
||||
crow = crow + 1
|
||||
|
||||
# bs
|
||||
# Deletes the element before the current cursor position.
|
||||
func backspace():
|
||||
rows[crow].remove(ccol - 1)
|
||||
ccol = ccol - 1
|
||||
|
||||
# cup
|
||||
# Move the cursor to the given row and column.
|
||||
# For example cursor_position(0, 0) would move
|
||||
# the cursor to the top left corner of the terminal.
|
||||
func cursor_position(params: Array) -> void:
|
||||
var row = params[0] if params.size() > 0 else 1
|
||||
var col = params[1] if params.size() > 1 else 1
|
||||
|
||||
# Origin is (0,0) so row 1, col 1 would be 0,0.
|
||||
if col != 0:
|
||||
self.ccol = col - 1
|
||||
else:
|
||||
self.ccol = 0
|
||||
if row != 0:
|
||||
self.crow = row - 1
|
||||
else:
|
||||
self.crow = 0
|
||||
|
||||
# ed 3
|
||||
func erase_saved_lines():
|
||||
rows = [[]]
|
||||
print("saved lines erased")
|
||||
|
||||
# el
|
||||
func erase_in_line(section):
|
||||
return
|
||||
match section:
|
||||
EL_LEFT, EL_ALL:
|
||||
for i in range(0, ccol):
|
||||
rows[crow][i] = CharData.new(" ")
|
||||
print("Erased the thing")
|
||||
if section == EL_ALL:
|
||||
continue
|
||||
EL_RIGHT, _:
|
||||
for i in range(ccol, rows[crow].size()):
|
||||
rows[crow][i] = CharData.new(" ")
|
||||
print("Erased the thing")
|
||||
|
||||
# ed 0 (default)
|
||||
func erase_below():
|
||||
# Erase from the cursor through to the end of the display.
|
||||
save_cursor()
|
||||
while crow < num_rows:
|
||||
erase_in_line(EL_RIGHT)
|
||||
_set_ccol(0)
|
||||
_set_crow(crow + 1)
|
||||
restore_cursor()
|
||||
|
||||
func set_scrolling_region(top: int, bottom: int):
|
||||
print("set_scrolling_position")
|
||||
# Not sure what this does yet.
|
||||
# Make default be full window size.
|
||||
pass
|
||||
|
||||
func set_font(fontState: int, set: bool = true):
|
||||
match fontState:
|
||||
FONT_NORMAL:
|
||||
pass
|
||||
|
||||
func set_font_flag(flag: int, set: bool = true):
|
||||
print("setting font flag!")
|
||||
if set: # Set the flag
|
||||
font_flags |= (1 << flag)
|
||||
else: # Clear the flag
|
||||
font_flags &= ~(1 << flag)
|
||||
print("font flag is set!")
|
||||
print(font_flags)
|
||||
|
||||
# Clear all font flags. Returns font to default state.
|
||||
func reset_font_flags():
|
||||
font_flags = FONT_NORMAL
|
||||
|
||||
# setf
|
||||
func set_foreground(color: Color = Color(1.0, 1.0, 1.0)):
|
||||
fg = color
|
||||
|
||||
# setb
|
||||
func set_background(color: Color = Color(0.0, 0.0, 0.0)):
|
||||
bg = color
|
||||
|
||||
# setaf
|
||||
func set_a_foreground(params):
|
||||
pass
|
||||
|
||||
# setab
|
||||
func set_a_background(params):
|
||||
pass
|
||||
|
||||
func reset_sgr():
|
||||
set_foreground()
|
||||
set_background()
|
||||
reset_font_flags()
|
||||
|
||||
func repeat_preceding_character(times: int = 0):
|
||||
|
||||
var preceding_char
|
||||
|
||||
if ccol == 0:
|
||||
preceding_char = rows[crow-1][-1]
|
||||
else:
|
||||
preceding_char = rows[crow][ccol-1]
|
||||
|
||||
print("Repeating preceding char ", preceding_char.ch, " ", times, " times")
|
||||
|
||||
for i in range(times):
|
||||
insert_at_cursor(preceding_char.ch)
|
||||
|
||||
# Save the buffer (useful when switching to the alternate buffer)
|
||||
func save():
|
||||
savedBuffer = rows
|
||||
savedCursorCol = ccol
|
||||
savedCursorRow = crow
|
||||
|
||||
# Full Reset
|
||||
func reset():
|
||||
rows = [[]]
|
||||
crow = 0
|
||||
ccol = 0
|
||||
fg = Color(1.0, 1.0, 1.0)
|
||||
bg = Color(0.0, 0.0, 0.0)
|
19
addons/godot_xterm/char_data.gd
Normal file
19
addons/godot_xterm/char_data.gd
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
var ch # character
|
||||
var fg = Color(1.0, 1.0, 1.0) # foreground color
|
||||
var bg = Color(0.0, 0.0, 0.0) # background color
|
||||
var ff = 0 # font flags
|
||||
|
||||
func _init(
|
||||
character: String,
|
||||
background_color: Color = bg,
|
||||
foreground_color: Color = fg,
|
||||
font_flags = ff # Does this work or will it cause problems (this assignement technique)
|
||||
):
|
||||
ch = character
|
||||
bg = background_color
|
||||
fg = foreground_color
|
||||
ff = font_flags
|
93
addons/godot_xterm/fonts/source_code_pro/OFL.txt
Normal file
93
addons/godot_xterm/fonts/source_code_pro/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
|||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@ -0,0 +1,6 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold_italic.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_italic.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
[gd_resource type="DynamicFont" load_steps=2 format=2]
|
||||
|
||||
[ext_resource path="res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.ttf" type="DynamicFontData" id=1]
|
||||
|
||||
[resource]
|
||||
font_data = ExtResource( 1 )
|
Binary file not shown.
93
addons/godot_xterm/fonts/vt323/OFL.txt
Normal file
93
addons/godot_xterm/fonts/vt323/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
|||
Copyright 2011, The VT323 Project Authors (peter.hull@oikoi.com)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
3
addons/godot_xterm/fonts/vt323/vt323_regular.tres
Normal file
3
addons/godot_xterm/fonts/vt323/vt323_regular.tres
Normal file
|
@ -0,0 +1,3 @@
|
|||
[gd_resource type="DynamicFont" format=2]
|
||||
|
||||
[resource]
|
BIN
addons/godot_xterm/fonts/vt323/vt323_regular.ttf
Normal file
BIN
addons/godot_xterm/fonts/vt323/vt323_regular.ttf
Normal file
Binary file not shown.
14
addons/godot_xterm/icon.svg
Normal file
14
addons/godot_xterm/icon.svg
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<path d="m4.5605 3.9746c-0.074558 0-0.14819 0.029049-0.20508 0.085938l-0.27539 0.27539c-0.11354 0.11358-0.11332 0.29631 0 0.41016l1.8691 1.8789-1.8691 1.8789c-0.11336 0.11385-0.11358 0.29657 0 0.41016l0.27539 0.27539c0.11377 0.11378 0.29833 0.11378 0.41211 0l2.3594-2.3594c0.11378-0.11378 0.11378-0.29834 0-0.41211l-2.3594-2.3574c-0.056882-0.056888-0.13247-0.085938-0.20703-0.085938zm3.2207 4.3984c-0.1609 0-0.29102 0.13012-0.29102 0.29102v0.38867c0 0.1609 0.13012 0.29102 0.29102 0.29102h3.6914c0.1609 0 0.29102-0.13012 0.29102-0.29102v-0.38867c0-0.1609-0.13012-0.29102-0.29102-0.29102z" fill="#a5efac" stroke-width=".012139"/>
|
||||
<path d="m3 1c-1.1046 0-2 0.8954-2 2v10c0 1.1046 0.89543 2 2 2h10c1.1046 0 2-0.8954 2-2v-10c0-1.1046-0.89543-2-2-2zm0 2h10v10h-10z" fill="#a5efac"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
34
addons/godot_xterm/icon.svg.import
Normal file
34
addons/godot_xterm/icon.svg.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/icon.svg-b0b594198f5f4040e8f0e39a8a353265.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/godot_xterm/icon.svg"
|
||||
dest_files=[ "res://.import/icon.svg-b0b594198f5f4040e8f0e39a8a353265.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
|
269
addons/godot_xterm/input/text_decoder.gd
Normal file
269
addons/godot_xterm/input/text_decoder.gd
Normal file
|
@ -0,0 +1,269 @@
|
|||
# Copyright (c) 2020 The GodotTerm authors.
|
||||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
# Convert a given to a utf8 PoolByteArray.
|
||||
# The code for this function is based on the stackoverflow
|
||||
# answer by user Schwern https://stackoverflow.com/a/42013984.
|
||||
static func utf32_to_utf8(codepoint: int):
|
||||
var utf8 = PoolByteArray([])
|
||||
|
||||
if codepoint <= 0x007F:
|
||||
utf8.append(codepoint)
|
||||
elif codepoint <= 0x07FF:
|
||||
utf8.append(0b11000000 | codepoint >> 6 & 0b00011111)
|
||||
utf8.append(0b10000000 | codepoint & 0b00111111)
|
||||
elif codepoint <= 0xFFFF:
|
||||
utf8.append(0b11100000 | codepoint >> 12 & 0b00001111)
|
||||
utf8.append(0b10000000 | codepoint >> 6 & 0b00111111)
|
||||
utf8.append(0b10000000 | codepoint & 0b00111111)
|
||||
elif codepoint <= 0x10FFFF:
|
||||
utf8.append(0b11110000 | codepoint >> 18 & 0b00000111)
|
||||
utf8.append(0b10000000 | codepoint >> 12 & 0b00111111)
|
||||
utf8.append(0b10000000 | codepoint >> 6 & 0b00111111)
|
||||
utf8.append(0b10000000 | codepoint & 0b00111111)
|
||||
else:
|
||||
push_warning("Codepoint " + String(codepoint) + " is out of UTF-8 range")
|
||||
|
||||
return utf8
|
||||
|
||||
# Convert UTF32 codepoint into a String.
|
||||
static func string_from_codepoint(codepoint: int):
|
||||
var utf8 = utf32_to_utf8(codepoint)
|
||||
return utf8.get_string_from_utf8()
|
||||
|
||||
# Covert UTF32 char codes into a String.
|
||||
# Basically the same as `string_from_codepoint` but for multiple codepoints
|
||||
# in a loop (which is a lot faster).
|
||||
static func utf32_to_string(data: Array, start: int = 0, end: int = -1):
|
||||
if end == -1:
|
||||
end = data.size()
|
||||
var result = ''
|
||||
for i in range(start, end):
|
||||
result += string_from_codepoint(data[i])
|
||||
return result
|
||||
|
||||
# Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.
|
||||
class Utf8ToUtf32:
|
||||
var interim = PoolByteArray()
|
||||
|
||||
func _init():
|
||||
interim.resize(3)
|
||||
|
||||
# Clears interim bytes and resets decoder to clean state.
|
||||
func clear():
|
||||
for i in interim.size():
|
||||
interim[i] = 0
|
||||
|
||||
# Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.
|
||||
# The methods assumes stream input and will store partly transmitted bytes
|
||||
# and decode them with the next data chunk.
|
||||
# Note: The method does no bound checks for target, therefore make sure
|
||||
# the provided data chunk does not exceed the size of `target`.
|
||||
# Returns the number of written codepoints in `target`.
|
||||
func decode(input: PoolByteArray, target: Array):
|
||||
var length = input.size()
|
||||
|
||||
if !length:
|
||||
return 0
|
||||
|
||||
if length > target.size():
|
||||
target.resize(length)
|
||||
|
||||
var size = 0
|
||||
var byte1: int
|
||||
var byte2: int
|
||||
var byte3: int
|
||||
var byte4: int
|
||||
var codepoint = 0
|
||||
var start_pos = 0
|
||||
|
||||
# handle leftover bytes
|
||||
if interim[0]:
|
||||
var discard_interim = false
|
||||
var cp = interim[0]
|
||||
cp &= 0x1F if (cp & 0xE0) == 0xC0 else 0x0F if (cp & 0xF0) == 0xE0 else 0x07
|
||||
var pos = 1
|
||||
var tmp = interim[pos] & 0x3F
|
||||
while tmp && pos < 4:
|
||||
cp <<= 6
|
||||
cp |= tmp
|
||||
pos += 1
|
||||
tmp = interim[pos] & 0x3F if interim.size() < pos else 0
|
||||
# missing bytes - read from input
|
||||
var type = 2 if (interim[0] & 0xE0) == 0xC0 else 3 if (interim[0] & 0xF0) == 0xE0 else 4
|
||||
var missing = type - pos
|
||||
while start_pos < missing:
|
||||
if start_pos >= length:
|
||||
return 0
|
||||
tmp = input[start_pos]
|
||||
start_pos += 1
|
||||
if (tmp & 0xC0) != 0x80:
|
||||
# wrong continuation, discard interim bytes completely
|
||||
start_pos -= 1
|
||||
discard_interim = true
|
||||
break
|
||||
else:
|
||||
# need to save so we can continue short inputs in next call
|
||||
interim[pos + 1] = tmp
|
||||
pos += 1
|
||||
cp <<= 6
|
||||
cp |= tmp & 0x3F
|
||||
if not discard_interim:
|
||||
# final test is type dependent
|
||||
match type:
|
||||
2:
|
||||
if cp < 0x80:
|
||||
# wrong starter byte
|
||||
start_pos -= 1
|
||||
else:
|
||||
target[size] = cp
|
||||
size += 1
|
||||
3:
|
||||
if cp < 0x0800 or (cp >= 0xD800 and cp <= 0xDFFF):
|
||||
# illegal codepoint
|
||||
pass
|
||||
else:
|
||||
target[size] = cp
|
||||
size += 1
|
||||
_:
|
||||
if cp < 0x10000 or cp > 0x10FFFF:
|
||||
# illegal codepoint
|
||||
pass
|
||||
else:
|
||||
target[size] = cp
|
||||
size += 1
|
||||
clear()
|
||||
|
||||
# loop through input
|
||||
var four_stop = length - 4
|
||||
var i = start_pos
|
||||
while i < length:
|
||||
# ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.
|
||||
# This is a compromise between speed gain for ASCII
|
||||
# and penalty for non ASCII:
|
||||
# For best ASCII performance the char should be stored directly into target,
|
||||
# but even a single attempt to write to target and compare afterwards
|
||||
# penalizes non ASCII really bad (-50%), thus we load the char into byteX first,
|
||||
# which reduces ASCII performance by ~15%.
|
||||
# This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible
|
||||
# compared to the gains.
|
||||
# Note that this optimization only takes place for 4 consecutive ASCII chars,
|
||||
# for any shorter it bails out. Worst case - all 4 bytes being read but
|
||||
# thrown away due to the last being a non ASCII char (-10% performance).
|
||||
while i < four_stop:
|
||||
byte1 = input[i]
|
||||
byte2 = input[i + 1]
|
||||
byte3 = input[i + 2]
|
||||
byte4 = input[i + 3]
|
||||
if not (byte1 & 0x80) | (byte2 & 0x80) | (byte3 & 0x80) | (byte4 & 0x80):
|
||||
target[size] = byte1
|
||||
target[size+1] = byte2
|
||||
target[size+2] = byte3
|
||||
target[size+3] = byte4
|
||||
size += 4
|
||||
i += 4
|
||||
else:
|
||||
break
|
||||
|
||||
# reread byte1
|
||||
byte1 = input[i]
|
||||
i += 1
|
||||
|
||||
# 1 byte
|
||||
if byte1 < 0x80:
|
||||
target[size] = byte1
|
||||
size += 1
|
||||
|
||||
# 2 bytes
|
||||
elif (byte1 & 0xE0) == 0xC0:
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
return size
|
||||
byte2 = input[i]
|
||||
i+=1
|
||||
if (byte2 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i-=1
|
||||
continue
|
||||
codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F)
|
||||
if (codepoint < 0x80):
|
||||
# wrong starter byte
|
||||
i-=1
|
||||
continue
|
||||
target[size] = codepoint
|
||||
size+=1
|
||||
|
||||
# 3 bytes
|
||||
elif (byte1 & 0xF0) == 0xE0:
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
return size
|
||||
byte2 = input[i]
|
||||
i+=1
|
||||
if (byte2 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i-=1
|
||||
continue
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
interim[1] = byte2
|
||||
return size
|
||||
byte3 = input[i]
|
||||
i+=1
|
||||
if (byte3 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i-=1
|
||||
continue
|
||||
codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F)
|
||||
if codepoint < 0x0800 or (codepoint >=0xD800 and codepoint <= 0xDFFF):
|
||||
# illegal codepoint, no i-- here
|
||||
continue
|
||||
target[size] = codepoint
|
||||
size+=1
|
||||
|
||||
# 4 bytes
|
||||
elif (byte1 & 0xF8) == 0xF0:
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
return size
|
||||
byte2 = input[i]
|
||||
i += 1
|
||||
if (byte2 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i -= 1
|
||||
continue
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
interim[1] = byte2
|
||||
return size
|
||||
byte3 = input[i]
|
||||
i += 1
|
||||
if (byte3 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i -= 1
|
||||
continue
|
||||
if i >= length:
|
||||
interim[0] = byte1
|
||||
interim[1] = byte2
|
||||
interim[2] = byte3
|
||||
return size
|
||||
byte4 = input[i]
|
||||
i += 1
|
||||
if (byte4 & 0xC0) != 0x80:
|
||||
# wrong continuation
|
||||
i -= 1
|
||||
continue
|
||||
codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F)
|
||||
if codepoint < 0x010000 or codepoint > 0x10FFFF:
|
||||
# illegal codepoint, no i-- here
|
||||
continue
|
||||
target[size] = codepoint
|
||||
size += 1
|
||||
else:
|
||||
# illegal byte, just skip
|
||||
pass
|
||||
|
||||
target.resize(size)
|
||||
return size
|
131
addons/godot_xterm/parser/constants.gd
Normal file
131
addons/godot_xterm/parser/constants.gd
Normal file
|
@ -0,0 +1,131 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors.
|
||||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
# Psuedo-character placeholder for non-ascii characters (unicode).
|
||||
const NON_ASCII_PRINTABLE = 0xa0
|
||||
|
||||
# Payload limit for OSC and DCS.
|
||||
const PAYLOAD_LIMIT = 10000000
|
||||
|
||||
# Internal states of EscapeSequenceParser.
|
||||
enum ParserState {
|
||||
GROUND
|
||||
ESCAPE
|
||||
ESCAPE_INTERMEDIATE
|
||||
CSI_ENTRY
|
||||
CSI_PARAM
|
||||
CSI_INTERMEDIATE
|
||||
CSI_IGNORE
|
||||
SOS_PM_APC_STRING
|
||||
OSC_STRING
|
||||
DCS_ENTRY
|
||||
DCS_PARAM
|
||||
DCS_IGNORE
|
||||
DCS_INTERMEDIATE
|
||||
DCS_PASSTHROUGH
|
||||
}
|
||||
|
||||
# Internal actions of EscapeSequenceParser.
|
||||
enum ParserAction {
|
||||
IGNORE
|
||||
ERROR
|
||||
PRINT
|
||||
EXECUTE
|
||||
OSC_START
|
||||
OSC_PUT
|
||||
OSC_END
|
||||
CSI_DISPATCH
|
||||
PARAM
|
||||
COLLECT
|
||||
ESC_DISPATCH
|
||||
CLEAR
|
||||
DCS_HOOK
|
||||
DCS_PUT
|
||||
DCS_UNHOOK
|
||||
}
|
||||
|
||||
# Internal states of OscParser.
|
||||
enum OscState {
|
||||
START
|
||||
ID
|
||||
PAYLOAD
|
||||
ABORT
|
||||
}
|
||||
|
||||
# C0 control codes
|
||||
# See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C0_controls
|
||||
enum C0 {
|
||||
NUL
|
||||
SOH
|
||||
STX
|
||||
ETX
|
||||
EOT
|
||||
ENQ
|
||||
ACK
|
||||
BEL
|
||||
BS
|
||||
HT
|
||||
LF
|
||||
VT
|
||||
FF
|
||||
CR
|
||||
SO
|
||||
SI
|
||||
DLE
|
||||
DC1
|
||||
DC2
|
||||
DC3
|
||||
DC4
|
||||
NAK
|
||||
SYN
|
||||
ETB
|
||||
CAN
|
||||
EM
|
||||
SUB
|
||||
ESC
|
||||
FS
|
||||
GS
|
||||
RS
|
||||
US
|
||||
SP
|
||||
DEL = 0x7f
|
||||
}
|
||||
|
||||
# C1 control codes
|
||||
# See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C1_controls
|
||||
enum C1 {
|
||||
PAD = 0x80
|
||||
HOP = 0x81
|
||||
BPH = 0x82
|
||||
NBH = 0x83
|
||||
IND = 0x84
|
||||
NEL = 0x85
|
||||
SSA = 0x86
|
||||
ESA = 0x87
|
||||
HTS = 0x88
|
||||
HTJ = 0x89
|
||||
VTS = 0x8a
|
||||
PLD = 0x8b
|
||||
PLU = 0x8c
|
||||
RI = 0x8d
|
||||
SS2 = 0x8e
|
||||
SS3 = 0x8f
|
||||
DCS = 0x90
|
||||
PU1 = 0x91
|
||||
PU2 = 0x92
|
||||
STS = 0x93
|
||||
CCH = 0x94
|
||||
MW = 0x95
|
||||
SPA = 0x96
|
||||
EPA = 0x97
|
||||
SOS = 0x98
|
||||
SGCI = 0x99
|
||||
SCI = 0x9a
|
||||
CSI = 0x9b
|
||||
ST = 0x9c
|
||||
OSC = 0x9d
|
||||
PM = 0x9e
|
||||
APC = 0x9f
|
||||
}
|
77
addons/godot_xterm/parser/dcs_parser.gd
Normal file
77
addons/godot_xterm/parser/dcs_parser.gd
Normal file
|
@ -0,0 +1,77 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors.
|
||||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
||||
|
||||
|
||||
const EMPTY_HANDLERS = []
|
||||
|
||||
|
||||
var _handlers: Dictionary = {}
|
||||
var _active: Array = EMPTY_HANDLERS
|
||||
var _ident: int = 0
|
||||
var _handler_fb: Dictionary
|
||||
|
||||
|
||||
func _init():
|
||||
pass
|
||||
|
||||
|
||||
func set_handler(ident: int, handler):
|
||||
_handlers[ident] = [handler]
|
||||
|
||||
|
||||
func clear_handler(ident: int):
|
||||
_handlers.erase(ident)
|
||||
|
||||
|
||||
func set_handler_fallback(target, method):
|
||||
_handler_fb = {'target': target, 'method': method}
|
||||
|
||||
|
||||
func reset():
|
||||
if _active.size():
|
||||
unhook(false)
|
||||
_active = EMPTY_HANDLERS
|
||||
_ident = 0
|
||||
|
||||
|
||||
func hook(ident: int, params):
|
||||
# always reset leftover handlers
|
||||
reset()
|
||||
_ident = ident
|
||||
_active = _handlers[ident] if _handlers.has(ident) else EMPTY_HANDLERS
|
||||
if _active.empty():
|
||||
_handler_fb['target'].call(_handler_fb['method'], _ident, 'HOOK', params)
|
||||
else:
|
||||
_active.invert()
|
||||
for handler in _active:
|
||||
handler.hook(params)
|
||||
_active.invert()
|
||||
|
||||
|
||||
func put(data: Array, start: int, end: int):
|
||||
if _active.empty():
|
||||
_handler_fb['target'].call(_handler_fb['method'], _ident, 'PUT',
|
||||
Decoder.utf32_to_string(data, start, end))
|
||||
else:
|
||||
_active.invert()
|
||||
for handler in _active:
|
||||
handler.put(data, start, end)
|
||||
_active.invert()
|
||||
|
||||
|
||||
func unhook(success: bool):
|
||||
if _active.empty():
|
||||
_handler_fb['target'].call(_handler_fb['method'], _ident, 'UNHOOK', success)
|
||||
else:
|
||||
_active.invert()
|
||||
for handler in _active:
|
||||
if handler.unhook(success) != false:
|
||||
success = false # will cleanup left over handlers
|
||||
_active.invert()
|
||||
_active = EMPTY_HANDLERS
|
||||
_ident = 0
|
329
addons/godot_xterm/parser/escape_sequence_parser.gd
Normal file
329
addons/godot_xterm/parser/escape_sequence_parser.gd
Normal file
|
@ -0,0 +1,329 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# Copyright (c) 2018 The xterm.js authers. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||
const TransitionTable = preload("res://addons/godot_xterm/parser/transition_table.gd")
|
||||
const VT500TransitionTable = preload("res://addons/godot_xterm/parser/vt500_transition_table.gd")
|
||||
const DcsParser = preload("res://addons/godot_xterm/parser/dcs_parser.gd")
|
||||
const Params = preload("res://addons/godot_xterm/parser/params.gd")
|
||||
|
||||
const TableAccess = TransitionTable.TableAccess
|
||||
const NON_ASCII_PRINTABLE = Constants.NON_ASCII_PRINTABLE
|
||||
const ParserState = Constants.ParserState
|
||||
const ParserAction = Constants.ParserAction
|
||||
|
||||
var initial_state
|
||||
var current_state
|
||||
var preceding_codepoint
|
||||
|
||||
var _transitions
|
||||
|
||||
# buffers over several parse calls
|
||||
var _params
|
||||
var _collect
|
||||
|
||||
# handler lookup containers
|
||||
var _print_handler
|
||||
var _execute_handlers
|
||||
var _csi_handlers
|
||||
var _esc_handlers
|
||||
var _osc_parser
|
||||
var _dcs_parser
|
||||
var _error_handler
|
||||
|
||||
# fallback handlers
|
||||
var _print_handler_fb
|
||||
var _execute_handler_fb
|
||||
var _csi_handler_fb
|
||||
var _esc_handler_fb
|
||||
var _error_handler_fb
|
||||
|
||||
|
||||
# Default do noting fallback handler.
|
||||
# Allows a variable number of arguments from 0 - 7.
|
||||
func noop(a = null, b = null, c = null, d = null, e = null, f = null, g = null):
|
||||
pass
|
||||
|
||||
|
||||
func _init(transitions = VT500TransitionTable.new().table):
|
||||
initial_state = ParserState.GROUND
|
||||
current_state = initial_state
|
||||
_transitions = transitions
|
||||
_params = Params.new() # Defaults to 32 storable params/subparams
|
||||
_params.add_param(0) # ZDM (Zero Default Mode
|
||||
_collect = 0
|
||||
preceding_codepoint = 0
|
||||
|
||||
# set default fallback handlers and handler lookup containers
|
||||
var noop = {'target': self, 'method': 'noop'}
|
||||
_print_handler_fb = noop
|
||||
_execute_handler_fb = noop
|
||||
_csi_handler_fb = noop
|
||||
_esc_handler_fb = noop
|
||||
_error_handler_fb = noop
|
||||
_print_handler = _print_handler_fb
|
||||
_execute_handlers = {}
|
||||
_csi_handlers = {}
|
||||
_esc_handlers = {}
|
||||
_osc_parser = null # TODO OscParser.new()
|
||||
_dcs_parser = DcsParser.new()
|
||||
_error_handler = _error_handler_fb
|
||||
|
||||
# swallow 7bit ST (ESC+\)
|
||||
set_esc_handler({'final': '\\'}, self, 'noop')
|
||||
|
||||
|
||||
static func identifier(id: Dictionary, final_range: Array = [0x40, 0x7e]):
|
||||
var res = 0
|
||||
|
||||
var prefix = id.get('prefix')
|
||||
var intermediates = id.get('intermediates')
|
||||
var final = id.get('final')
|
||||
|
||||
if prefix:
|
||||
if prefix.length() > 1:
|
||||
push_error("only one byte prefix supported")
|
||||
res = prefix.to_ascii()[0]
|
||||
if res and 0x3c > res or res > 0x3f:
|
||||
push_error("prefix must be in the range 0x3c-0x3f")
|
||||
|
||||
if intermediates:
|
||||
if intermediates.length() > 2:
|
||||
push_error("only two bytes as intermediates are supported")
|
||||
for intermediate in intermediates:
|
||||
var im = intermediate.to_ascii()[0]
|
||||
if 0x20 > im or im > 0x2f:
|
||||
push_error("intermediate must be in the range 0x20-0x2f")
|
||||
res = res << 8
|
||||
res = res | im
|
||||
|
||||
if final.length() != 1:
|
||||
push_error("final must be a single byte")
|
||||
var final_code = final.to_ascii()[0]
|
||||
if final_range[0] > final_code or final_code > final_range[1]:
|
||||
push_error("final must be in the range " + String(final_range[0]) + "-" + String(final_range[1]))
|
||||
res = res << 8
|
||||
res = res | final_code
|
||||
|
||||
return res
|
||||
|
||||
static func ident_to_string(ident: int):
|
||||
var res = PoolStringArray([])
|
||||
while ident:
|
||||
res.append(PoolByteArray([ident & 0xFF]).get_string_from_ascii())
|
||||
ident >>= 8
|
||||
res.invert()
|
||||
return res.join('')
|
||||
|
||||
func set_print_handler(target: Object, method: String):
|
||||
_print_handler = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func add_esc_handler(id, target, method):
|
||||
var ident = identifier(id, [0x30, 0x7e])
|
||||
if not _esc_handlers.has(ident):
|
||||
_esc_handlers[ident] = []
|
||||
var handler_list = _esc_handlers[ident]
|
||||
handler_list.append({'target': target, 'method': method})
|
||||
|
||||
|
||||
func set_csi_handler(id: Dictionary, target: Object, method: String):
|
||||
_csi_handlers[identifier(id)] = [{ 'target': target, 'method': method }]
|
||||
|
||||
|
||||
func set_csi_handler_fallback(target, method):
|
||||
_csi_handler_fb = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func set_execute_handler(flag: int, target: Object, method: String):
|
||||
_execute_handlers[flag] = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func set_execute_handler_fallback(target: Object, method: String):
|
||||
_execute_handler_fb = { 'target': target, 'method': method }
|
||||
|
||||
|
||||
func set_esc_handler(id, target, method):
|
||||
_esc_handlers[identifier(id, [0x30, 0x7e])] = [{'target': target, 'method': method}]
|
||||
|
||||
|
||||
func set_esc_handler_fallback(target: Object, method: String):
|
||||
_esc_handler_fb = {'target': target, 'method': method}
|
||||
|
||||
|
||||
func add_dcs_handler(id, target, method):
|
||||
pass
|
||||
# TODO!!!
|
||||
|
||||
func set_dcs_handler(id, target: Object, method: String):
|
||||
_dcs_parser.set_handler(id, {'target': target, 'method': method})
|
||||
|
||||
func set_dcs_handler_fallback(target: Object, method: String):
|
||||
_dcs_parser.set_handler_fallback(target, method)
|
||||
|
||||
func reset():
|
||||
current_state = initial_state
|
||||
_params.reset()
|
||||
_params.add_param(0) # ZDM
|
||||
_collect = 0
|
||||
preceding_codepoint = 0
|
||||
|
||||
func parse(data: Array, length: int):
|
||||
var code = 0
|
||||
var transition = 0
|
||||
var _current_state = current_state
|
||||
var dcs = _dcs_parser
|
||||
var collect = _collect
|
||||
var params = _params
|
||||
|
||||
#print("table", table)
|
||||
|
||||
#print("parse -> data: ", data, " length: ", length)
|
||||
|
||||
# Process input string.
|
||||
var i = 0
|
||||
while i < length:
|
||||
#print("i: ", i)
|
||||
code = data[i]
|
||||
|
||||
#print("code: ", code)
|
||||
|
||||
# Normal transition and action lookup.
|
||||
transition = _transitions[_current_state << TableAccess.INDEX_STATE_SHIFT | code if code < 0xa0 else NON_ASCII_PRINTABLE]
|
||||
|
||||
#print ("transition: ", transition)
|
||||
#print("current state: ", current_state)
|
||||
|
||||
match transition >> TableAccess.TRANSITION_ACTION_SHIFT:
|
||||
ParserAction.PRINT:
|
||||
# read ahead with loop unrolling
|
||||
# # Note: 0x20 (SP) is included, 0x7F (DEL) is excluded
|
||||
var j = i + 1
|
||||
while true:
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
code = data[j] if j < data.size() else 0
|
||||
if j >= length or code < 0x20 or (code > 0x7e and code < NON_ASCII_PRINTABLE):
|
||||
_print_handler['target'].call(_print_handler['method'], data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
j += 1
|
||||
ParserAction.EXECUTE:
|
||||
var handler = _execute_handlers.get(code)
|
||||
if handler:
|
||||
handler['target'].call(handler['method'])
|
||||
elif _execute_handler_fb:
|
||||
_execute_handler_fb['target'].call(_execute_handler_fb['method'], code)
|
||||
preceding_codepoint = 0
|
||||
ParserAction.IGNORE:
|
||||
pass
|
||||
ParserAction.ERROR:
|
||||
print("Parser error!")
|
||||
|
||||
ParserAction.CSI_DISPATCH:
|
||||
# Trigger CSI Handler
|
||||
var handlers = _csi_handlers.get((collect << 8 | code), [])
|
||||
handlers.invert()
|
||||
for handler in handlers:
|
||||
# undefined or true means success and to stop bubbling
|
||||
if handler['target'].call(handler['method'], params.to_array()):
|
||||
continue
|
||||
handlers.invert()
|
||||
if handlers.empty():
|
||||
_csi_handler_fb['target'].call(_csi_handler_fb['method'], collect << 8 | code, params.to_array())
|
||||
preceding_codepoint = 0
|
||||
|
||||
|
||||
ParserAction.PARAM:
|
||||
# Inner loop digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
|
||||
var do = true
|
||||
while do:
|
||||
match code:
|
||||
0x3b:
|
||||
params.add_param(0)
|
||||
0x3a:
|
||||
params.add_sub_param(-1)
|
||||
_:
|
||||
params.add_digit(code - 48)
|
||||
i += 1
|
||||
code = data[i] if i < data.size() else 0
|
||||
do = i < length and code > 0x2f and code < 0x3c
|
||||
i-=1
|
||||
|
||||
ParserAction.COLLECT:
|
||||
collect <<= 8
|
||||
collect |= code
|
||||
|
||||
ParserAction.ESC_DISPATCH:
|
||||
var handlers = _esc_handlers.get((collect << 8 | code), [])
|
||||
handlers.invert()
|
||||
for handler in handlers:
|
||||
# undefined or true means success and to stop bubbling
|
||||
if handler['target'].call(handler['method']) != false:
|
||||
continue
|
||||
handlers.invert()
|
||||
if handlers.empty():
|
||||
_esc_handler_fb['target'].call(_esc_handler_fb['method'], collect << 8 | code)
|
||||
preceding_codepoint = 0
|
||||
|
||||
ParserAction.CLEAR:
|
||||
params.reset()
|
||||
params.add_param(0) # ZDM
|
||||
collect = 0
|
||||
|
||||
ParserAction.DCS_HOOK:
|
||||
dcs.hook(collect << 8 | code, params.to_array())
|
||||
|
||||
ParserAction.DCS_PUT:
|
||||
# inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
|
||||
# unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
|
||||
for j in range(i + 1, length + 1):
|
||||
code = data[j]
|
||||
if code == 0x18 or code == 0x1a or code == 0x1b or (code > 0x7f and code < NON_ASCII_PRINTABLE):
|
||||
dcs.put(data, i, j)
|
||||
i = j - 1
|
||||
break
|
||||
break
|
||||
ParserAction.DCS_UNHOOK:
|
||||
_dcs_parser.unhook(code != 0x18 and code != 0x1a)
|
||||
if code == 0x1b:
|
||||
transition |= ParserState.ESCAPE
|
||||
params.reset()
|
||||
params.add_param(0) # ZDM
|
||||
collect = 0;
|
||||
preceding_codepoint = 0
|
||||
ParserAction.OSC_START:
|
||||
pass
|
||||
|
||||
ParserAction.OSC_PUT:
|
||||
pass
|
||||
|
||||
ParserAction.OSC_END:
|
||||
pass
|
||||
|
||||
_current_state = transition & TableAccess.TRANSITION_STATE_MASK
|
||||
i += 1
|
||||
|
||||
# save collected intermediates
|
||||
_collect = collect
|
||||
|
||||
# save state
|
||||
current_state = _current_state
|
104
addons/godot_xterm/parser/params.gd
Normal file
104
addons/godot_xterm/parser/params.gd
Normal file
|
@ -0,0 +1,104 @@
|
|||
# Copyright (c) 2020 The GodotTerm authors.
|
||||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
|
||||
# Max value supported for a single param/subparam (clamped to positive int32 range).
|
||||
const MAX_VALUE = 0x7FFFFFFF;
|
||||
# Max allowed subparams for a single sequence (hardcoded limitation).
|
||||
const MAX_SUBPARAMS = 256;
|
||||
|
||||
var params = []
|
||||
var length = 0
|
||||
|
||||
var sub_params = []
|
||||
var sub_params_length = 0
|
||||
var _max_length
|
||||
var _max_sub_params_length
|
||||
var sub_params_idx = []
|
||||
var _reject_digits = false
|
||||
var _reject_sub_digits = false
|
||||
var digit_is_sub = false
|
||||
|
||||
|
||||
static func from_array(values: Array):
|
||||
# Workaround as per: https://github.com/godotengine/godot/issues/19345#issuecomment-471218401
|
||||
var params = load("res://addons/godot_xterm/parser/params.gd").new()
|
||||
if values.empty():
|
||||
return params
|
||||
# skip leading sub params
|
||||
for i in range(values.size()):
|
||||
var value = values[i]
|
||||
if typeof(value) == TYPE_ARRAY:
|
||||
if i == 0:
|
||||
# skip leading sub params
|
||||
continue
|
||||
else:
|
||||
for sub_param in value:
|
||||
params.add_sub_param(sub_param)
|
||||
else:
|
||||
params.add_param(value)
|
||||
return params
|
||||
|
||||
|
||||
func _init(max_length: int = 32, max_sub_params_length: int = 32):
|
||||
_max_length = max_length
|
||||
_max_sub_params_length = max_sub_params_length
|
||||
|
||||
if (max_sub_params_length > MAX_SUBPARAMS):
|
||||
push_error("max_sub_params_length must not be greater than 256")
|
||||
|
||||
params.resize(max_length)
|
||||
sub_params.resize(max_sub_params_length)
|
||||
sub_params_idx.resize(max_length)
|
||||
|
||||
func add_param(value: int):
|
||||
digit_is_sub = false
|
||||
if length >= _max_length:
|
||||
_reject_digits = true
|
||||
return
|
||||
if value < -1:
|
||||
push_error('values lesser than -1 are not allowed')
|
||||
sub_params_idx[length] = sub_params_length << 8 | sub_params_length
|
||||
params[length] = MAX_VALUE if value > MAX_VALUE else value
|
||||
length += 1
|
||||
|
||||
func add_sub_param(value: int):
|
||||
digit_is_sub = true
|
||||
if !length:
|
||||
return
|
||||
if _reject_digits or sub_params_length >= _max_sub_params_length:
|
||||
_reject_sub_digits = true
|
||||
return
|
||||
if value < -1:
|
||||
push_error('values lesser than -1 are not allowed')
|
||||
sub_params[sub_params_length] = MAX_VALUE if value > MAX_VALUE else value
|
||||
sub_params_length += 1
|
||||
sub_params_idx[length - 1] += 1
|
||||
|
||||
func add_digit(value: int):
|
||||
print("adding digit: ", value, " is sub: ", digit_is_sub)
|
||||
var _length = sub_params_length if digit_is_sub else length
|
||||
if _reject_digits or (not _length) or (digit_is_sub and _reject_sub_digits):
|
||||
return
|
||||
var store = sub_params if digit_is_sub else params
|
||||
var cur = store[_length - 1]
|
||||
store[_length - 1] = min(cur * 10 + value, MAX_VALUE) if ~cur else value
|
||||
|
||||
func to_array():
|
||||
var res = []
|
||||
for i in range(length):
|
||||
res.append(params[i])
|
||||
var start = sub_params_idx[i] >> 8
|
||||
var end = sub_params_idx[i] & 0xff
|
||||
if end - start > 0:
|
||||
res.append(sub_params.slice(start, end - 1))
|
||||
return res
|
||||
|
||||
func reset():
|
||||
length = 0
|
||||
sub_params_length = 0
|
||||
_reject_digits = false
|
||||
_reject_sub_digits = false
|
||||
digit_is_sub = false
|
26
addons/godot_xterm/parser/transition_table.gd
Normal file
26
addons/godot_xterm/parser/transition_table.gd
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors.
|
||||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# License MIT
|
||||
extends Reference
|
||||
|
||||
enum TableAccess {
|
||||
TRANSITION_ACTION_SHIFT = 4,
|
||||
TRANSITION_STATE_MASK = 15,
|
||||
INDEX_STATE_SHIFT = 8
|
||||
}
|
||||
|
||||
var table: PoolByteArray = PoolByteArray()
|
||||
|
||||
func _init(length: int):
|
||||
table.resize(length)
|
||||
|
||||
func setDefault(action: int, next: int):
|
||||
for i in range(table.size()):
|
||||
table[i] = action << TableAccess.TRANSITION_ACTION_SHIFT | next
|
||||
|
||||
func add(code: int, state: int, action: int, next: int):
|
||||
table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next
|
||||
|
||||
func addMany(codes: Array, state: int, action: int, next: int):
|
||||
for code in codes:
|
||||
add(code, state, action, next)
|
123
addons/godot_xterm/parser/vt500_transition_table.gd
Normal file
123
addons/godot_xterm/parser/vt500_transition_table.gd
Normal file
|
@ -0,0 +1,123 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors.
|
||||
# Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
# License MIT
|
||||
extends "res://addons/godot_xterm/parser/transition_table.gd"
|
||||
|
||||
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||
const ParserState = Constants.ParserState
|
||||
const ParserAction = Constants.ParserAction
|
||||
const NON_ASCII_PRINTABLE = Constants.NON_ASCII_PRINTABLE
|
||||
|
||||
var PRINTABLES = Array(range(0x20, 0x7f)) # 0x20 (SP) included, 0x7f (DEL) excluded.
|
||||
var EXECUTABLES = Array(range(0x00, 0x18)) + [0x19] + Array(range(0x1c, 0x20))
|
||||
|
||||
func _init().(4096):
|
||||
# Set default transition.
|
||||
setDefault(ParserAction.ERROR, ParserState.GROUND)
|
||||
|
||||
# Printables.
|
||||
addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND)
|
||||
|
||||
# Global anywhere rules.
|
||||
for state in ParserState.values():
|
||||
addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
addMany(range(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
addMany(range(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND) # ST as terminator
|
||||
add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE) # ESC
|
||||
add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING) # OSC
|
||||
addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY) # CSI
|
||||
add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY) # DCS
|
||||
|
||||
# Rules for executables and 7f.
|
||||
addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND)
|
||||
addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE)
|
||||
add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE)
|
||||
addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING)
|
||||
addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY)
|
||||
add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY)
|
||||
addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM)
|
||||
add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);
|
||||
addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE)
|
||||
addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE)
|
||||
add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE)
|
||||
addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE)
|
||||
add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);
|
||||
|
||||
# OSC.
|
||||
add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING)
|
||||
addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
|
||||
add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
|
||||
addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND)
|
||||
addMany(range(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING)
|
||||
|
||||
# SOS/PM/APC does nothing.
|
||||
addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND)
|
||||
add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING)
|
||||
# csi entries
|
||||
add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM)
|
||||
addMany(range(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
addMany(range(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND)
|
||||
addMany(range(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
|
||||
addMany(range(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
addMany(range(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE)
|
||||
# esc_intermediate
|
||||
addMany(range(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE)
|
||||
addMany(range(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND)
|
||||
addMany(range(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
|
||||
# dcs entry
|
||||
add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY)
|
||||
addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
|
||||
add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY)
|
||||
addMany(range(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM)
|
||||
addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
|
||||
add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM)
|
||||
addMany(range(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM)
|
||||
addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
|
||||
add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE)
|
||||
addMany(range(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
addMany(range(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(range(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(range(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
|
||||
addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
|
||||
add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH)
|
||||
addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
|
||||
|
||||
# special handling of unicode chars
|
||||
add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE)
|
||||
add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH)
|
||||
|
||||
return table
|
7
addons/godot_xterm/plugin.cfg
Normal file
7
addons/godot_xterm/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="GodotXterm"
|
||||
description="Xterm.js for Godot"
|
||||
author="Leroy Hopson"
|
||||
version="0.1.0"
|
||||
script="plugin.gd"
|
14
addons/godot_xterm/plugin.gd
Normal file
14
addons/godot_xterm/plugin.gd
Normal file
|
@ -0,0 +1,14 @@
|
|||
tool
|
||||
extends EditorPlugin
|
||||
|
||||
|
||||
func _enter_tree():
|
||||
var script = preload("res://addons/godot_xterm/terminal.gd")
|
||||
var texture = preload("res://addons/godot_xterm/icon.svg")
|
||||
add_custom_type("Terminal", "Control", script, texture)
|
||||
pass
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
remove_custom_type("Terminal")
|
||||
pass
|
367
addons/godot_xterm/terminal.gd
Normal file
367
addons/godot_xterm/terminal.gd
Normal file
|
@ -0,0 +1,367 @@
|
|||
# Copyright (c) 2020 The GodotXterm authors. All rights reserved.
|
||||
# License MIT
|
||||
tool
|
||||
extends Control
|
||||
|
||||
|
||||
signal data_sent(data)
|
||||
|
||||
const Const = preload("res://addons/godot_xterm/Constants.gd")
|
||||
const Constants = preload("res://addons/godot_xterm/parser/constants.gd")
|
||||
const Parser = preload("res://addons/godot_xterm/parser/escape_sequence_parser.gd")
|
||||
const Buffer = preload("res://addons/godot_xterm/buffer.gd")
|
||||
const Decoder = preload("res://addons/godot_xterm/input/text_decoder.gd")
|
||||
const SourceCodeProRegular = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_regular.tres")
|
||||
const SourceCodeProBold = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold.tres")
|
||||
const SourceCodeProItalic = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_italic.tres")
|
||||
const SourceCodeProBoldItalic = preload("res://addons/godot_xterm/fonts/source_code_pro/source_code_pro_bold_italic.tres")
|
||||
|
||||
const C0 = Constants.C0
|
||||
const C1 = Constants.C1
|
||||
const ESCAPE = 27
|
||||
const BACKSPACE = 8
|
||||
const BEEP = 7
|
||||
const SPACE = 32
|
||||
const LEFT_BRACKET = 91
|
||||
const ENTER = 10
|
||||
const BACKSPACE_ALT = 127
|
||||
|
||||
export (Font) var normal_font = SourceCodeProRegular setget _set_normal_font
|
||||
export (Font) var bold_font = SourceCodeProBold setget _set_bold_font
|
||||
export (Font) var italic_font = SourceCodeProItalic setget _set_italics_font
|
||||
export (Font) var bold_italic_font = SourceCodeProBoldItalic setget _set_bold_italics_font
|
||||
var buffer
|
||||
var alternate_buffer
|
||||
var parser
|
||||
var decoder
|
||||
var cols = 80
|
||||
var rows = 24
|
||||
var cell: Vector2
|
||||
|
||||
# font flags
|
||||
export(int, FLAGS,
|
||||
"Bold",
|
||||
"Italic", # Not xterm-256color
|
||||
"Underlined",
|
||||
"Blink",
|
||||
"Inverse",
|
||||
"Invisible",
|
||||
"Strikethrough" # Not xterm-256color
|
||||
) var font_flags = Const.FONT_NORMAL
|
||||
|
||||
|
||||
func _init():
|
||||
pass
|
||||
|
||||
|
||||
func _set_normal_font(font: Font) -> void:
|
||||
normal_font = font
|
||||
_calculate_cell_size()
|
||||
|
||||
|
||||
func _set_bold_font(font: Font) -> void:
|
||||
bold_font = font
|
||||
_calculate_cell_size()
|
||||
|
||||
|
||||
func _set_italics_font(font: Font) -> void:
|
||||
italic_font = font
|
||||
_calculate_cell_size()
|
||||
|
||||
|
||||
func _set_bold_italics_font(font: Font) -> void:
|
||||
bold_italic_font = font
|
||||
_calculate_cell_size()
|
||||
|
||||
|
||||
func _calculate_cell_size() -> void:
|
||||
var x = 0.0
|
||||
var y = 0.0
|
||||
var fonts = [normal_font, bold_font, italic_font, bold_italic_font]
|
||||
for font in fonts:
|
||||
if not font:
|
||||
continue
|
||||
var size = font.get_string_size("W")
|
||||
x = max(x, size.x)
|
||||
y = max(y, size.y)
|
||||
cell.x = x
|
||||
cell.y = y
|
||||
|
||||
|
||||
func _ready():
|
||||
_calculate_cell_size()
|
||||
var rect = get_rect()
|
||||
var rs = rect_size
|
||||
cols = (rect_size.x / cell.x) as int
|
||||
rows = (rect_size.y / cell.y) as int
|
||||
|
||||
decoder = Decoder.Utf8ToUtf32.new()
|
||||
|
||||
buffer = Buffer.new(rows, cols)
|
||||
alternate_buffer = Buffer.new(rows, cols, true)
|
||||
|
||||
parser = Parser.new()
|
||||
|
||||
# Print handler
|
||||
parser.set_print_handler(buffer, "insert_at_cursor")
|
||||
|
||||
# Execute handlers
|
||||
parser.set_execute_handler(C0.BEL, self, 'bell')
|
||||
parser.set_execute_handler(C0.LF, buffer, 'line_feed')
|
||||
parser.set_execute_handler(C0.VT, buffer, 'line_feed')
|
||||
parser.set_execute_handler(C0.FF, buffer, 'line_feed')
|
||||
parser.set_execute_handler(C0.CR, buffer, 'carriage_return')
|
||||
parser.set_execute_handler(C0.BS, buffer, 'backspace')
|
||||
parser.set_execute_handler(C0.HT, buffer, 'insert_tab');
|
||||
parser.set_execute_handler(C0.SO, self, 'shift_out')
|
||||
parser.set_execute_handler(C0.SI, self, 'shift_in')
|
||||
parser.set_execute_handler(C1.IND, self, 'index')
|
||||
parser.set_execute_handler(C1.NEL, self, 'next_line')
|
||||
parser.set_execute_handler(C1.HTS, self, 'tab_set')
|
||||
|
||||
# CSI handlers
|
||||
parser.set_csi_handler({'final': '@'}, self, 'insert_chars')
|
||||
parser.set_csi_handler({'intermediates': ' ', 'final': '@'}, self, 'scroll_left')
|
||||
parser.set_csi_handler({'final': 'A'}, self, 'cursor_up')
|
||||
parser.set_csi_handler({'intermediates': ' ', 'final': 'A'}, self, 'scroll_right')
|
||||
parser.set_csi_handler({'final': 'B'}, self, 'cursor_down')
|
||||
parser.set_csi_handler({'final': 'C'}, self, 'cursor_forward')
|
||||
parser.set_csi_handler({'final': 'D'}, self, 'cursor_backward')
|
||||
parser.set_csi_handler({'final': 'E'}, self, 'cursor_nextLine')
|
||||
parser.set_csi_handler({'final': 'F'}, self, 'cursor_precedingLine')
|
||||
parser.set_csi_handler({'final': 'G'}, self, 'cursor_charAbsolute')
|
||||
parser.set_csi_handler({'final': 'H'}, buffer, 'cursor_position')
|
||||
parser.set_csi_handler({'final': 'I'}, self, 'cursor_forward_tab')
|
||||
parser.set_csi_handler({'final': 'J'}, self, 'erase_in_display')
|
||||
parser.set_csi_handler({'prefix': '?', 'final': 'J'}, self, 'erase_in_display')
|
||||
parser.set_csi_handler({'final': 'K'}, self, 'erase_in_line')
|
||||
parser.set_csi_handler({'prefix': '?', 'final': 'K'}, self, 'erase_in_line')
|
||||
parser.set_csi_handler({'final': 'L'}, self, 'insert_lines')
|
||||
parser.set_csi_handler({'final': 'M'}, self, 'delete_lines')
|
||||
parser.set_csi_handler({'final': 'P'}, self, 'delete_chars')
|
||||
parser.set_csi_handler({'final': 'S'}, self, 'scroll_up')
|
||||
parser.set_csi_handler({'final': 'T'}, self, 'scroll_down')
|
||||
parser.set_csi_handler({'final': 'X'}, self, 'erase_chars')
|
||||
parser.set_csi_handler({'final': 'Z'}, self, 'cursor_backward_tab')
|
||||
parser.set_csi_handler({'final': '`'}, self, 'char_pos_absolute')
|
||||
parser.set_csi_handler({'final': 'a'}, self, 'h_position_relative')
|
||||
parser.set_csi_handler({'final': 'b'}, self, 'repeat_preceding_character')
|
||||
parser.set_csi_handler({'final': 'c'}, self, 'send_device_attributes_primary')
|
||||
parser.set_csi_handler({'prefix': '>', 'final': 'c'}, self, 'send_device_attributes_secondary')
|
||||
parser.set_csi_handler({'final': 'd'}, self, 'line_pos_absolute')
|
||||
parser.set_csi_handler({'final': 'e'}, self, 'v_position_relative')
|
||||
parser.set_csi_handler({'final': 'f'}, self, 'h_v_position')
|
||||
parser.set_csi_handler({'final': 'g'}, self, 'tab_clear')
|
||||
parser.set_csi_handler({'final': 'h'}, self, 'set_mode')
|
||||
parser.set_csi_handler({'prefix': '?', 'final': 'h'}, self, 'set_mode_private')
|
||||
parser.set_csi_handler({'final': 'l'}, self, 'reset_mode')
|
||||
parser.set_csi_handler({'prefix': '?', 'final': 'l'}, self, 'reset_mode_private')
|
||||
parser.set_csi_handler({'final': 'm'}, self, 'char_attributes')
|
||||
parser.set_csi_handler({'final': 'n'}, self, 'device_status')
|
||||
parser.set_csi_handler({'prefix': '?', 'final': 'n'}, self, 'device_status_private')
|
||||
parser.set_csi_handler({'intermediates': '!', 'final': 'p'}, self, 'soft_reset')
|
||||
parser.set_csi_handler({'intermediates': ' ', 'final': 'q'}, self, 'set_cursor_style')
|
||||
parser.set_csi_handler({'final': 'r'}, self, 'set_scroll_region')
|
||||
parser.set_csi_handler({'final': 's'}, self, 'save_cursor')
|
||||
parser.set_csi_handler({'final': 't'}, self, 'window_options')
|
||||
parser.set_csi_handler({'final': 'u'}, self, 'restore_cursor')
|
||||
parser.set_csi_handler({'intermediates': '\'', 'final': '}'}, self, 'insert_columns')
|
||||
parser.set_csi_handler({'intermediates': '\'', 'final': '~'}, self, 'delete_columns')
|
||||
|
||||
func print(data, start, end):
|
||||
print(data.substr(start, end))
|
||||
|
||||
func bell():
|
||||
print("The bell signal was emited!")
|
||||
|
||||
func line_feed():
|
||||
pass
|
||||
|
||||
func carriage_return():
|
||||
print("carriage return!")
|
||||
|
||||
func backspace():
|
||||
print("backspace!")
|
||||
pass
|
||||
|
||||
func tab():
|
||||
pass
|
||||
|
||||
func shift_out():
|
||||
pass
|
||||
|
||||
func shift_in():
|
||||
pass
|
||||
|
||||
func index():
|
||||
pass
|
||||
|
||||
func next_line():
|
||||
pass
|
||||
|
||||
func tab_set():
|
||||
pass
|
||||
|
||||
func insert_chars(params):
|
||||
pass
|
||||
|
||||
func scroll_left(params):
|
||||
pass
|
||||
func cursor_up(params):
|
||||
pass
|
||||
func scroll_right(params):
|
||||
pass
|
||||
func cursor_down(params):
|
||||
pass
|
||||
func cursor_forward(params):
|
||||
pass
|
||||
func cursor_backward(params):
|
||||
pass
|
||||
func cursor_next_line(params):
|
||||
pass
|
||||
func cursor_preceding_line(params):
|
||||
pass
|
||||
func cursor_char_absolute(params):
|
||||
pass
|
||||
func cursor_position(params):
|
||||
pass
|
||||
func cursor_forward_tab(params):
|
||||
pass
|
||||
func erase_in_display(params):
|
||||
pass
|
||||
func erase_in_line(params):
|
||||
pass
|
||||
func insert_lines(params):
|
||||
pass
|
||||
func delete_lines(params):
|
||||
pass
|
||||
func delete_chars(params):
|
||||
pass
|
||||
func scroll_up(params):
|
||||
pass
|
||||
func scroll_down(params):
|
||||
pass
|
||||
func erase_chars(params):
|
||||
pass
|
||||
func cursor_backward_tab(params):
|
||||
pass
|
||||
func char_pos_absolute(params):
|
||||
pass
|
||||
func h_position_relative(params):
|
||||
pass
|
||||
func repeat_preceding_character(params):
|
||||
pass
|
||||
func send_device_attributes_primary(params):
|
||||
pass
|
||||
func send_device_attributes_secondary(params):
|
||||
pass
|
||||
func line_pos_absolute(params):
|
||||
pass
|
||||
func v_position_relative(params):
|
||||
pass
|
||||
func h_v_position(params):
|
||||
pass
|
||||
func tab_clear(params):
|
||||
pass
|
||||
func set_mode(params):
|
||||
pass
|
||||
func set_mode_private(params):
|
||||
pass
|
||||
func reset_mode(params):
|
||||
pass
|
||||
func char_attributes(params):
|
||||
pass
|
||||
func device_status(params):
|
||||
pass
|
||||
func device_status_private(params):
|
||||
pass
|
||||
func soft_reset(params):
|
||||
pass
|
||||
func set_cursor_style(params):
|
||||
pass
|
||||
func set_scroll_region(params):
|
||||
pass
|
||||
func save_cursor(params):
|
||||
pass
|
||||
func window_options(params):
|
||||
pass
|
||||
func restore_cursor(params):
|
||||
pass
|
||||
func insert_columns(params):
|
||||
pass
|
||||
func delete_columns(params):
|
||||
pass
|
||||
|
||||
func _input(event):
|
||||
if event is InputEventKey and event.pressed:
|
||||
accept_event()
|
||||
|
||||
# TODO: Handle more of these.
|
||||
if (event.control and event.scancode == KEY_C):
|
||||
send_data(PoolByteArray([3]))
|
||||
elif event.unicode:
|
||||
send_data(PoolByteArray([event.unicode]))
|
||||
elif event.scancode == KEY_ENTER:
|
||||
send_data(PoolByteArray([ENTER]))
|
||||
elif event.scancode == KEY_BACKSPACE:
|
||||
send_data(PoolByteArray([BACKSPACE_ALT]))
|
||||
elif event.scancode == KEY_ESCAPE:
|
||||
send_data(PoolByteArray([27]))
|
||||
elif event.scancode == KEY_TAB:
|
||||
send_data(PoolByteArray([9]))
|
||||
elif OS.get_scancode_string(event.scancode) == "Shift":
|
||||
pass
|
||||
elif OS.get_scancode_string(event.scancode) == "Control":
|
||||
pass
|
||||
else:
|
||||
push_warning('Unhandled input. scancode: ' + str(OS.get_scancode_string(event.scancode)))
|
||||
|
||||
|
||||
func send_data(data: PoolByteArray):
|
||||
emit_signal("data_sent", data)
|
||||
|
||||
|
||||
func _draw():
|
||||
# Draw the terminal background
|
||||
draw_rect(get_rect(), Color(0.0, 0.5, 0.0))
|
||||
|
||||
# Naive method. Draw the entire buffer starting with row 0.
|
||||
for row in range(buffer.rows.size()):
|
||||
#print("Doing the thing for row: ", row)
|
||||
# Draw each CharacterData.
|
||||
for col in range(buffer.rows[row].size()):
|
||||
var data = buffer.rows[row][col]
|
||||
#print("row: ", ((row + 1) * charHeight), " col: ", (col * charWidth))
|
||||
_draw_character(col, row, data)
|
||||
|
||||
# Draw the cursor.
|
||||
_draw_cursor()
|
||||
|
||||
|
||||
func _draw_character(col, row, data):
|
||||
# Draw the background.
|
||||
draw_rect(Rect2(Vector2(col * cell.x, row * cell.y), Vector2(cell.x, cell.y)), data.bg)
|
||||
|
||||
var font
|
||||
if data.ff & (1 << Const.FONT_BOLD) and data.ff & (1 << Const.FONT_ITALIC):
|
||||
font = bold_italic_font
|
||||
elif data.ff & (1 << Const.FONT_BOLD):
|
||||
font = bold_font
|
||||
elif data.ff & (1 << Const.FONT_ITALIC):
|
||||
font = italic_font
|
||||
else:
|
||||
font = normal_font
|
||||
|
||||
# Draw the character using foreground color.
|
||||
draw_char(font, Vector2(col * cell.x, (row + 1) * cell.y), data.ch, '', data.fg)
|
||||
|
||||
|
||||
func _draw_cursor():
|
||||
draw_rect(Rect2(Vector2(buffer.ccol * cell.x, buffer.crow * cell.y), Vector2(cell.x, cell.y)), Color(1.0, 0.0, 1.0))
|
||||
|
||||
|
||||
func receive_data(data: PoolByteArray):
|
||||
var utf32 = []
|
||||
var length = decoder.decode(data, utf32)
|
||||
parser.parse(utf32, length)
|
||||
update()
|
347
addons/gut/GutScene.gd
Normal file
347
addons/gut/GutScene.gd
Normal file
|
@ -0,0 +1,347 @@
|
|||
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 _start_time = 0.0
|
||||
var _time = 0.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
|
||||
|
||||
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 = OS.get_unix_time() - _start_time
|
||||
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):
|
||||
_start_time = OS.get_unix_time()
|
||||
_time = _start_time
|
||||
_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(max(value, 1))
|
||||
|
||||
func set_progress_script_value(value):
|
||||
_progress.script.set_value(value)
|
||||
|
||||
func set_progress_test_max(value):
|
||||
_progress.test.set_max(max(value, 1))
|
||||
|
||||
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()
|
||||
set_position(Vector2(0, 0))
|
||||
|
299
addons/gut/GutScene.tscn
Normal file
299
addons/gut/GutScene.tscn
Normal file
|
@ -0,0 +1,299 @@
|
|||
[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]
|
||||
bg_color = Color( 0.193863, 0.205501, 0.214844, 1 )
|
||||
corner_radius_top_left = 20
|
||||
corner_radius_top_right = 20
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id=2]
|
||||
bg_color = Color( 1, 1, 1, 1 )
|
||||
border_color = Color( 0, 0, 0, 1 )
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
|
||||
[sub_resource type="Theme" id=3]
|
||||
resource_local_to_scene = true
|
||||
Panel/styles/panel = SubResource( 2 )
|
||||
Panel/styles/panelf = null
|
||||
Panel/styles/panelnc = null
|
||||
|
||||
[node name="Gut" type="Panel"]
|
||||
margin_right = 740.0
|
||||
margin_bottom = 320.0
|
||||
rect_min_size = Vector2( 740, 250 )
|
||||
custom_styles/panel = SubResource( 1 )
|
||||
script = ExtResource( 1 )
|
||||
|
||||
[node name="TitleBar" type="Panel" parent="."]
|
||||
anchor_right = 1.0
|
||||
margin_bottom = 40.0
|
||||
theme = SubResource( 3 )
|
||||
|
||||
[node name="Title" type="Label" parent="TitleBar"]
|
||||
anchor_right = 1.0
|
||||
margin_bottom = 40.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "Gut"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="Time" type="Label" parent="TitleBar"]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -114.0
|
||||
margin_right = -53.0
|
||||
margin_bottom = 40.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "9999.99"
|
||||
valign = 1
|
||||
|
||||
[node name="Maximize" type="Button" parent="TitleBar"]
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
margin_left = -30.0
|
||||
margin_top = 10.0
|
||||
margin_right = -6.0
|
||||
margin_bottom = 30.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "M"
|
||||
flat = true
|
||||
|
||||
[node name="ScriptProgress" type="ProgressBar" parent="."]
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 70.0
|
||||
margin_top = -100.0
|
||||
margin_right = 180.0
|
||||
margin_bottom = -70.0
|
||||
step = 1.0
|
||||
|
||||
[node name="Label" type="Label" parent="ScriptProgress"]
|
||||
margin_left = -70.0
|
||||
margin_right = -10.0
|
||||
margin_bottom = 24.0
|
||||
text = "Scripts"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="TestProgress" type="ProgressBar" parent="."]
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 70.0
|
||||
margin_top = -70.0
|
||||
margin_right = 180.0
|
||||
margin_bottom = -40.0
|
||||
step = 1.0
|
||||
|
||||
[node name="Label" type="Label" parent="TestProgress"]
|
||||
margin_left = -70.0
|
||||
margin_right = -10.0
|
||||
margin_bottom = 24.0
|
||||
text = "Tests"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="TextDisplay" type="Panel" parent="."]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_top = 40.0
|
||||
margin_bottom = -107.0
|
||||
__meta__ = {
|
||||
"_edit_group_": true
|
||||
}
|
||||
|
||||
[node name="RichTextLabel" type="TextEdit" parent="TextDisplay"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
mouse_default_cursor_shape = 0
|
||||
readonly = true
|
||||
syntax_highlighting = true
|
||||
smooth_scrolling = true
|
||||
|
||||
[node name="FocusBlocker" type="Panel" parent="TextDisplay"]
|
||||
self_modulate = Color( 1, 1, 1, 0 )
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_right = -10.0
|
||||
|
||||
[node name="Navigation" type="Panel" parent="."]
|
||||
self_modulate = Color( 1, 1, 1, 0 )
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 220.0
|
||||
margin_top = -100.0
|
||||
margin_right = 580.0
|
||||
|
||||
[node name="Previous" type="Button" parent="Navigation"]
|
||||
margin_left = -30.0
|
||||
margin_right = 50.0
|
||||
margin_bottom = 40.0
|
||||
text = "<"
|
||||
|
||||
[node name="Next" type="Button" parent="Navigation"]
|
||||
margin_left = 230.0
|
||||
margin_right = 310.0
|
||||
margin_bottom = 40.0
|
||||
text = ">"
|
||||
|
||||
[node name="Run" type="Button" parent="Navigation"]
|
||||
margin_left = 60.0
|
||||
margin_right = 220.0
|
||||
margin_bottom = 40.0
|
||||
text = "Run"
|
||||
|
||||
[node name="CurrentScript" type="Button" parent="Navigation"]
|
||||
margin_left = -30.0
|
||||
margin_top = 50.0
|
||||
margin_right = 310.0
|
||||
margin_bottom = 90.0
|
||||
text = "res://test/unit/test_gut.gd"
|
||||
clip_text = true
|
||||
|
||||
[node name="ShowScripts" type="Button" parent="Navigation"]
|
||||
margin_left = 320.0
|
||||
margin_top = 50.0
|
||||
margin_right = 360.0
|
||||
margin_bottom = 90.0
|
||||
text = "..."
|
||||
|
||||
[node name="LogLevelSlider" type="HSlider" parent="."]
|
||||
anchor_top = 1.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 )
|
||||
max_value = 2.0
|
||||
tick_count = 3
|
||||
ticks_on_borders = true
|
||||
|
||||
[node name="Label" type="Label" parent="LogLevelSlider"]
|
||||
margin_left = -35.0
|
||||
margin_top = 5.0
|
||||
margin_right = 25.0
|
||||
margin_bottom = 25.0
|
||||
rect_scale = Vector2( 0.5, 0.5 )
|
||||
text = "Log Level"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="ScriptsList" type="ItemList" parent="."]
|
||||
anchor_bottom = 1.0
|
||||
margin_left = 180.0
|
||||
margin_top = 40.0
|
||||
margin_right = 620.0
|
||||
margin_bottom = -108.0
|
||||
allow_reselect = true
|
||||
|
||||
[node name="ExtraOptions" type="Panel" parent="."]
|
||||
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
|
||||
custom_styles/panel = SubResource( 1 )
|
||||
|
||||
[node name="IgnorePause" type="CheckBox" parent="ExtraOptions"]
|
||||
margin_left = 10.0
|
||||
margin_top = 10.0
|
||||
margin_right = 128.0
|
||||
margin_bottom = 34.0
|
||||
rect_scale = Vector2( 1.5, 1.5 )
|
||||
text = "Ignore Pauses"
|
||||
|
||||
[node name="DisableBlocker" type="CheckBox" parent="ExtraOptions"]
|
||||
margin_left = 10.0
|
||||
margin_top = 50.0
|
||||
margin_right = 130.0
|
||||
margin_bottom = 74.0
|
||||
rect_scale = Vector2( 1.5, 1.5 )
|
||||
text = "Selectable"
|
||||
|
||||
[node name="Copy" type="Button" parent="ExtraOptions"]
|
||||
margin_left = 20.0
|
||||
margin_top = 90.0
|
||||
margin_right = 200.0
|
||||
margin_bottom = 130.0
|
||||
text = "Copy"
|
||||
|
||||
[node name="ResizeHandle" type="Control" parent="."]
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
margin_left = -40.0
|
||||
margin_top = -40.0
|
||||
|
||||
[node name="Continue" type="Panel" parent="."]
|
||||
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
|
||||
|
||||
[node name="Continue" type="Button" parent="Continue"]
|
||||
margin_top = 50.0
|
||||
margin_right = 119.0
|
||||
margin_bottom = 90.0
|
||||
disabled = true
|
||||
text = "Continue"
|
||||
|
||||
[node name="ShowExtras" type="Button" parent="Continue"]
|
||||
margin_left = 50.0
|
||||
margin_right = 120.0
|
||||
margin_bottom = 40.0
|
||||
rect_pivot_offset = Vector2( 35, 20 )
|
||||
toggle_mode = true
|
||||
text = "_"
|
||||
|
||||
[node name="Summary" type="Node2D" parent="."]
|
||||
position = Vector2( 0, 3 )
|
||||
|
||||
[node name="Passing" type="Label" parent="Summary"]
|
||||
margin_top = 10.0
|
||||
margin_right = 40.0
|
||||
margin_bottom = 24.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "0"
|
||||
align = 1
|
||||
valign = 1
|
||||
|
||||
[node name="Failing" type="Label" parent="Summary"]
|
||||
margin_left = 40.0
|
||||
margin_top = 10.0
|
||||
margin_right = 80.0
|
||||
margin_bottom = 24.0
|
||||
custom_colors/font_color = Color( 0, 0, 0, 1 )
|
||||
text = "0"
|
||||
align = 1
|
||||
valign = 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"]
|
22
addons/gut/LICENSE.md
Normal file
22
addons/gut/LICENSE.md
Normal 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.
|
6
addons/gut/double_templates/function_template.gd
Normal file
6
addons/gut/double_templates/function_template.gd
Normal file
|
@ -0,0 +1,6 @@
|
|||
{func_decleration}
|
||||
__gut_spy('{method_name}', {param_array})
|
||||
if(__gut_should_call_super('{method_name}', {param_array})):
|
||||
return {super_call}
|
||||
else:
|
||||
return __gut_get_stubbed_return('{method_name}', {param_array})
|
36
addons/gut/double_templates/script_template.gd
Normal file
36
addons/gut/double_templates/script_template.gd
Normal file
|
@ -0,0 +1,36 @@
|
|||
{extends}
|
||||
|
||||
var __gut_metadata_ = {
|
||||
path = '{path}',
|
||||
subpath = '{subpath}',
|
||||
stubber = __gut_instance_from_id({stubber_id}),
|
||||
spy = __gut_instance_from_id({spy_id}),
|
||||
}
|
||||
|
||||
func __gut_instance_from_id(inst_id):
|
||||
if(inst_id == -1):
|
||||
return null
|
||||
else:
|
||||
return instance_from_id(inst_id)
|
||||
|
||||
func __gut_should_call_super(method_name, called_with):
|
||||
if(__gut_metadata_.stubber != null):
|
||||
return __gut_metadata_.stubber.should_call_super(self, method_name, called_with)
|
||||
else:
|
||||
return false
|
||||
|
||||
var __gut_utils_ = load('res://addons/gut/utils.gd').new()
|
||||
|
||||
func __gut_spy(method_name, called_with):
|
||||
if(__gut_metadata_.spy != null):
|
||||
__gut_metadata_.spy.add_call(self, method_name, called_with)
|
||||
|
||||
func __gut_get_stubbed_return(method_name, called_with):
|
||||
if(__gut_metadata_.stubber != null):
|
||||
return __gut_metadata_.stubber.get_return(self, method_name, called_with)
|
||||
else:
|
||||
return null
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Methods start here
|
||||
# ------------------------------------------------------------------------------
|
525
addons/gut/doubler.gd
Normal file
525
addons/gut/doubler.gd
Normal file
|
@ -0,0 +1,525 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# Utility class to hold the local and built in methods separately. 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.
|
||||
'draw_mesh', # issue with one parameter, value is `Null((..), (..), (..))``
|
||||
'_to_string', # nonexistant function ._to_string
|
||||
'_get_minimum_size', # Nonexistent function _get_minimum_size
|
||||
]
|
||||
|
||||
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()
|
||||
var _method_strategy = null
|
||||
var make_partial_double = false
|
||||
var scene_path = null
|
||||
var _native_class = null
|
||||
var _native_class_instance = null
|
||||
|
||||
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():
|
||||
var to_return = null
|
||||
if(is_native()):
|
||||
to_return = _native_class.new()
|
||||
else:
|
||||
to_return = get_loaded_class().new()
|
||||
return to_return
|
||||
|
||||
# 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 = null
|
||||
if(is_native()):
|
||||
extend = str("extends ", get_native_class_name())
|
||||
else:
|
||||
extend = str("extends '", get_path(), "'")
|
||||
|
||||
if(has_subpath()):
|
||||
extend += str('.', get_subpath().replace('/', '.'))
|
||||
|
||||
return extend
|
||||
|
||||
func get_method_strategy():
|
||||
return _method_strategy
|
||||
|
||||
func set_method_strategy(method_strategy):
|
||||
_method_strategy = method_strategy
|
||||
|
||||
func is_native():
|
||||
return _native_class != null
|
||||
|
||||
func set_native_class(native_class):
|
||||
_native_class = native_class
|
||||
_native_class_instance = native_class.new()
|
||||
_path = _native_class_instance.get_class()
|
||||
|
||||
func get_native_class_name():
|
||||
return _native_class_instance.get_class()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Allows for interacting with a file but only creating a string. This was done
|
||||
# to ease the transition from files being created for doubles to loading
|
||||
# doubles from a string. This allows the files to be created for debugging
|
||||
# purposes since reading a file is easier than reading a dumped out string.
|
||||
# ------------------------------------------------------------------------------
|
||||
class FileOrString:
|
||||
extends File
|
||||
|
||||
var _do_file = false
|
||||
var _contents = ''
|
||||
var _path = null
|
||||
|
||||
func open(path, mode):
|
||||
_path = path
|
||||
if(_do_file):
|
||||
return .open(path, mode)
|
||||
else:
|
||||
return OK
|
||||
|
||||
func close():
|
||||
if(_do_file):
|
||||
return .close()
|
||||
|
||||
func store_string(s):
|
||||
if(_do_file):
|
||||
.store_string(s)
|
||||
_contents += s
|
||||
|
||||
func get_contents():
|
||||
return _contents
|
||||
|
||||
func get_path():
|
||||
return _path
|
||||
|
||||
func load_it():
|
||||
if(_contents != ''):
|
||||
var script = GDScript.new()
|
||||
script.set_source_code(get_contents())
|
||||
script.reload()
|
||||
return script
|
||||
else:
|
||||
return load(_path)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# A stroke of genius if I do say so. This allows for doubling a scene without
|
||||
# having to write any files. By overloading instance we can make whatever
|
||||
# we want.
|
||||
# ------------------------------------------------------------------------------
|
||||
class PackedSceneDouble:
|
||||
extends PackedScene
|
||||
var _script = null
|
||||
var _scene = null
|
||||
|
||||
func set_script_obj(obj):
|
||||
_script = obj
|
||||
|
||||
func instance(edit_state=0):
|
||||
var inst = _scene.instance(edit_state)
|
||||
if(_script != null):
|
||||
inst.set_script(_script)
|
||||
return inst
|
||||
|
||||
func load_scene(path):
|
||||
_scene = load(path)
|
||||
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# START Doubler
|
||||
# ------------------------------------------------------------------------------
|
||||
var _utils = load('res://addons/gut/utils.gd').new()
|
||||
|
||||
var _ignored_methods = _utils.OneToMany.new()
|
||||
var _stubber = _utils.Stubber.new()
|
||||
var _lgr = _utils.get_logger()
|
||||
var _method_maker = _utils.MethodMaker.new()
|
||||
|
||||
var _output_dir = 'user://gut_temp_directory'
|
||||
var _double_count = 0 # used in making files names unique
|
||||
var _spy = null
|
||||
var _strategy = null
|
||||
var _base_script_text = _utils.get_file_as_text('res://addons/gut/double_templates/script_template.gd')
|
||||
var _make_files = false
|
||||
|
||||
# These methods all call super implicitly. Stubbing them to call super causes
|
||||
# super to be called twice.
|
||||
var _non_super_methods = [
|
||||
"_init",
|
||||
"_ready",
|
||||
"_notification",
|
||||
"_enter_world",
|
||||
"_exit_world",
|
||||
"_process",
|
||||
"_physics_process",
|
||||
"_exit_tree",
|
||||
"_gui_input ",
|
||||
]
|
||||
|
||||
func _init(strategy=_utils.DOUBLE_STRATEGY.PARTIAL):
|
||||
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 _stub_to_call_super(obj_info, method_name):
|
||||
if(_non_super_methods.has(method_name)):
|
||||
return
|
||||
var path = obj_info.get_path()
|
||||
if(obj_info.scene_path != null):
|
||||
path = obj_info.scene_path
|
||||
var params = _utils.StubParams.new(path, method_name, obj_info.get_subpath())
|
||||
params.to_call_super()
|
||||
_stubber.add_stub(params)
|
||||
|
||||
func _get_base_script_text(obj_info, override_path):
|
||||
var path = obj_info.get_path()
|
||||
if(override_path != null):
|
||||
path = override_path
|
||||
|
||||
var stubber_id = -1
|
||||
if(_stubber != null):
|
||||
stubber_id = _stubber.get_instance_id()
|
||||
|
||||
var spy_id = -1
|
||||
if(_spy != null):
|
||||
spy_id = _spy.get_instance_id()
|
||||
|
||||
var values = {
|
||||
"path":path,
|
||||
"subpath":obj_info.get_subpath(),
|
||||
"stubber_id":stubber_id,
|
||||
"spy_id":spy_id,
|
||||
"extends":obj_info.get_extends_text()
|
||||
}
|
||||
return _base_script_text.format(values)
|
||||
|
||||
func _write_file(obj_info, dest_path, override_path=null):
|
||||
var base_script = _get_base_script_text(obj_info, override_path)
|
||||
var script_methods = _get_methods(obj_info)
|
||||
|
||||
var f = FileOrString.new()
|
||||
f._do_file = _make_files
|
||||
var f_result = f.open(dest_path, f.WRITE)
|
||||
|
||||
if(f_result != OK):
|
||||
_lgr.error(str('Error creating file ', dest_path))
|
||||
_lgr.error(str('Could not create double for :', obj_info.to_s()))
|
||||
return
|
||||
|
||||
f.store_string(base_script)
|
||||
|
||||
for i in range(script_methods.local_methods.size()):
|
||||
if(obj_info.make_partial_double):
|
||||
_stub_to_call_super(obj_info, script_methods.local_methods[i].name)
|
||||
f.store_string(_get_func_text(script_methods.local_methods[i]))
|
||||
|
||||
for i in range(script_methods.built_ins.size()):
|
||||
_stub_to_call_super(obj_info, script_methods.built_ins[i].name)
|
||||
f.store_string(_get_func_text(script_methods.built_ins[i]))
|
||||
|
||||
f.close()
|
||||
return f
|
||||
|
||||
func _double_scene_and_script(scene_info):
|
||||
var to_return = PackedSceneDouble.new()
|
||||
to_return.load_scene(scene_info.get_path())
|
||||
|
||||
var inst = load(scene_info.get_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)
|
||||
oi.set_method_strategy(scene_info.get_method_strategy())
|
||||
oi.make_partial_double = scene_info.make_partial_double
|
||||
oi.scene_path = scene_info.get_path()
|
||||
to_return.set_script_obj(_double(oi, scene_info.get_path()).load_it())
|
||||
|
||||
return to_return
|
||||
|
||||
func _get_methods(object_info):
|
||||
var obj = object_info.instantiate()
|
||||
# any method in the script or super script
|
||||
var script_methods = ScriptMethods.new()
|
||||
var methods = obj.get_method_list()
|
||||
|
||||
# first pass is for local methods 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 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])):
|
||||
script_methods.add_local_method(methods[i])
|
||||
|
||||
|
||||
if(object_info.get_method_strategy() == _utils.DOUBLE_STRATEGY.FULL):
|
||||
# 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 and !_ignored_methods.has(object_info.get_path(), methods[i]['name'])):
|
||||
script_methods.add_built_in_method(methods[i])
|
||||
|
||||
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_func_text(method_hash):
|
||||
return _method_maker.get_function_text(method_hash) + "\n"
|
||||
|
||||
# returns the path to write the double file to
|
||||
func _get_temp_path(object_info):
|
||||
var file_name = null
|
||||
var extension = null
|
||||
if(object_info.is_native()):
|
||||
file_name = object_info.get_native_class_name()
|
||||
extension = 'gd'
|
||||
else:
|
||||
file_name = object_info.get_path().get_file().get_basename()
|
||||
extension = object_info.get_path().get_extension()
|
||||
|
||||
if(object_info.has_subpath()):
|
||||
file_name += '__' + object_info.get_subpath().replace('/', '__')
|
||||
|
||||
file_name += str('__dbl', _double_count, '__.', extension)
|
||||
|
||||
var to_return = _output_dir.plus_file(file_name)
|
||||
return to_return
|
||||
|
||||
func _load_double(fileOrString):
|
||||
return fileOrString.load_it()
|
||||
|
||||
func _double(obj_info, override_path=null):
|
||||
var temp_path = _get_temp_path(obj_info)
|
||||
var result = _write_file(obj_info, temp_path, override_path)
|
||||
_double_count += 1
|
||||
return result
|
||||
|
||||
func _double_script(path, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(path)
|
||||
oi.make_partial_double = make_partial
|
||||
oi.set_method_strategy(strategy)
|
||||
return _double(oi).load_it()
|
||||
|
||||
func _double_inner(path, subpath, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(path, subpath)
|
||||
oi.set_method_strategy(strategy)
|
||||
oi.make_partial_double = make_partial
|
||||
return _double(oi).load_it()
|
||||
|
||||
func _double_scene(path, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(path)
|
||||
oi.set_method_strategy(strategy)
|
||||
oi.make_partial_double = make_partial
|
||||
return _double_scene_and_script(oi)
|
||||
|
||||
func _double_gdnative(native_class, make_partial, strategy):
|
||||
var oi = ObjectInfo.new(null)
|
||||
oi.set_native_class(native_class)
|
||||
oi.set_method_strategy(strategy)
|
||||
oi.make_partial_double = make_partial
|
||||
return _double(oi).load_it()
|
||||
|
||||
# ###############
|
||||
# Public
|
||||
# ###############
|
||||
func get_output_dir():
|
||||
return _output_dir
|
||||
|
||||
func set_output_dir(output_dir):
|
||||
if(output_dir != null):
|
||||
_output_dir = output_dir
|
||||
if(_make_files):
|
||||
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
|
||||
|
||||
func partial_double_scene(path, strategy=_strategy):
|
||||
return _double_scene(path, true, strategy)
|
||||
|
||||
# double a scene
|
||||
func double_scene(path, strategy=_strategy):
|
||||
return _double_scene(path, false, strategy)
|
||||
|
||||
# double a script/object
|
||||
func double(path, strategy=_strategy):
|
||||
return _double_script(path, false, strategy)
|
||||
|
||||
func partial_double(path, strategy=_strategy):
|
||||
return _double_script(path, true, strategy)
|
||||
|
||||
func partial_double_inner(path, subpath, strategy=_strategy):
|
||||
return _double_inner(path, subpath, true, strategy)
|
||||
|
||||
# double an inner class in a script
|
||||
func double_inner(path, subpath, strategy=_strategy):
|
||||
return _double_inner(path, subpath, false, strategy)
|
||||
|
||||
# must always use FULL strategy since this is a native class and you won't get
|
||||
# any methods if you don't use FULL
|
||||
func double_gdnative(native_class):
|
||||
return _double_gdnative(native_class, false, _utils.DOUBLE_STRATEGY.FULL)
|
||||
|
||||
# must always use FULL strategy since this is a native class and you won't get
|
||||
# any methods if you don't use FULL
|
||||
func partial_double_gdnative(native_class):
|
||||
return _double_gdnative(native_class, true, _utils.DOUBLE_STRATEGY.FULL)
|
||||
|
||||
func clear_output_directory():
|
||||
if(!_make_files):
|
||||
return false
|
||||
|
||||
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 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)
|
||||
|
||||
func add_ignored_method(path, method_name):
|
||||
_ignored_methods.add(path, method_name)
|
||||
|
||||
func get_ignored_methods():
|
||||
return _ignored_methods
|
||||
|
||||
func get_make_files():
|
||||
return _make_files
|
||||
|
||||
func set_make_files(make_files):
|
||||
_make_files = make_files
|
||||
set_output_dir(_output_dir)
|
1343
addons/gut/gut.gd
Normal file
1343
addons/gut/gut.gd
Normal file
File diff suppressed because it is too large
Load diff
366
addons/gut/gut_cmdln.gd
Normal file
366
addons/gut/gut_cmdln.gd
Normal file
|
@ -0,0 +1,366 @@
|
|||
################################################################################
|
||||
#(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.8.2
|
||||
################################################################################
|
||||
extends SceneTree
|
||||
|
||||
|
||||
var Optparse = load('res://addons/gut/optparse.gd')
|
||||
var Gut = load('res://addons/gut/gut.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 _final_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 = [],
|
||||
disable_colors = false,
|
||||
double_strategy = 'partial',
|
||||
ignore_pause = false,
|
||||
include_subdirs = false,
|
||||
inner_class = '',
|
||||
log_level = 1,
|
||||
opacity = 100,
|
||||
post_run_script = '',
|
||||
pre_run_script = '',
|
||||
prefix = 'test_',
|
||||
selected = '',
|
||||
should_exit = false,
|
||||
should_exit_on_success = 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('-gexit_on_success', false, 'Only exit if all tests pass.')
|
||||
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('-gdisable_colors', false, 'Disable command line colors.')
|
||||
opts.add('-gpre_run_script', '', 'pre-run hook script path')
|
||||
opts.add('-gpost_run_script', '', 'post-run hook script path')
|
||||
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.config_file = from.get_value('-gconfig')
|
||||
to.dirs = from.get_value('-gdir')
|
||||
to.disable_colors = from.get_value('-gdisable_colors')
|
||||
to.double_strategy = from.get_value('-gdouble_strategy')
|
||||
to.ignore_pause = from.get_value('-gignore_pause')
|
||||
to.include_subdirs = from.get_value('-ginclude_subdirs')
|
||||
to.inner_class = from.get_value('-ginner_class')
|
||||
to.log_level = from.get_value('-glog')
|
||||
to.opacity = from.get_value('-gopacity')
|
||||
to.post_run_script = from.get_value('-gpost_run_script')
|
||||
to.pre_run_script = from.get_value('-gpre_run_script')
|
||||
to.prefix = from.get_value('-gprefix')
|
||||
to.selected = from.get_value('-gselect')
|
||||
to.should_exit = from.get_value('-gexit')
|
||||
to.should_exit_on_success = from.get_value('-gexit_on_success')
|
||||
to.should_maximize = from.get_value('-gmaximize')
|
||||
to.suffix = from.get_value('-gsuffix')
|
||||
to.tests = from.get_value('-gtest')
|
||||
to.unit_test_name = from.get_value('-gunit_test_name')
|
||||
|
||||
|
||||
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 = Gut.new()
|
||||
get_root().add_child(_tester)
|
||||
_tester.connect('tests_finished', self, '_on_tests_finished', [opts.should_exit, opts.should_exit_on_success])
|
||||
_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)
|
||||
_tester.set_pre_run_script(opts.pre_run_script)
|
||||
_tester.set_post_run_script(opts.post_run_script)
|
||||
_tester.set_color_output(!opts.disable_colors)
|
||||
|
||||
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')):
|
||||
var v_info = Engine.get_version_info()
|
||||
print(str('Godot version: ', v_info.major, '.', v_info.minor, '.', v_info.patch))
|
||||
print(str('GUT version: ', Gut.new().get_version()))
|
||||
|
||||
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:
|
||||
_final_opts = opt_resolver.get_resolved_values();
|
||||
apply_options(_final_opts)
|
||||
_tester.test_scripts(!_run_single)
|
||||
|
||||
# exit if option is set.
|
||||
func _on_tests_finished(should_exit, should_exit_on_success):
|
||||
if(_final_opts.dirs.size() == 0):
|
||||
if(_tester.get_summary().get_totals().scripts == 0):
|
||||
var lgr = _tester.get_logger()
|
||||
lgr.error('No directories configured. Add directories with options or a .gutconfig.json file. Use the -gh option for more information.')
|
||||
|
||||
if(_tester.get_fail_count()):
|
||||
OS.exit_code = 1
|
||||
|
||||
# Overwrite the exit code with the post_script
|
||||
var post_inst = _tester.get_post_run_script_instance()
|
||||
if(post_inst != null and post_inst.get_exit_code() != null):
|
||||
OS.exit_code = post_inst.get_exit_code()
|
||||
|
||||
if(should_exit or (should_exit_on_success and _tester.get_fail_count() == 0)):
|
||||
quit()
|
||||
else:
|
||||
print("Tests finished, exit manually")
|
12
addons/gut/gut_plugin.gd
Normal file
12
addons/gut/gut_plugin.gd
Normal 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")
|
35
addons/gut/hook_script.gd
Normal file
35
addons/gut/hook_script.gd
Normal file
|
@ -0,0 +1,35 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This script is the base for custom scripts to be used in pre and post
|
||||
# run hooks.
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# This is the instance of GUT that is running the tests. You can get
|
||||
# information about the run from this object. This is set by GUT when the
|
||||
# script is instantiated.
|
||||
var gut = null
|
||||
|
||||
# the exit code to be used by gut_cmdln. See set method.
|
||||
var _exit_code = null
|
||||
|
||||
var _should_abort = false
|
||||
# Virtual method that will be called by GUT after instantiating
|
||||
# this script.
|
||||
func run():
|
||||
pass
|
||||
|
||||
# Set the exit code when running from the command line. If not set then the
|
||||
# default exit code will be returned (0 when no tests fail, 1 when any tests
|
||||
# fail).
|
||||
func set_exit_code(code):
|
||||
_exit_code = code
|
||||
|
||||
func get_exit_code():
|
||||
return _exit_code
|
||||
|
||||
# Usable by pre-run script to cause the run to end AFTER the run() method
|
||||
# finishes. post-run script will not be ran.
|
||||
func abort():
|
||||
_should_abort = true
|
||||
|
||||
func should_abort():
|
||||
return _should_abort
|
BIN
addons/gut/icon.png
Normal file
BIN
addons/gut/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 320 B |
34
addons/gut/icon.png.import
Normal file
34
addons/gut/icon.png.import
Normal file
|
@ -0,0 +1,34 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="StreamTexture"
|
||||
path="res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.stex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/gut/icon.png"
|
||||
dest_files=[ "res://.import/icon.png-91b084043b8aaf2f1c906e7b9fa92969.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
|
105
addons/gut/logger.gd
Normal file
105
addons/gut/logger.gd
Normal 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()
|
211
addons/gut/method_maker.gd
Normal file
211
addons/gut/method_maker.gd
Normal file
|
@ -0,0 +1,211 @@
|
|||
# This class will generate method declaration 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 Vector 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
|
||||
# ###############
|
||||
var _func_text = _utils.get_file_as_text('res://addons/gut/double_templates/function_template.gd')
|
||||
|
||||
func _is_supported_default(type_flag):
|
||||
return type_flag >= 0 and type_flag < _supported_defaults.size() and [type_flag] != null
|
||||
|
||||
# Creates a list of parameters 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]), ')')
|
||||
elif(t == TYPE_OBJECT):
|
||||
if(str(method_meta.default_args[i]) == "[Object:null]"):
|
||||
value = str(_supported_defaults[t], 'null')
|
||||
else:
|
||||
value = str(_supported_defaults[t], str(method_meta.default_args[i]).to_lower())
|
||||
|
||||
# Everything else puts the prefix (if one is there) form _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 parameter has a default, a warning message will be
|
||||
# printed and the declaration will return null.
|
||||
func get_function_text(meta):
|
||||
var method_params = _get_arg_text(meta)
|
||||
var text = null
|
||||
|
||||
var param_array = get_spy_call_parameters_text(meta)
|
||||
if(param_array == 'null'):
|
||||
param_array = '[]'
|
||||
|
||||
if(method_params != null):
|
||||
var decleration = str('func ', meta.name, '(', method_params, '):')
|
||||
text = _func_text.format({
|
||||
"func_decleration":decleration,
|
||||
"method_name":meta.name,
|
||||
"param_array":param_array,
|
||||
"super_call":get_super_call_text(meta)
|
||||
})
|
||||
return text
|
||||
|
||||
# creates a call to the function in meta in the super's class.
|
||||
func get_super_call_text(meta):
|
||||
var params = ''
|
||||
|
||||
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
|
38
addons/gut/one_to_many.gd
Normal file
38
addons/gut/one_to_many.gd
Normal file
|
@ -0,0 +1,38 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# This datastructure represents a simple one-to-many relationship. It manages
|
||||
# a dictionary of value/array pairs. It ignores duplicates of both the "one"
|
||||
# and the "many".
|
||||
# ------------------------------------------------------------------------------
|
||||
var _items = {}
|
||||
|
||||
# return the size of _items or the size of an element in _items if "one" was
|
||||
# specified.
|
||||
func size(one=null):
|
||||
var to_return = 0
|
||||
if(one == null):
|
||||
to_return = _items.size()
|
||||
elif(_items.has(one)):
|
||||
to_return = _items[one].size()
|
||||
return to_return
|
||||
|
||||
# Add an element to "one" if it does not already exist
|
||||
func add(one, many_item):
|
||||
if(_items.has(one) and !_items[one].has(many_item)):
|
||||
_items[one].append(many_item)
|
||||
else:
|
||||
_items[one] = [many_item]
|
||||
|
||||
func clear():
|
||||
_items.clear()
|
||||
|
||||
func has(one, many_item):
|
||||
var to_return = false
|
||||
if(_items.has(one)):
|
||||
to_return = _items[one].has(many_item)
|
||||
return to_return
|
||||
|
||||
func to_s():
|
||||
var to_return = ''
|
||||
for key in _items:
|
||||
to_return += str(key, ": ", _items[key], "\n")
|
||||
return to_return
|
250
addons/gut/optparse.gd
Normal file
250
addons/gut/optparse.gd
Normal file
|
@ -0,0 +1,250 @@
|
|||
################################################################################
|
||||
#(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.8.2
|
||||
################################################################################
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 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 = []
|
||||
# an array of arrays. Each element in this array will contain an option
|
||||
# name and if that option contains a value then it will have a sedond
|
||||
# element. For example:
|
||||
# [[-gselect, test.gd], [-gexit]]
|
||||
var _opts = []
|
||||
|
||||
func _init():
|
||||
for i in range(OS.get_cmdline_args().size()):
|
||||
var opt_val = OS.get_cmdline_args()[i].split('=')
|
||||
_opts.append(opt_val)
|
||||
|
||||
# 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):
|
||||
if(full_option.size() > 1):
|
||||
return full_option[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][0] == name):
|
||||
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 specified, 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][0])
|
||||
|
||||
var script_option = to_return.find('-s')
|
||||
if script_option != -1:
|
||||
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(to_pad, size, pad_with=' '):
|
||||
var to_return = to_pad
|
||||
for _i in range(to_pad.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 processed, it has unknown datatype:' + str(t))
|
||||
|
||||
var unused = parser.get_unused_options()
|
||||
if(unused.size() > 0):
|
||||
print("Unrecognized options: ", unused)
|
||||
return false
|
||||
|
||||
return true
|
7
addons/gut/plugin.cfg
Normal file
7
addons/gut/plugin.cfg
Normal file
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Gut"
|
||||
description="Unit Testing tool for Godot."
|
||||
author="Butch Wesley"
|
||||
version="6.8.2"
|
||||
script="gut_plugin.gd"
|
166
addons/gut/signal_watcher.gd
Normal file
166
addons/gut/signal_watcher.gd
Normal 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
|
BIN
addons/gut/source_code_pro.fnt
Normal file
BIN
addons/gut/source_code_pro.fnt
Normal file
Binary file not shown.
96
addons/gut/spy.gd
Normal file
96
addons/gut/spy.gd
Normal 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
|
43
addons/gut/stub_params.gd
Normal file
43
addons/gut/stub_params.gd
Normal file
|
@ -0,0 +1,43 @@
|
|||
var return_val = null
|
||||
var stub_target = null
|
||||
var target_subpath = null
|
||||
var parameters = null
|
||||
var stub_method = null
|
||||
var call_super = false
|
||||
|
||||
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
|
||||
call_super = false
|
||||
return self
|
||||
|
||||
func to_do_nothing():
|
||||
return to_return(null)
|
||||
|
||||
func to_call_super():
|
||||
call_super = true
|
||||
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():
|
||||
var base_string = str(stub_target, '[', target_subpath, '].', stub_method)
|
||||
if(call_super):
|
||||
base_string += " to call SUPER"
|
||||
else:
|
||||
base_string += str(' with (', parameters, ') = ', return_val)
|
||||
return base_string
|
162
addons/gut/stubber.gd
Normal file
162
addons/gut/stubber.gd
Normal file
|
@ -0,0 +1,162 @@
|
|||
# {
|
||||
# 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 and subpath != ''):
|
||||
to_return += str('-', subpath)
|
||||
TYPE_OBJECT:
|
||||
if(_is_instance(obj)):
|
||||
to_return = _make_key_from_metadata(obj)
|
||||
elif(_utils.is_native_class(obj)):
|
||||
to_return = _utils.get_native_class_name(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)
|
||||
|
||||
# Searches returns for an entry that matches the instance or the class that
|
||||
# passed in obj is.
|
||||
#
|
||||
# obj can be an instance, class, or a path.
|
||||
func _find_stub(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]
|
||||
# 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]
|
||||
else:
|
||||
_lgr.warn(str('Call to [', method, '] was not stubbed for the supplied parameters ', parameters, '. Null was returned.'))
|
||||
|
||||
return to_return
|
||||
|
||||
# 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 object was stubbed with no parameters than any parameters will match.
|
||||
# If it was stubbed with specific parameter values then it will try to match.
|
||||
# If the parameters do not match BUT there was also an empty parameter 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
|
||||
# parameters: optional array of parameter vales to find a return value for.
|
||||
func get_return(obj, method, parameters=null):
|
||||
var stub_info = _find_stub(obj, method, parameters)
|
||||
|
||||
if(stub_info != null):
|
||||
return stub_info.return_val
|
||||
else:
|
||||
return null
|
||||
|
||||
func should_call_super(obj, method, parameters=null):
|
||||
var stub_info = _find_stub(obj, method, parameters)
|
||||
if(stub_info != null):
|
||||
return stub_info.call_super
|
||||
else:
|
||||
# this log message is here because of how the generated doubled scripts
|
||||
# are structured. With this log msg here, you will only see one
|
||||
# "unstubbed" info instead of multiple.
|
||||
_lgr.info('Unstubbed call to ' + method + '::' + str(obj))
|
||||
return false
|
||||
|
||||
|
||||
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
|
152
addons/gut/summary.gd
Normal file
152
addons/gut/summary.gd
Normal file
|
@ -0,0 +1,152 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# Contains all the results of a single test. Allows for multiple asserts results
|
||||
# and pending calls.
|
||||
# ------------------------------------------------------------------------------
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Contains all the results for a single test-script/inner class. Persists the
|
||||
# names of the tests and results and the order in which the tests were run.
|
||||
# ------------------------------------------------------------------------------
|
||||
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(obj_name):
|
||||
if(!_tests.has(obj_name)):
|
||||
_tests[obj_name] = Test.new()
|
||||
_test_order.append(obj_name)
|
||||
return _tests[obj_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)
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Summary Class
|
||||
#
|
||||
# This class holds the results of all the test scripts and Inner Classes that
|
||||
# were run.
|
||||
# -------------------------------------------d-----------------------------------
|
||||
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 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
|
1173
addons/gut/test.gd
Normal file
1173
addons/gut/test.gd
Normal file
File diff suppressed because it is too large
Load diff
241
addons/gut/test_collector.gd
Normal file
241
addons/gut/test_collector.gd
Normal file
|
@ -0,0 +1,241 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
# 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(source_path):
|
||||
var to_return = source_path
|
||||
if(!_utils.file_exists(source_path)):
|
||||
_lgr.debug('Checking for remap for: ' + source_path)
|
||||
var remap_path = source_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):
|
||||
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):
|
||||
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
|
43
addons/gut/thing_counter.gd
Normal file
43
addons/gut/thing_counter.gd
Normal 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])
|
160
addons/gut/utils.gd
Normal file
160
addons/gut/utils.gd
Normal file
|
@ -0,0 +1,160 @@
|
|||
var _Logger = load('res://addons/gut/logger.gd') # everything should use get_logger
|
||||
|
||||
var Doubler = load('res://addons/gut/doubler.gd')
|
||||
var Gut = load('res://addons/gut/gut.gd')
|
||||
var HookScript = load('res://addons/gut/hook_script.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')
|
||||
var OneToMany = load('res://addons/gut/one_to_many.gd')
|
||||
|
||||
const GUT_METADATA = '__gut_metadata_'
|
||||
|
||||
enum DOUBLE_STRATEGY{
|
||||
FULL,
|
||||
PARTIAL
|
||||
}
|
||||
|
||||
var escape = PoolByteArray([0x1b]).get_string_from_ascii()
|
||||
var CMD_COLORS = {
|
||||
RED = escape + '[31m',
|
||||
YELLOW = escape + '[33m',
|
||||
DEFAULT = escape + '[0m',
|
||||
GREEN = escape + '[32m',
|
||||
UNDERLINE = escape + '[4m',
|
||||
BOLD = escape + '[1m'
|
||||
}
|
||||
|
||||
func colorize_word(source, word, c):
|
||||
var new_word = c + word + CMD_COLORS.DEFAULT
|
||||
return source.replace(word, new_word)
|
||||
|
||||
func colorize_text(text):
|
||||
var t = colorize_word(text, 'FAILED', CMD_COLORS.RED)
|
||||
t = colorize_word(t, 'PASSED', CMD_COLORS.GREEN)
|
||||
t = colorize_word(t, 'PENDING', CMD_COLORS.YELLOW)
|
||||
t = colorize_word(t, '[ERROR]', CMD_COLORS.RED)
|
||||
t = colorize_word(t, '[WARNING]', CMD_COLORS.YELLOW)
|
||||
t = colorize_word(t, '[DEBUG]', CMD_COLORS.BOLD)
|
||||
t = colorize_word(t, '[DEPRECATED]', CMD_COLORS.BOLD)
|
||||
t = colorize_word(t, '[INFO]', CMD_COLORS.BOLD)
|
||||
return t
|
||||
|
||||
|
||||
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 separated 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 == ''
|
||||
|
||||
func get_native_class_name(thing):
|
||||
var to_return = null
|
||||
if(is_native_class(thing)):
|
||||
to_return = thing.new().get_class()
|
||||
return to_return
|
||||
|
||||
func is_native_class(thing):
|
||||
var it_is = false
|
||||
if(typeof(thing) == TYPE_OBJECT):
|
||||
it_is = str(thing).begins_with("[GDScriptNativeClass:")
|
||||
return it_is
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ------------------------------------------------------------------------------
|
||||
func get_file_as_text(path):
|
||||
var to_return = ''
|
||||
var f = File.new()
|
||||
var result = f.open(path, f.READ)
|
||||
if(result == OK):
|
||||
to_return = f.get_as_text()
|
||||
f.close()
|
||||
return to_return
|
Loading…
Add table
Add a link
Reference in a new issue