[GdScript] Templates for GdScript (Godot 4) (#19267)

* feat(gdscript): sketch implementation of gdscript target language

This does not really work yet, but it's a start.
Results are not denormalized, no support for enums nor datetimes,
and thousands of other features are missing.

I still don't know how we are going to denormalize JSON+LD
without writing a whole GDScript lib for it…

* feat: add an exhaustive list of keywords reserved in GDScript

I've also provided the small python script I used to generate the list.

* refacto(gdscript): start using partials in templates

Whilst I'm racking my brains trying to figure out integration testing…

* test(gdscript): prepare a demo and integration testing

* fix(gdscript): do not use subclasses, use plain POGO

(plain ol' godot object)

One: I don't know how they work under-the-hood.
Two: I'm very confused over-the-hood.
Tri: We do not need them.

* refacto(gdscript): move demo files to their own directory

I know I'm making a lot of commits for not much,
but now I'm opening the sample files with Godot as well,
and doing unholy things with filesystems,
so I'm not taking any chances.

It's all going to be squashed anyway.  :)

* fix(gdscript): sample as a Godot project

It works !  I can now write integration tests in GDScript.
The real work starts now.

/spend 25h

* feat(gdscript): serialize and send body params

The test suite is now past its first hurdle, the 415 HTTP status code,
and went straight into an unexpected error 500.

I suspect the server does not like me trying to set the pet id at 0,
because that's what we're trying to do right now.

Godot is crashing a lot, mostly because I don't know how to make Callable.NOOP
and my current solution hints at optional on_success and on_failure,
yet if we omit them the engine will ragequit.

* feat(gdscript): check request body for required yet missing properties

Now we'll get a nice error when we forget to set a required property.

The demo is now able to:
- connect
- create a user
- login as that user
- create a pet

* feat(gdscript): namespace core classes as well with apiPrefixName

This makes our usage of `class_name` a little more acceptable.

* feat(gdscript): support prefixes and suffixes for class names

This will crutch namespacing well enough for most uses.

* feat(gdscript): handle enums, naively

* feat(gdscript): support basic API endpoint param constraints

- minLength
- maxLength
- minItems
- maxItems
- minimum
- maximum
- pattern (no flags)

* feat(gdscript): handle header params and header customization

We also support serializing to application/x-www-form-urlencoded now.

Next up: DateTimes !

* feat(gdscript): handle Date and DateTime like Strings

There's no timezone support in Godot for DateTimes.

* feat(gdscript): support plain text responses

* feat(gdscript): support collections of models

Those are Arrays, not custom collection objects.

* feat(gdscript): configure default host from OAS

* feat(gdscript): some documentation and better config

We don't need no factories nor singletons ; config is enough.

* docs(gdscript): document usage a little

* feat(gdscript): add more reserved words, skip jsonld models and configure features

We can now generate a client for an OAS server running ApiPlatform (PHP).

* feat(gdscript): improve logging with a configurable log level

* feat(gdscript): add support for Basic Bearer and Header ApiKey

(but I can't find the `description` template handler)

* fix(gdscript)

Too late to amend >.<

* fix(gdscript)

dangsarnit

* chore(gdscript): clean up a sprint artifact

* fix: don't forget the HTTP error code when relevant

* feat: use Resource as base class for models

* fix. Default string values now with "quotes"

* temporary remove settings as godot api have changed

* kick ci

* docs: review gdscript java class

* feat: support for TLS, some refacto, some review

There's still a lot of holes, TODOs and FIXMEs.

* feat: experimental support of Request inline objects

The inline response objects are still not supported.

* feat(gdscript): support inline request and response objects

* chore(gdscript): review the templates

* fix(gdscript): unexpected nulls in default values

{{#if defaultValue}} evaluates to true for null if we call super here.

* refacto(gdscript): replace "bee" prefix by "bzz", use a constructor

Now we pass the config and the client via the constructor.
This reduces the area of the public surface a bit, for the better I think.
This commit also cleans up the class name shenanigans.

* fix(gdscript): add missing file

* test(gdscript): refactor the test project to use the generated lib as addon

Since there is no singleton in the generated client, the addon need not be enabled in the project configuration to be usable.

The --headless mode is broken for now, as things changed in Godot 4 since the beta.

* docs(gdscript): document petstore server ADR

* test(gdscript): add GUT and an integration test

We used the latest stable GUT, but a feature we're going to need was merged today, so we'll need to update it either to master or to the next release at some point.

* refacto(gdscript, breaking): use an ApiResponse object in success callbacks

/spent 6d since the beginning

* test(gdscript): update integration tests

/spend 2h

* docs(gdscript): explain the new ApiResponse

Also moving core templates to their own subdir, for clarity.

/spend 10m

* chore(gdscript): review, document, clean up

/spend 2h

* test(gdscript): test the delete operation as well

/spend 7m

* feat(gdscript): update GUT and exit with appropriate code

/spend 2h

* docs(gdscript): add Gdscript's PI

Hire me while I'm available !  :D
I'd rather code than make a CV.

* feat(gdscript): support reserved keywords

Also adding some more assertions,
and using our own OAS file now.

/spend 3h

* refacto(gdscript): use "base" instead of "bee"

/spend 1h

* feat(gdscript): improve descriptions

/spend 1h

* fix(gdscript): await before polling

Contributed by @jchu231

* docs(gdscript): review the template files

* docs(gdscript): review and generate docs

---------

Co-authored-by: Bagrat <b.saatsazov@gmail.com>
This commit is contained in:
Antoine Goutenoir
2024-11-17 03:48:19 +01:00
committed by GitHub
parent 86a18bfb62
commit 959cf1c3c9
180 changed files with 25058 additions and 0 deletions

View File

@@ -998,6 +998,7 @@ Here is a list of template creators:
* Groovy: @victorgit
* Go: @wing328 [:heart:](https://www.patreon.com/wing328)
* Go (rewritten in 2.3.0): @antihax
* Godot (GDScript): @Goutte [:heart:](https://liberapay.com/Goutte)
* Haskell (http-client): @jonschoning
* Java (Feign): @davidkiss
* Java (Retrofit): @0legg

View File

@@ -0,0 +1,35 @@
# Run this configuration to update the sample project used in integration testing:
# bin/generate-samples.sh bin/configs/gdscript-petstore.yaml
generatorName: gdscript
# We output straight into an addon directory. (any addon name will work)
# The addon need not be enabled in Project Settings to be used, for now,
# but that may change, so as best practice you should enable it anyway.
outputDir: samples/client/petstore/gdscript/addons/oas.petstore.client
# We have two test servers available.
# See https://github.com/OpenAPITools/openapi-generator/wiki/Integration-Tests
# A: Newer, recommended echo server OAS, that we're failing for now:
# Exception: Could not process model 'Bird'. Please make sure that your schema is correct!
# Caused by: java.lang.RuntimeException: reserved word color not allowed
# Perhaps try this again later, using another config file like gdscript-echo.yaml
# > Later: this has been solved, we should now be able to use echo as well
#inputSpec: modules/openapi-generator/src/test/resources/3_0/echo_api.yaml
# B: Older (legacy, deprecated) petstore server OAS
inputSpec: modules/openapi-generator/src/test/resources/3_0/gdscript/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/gdscript
additionalProperties:
# Timestamping the generated sample project would only add noise to git
hideGenerationTimestamp: "true"
# Since we're polluting the global namespace with class_name (it's really convenient),
# we can use these affixes to namespace the generated classes.
# Best make sure the words you use here are not part of the domain of Godot.
apiNamePrefix: Demo
modelNamePrefix: Demo
modelNameSuffix: Model
coreNamePrefix: Demo

1704
docs/generators/gdscript.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,7 @@ org.openapitools.codegen.languages.ErlangServerCodegen
org.openapitools.codegen.languages.ErlangServerDeprecatedCodegen
org.openapitools.codegen.languages.FsharpFunctionsServerCodegen
org.openapitools.codegen.languages.FsharpGiraffeServerCodegen
org.openapitools.codegen.languages.GdscriptClientCodegen
org.openapitools.codegen.languages.GoClientCodegen
org.openapitools.codegen.languages.GoEchoServerCodegen
org.openapitools.codegen.languages.GoServerCodegen

View File

@@ -0,0 +1,129 @@
# {{{openAPI.info.title}}} GDScript Client
{{#if appDescriptionWithNewLines}}
{{{appDescriptionWithNewLines}}}
{{/if}}
This *Godot 4* addon was automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
{{#if servers}}
Servers:
{{#each servers}}
- [{{#if this.description}}{{this.description}}{{else}}{{this.url}}{{/if}}]({{this.url}})
{{/each}}
{{/if}}
- API version: {{appVersion}}
{{#unless hideGenerationTimestamp}}
- Build date: {{generatedDate}}
{{/unless}}
- Build package: {{generatorClass}}
{{#if infoUrl}}
For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}).
{{/if}}
## Requirements.
Godot `4.x`.
## Installation & Usage
Copy the files into the `addon` directory of your Godot project.
Then, enable the addon in your project settings.
You can now use it anywhere in your code:
{{#with apiInfo}}
{{#each apis}}
{{#if @first}}
{{#with operations}}
{{#each operation}}
{{#if @first}}
{{>api_doc_example}}
{{/if}}
{{/each}}
{{/with}}
{{/if}}
{{/each}}
{{/with}}
## Customization
You can [override the templates](https://openapi-generator.tech/docs/templating/).
> 1. Grab templates with `openapi-generator author template -g gdscript --library gdscript`
> 2. Hack around
> 3. Regenerate using `--template <dir>` to target your custom templates dir
## Documentation for API Endpoints
All URIs are relative to *{{basePath}}*
Class | Method | HTTP request | Description
------------ | ------------- | ------------- | -------------
{{#with apiInfo}}
{{#each apis}}
{{#with operations}}
{{#each operation}}*{{classname}}* | [**{{operationIdSnakeCase}}**]({{apiDocPath}}{{classname}}.md#{{operationIdSnakeCase}}) | **{{httpMethod}}** {{path}} | {{#if summary}}{{summary}}{{/if}}
{{/each}}
{{/with}}
{{/each}}
{{/with}}
## Documentation For Models
{{#each models}}
{{#with model}}
- [{{{classname}}}]({{modelDocPath}}{{{classname}}}.md)
{{/with}}
{{/each}}
## Documentation For Authorization
{{#unless authMethods}}
All endpoints do not require authorization.
{{/unless}}
{{#each authMethods}}
{{#if @last}} Authentication schemes defined for the API:{{/if}}
## {{{name}}}
{{#if isApiKey}}
- **Type**: API key
- **API key parameter name**: {{{keyParamName}}}
- **Location**: {{#if isKeyInQuery}}URL query string{{/if}}{{#if isKeyInHeader}}HTTP header{{/if}}
{{/if}}
{{#if isBasic}}
{{#if isBasicBasic}}
- **Type**: HTTP basic authentication
{{/if}}
{{#if isBasicBearer}}
- **Type**: Bearer authentication{{#if bearerFormat}} ({{{bearerFormat}}}){{/if}}
{{/if}}
{{#if isHttpSignature}}
- **Type**: HTTP signature authentication
{{/if}}
{{/if}}
{{#if isOAuth}}
- **Type**: OAuth
- **Flow**: {{{flow}}}
- **Authorization URL**: {{{authorizationUrl}}}
- **Scopes**: {{#unless scopes}}N/A{{/unless}}
{{#each scopes}} - **{{{scope}}}**: {{#if description}}{{{description}}}{{/if}}
{{/each}}
{{/if}}
{{/each}}
## Troubleshooting
### `TLS handshake error: -9984`
https://github.com/godotengine/godot/issues/59080#issuecomment-1065973210

View File

@@ -0,0 +1,41 @@
In here are the `handlebars` templates used to generate the `GDScript` client.
All files without the `.handlebars` extension (including this very `README.md` file) are ignored.
You can copy them all (or parts) and override them as needed.
## Domain Overview
### ApiBee
Base class for all Api endpoints classes.
Holds most of the nitty-gritty.
### ApiConfig
Reusable configuration (host, port, etc.) for Apis, injected into their constructor.
### ApiError
Godot does not have an `Exception` (`try / catch`) mechanism, by design.
> It actually makes _some_ sense in the volatile environment that are video games,
> due to their gigantic amount of user inputs and userland data,
> inherent complexity, low stakes, and entertainment value of glitches.
Therefore, whenever there's trouble in paradise, we pass around an `ApiError` object. (a `RefCounted`, don't worry about garbage collection)
### ApiResponse
A wrapper for an API Response, used in callbacks.
Holds the HTTP components of the Response, as well as the deserialized `data` (if any).
## Extending
Most classes can be configured to extend your own class.
Override the `partials/*_parent_class.handlebars` to define them.

View File

@@ -0,0 +1,206 @@
{{>partials/api_statement_extends}}
{{>partials/api_statement_class_name}}
{{>partials/api_headers}}
func _bzz_get_api_name() -> String:
return "{{classname}}"
{{#with operations}}
{{#each operation}}
{{#if isDeprecated}}
# /!. DEPRECATED
{{/if}}
# Operation {{{operationId}}}{{{httpMethod}}} {{{path}}}
{{#if summary}}
# {{{summary}}}
{{/if}}
{{#if description}}
#
# {{{description}}}
{{/if}}
{{#if notes}}
#
# {{{notes}}}
{{/if}}
func {{operationIdSnakeCase}}(
{{>partials/api_method_params}}
):
{{#if isDeprecated}}
push_warning("Usage of `{{operationIdSnakeCase}}()` is deprecated.")
{{/if}}
{{#each allParams}}
{{#if hasValidation}}
# Validate param `{{paramName}}` constraints
{{#if maxLength}}
{{#if isString}}
if ({{paramName}} is String) and {{paramName}}.length() > {{maxLength}}:
var error := {{>partials/api_error_class_name}}.new()
#error.internal_code = ERR_INVALID_PARAMETER
error.identifier = "{{operationIdSnakeCase}}.param.validation.max_length"
error.message = "Invalid length for `{{paramName}}`, must be smaller than or equal to {{maxLength}}."
on_failure.call(error)
return
{{/if}}
{{/if}}
{{#if minLength}}
{{#if isString}}
if ({{paramName}} is String) and {{paramName}}.length() < {{minLength}}:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "{{operationIdSnakeCase}}.param.validation.min_length"
error.message = "Invalid length for `{{paramName}}`, must be greater than or equal to {{minLength}}."
on_failure.call(error)
return
{{/if}}
{{/if}}
{{#if maximum}}
{{! isNumeric / isNumber yields false yet isLong yields true }}
{{! not sure if bug 'cause of handlebars or not ; let's skip }}
{{!#if isNumeric}}
if {{paramName}} >{{#if exclusiveMaximum}}={{/if}} {{maximum}}:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "{{operationIdSnakeCase}}.param.validation.maximum"
error.message = "Invalid value for `{{paramName}}`, must be smaller than{{#unless exclusiveMaximum}} or equal to{{/unless}} {{maximum}}."
on_failure.call(error)
return
{{!/if}}
{{/if}}
{{#if minimum}}
{{!#if isNumeric}}
if {{paramName}} <{{#if exclusiveMinimum}}={{/if}} {{minimum}}:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "{{operationIdSnakeCase}}.param.validation.minimum"
error.message = "Invalid value for `{{paramName}}`, must be greater than{{#unless exclusiveMinimum}} or equal to{{/unless}} {{minimum}}."
on_failure.call(error)
return
{{!/if}}
{{/if}}
{{#if maxItems}}
{{#if isArray}}
if ({{paramName}} is Array) and {{paramName}}.size() > {{maxItems}}:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "{{operationIdSnakeCase}}.param.validation.max_items"
error.message = "Invalid array size for `{{paramName}}`, must hold at most {{maxItems}} elements."
on_failure.call(error)
return
{{/if}}
{{/if}}
{{#if minItems}}
{{#if isArray}}
if ({{paramName}} is Array) and {{paramName}}.size() < {{minItems}}:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "{{operationIdSnakeCase}}.param.validation.min_items"
error.message = "Invalid array size for `{{paramName}}`, must hold at least {{minItems}} elements."
on_failure.call(error)
return
{{/if}}
{{/if}}
{{#if pattern}}
var bzz_{{paramName}}_regex := RegEx.new()
{{! These regex trimming shenanigans will fail if regex has flags }}
{{! A solution would be to use another RegEx to extract that data from the pattern ? }}
bzz_{{paramName}}_regex.compile("{{{pattern}}}".trim_prefix('/').trim_suffix('/'))
if not bzz_{{paramName}}_regex.search(str({{paramName}})):
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "{{operationIdSnakeCase}}.param.validation.pattern"
error.message = "Invalid value for `{{paramName}}`, must conform to the pattern `{{{pattern}}}`."
on_failure.call(error)
return
{{/if}}
{{/if}}
{{/each}}
# Convert the String HTTP method to a Constant Godot understands
var bzz_method := self._bzz_convert_http_method("{{httpMethod}}")
# Compute the URL path to the API resource
var bzz_path := "{{{contextPath}}}{{{path}}}"{{#each pathParams}}.replace("{" + "{{baseName}}" + "}", _bzz_urlize_path_param({{{paramName}}})){{/each}}
# Collect the headers
var bzz_headers := Dictionary()
{{#each headerParams}}
bzz_headers["{{baseName}}"] = {{paramName}}
{{/each}}
{{#if consumes}}
var bzz_mimes_consumable_by_server := [{{#each consumes}}'{{{mediaType}}}'{{#unless @last}}, {{/unless}}{{/each}}]
var bzz_found_producible_mime := false
for bzz_mime in BZZ_PRODUCIBLE_CONTENT_TYPES:
if bzz_mime in bzz_mimes_consumable_by_server:
bzz_headers["Content-Type"] = bzz_mime
bzz_found_producible_mime = true
break
if not bzz_found_producible_mime:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "{{operationIdSnakeCase}}.headers.content_type"
error.message = "That endpoint only accepts %s as content type(s) and none are supported by this client."
on_failure.call(error)
return
{{/if}}
{{#if produces}}
var bzz_mimes_produced_by_server := [{{#each produces}}'{{{mediaType}}}'{{#unless @last}}, {{/unless}}{{/each}}]
for bzz_mime in BZZ_CONSUMABLE_CONTENT_TYPES:
if bzz_mime in bzz_mimes_produced_by_server:
bzz_headers["Accept"] = bzz_mime
break
{{/if}}
# Collect the query parameters
# Note: we do not support multiple values for a single param (for now), nor arrays
var bzz_query := Dictionary()
{{#each queryParams}}
bzz_query["{{baseName}}"] = {{paramName}}
{{/each}}
var bzz_body = null
{{#if bodyParams}}
{{#each bodyParams}}
{{! What should happen here when there are multiple body params? for now last wins }}
bzz_body = {{paramName}}
{{/each}}
{{/if}}
{{#if formParams}}
bzz_body = Dictionary()
{{#each formParams}}
bzz_body["{{paramName}}"] = {{paramName}}
{{/each}}
{{/if}}
self._bzz_request(
bzz_method, bzz_path, bzz_headers, bzz_query, bzz_body,
func(bzz_response):
{{#with returnProperty}}
{{#if isArray}}
bzz_response.data = {{>partials/complex_type}}.bzz_denormalize_multiple(bzz_response.data)
{{else if isModel}}
bzz_response.data = {{>partials/data_type}}.bzz_denormalize_single(bzz_response.data)
{{/if}}
{{/with}}
on_success.call(bzz_response)
,
func(bzz_error):
on_failure.call(bzz_error)
, # ざわ‥
)
func {{operationIdSnakeCase}}_threaded(
{{>partials/api_method_params}}
) -> Thread:
var bzz_thread := Thread.new()
var bzz_callable := Callable(self, "{{operationIdSnakeCase}}")
bzz_callable.bind(
{{#each allParams}}
{{paramName}},
{{/each}}
on_success,
on_failure,
)
bzz_thread.start(bzz_callable)
return bzz_thread
{{/each}}
{{/with}}

View File

@@ -0,0 +1,64 @@
<a name="__pageTop"></a>
# {{{classname}}} { #{{classname}} }
{{#if description}}{{description}}{{/if}}
All URIs are relative to *{{basePath}}*
Method | HTTP request | Description
------------- | ------------- | -------------
{{#with operations}}
{{#each operation}}
[**{{operationIdSnakeCase}}**](#{{operationIdSnakeCase}}) | **{{httpMethod}}** `{{path}}` | {{#if summary}}{{summary}}{{/if}}
{{/each}}
{{/with}}
{{#with operations}}
{{#each operation}}
# **{{{operationIdSnakeCase}}}** { #{{{operationIdSnakeCase}}} }
<a name="{{{operationIdSnakeCase}}}"></a>
> `{{operationIdSnakeCase}}({{#each allParams}}{{paramName}}{{#if required}}{{#if dataType}}: {{{dataType}}}{{/if}}{{/if}}{{#unless required}} = {{#if defaultValue}}{{{defaultValue}}}{{/if}}{{#unless defaultValue}}null{{/unless}}{{/unless}},{{/each}} on_success: Callable, on_failure: Callable)`
{{#if summary}}{{{summary}}}{{/if}}
{{#if notes}}{{{notes}}}{{/if}}
### Example
{{#if hasAuthMethods}}
{{#each authMethods}}
{{#if isBasic}}
{{#if isBasicBasic}}
* Basic Authentication (`{{name}}`)
{{/if}}
{{#if isBasicBearer}}
* Bearer{{#if bearerFormat}} ({{{bearerFormat}}}){{/if}} Authentication (`{{name}}`)
{{/if}}
{{/if}}
{{#if isApiKey}}
* Api Key Authentication (`{{name}}`)
{{/if}}
{{#if isOAuth}}
* OAuth Authentication (`{{name}}`)
{{/if}}
{{/each}}
{{/if}}
{{> api_doc_example }}
{{/each}}
### Authorization
{{#each authMethods}}
[{{{name}}}](../README.md#{{name}}){{#unless @last}}, {{/unless}}
{{else}}
No authorization required.
{{/each}}
[[Back to top]](#__pageTop) \
[[Back to API list]](../README.md#documentation-for-api-endpoints) \
[[Back to Model list]](../README.md#documentation-for-models) \
[[Back to README]](../README.md) \
{{/with}}

View File

@@ -0,0 +1,45 @@
```gdscript
# Customize configuration
var config := {{>partials/api_config_class_name}}.new()
config.host = "localhost"
config.port = 8080
#config.tls_enabled = true
#config.trusted_chain = preload("res://my_cert_chain.crt")
# Instantiate the api
var api = {{{classname}}}.new(config)
# You can also provide your own HTTPClient, to re-use it across apis.
#var api = {{{classname}}}.new(config, client)
{{#each bodyParams}}
{{#if dataType}}
{{#if isModel}}
var {{paramName}} = {{dataType}}.new()
# … fill model {{paramName}} with data
{{/if}}
{{/if}}
{{/each}}
# Invoke an endpoint
api.{{{operationIdSnakeCase}}}(
{{#each allParams}}
# {{paramName}}{{#if dataType}}: {{dataType}}{{/if}}{{#if defaultValue}} = {{{defaultValue}}}{{/if}}{{#if example}} Eg: {{{example}}}{{/if}}
{{~#if description}}
# {{{description}}}{{/if}}
{{paramName}},
{{/each}}
# On Success
func(response):{{#with returnProperty}} # response is {{>partials/api_response_class_name}}{{/with}}
prints("Success!", "{{operationIdSnakeCase}}", response)
{{#with returnProperty}}assert(response.data is {{>partials/complex_type}}){{/with}}
pass # do things, make stuff
,
# On Error
func(error): # error is {{>partials/api_error_class_name}}
push_error(str(error))
,
)
```

View File

@@ -0,0 +1,502 @@
extends {{>partials/api_base_parent_class}}
class_name {{>partials/api_base_class_name}}
{{>partials/disclaimer_autogenerated}}
# Base class for all generated API endpoints
# ==========================================
#
# Every property/method defined here may collide with userland,
# so these are all listed and excluded in our CodeGen Java file.
# We want to keep the amount of renaming to a minimum, though.
# Therefore, we use the _bzz_ prefix, even if awkward.
const BZZ_CONTENT_TYPE_TEXT := "text/plain"
const BZZ_CONTENT_TYPE_HTML := "text/html"
const BZZ_CONTENT_TYPE_JSON := "application/json"
const BZZ_CONTENT_TYPE_FORM := "application/x-www-form-urlencoded"
const BZZ_CONTENT_TYPE_JSONLD := "application/json+ld" # unsupported (for now)
const BZZ_CONTENT_TYPE_XML := "application/xml" # unsupported (for now)
# From this client's point of view.
# Adding a content type here won't magically make the client support it, but you may reorder.
# These are sorted by decreasing preference. (first → preferred)
const BZZ_PRODUCIBLE_CONTENT_TYPES := [
BZZ_CONTENT_TYPE_JSON,
BZZ_CONTENT_TYPE_FORM,
]
# From this client's point of view.
# Adding a content type here won't magically make the client support it, but you may reorder.
# These are sorted by decreasing preference. (first → preferred)
const BZZ_CONSUMABLE_CONTENT_TYPES := [
BZZ_CONTENT_TYPE_JSON,
]
# Godot's HTTP Client this Api instance is using.
# If none was set (by you), we'll lazily make one.
var _bzz_client: HTTPClient:
set(value):
_bzz_client = value
get:
if not _bzz_client:
_bzz_client = HTTPClient.new()
return _bzz_client
# General configuration that can be shared across Api instances for convenience.
# If no configuration was provided, we'll lazily make one with defaults,
# but you probably want to make your own with your own domain and scheme.
var _bzz_config: {{>partials/api_config_class_name}}:
set(value):
_bzz_config = value
get:
if not _bzz_config:
_bzz_config = {{>partials/api_config_class_name}}.new()
return _bzz_config
# Useful in logs
var _bzz_name: String:
get:
return _bzz_get_api_name()
# Constructor, where you probably want to inject your configuration,
# and as Godot recommends re-using HTTP clients, your client as well.
func _init(config : {{>partials/api_config_class_name}} = null, client : HTTPClient = null):
if config != null:
self._bzz_config = config
if client != null:
self._bzz_client = client
{{! We'll probably only use this for logging. }}
{{! Each Api child can define its own, and it should be similar to class_name. }}
{{! https://github.com/godotengine/godot/issues/21789 }}
func _bzz_get_api_name() -> String:
return "ApiBee"
func _bzz_next_loop_iteration():
# I can't find `idle_frame` in 4.0, but we probably want idle_frame here
return Engine.get_main_loop().process_frame
func _bzz_connect_client_if_needed(
on_success: Callable, # func()
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
#finally: Callable,
):
if (
self._bzz_client.get_status() == HTTPClient.STATUS_CONNECTED
or
self._bzz_client.get_status() == HTTPClient.STATUS_RESOLVING
or
self._bzz_client.get_status() == HTTPClient.STATUS_CONNECTING
or
self._bzz_client.get_status() == HTTPClient.STATUS_REQUESTING
or
self._bzz_client.get_status() == HTTPClient.STATUS_BODY
):
on_success.call()
var connecting := self._bzz_client.connect_to_host(
self._bzz_config.host, self._bzz_config.port, self._bzz_config.tls_options
)
if connecting != OK:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = connecting
error.identifier = "apibee.connect_to_host.failure"
error.message = "%s: failed to connect to `%s' port `%d' with error: %s" % [
_bzz_name, self._bzz_config.host, self._bzz_config.port,
_bzz_httpclient_status_string(connecting),
]
on_failure.call(error)
return
# Wait until resolved and connected.
while (
self._bzz_client.get_status() == HTTPClient.STATUS_CONNECTING
or
self._bzz_client.get_status() == HTTPClient.STATUS_RESOLVING
):
self._bzz_client.poll()
self._bzz_config.log_debug("Connecting…")
if self._bzz_config.polling_interval_ms:
OS.delay_msec(self._bzz_config.polling_interval_ms)
await _bzz_next_loop_iteration()
var connected := self._bzz_client.get_status()
if connected != HTTPClient.STATUS_CONNECTED:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = connected as Error
error.identifier = "apibee.connect_to_host.status_failure"
error.message = "%s: failed to connect to `%s' port `%d' : %s" % [
_bzz_name, self._bzz_config.host, self._bzz_config.port,
_bzz_httpclient_status_string(connected),
]
on_failure.call(error)
return
on_success.call()
func _bzz_request(
method: int, # one of HTTPClient.METHOD_XXXXX
path: String,
headers: Dictionary,
query: Dictionary,
body, # Variant that will be serialized and sent
on_success: Callable, # func(response: {{>partials/api_response_class_name}})
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
):
# This method does not handle full deserialization, it only handles decode and not denormalization.
# Denormalization is handled in each generated API endpoint in the on_success callable of this method.
_bzz_request_text(
method, path, headers, query, body,
func(response):
var mime: String = response.headers['Mime']
var decodedBody # Variant
# Isn't there a match/case now in Gdscript?
if BZZ_CONTENT_TYPE_TEXT == mime:
decodedBody = response.body
elif BZZ_CONTENT_TYPE_HTML == mime:
decodedBody = response.body
elif BZZ_CONTENT_TYPE_JSON == mime:
var parser := JSON.new()
var parsing := parser.parse(response.body)
if OK != parsing:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = parsing
error.identifier = "apibee.decode.cannot_parse_json"
error.response_code = response.code
error.response = response
error.message = "%s: failed to parse JSON response at line %d.\n%s" % [
_bzz_name, parser.get_error_line(), parser.get_error_message()
]
on_failure.call(error)
return
decodedBody = parser.data
else:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = ERR_INVALID_DATA
error.identifier = "apibee.decode.mime_type_unsupported"
error.response_code = response.code
error.response = response
error.message = "%s: mime type `%s' is not supported (yet -- MRs welcome)" % [
_bzz_name, mime
]
on_failure.call(error)
return
response.data = decodedBody
on_success.call(response)
,
func(error):
on_failure.call(error)
,
)
func _bzz_request_text(
method: int, # one of HTTPClient.METHOD_XXXXX
path: String,
headers: Dictionary,
query: Dictionary,
body, # Variant that will be serialized
on_success: Callable, # func(response: {{>partials/api_response_class_name}})
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
):
_bzz_connect_client_if_needed(
func():
_bzz_do_request_text(method, path, headers, query, body, on_success, on_failure)
,
func(error):
on_failure.call(error)
,
)
func _bzz_do_request_text(
method: int, # one of HTTPClient.METHOD_XXXXX
path: String,
headers: Dictionary,
query: Dictionary,
body, # Variant that will be serialized
on_success: Callable, # func(response: {{>partials/api_response_class_name}})
on_failure: Callable, # func(error: {{>partials/api_error_class_name}})
):
headers = headers.duplicate(true)
headers.merge(self._bzz_config.headers_base)
headers.merge(self._bzz_config.headers_override, true)
var body_normalized = body
if body is Object:
if body.has_method('bzz_collect_missing_properties'):
var missing_properties : Array = body.bzz_collect_missing_properties()
if missing_properties:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.request.body.missing_properties"
error.message = "%s: `%s' is missing required properties %s." % [
_bzz_name, body.bzz_class_name, missing_properties
]
on_failure.call(error)
return
if body.has_method('bzz_normalize'):
body_normalized = body.bzz_normalize()
var body_serialized := ""
var content_type := self._bzz_get_content_type(headers)
if content_type == BZZ_CONTENT_TYPE_JSON:
body_serialized = JSON.stringify(body_normalized)
elif content_type == BZZ_CONTENT_TYPE_FORM:
body_serialized = self._bzz_client.query_string_from_dict(body_normalized)
else:
# TODO: Handle other serialization schemes (json+ld, xml…)
push_warning("Unsupported content-type `%s`." % content_type)
var path_queried := path
var query_string := self._bzz_client.query_string_from_dict(query)
if query_string:
path_queried = "%s?%s" % [path, query_string]
{{! Godot HTTP Client expects an array of strings, not a dictionary }}
var headers_for_godot := Array() # of String
for key in headers:
headers_for_godot.append("%s: %s" % [key, headers[key]])
self._bzz_config.log_info("%s: REQUEST %s %s" % [_bzz_name, method, path_queried])
if not headers.is_empty():
self._bzz_config.log_debug("→ HEADERS: %s" % [str(headers)])
if body_serialized:
self._bzz_config.log_debug("→ BODY: \n%s" % [body_serialized])
var requesting := self._bzz_client.request(method, path_queried, headers_for_godot, body_serialized)
if requesting != OK:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = requesting
error.identifier = "apibee.request.failure"
error.message = "%s: failed to request to path `%s'." % [
_bzz_name, path
]
on_failure.call(error)
return
# Keep polling for as long as the request is being processed.
while self._bzz_client.get_status() == HTTPClient.STATUS_REQUESTING:
self._bzz_config.log_debug("Requesting…")
if self._bzz_config.polling_interval_ms:
OS.delay_msec(self._bzz_config.polling_interval_ms)
await _bzz_next_loop_iteration()
self._bzz_client.poll()
if not self._bzz_client.has_response():
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.request.no_response"
error.message = "%s: request to `%s' returned no response whatsoever. (status=%d)" % [
_bzz_name, path, self._bzz_client.get_status(),
]
on_failure.call(error)
return
var response := {{>partials/api_response_class_name}}.new()
#response.collect_meta_from_client(self._bzz_client) # to refactor
response.code = self._bzz_client.get_response_code()
response.headers = self._bzz_client.get_response_headers_as_dictionary()
# FIXME: extract from headers "Content-Type": "application/json; charset=utf-8"
# Perhaps use a method of {{>partials/api_response_class_name}} for this?
var encoding := "utf-8"
var mime := "application/json"
response.headers['Encoding'] = encoding
response.headers['Mime'] = mime
#response.collect_body_from_client(self._bzz_client, self._bzz_config) # to refactor
# TODO: cap the size of this, perhaps?
var response_bytes := PackedByteArray()
while self._bzz_client.get_status() == HTTPClient.STATUS_BODY:
var chunk = self._bzz_client.read_response_body_chunk()
if chunk.size() == 0: # Got nothing, wait for buffers to fill a bit.
if self._bzz_config.polling_interval_ms:
OS.delay_usec(self._bzz_config.polling_interval_ms)
await _bzz_next_loop_iteration()
else: # Yummy data has arrived
response_bytes = response_bytes + chunk
self._bzz_client.poll()
self._bzz_config.log_info("%s: RESPONSE %d (%d bytes)" % [
_bzz_name, response.code, response_bytes.size()
])
if not response.headers.is_empty():
self._bzz_config.log_debug("→ HEADERS: %s" % str(response.headers))
var response_text: String
if encoding == "utf-8":
response_text = response_bytes.get_string_from_utf8()
elif encoding == "utf-16":
response_text = response_bytes.get_string_from_utf16()
elif encoding == "utf-32":
response_text = response_bytes.get_string_from_utf32()
else:
response_text = response_bytes.get_string_from_ascii()
if response_text:
self._bzz_config.log_debug("→ BODY: \n%s" % response_text)
response.body = response_text
if response.code >= 500:
var error := {{>partials/api_error_class_name}}.new()
error.internal_code = ERR_PRINTER_ON_FIRE
error.response_code = response.code
error.response = response
error.identifier = "apibee.response.5xx"
error.message = "%s: request to `%s' made the server hiccup with a %d." % [
_bzz_name, path, response.code
]
error.message += "\n%s" % [
_bzz_format_error_response(response_text)
]
on_failure.call(error)
return
elif response.code >= 400:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.response.4xx"
error.response_code = response.code
error.response = response
error.message = "%s: request to `%s' was denied with a %d." % [
_bzz_name, path, response.code
]
error.message += "\n%s" % [
_bzz_format_error_response(response_text)
]
on_failure.call(error)
return
elif response.code >= 300:
var error := {{>partials/api_error_class_name}}.new()
error.identifier = "apibee.response.3xx"
error.response_code = response.code
error.response = response
error.message = "%s: request to `%s' was redirected with a %d. We do not support redirects in that client yet." % [
_bzz_name, path, response.code
]
on_failure.call(error)
return
# Should we close ?
#self._bzz_client.close()
on_success.call(response)
func _bzz_convert_http_method(method: String) -> int:
match method:
'GET': return HTTPClient.METHOD_GET
'POST': return HTTPClient.METHOD_POST
'PUT': return HTTPClient.METHOD_PUT
'PATCH': return HTTPClient.METHOD_PATCH
'DELETE': return HTTPClient.METHOD_DELETE
'CONNECT': return HTTPClient.METHOD_CONNECT
'HEAD': return HTTPClient.METHOD_HEAD
'MAX': return HTTPClient.METHOD_MAX
'OPTIONS': return HTTPClient.METHOD_OPTIONS
'TRACE': return HTTPClient.METHOD_TRACE
_:
push_error("%s: unknown http method `%s`, assuming GET." % [
_bzz_name, method
])
return HTTPClient.METHOD_GET
func _bzz_urlize_path_param(anything) -> String:
var serialized := _bzz_escape_path_param(str(anything))
return serialized
func _bzz_escape_path_param(value: String) -> String:
# TODO: escape for URL
return value
func _bzz_get_content_type(headers: Dictionary) -> String:
if headers.has("Content-Type"):
return headers["Content-Type"]
return BZZ_PRODUCIBLE_CONTENT_TYPES[0]
func _bzz_format_error_response(response: String) -> String:
# TODO: handle other (de)serialization schemes
var parser := JSON.new()
var parsing := parser.parse(response)
if OK != parsing:
return response
if not (parser.data is Dictionary):
return response
var s := "ERROR"
if parser.data.has("code"):
s += " %d" % parser.data['code']
if parser.data.has("message"):
s += "\n%s" % parser.data['message']
else:
return response
return s
func _bzz_httpclient_status_info(status: int) -> Dictionary:
# At some point Godot ought to natively implement this and we won't need this "shim" anymore.
match status:
HTTPClient.STATUS_DISCONNECTED: return {
"name": "STATUS_DISCONNECTED",
"description": "Disconnected from the server."
}
HTTPClient.STATUS_RESOLVING: return {
"name": "STATUS_RESOLVING",
"description": "Currently resolving the hostname for the given URL into an IP."
}
HTTPClient.STATUS_CANT_RESOLVE: return {
"name": "STATUS_CANT_RESOLVE",
"description": "DNS failure: Can't resolve the hostname for the given URL."
}
HTTPClient.STATUS_CONNECTING: return {
"name": "STATUS_CONNECTING",
"description": "Currently connecting to server."
}
HTTPClient.STATUS_CANT_CONNECT: return {
"name": "STATUS_CANT_CONNECT",
"description": "Can't connect to the server."
}
HTTPClient.STATUS_CONNECTED: return {
"name": "STATUS_CONNECTED",
"description": "Connection established."
}
HTTPClient.STATUS_REQUESTING: return {
"name": "STATUS_REQUESTING",
"description": "Currently sending request."
}
HTTPClient.STATUS_BODY: return {
"name": "STATUS_BODY",
"description": "HTTP body received."
}
HTTPClient.STATUS_CONNECTION_ERROR: return {
"name": "STATUS_CONNECTION_ERROR",
"description": "Error in HTTP connection."
}
HTTPClient.STATUS_TLS_HANDSHAKE_ERROR: return {
"name": "STATUS_TLS_HANDSHAKE_ERROR",
"description": "Error in TLS handshake."
}
return {
"name": "UNKNOWN (%d)" % status,
"description": "Unknown HTTPClient status."
}
func _bzz_httpclient_status_string(status: int) -> String:
var info := _bzz_httpclient_status_info(status)
return "%s (%s)" % [info["description"], info["name"]]

View File

@@ -0,0 +1,170 @@
extends {{>partials/api_config_parent_class}}
class_name {{>partials/api_config_class_name}}
{{>partials/disclaimer_autogenerated}}
# Configuration options for Api endpoints
# =======================================
#
# Helps share configuration customizations across Apis:
# - host, port & scheme
# - extra headers (low priority, high priority)
# - transport layer security options (TLS certificates)
# - log level
#
# You probably want to make an instance of this class with your own values,
# and feed it to each Api's constructor, before calling the Api's methods.
#
# Since it is a Resource, you may use `ResourceSaver.save()` and `preload()`
# to save it and load it from file, for convenience.
#
# These are constant, immutable default values. Best not edit them.
# To set different values at runtime, use the @export'ed properties below.
const BEE_DEFAULT_HOST := "{{#if host}}{{{host}}}{{else}}localhost{{/if}}"
const BEE_DEFAULT_PORT_HTTP := 80
const BEE_DEFAULT_PORT_HTTPS := 443
const BEE_DEFAULT_POLLING_INTERVAL_MS := 333 # milliseconds
# Configuration also handles logging because it's convenient.
enum LogLevel {
SILENT,
ERROR,
WARNING,
INFO,
DEBUG,
}
## Log level to configure verbosity.
@export var log_level := LogLevel.WARNING
{{!--
# Not sure if this should hold the HTTPClient instance or not. Not for now.
# Godot recommends using a single client for all requests, so it perhaps should.
# Godot's HTTP Client we are using.
# If none was set (by you), we'll lazily make one.
var bee_client: HTTPClient:
set(value):
bee_client = value
get:
if not bee_client:
bee_client = HTTPClient.new()
return bee_client
--}}
## The host to connect to, with or without the protocol scheme.
## Eg: "gitea.com", "https://gitea.com"
## We toggle TLS accordingly to the provided scheme, if any.
@export var host := BEE_DEFAULT_HOST:
set(value):
if value.begins_with("https://"):
tls_enabled = true
value = value.substr(8) # "https://".length() == 8
elif value.begins_with("http://"):
tls_enabled = false
value = value.substr(7) # "http://".length() == 7
host = value
## Port through which the connection will be established.
## NOTE: changing the host may change the port as well if the scheme was provided, see above.
@export var port := BEE_DEFAULT_PORT_HTTP
## Headers used as base for all requests made by Api instances using this config.
## Those are the lowest priority headers, and are merged with custom headers provided in the bee_request() method call
## as well as the headers override below, to compute the final, actually sent headers.
@export var headers_base := {
# Stigmergy: everyone does what is left to do (like ants do)
"User-Agent": "Stigmergiac/1.0 (Godot)",
# For my mental health's sake, only JSON is supported for now
"Accept": "application/json",
"Content-Type": "application/json",
}
## High-priority headers, they will always override other headers coming from the base above or the method call.
@export var headers_override := {}
## Duration of sleep between poll() calls.
@export var polling_interval_ms := BEE_DEFAULT_POLLING_INTERVAL_MS # milliseconds
## Enable the Transport Security Layer (packet encryption, HTTPS)
@export var tls_enabled := false:
set(value):
tls_enabled = value
port = BEE_DEFAULT_PORT_HTTPS if tls_enabled else BEE_DEFAULT_PORT_HTTP
## You should preload your *.crt file (the whole chain) in here if you want TLS.
## I usually concatenate my /etc/ssl/certs/ca-certificates.crt and webserver certs here.
## Remember to add the *.crt file to the exports, if necessary.
@export var trusted_chain: X509Certificate # only used if tls is enabled
@export var common_name_override := "" # for TLSOptions
## Dynamic accessor using the TLS properties above, but you may inject your own
## for example if you need to use TLSOptions.client_unsafe. Best not @export this.
var tls_options: TLSOptions:
set(value):
tls_options = value
get:
if not tls_enabled:
return null
if not tls_options:
tls_options = TLSOptions.client(trusted_chain, common_name_override)
return tls_options
func log_error(message: String):
if self.log_level >= LogLevel.ERROR:
push_error(message)
func log_warning(message: String):
if self.log_level >= LogLevel.WARNING:
push_warning(message)
func log_info(message: String):
if self.log_level >= LogLevel.INFO:
print(message)
func log_debug(message: String):
if self.log_level >= LogLevel.DEBUG:
print(message)
{{#each authMethods}}
# Authentication method `{{name}}`.
{{#if isBasicBearer }}
# Basic Bearer Authentication `{{bearerFormat}}`
func set_security_{{name}}(value: String):
self.headers_base["Authorization"] = "Bearer %s" % value
{{else if isApiKey }}
# Api Key Authentication `{{keyParamName}}`
func set_security_{{name}}(value: String):
{{#if isKeyInHeader }}
self.headers_base["{{keyParamName}}"] = value
{{else if isKeyInQuery }}
# Implementing this should be straightforward
log_error("Api Key in Query is not supported at the moment. (contribs welcome)")
{{else if isKeyInCookie }}
log_error("Api Key in Cookie is not supported at the moment. (contribs welcome)")
{{else }}
log_error("Unrecognized Api Key format (contribs welcome).")
{{/if}}
{{else}}
# → Skipped: not implemented in the gdscript templates. (contribs are welcome)
{{/if}}
{{/each}}

View File

@@ -0,0 +1,41 @@
extends {{>partials/api_error_parent_class}}
class_name {{>partials/api_error_class_name}}
{{>partials/disclaimer_autogenerated}}
# Error wrapper provided to error callbacks
# =========================================
#
# Whenever this OAS client fails to comply to your request, for any reason,
# it will trigger the error callback, with an instance of this as parameter.
#
## Helps finding the error in the code, among other things.
## Could be a UUID, or even a translation key, so long as it's unique.
## Right now we're mostly using a lowercase ~namespace joined by dots. (.)
@export var identifier := ""
## A message for humans. May be multiline.
@export var message := ""
## One of Godot's ERR_XXXX, when relevant.
@export var internal_code := OK
## The HTTP response code, if any. (usually >= 400)
## DEPRECATED: prefer reading from response object below
@export var response_code := HTTPClient.RESPONSE_OK
## The HTTP response, if any.
@export var response: {{>partials/api_response_class_name}}
func _to_string() -> String:
var s := "{{>partials/api_error_class_name}}"
if identifier:
s += " %s" % identifier
if message:
s += " %s" % message
if response:
s += "\n%s" % str(response)
return s

View File

@@ -0,0 +1,33 @@
extends {{>partials/api_response_parent_class}}
class_name {{>partials/api_response_class_name}}
{{>partials/disclaimer_autogenerated}}
# Response wrapper provided to success callbacks
# ==============================================
#
# Holds the response metadata, its body, and the deserialized model(s), if any.
# This object is directly passed to the success callback, and in case of failure
# is injected in the error object.
#
## Headers sent back by the server
@export var headers := Dictionary()
## The HTTP response code, if any. A constant like HTTPClient.RESPONSE_XXXX
@export var code := 0
## Raw body of this response, in String form (before deserialization)
@export var body := ""
## Deserialized body (may be pretty much any type)
var data
func _to_string() -> String:
var s := "{{>partials/api_response_class_name}}"
if code:
s += " %d" % code
if body:
s += " %s" % body
return s

View File

@@ -0,0 +1,121 @@
{{#each models}}
{{#with model}}
{{>partials/model_statement_extends}}
{{>partials/model_statement_class_name}}
{{>partials/model_headers}}
{{#each vars}}
{{#if deprecated}}
# /!. DEPRECATED
{{/if}}
{{#if description}}
{{! FIXME: multiline description (how?) }}
# {{{description}}}
{{/if}}
{{#if isDate}}
# (but it's actually a Date ; no timezones support in Gdscript)
{{/if}}
{{#if isDateTime}}
# (but it's actually a DateTime ; no timezones support in Gdscript)
{{/if}}
# Required: {{#unless required}}False{{/unless}}{{#if required}}True{{/if}}
{{#if example}}
# Example: {{{example}}}
{{/if}}
# isArray: {{isArray}}
{{#if isEnum}}
# Allowed values: {{#with allowableValues}}{{#each values}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}{{/with}}
{{/if}}
@export var {{name}}: {{>partials/data_type}}{{#if defaultValue}} = {{{defaultValue}}}{{/if}}:
set(value):
{{#if deprecated}}
if str(value) != "":
push_warning("{{classname}}: property `{{name}}` is deprecated.")
{{/if}}
{{#if isEnum}}
if str(value) != "" and not (str(value) in __{{name}}__allowable__values):
push_error("{{classname}}: tried to set property `{{name}}` to a value that is not allowed." +
" Allowed values: {{#with allowableValues}}{{#each values}}`{{this}}`{{#unless @last}}, {{/unless}}{{/each}}{{/with}}")
return
{{/if}}
__{{name}}__was__set = true
{{name}} = value
{{! Flag used to only serialize what has been explicitely set. (no nullable types, anyway null might be legit) }}
var __{{name}}__was__set := false
{{! Store the allowed values if the property is an enum }}
{{#if isEnum}}
var __{{name}}__allowable__values := [
{{~#with allowableValues}}{{#each values}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}{{~/with~}}
]
{{/if}}
{{/each}}
func bzz_collect_missing_properties() -> Array:
var bzz_missing_properties := Array()
{{#each vars}}
{{#if required}}
if not self.__{{name}}__was__set:
bzz_missing_properties.append("{{name}}")
{{/if}}
{{/each}}
return bzz_missing_properties
func bzz_normalize() -> Dictionary:
var bzz_dictionary := Dictionary()
{{#each vars}}
if self.__{{name}}__was__set:
bzz_dictionary["{{name}}"] = self.{{name}}
{{/each}}
return bzz_dictionary
# Won't work for JSON+LD
{{!-- LEAKING if we specify return -> {{classname}} in func def --}}
static func bzz_denormalize_single(from_dict: Dictionary):
var me := new()
{{#each vars}}
if from_dict.has("{{name}}"):
{{#if isModel}}
me.{{name}} = {{>partials/complex_type}}.bzz_denormalize_single(from_dict["{{name}}"])
{{else if isArray}}
{{#if mostInnerItems.isModel}}
me.{{name}} = {{>partials/complex_type}}.bzz_denormalize_multiple(from_dict["{{name}}"])
{{else}}
me.{{name}} = from_dict["{{name}}"]
{{/if}}
{{else}}
me.{{name}} = from_dict["{{name}}"]
{{/if}}
{{/each}}
return me
# Won't work for JSON+LD
{{!-- LEAKING if we specify return -> {{classname}} in func def --}}
static func bzz_denormalize_multiple(from_array: Array):
var mes := Array()
for element in from_array:
if element is Array:
mes.append(bzz_denormalize_multiple(element))
elif element is Dictionary:
# TODO: perhaps check first if it looks like a match or an intermediate container
mes.append(bzz_denormalize_single(element))
else:
mes.append(element)
return mes
{{!-- UNUSED
func bzz_normalize_fully() -> Dictionary:
return {
{{#each vars}}
"{{name}}": self.{{name}},
{{/each}}
}
--}}
{{/with}}
{{/each}}

View File

@@ -0,0 +1,10 @@
**Partials** are bits of code that we reuse through templates,
or use once but provide anyway in order to make template customization easier.
For example, if you only want to change the utmost parent class of API classes,
you may override `api_bee_parent_class.handlebars` in this directory.
Note that these are probably not registered as actual partials to Handlebars,
so they are _pseuds-partials_ and won't allow recursion.
> _May the fork be with you, always._

View File

@@ -0,0 +1 @@
{{coreNamePrefix}}ApiBee{{coreNameSuffix}}

View File

@@ -0,0 +1,8 @@
{{!--
Override this template to let Apis inherit from your own desired class.
You can for example use Node here of you want to use Apis as Autoloads,
or provide the class_name of your own custom class, or even a quoted filepath.
Anything that goes after the `extends` statement will work.
TODO: would be nice to be able to customize this parent class from CLI.
--}}
RefCounted

View File

@@ -0,0 +1 @@
{{coreNamePrefix}}ApiConfig{{coreNameSuffix}}

View File

@@ -0,0 +1,6 @@
{{!--
Override this template to let config inherit from your own desired class.
It's probably best to use a descendant of Resource here (@export is used).
TODO: would be neat to be able to customize this parent class from CLI.
--}}
Resource

View File

@@ -0,0 +1 @@
{{coreNamePrefix}}ApiError{{coreNameSuffix}}

View File

@@ -0,0 +1,6 @@
{{!--
Override this template to let error inherit from your own desired class.
It's probably best to use a descendant of Resource here (@export is used).
TODO: would be cool to be able to customize this parent class from CLI.
--}}
Resource

View File

@@ -0,0 +1,4 @@
{{>partials/disclaimer_autogenerated}}
# API {{classname}}
# Instantiate this object and use it to make requests to the API.

View File

@@ -0,0 +1,9 @@
{{#each allParams}}
# {{paramName}}{{#if dataType}}: {{dataType}}{{/if}}{{#if defaultValue}} = {{{defaultValue}}}{{/if}}{{#if example}} Eg: {{{example}}}{{/if}}
{{~#if description}}
# {{{description}}}{{/if}}
{{paramName}}{{#if required}}{{#if dataType}}: {{{dataType}}}{{/if}}{{/if}}{{#unless required}} = {{#if defaultValue}}{{{defaultValue}}}{{/if}}{{#unless defaultValue}}null{{/unless}}{{/unless}},
{{/each}}
on_success: Callable = Callable(), # func(response: {{>partials/api_response_class_name}})
on_failure: Callable = Callable(), # func(error: {{>partials/api_error_class_name}})

View File

@@ -0,0 +1 @@
{{coreNamePrefix}}ApiResponse{{coreNameSuffix}}

View File

@@ -0,0 +1,6 @@
{{!--
Override this template to let response inherit from your own desired class.
It's probably best to use a descendant of Resource here (@export is used).
TODO: would be cool to be able to customize this parent class from CLI.
--}}
Resource

View File

@@ -0,0 +1 @@
{{>partials/statement_class_name}}

View File

@@ -0,0 +1 @@
extends {{>partials/api_base_class_name}}

View File

@@ -0,0 +1,2 @@
{{! Hotfix to ensure our complex types include the "namespace" ~}}
{{modelNamePrefix}}{{complexType}}{{modelNameSuffix~}}

View File

@@ -0,0 +1,3 @@
{{! Hotfix to ensure our data types include the "namespace" ~}}
{{#if isModel}}{{modelNamePrefix}}{{{dataType}}}{{modelNameSuffix}}{{/if~}}
{{#unless isModel}}{{{dataType}}}{{/unless~}}

View File

@@ -0,0 +1,5 @@
# THIS FILE WAS AUTOMATICALLY GENERATED by the OpenAPI Generator project.
# For more information on how to customize templates, see:
# https://openapi-generator.tech
# https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/gdscript
# The OpenAPI Generator Community, © Public Domain, 2022

View File

@@ -0,0 +1,7 @@
{{>partials/disclaimer_autogenerated}}
# {{classname}} Model
{{#if description}}
{{! FIXME: multiline description (how?) }}
# {{{description}}}
{{/if}}

View File

@@ -0,0 +1 @@
{{>partials/statement_class_name}}

View File

@@ -0,0 +1 @@
class_name {{classname}}

View File

@@ -0,0 +1,5 @@
Utils are not part of the template.
No need to customize them.
They probably ought not be here, since this whole dir may be downloaded by generator consumers.
Help me find the right spot for these !

View File

@@ -0,0 +1,62 @@
#!/bin/env python
# Generates and prints a list of reserved words in Godot
import xml.etree.ElementTree as ET
import requests
words_code = "" # output
indentation = " "
max_line_len = 119
url = "https://raw.githubusercontent.com/godotengine/godot/master/doc/classes/%40GlobalScope.xml"
xml_string = requests.get(url).content
root = ET.fromstring(xml_string)
methods_list = [] # of string
constants_list = [] # of string
singletons_list = [] # of string
for method in root.iter('method'):
methods_list.append(method.attrib['name'])
for constant in root.iter('constant'):
constants_list.append(constant.attrib['name'])
for singleton in root.iter('member'):
singletons_list.append(singleton.attrib['name'])
words_code += "%s// List generated from modules/openapi-generator/src/main/resources/gdscript/utils/extract_reserved_words.py\n" % indentation
current_line = ""
def new_line():
global current_line
current_line = "%s" % indentation
def write_line():
global current_line, words_code
words_code += "%s\n" % current_line
def add_word(word):
global current_line, words_code
words_string = "\"%s\", " % word
if len(current_line) + len(words_string) > max_line_len:
write_line()
new_line()
current_line += words_string
new_line()
words_code += "%s// Godot's global functions\n" % indentation
for reserved in methods_list:
add_word(reserved)
words_code += "%s// Godot's global constants\n" % indentation
for reserved in constants_list:
add_word(reserved)
words_code += "%s// Godot's singletons\n" % indentation
for reserved in singletons_list:
add_word(reserved)
write_line()
print(words_code)

View File

@@ -0,0 +1,29 @@
package org.openapitools.codegen.gdscript;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.GdscriptClientCodegen;
import io.swagger.models.*;
import io.swagger.models.properties.*;
import org.testng.Assert;
import org.testng.annotations.Test;
@SuppressWarnings("static-method")
public class GdscriptClientCodegenModelTest {
@Test(description = "convert a simple java model")
public void simpleModelTest() {
final Model model = new ModelImpl()
.description("a sample model")
.property("id", new LongProperty())
.property("name", new StringProperty())
.required("id")
.required("name");
final DefaultCodegen codegen = new GdscriptClientCodegen();
// TODO: Complete this test.
//Assert.fail("Not implemented.");
}
}

View File

@@ -0,0 +1,18 @@
package org.openapitools.codegen.gdscript;
import org.openapitools.codegen.languages.GdscriptClientCodegen;
import org.testng.annotations.Test;
import static org.mockito.Mockito.verify;
public class GdscriptClientCodegenTest {
GdscriptClientCodegen clientCodegen = new GdscriptClientCodegen();
@Test
public void shouldSucceed() throws Exception {
// TODO: Complete this test.
//Assert.fail("Not implemented.");
// verify(clientCodegen).setArtifactVersion(GdscriptClientCodegenOptionsProvider.ARTIFACT_VERSION_VALUE);
}
}

View File

@@ -0,0 +1,37 @@
package org.openapitools.codegen.gdscript;
//import org.openapitools.codegen.AbstractOptionsTest;
//import org.openapitools.codegen.CodegenConfig;
//import org.openapitools.codegen.languages.GdscriptClientCodegen;
//import org.openapitools.codegen.options.GdscriptClientOptionsProvider;
//
//import static org.mockito.Mockito.mock;
//import static org.mockito.Mockito.verify;
// NOTE:
// This is commented out because it fails to use handlebars.
// It is perhaps trivial to fix, but I could not figure it out.
// Anyway, there's no test case in here right now, so it does not matter much.
//public class GdscriptClientOptionsTest extends AbstractOptionsTest {
// private final GdscriptClientCodegen codegen = mock(GdscriptClientCodegen.class, mockSettings);
//
// public GdscriptClientOptionsTest() {
// super(new GdscriptClientOptionsProvider());
// }
//
// @Override
// protected CodegenConfig getCodegenConfig() {
// return codegen;
// }
//
// @SuppressWarnings("unused")
// @Override
// protected void verifyOptions() {
// // TODO: Complete options using Mockito
// // verify(codegen).someMethod(arguments)
// }
//}

View File

@@ -0,0 +1,74 @@
package org.openapitools.codegen.options;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.languages.GdscriptClientCodegen;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
public class GdscriptClientOptionsProvider implements OptionsProvider {
// public static final String MODEL_PACKAGE_VALUE = "package";
// public static final String API_PACKAGE_VALUE = "apiPackage";
// public static final String VARIABLE_NAMING_CONVENTION_VALUE = "snake_case";
// public static final String INVOKER_PACKAGE_VALUE = "OpenAPITools\\Client\\Php";
// public static final String PACKAGE_NAME_VALUE = "OpenAPIToolsClient-php";
// public static final String SRC_BASE_PATH_VALUE = "libPhp";
// public static final String ARTIFACT_VERSION_VALUE = "1.0.0-SNAPSHOT";
public static final String SORT_PARAMS_VALUE = "false";
public static final String SORT_MODEL_PROPERTIES_VALUE = "false";
public static final String ENSURE_UNIQUE_PARAMS_VALUE = "true";
public static final String ALLOW_UNICODE_IDENTIFIERS_VALUE = "false";
public static final String PREPEND_FORM_OR_BODY_PARAMETERS_VALUE = "true";
public static final String LEGACY_DISCRIMINATOR_BEHAVIOR_VALUE = "true";
public static final String NO_ADDITIONAL_PROPERTIES_VALUE = "true";
public static final String ENUM_UNKNOWN_DEFAULT_CASE_VALUE = "false";
@Override
public String getLanguage() {
return "gdscript";
}
@Override
public Map<String, String> createOptions() {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<String, String>();
return builder
.put(GdscriptClientCodegen.CORE_NAME_PREFIX, GdscriptClientCodegen.CORE_NAME_PREFIX_VALUE)
.put(GdscriptClientCodegen.CORE_NAME_SUFFIX, GdscriptClientCodegen.CORE_NAME_SUFFIX_VALUE)
.put(GdscriptClientCodegen.ANTICOLLISION_PREFIX, GdscriptClientCodegen.ANTICOLLISION_PREFIX_VALUE)
.put(GdscriptClientCodegen.ANTICOLLISION_SUFFIX, GdscriptClientCodegen.ANTICOLLISION_SUFFIX_VALUE)
// Things we *might* need (we'll see)
// .put(CodegenConstants.API_PACKAGE, API_PACKAGE_VALUE)
// .put(PhpClientCodegen.VARIABLE_NAMING_CONVENTION, VARIABLE_NAMING_CONVENTION_VALUE)
// .put(CodegenConstants.INVOKER_PACKAGE, INVOKER_PACKAGE_VALUE)
// .put(PhpClientCodegen.PACKAGE_NAME, PACKAGE_NAME_VALUE)
// .put(PhpClientCodegen.SRC_BASE_PATH, SRC_BASE_PATH_VALUE)
// .put(CodegenConstants.ARTIFACT_VERSION, ARTIFACT_VERSION_VALUE)
// .put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, "true")
// The following is required by CodeGen
//.put(CodegenConstants.TEMPLATING_ENGINE, "handlebars")
.put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, SORT_PARAMS_VALUE)
.put(CodegenConstants.SORT_MODEL_PROPERTIES_BY_REQUIRED_FLAG, SORT_MODEL_PROPERTIES_VALUE)
.put(CodegenConstants.ENSURE_UNIQUE_PARAMS, ENSURE_UNIQUE_PARAMS_VALUE)
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, LEGACY_DISCRIMINATOR_BEHAVIOR_VALUE)
.put(CodegenConstants.DISALLOW_ADDITIONAL_PROPERTIES_IF_NOT_PRESENT, NO_ADDITIONAL_PROPERTIES_VALUE)
.put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, ENUM_UNKNOWN_DEFAULT_CASE_VALUE)
.build();
}
@Override
public boolean isServer() {
return false;
}
}

View File

@@ -0,0 +1,915 @@
openapi: 3.0.0
servers:
- url: 'http://petstore.swagger.io/v2'
info:
description: >-
This is a sample server Petstore server. For this sample, you can use the api key
`special-key` to test the authorization filters.
version: 1.0.0
title: OpenAPI Petstore
license:
name: Apache-2.0
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
tags:
- name: pet
description: Everything about your Pets
- name: store
description: Access to Petstore orders
- name: user
description: Operations about user
paths:
/pet:
post:
tags:
- pet
summary: Add a new pet to the store
description: ''
operationId: addPet
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
$ref: '#/components/requestBodies/Pet'
put:
tags:
- pet
summary: Update an existing pet
description: ''
operationId: updatePet
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
'405':
description: Validation exception
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
$ref: '#/components/requestBodies/Pet'
/pet/findByStatus:
get:
tags:
- pet
summary: Finds Pets by status
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- name: status
in: query
description: Status values that need to be considered for filter
required: true
style: form
explode: false
deprecated: true
schema:
type: array
items:
type: string
enum:
- available
- pending
- sold
default: available
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid status value
security:
- petstore_auth:
- 'read:pets'
/pet/findByTags:
get:
tags:
- pet
summary: Finds Pets by tags
description: >-
Multiple tags can be provided with comma separated strings. Use tag1,
tag2, tag3 for testing.
operationId: findPetsByTags
parameters:
- name: tags
in: query
description: Tags to filter by
required: true
style: form
explode: false
schema:
type: array
items:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid tag value
security:
- petstore_auth:
- 'read:pets'
deprecated: true
'/pet/{petId}':
get:
tags:
- pet
summary: Find pet by ID
description: Returns a single pet
operationId: getPetById
parameters:
- name: petId
in: path
description: ID of pet to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
security:
- api_key: []
post:
tags:
- pet
summary: Updates a pet in the store with form data
description: ''
operationId: updatePetWithForm
parameters:
- name: petId
in: path
description: ID of pet that needs to be updated
required: true
schema:
type: integer
format: int64
responses:
'405':
description: Invalid input
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
name:
description: Updated name of the pet
type: string
status:
description: Updated status of the pet
type: string
delete:
tags:
- pet
summary: Deletes a pet
description: ''
operationId: deletePet
parameters:
- name: api_key
in: header
required: false
schema:
type: string
- name: petId
in: path
description: Pet id to delete
required: true
schema:
type: integer
format: int64
responses:
'400':
description: Invalid pet value
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
'/pet/{petId}/uploadImage':
post:
tags:
- pet
summary: uploads an image
description: ''
operationId: uploadFile
parameters:
- name: petId
in: path
description: ID of pet to update
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
security:
- petstore_auth:
- 'write:pets'
- 'read:pets'
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
additionalMetadata:
description: Additional data to pass to server
type: string
file:
description: file to upload
type: string
format: binary
/store/inventory:
get:
tags:
- store
summary: Returns pet inventories by status
description: Returns a map of status codes to quantities
operationId: getInventory
responses:
'200':
description: successful operation
content:
application/json:
schema:
type: object
additionalProperties:
type: integer
format: int32
security:
- api_key: []
/store/order:
post:
tags:
- store
summary: Place an order for a pet
description: ''
operationId: placeOrder
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Invalid Order
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
description: order placed for purchasing the pet
required: true
'/store/order/{orderId}':
get:
tags:
- store
summary: Find purchase order by ID
description: >-
For valid response try integer IDs with value <= 5 or > 10. Other values
will generate exceptions
operationId: getOrderById
parameters:
- name: orderId
in: path
description: ID of pet that needs to be fetched
required: true
schema:
type: integer
format: int64
minimum: 1
maximum: 5
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/json:
schema:
$ref: '#/components/schemas/Order'
'400':
description: Invalid ID supplied
'404':
description: Order not found
delete:
tags:
- store
summary: Delete purchase order by ID
description: >-
For valid response try integer IDs with value < 1000. Anything above
1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- name: orderId
in: path
description: ID of the order that needs to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid ID supplied
'404':
description: Order not found
/user:
post:
tags:
- user
summary: Create user
description: This can only be done by the logged in user.
operationId: createUser
responses:
default:
description: successful operation
security:
- api_key: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: Created user object
required: true
/user/createWithArray:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
operationId: createUsersWithArrayInput
responses:
default:
description: successful operation
security:
- api_key: []
requestBody:
$ref: '#/components/requestBodies/UserArray'
/user/createWithList:
post:
tags:
- user
summary: Creates list of users with given input array
description: ''
operationId: createUsersWithListInput
responses:
default:
description: successful operation
security:
- api_key: []
requestBody:
$ref: '#/components/requestBodies/UserArray'
/user/login:
get:
tags:
- user
summary: Logs user into the system
description: ''
operationId: loginUser
parameters:
- name: username
in: query
description: The user name for login
required: true
schema:
type: string
pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
- name: password
in: query
description: The password for login in clear text
required: true
schema:
type: string
responses:
'200':
description: successful operation
headers:
Set-Cookie:
description: >-
Cookie authentication key for use with the `api_key`
apiKey authentication.
schema:
type: string
example: AUTH_KEY=abcde12345; Path=/; HttpOnly
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
type: integer
format: int32
X-Expires-After:
description: date in UTC when token expires
schema:
type: string
format: date-time
content:
application/xml:
schema:
type: string
application/json:
schema:
type: string
'400':
description: Invalid username/password supplied
/user/logout:
get:
tags:
- user
summary: Logs out current logged in user session
description: ''
operationId: logoutUser
responses:
default:
description: successful operation
security:
- api_key: []
'/user/{username}':
get:
tags:
- user
summary: Get user by user name
description: ''
operationId: getUserByName
parameters:
- name: username
in: path
description: The name that needs to be fetched. Use user1 for testing.
required: true
schema:
type: string
responses:
'200':
description: successful operation
content:
application/xml:
schema:
$ref: '#/components/schemas/User'
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Invalid username supplied
'404':
description: User not found
put:
tags:
- user
summary: Updated user
description: This can only be done by the logged in user.
operationId: updateUser
parameters:
- name: username
in: path
description: name that need to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid user supplied
'404':
description: User not found
security:
- api_key: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
description: Updated user object
required: true
delete:
tags:
- user
summary: Delete user
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- name: username
in: path
description: The name that needs to be deleted
required: true
schema:
type: string
responses:
'400':
description: Invalid username supplied
'404':
description: User not found
security:
- api_key: []
'/fake/user/{username}':
get:
tags:
- fake
summary: To test nullable required parameters
description: ''
operationId: test_nullable_required_param
parameters:
- name: username
in: path
description: The name that needs to be fetched. Use user1 for testing.
required: true
schema:
type: string
- name: dummy_required_nullable_param
in: header
description: To test nullable required parameters
required: true
schema:
type: string
nullable: true
- name: UPPERCASE
in: header
description: To test parameter names in upper case
schema:
type: string
responses:
'200':
description: successful operation
'400':
description: Invalid username supplied
'404':
description: User not found
'/tests/fileResponse':
get:
tags:
- testing
summary: Returns an image file
responses:
'200':
description: An image file
content:
image/jpeg:
schema:
type: string
format: binary
'/tests/typeTesting':
get:
tags:
- testing
summary: Route to test the TypeTesting schema
responses:
'200':
description: The TypeTesting response
content:
application/json:
schema:
$ref: '#/components/schemas/TypeTesting'
externalDocs:
description: Find out more about Swagger
url: 'http://swagger.io'
components:
requestBodies:
UserArray:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
description: List of user object
required: true
Pet:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
description: Pet object that needs to be added to the store
required: true
securitySchemes:
petstore_auth:
type: oauth2
flows:
implicit:
authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog'
scopes:
'write:pets': modify pets in your account
'read:pets': read your pets
api_key:
type: apiKey
name: api_key
in: header
schemas:
Order:
title: Pet Order
description: An order for a pets from the pet store
type: object
properties:
id:
type: integer
format: int64
petId:
type: integer
format: int64
quantity:
type: integer
format: int32
shipDate:
type: string
format: date-time
status:
type: string
description: Order Status
enum:
- placed
- approved
- delivered
complete:
type: boolean
default: false
xml:
name: Order
Category:
title: Pet category
description: A category for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
xml:
name: Category
User:
title: a User
description: A User who is purchasing from the pet store
type: object
properties:
id:
type: integer
format: int64
username:
type: string
firstName:
type: string
lastName:
type: string
email:
type: string
password:
type: string
phone:
type: string
userStatus:
type: integer
format: int32
description: User Status
xml:
name: User
Tag:
title: Pet Tag
description: A tag for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
xml:
name: Tag
Pet:
title: a Pet
description: A pet for sale in the pet store
type: object
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
category:
$ref: '#/components/schemas/Category'
name:
type: string
example: doggie
photoUrls:
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
tags:
type: array
xml:
name: tag
wrapped: true
items:
$ref: '#/components/schemas/Tag'
status:
type: string
description: pet status in the store
deprecated: true
enum:
- available
- pending
- sold
xml:
name: Pet
ApiResponse:
title: An uploaded response
description: Describes the result of uploading an image resource
type: object
properties:
code:
type: integer
format: int32
type:
type: string
message:
type: string
property_test:
title: A model to test various formats, e.g. UUID
description: A model to test various formats, e.g. UUID
type: object
properties:
uuid:
type: string
format: uuid
ActionContainer:
required:
- action
type: object
properties:
action:
allOf:
- $ref: '#/components/schemas/Baz'
- nullable: false
Baz:
description: Test handling of empty variants
enum:
- A
- B
- ""
type: string
TypeTesting:
description: Test handling of different field data types
type: object
required:
- int32
- int64
- float
- double
- string
- boolean
- uuid
properties:
int32:
type: integer
format: int32
int64:
type: integer
format: int64
float:
type: number
format: float
double:
type: number
format: double
string:
type: string
boolean:
type: boolean
uuid:
type: string
format: uuid
Return:
description: Test using keywords
type: object
properties:
match:
type: integer
async:
type: boolean
super:
type: boolean
OptionalTesting:
description: Test handling of optional and nullable fields
type: object
required:
- required_nonnull
- required_nullable
properties:
optional_nonnull:
type: string
nullable: false
required_nonnull:
type: string
nullable: false
optional_nullable:
type: string
nullable: true
required_nullable:
type: string
nullable: true
EnumArrayTesting:
description: Test of enum array
type: object
required:
- required_enums
properties:
required_enums:
type: array
items:
type: string
enum: ["A", "B", "C"]
ArrayRefItem:
description: Helper object for the array item ref test
type: array
items:
type: string
ObjectRefItem:
description: Helper object for the array item ref test
type: object
additionalProperties: true
ArrayItemRefTest:
description: Test handling of object reference in arrays
type: object
required:
- list_with_array_ref
- list_with_object_ref
properties:
list_with_array_ref:
type: array
items:
$ref: '#/components/schemas/ArrayRefItem'
list_with_object_ref:
type: array
items:
$ref: '#/components/schemas/ObjectRefItem'

View File

@@ -0,0 +1,4 @@
# Godot 4+ specific ignores
.godot/
*.import

View File

@@ -0,0 +1,42 @@
{
"background_color": "262626ff",
"compact_mode": false,
"config_file": "res://.gutconfig.json",
"dirs": [
"res://test/integration"
],
"disable_colors": false,
"double_strategy": "partial",
"font_color": "ccccccff",
"font_name": "CourierPrime",
"font_size": 16,
"gut_on_top": true,
"hide_orphans": false,
"ignore_pause": false,
"include_subdirs": false,
"inner_class": null,
"junit_xml_file": "",
"junit_xml_timestamp": false,
"log_level": 1,
"opacity": 100,
"paint_after": 0.5,
"panel_options": {
"font_name": "CourierPrime",
"font_size": 30,
"hide_output_text": false,
"hide_result_tree": false,
"hide_settings": false,
"use_colors": false
},
"post_run_script": "",
"pre_run_script": "",
"prefix": "test_",
"selected": null,
"should_exit": false,
"should_exit_on_success": false,
"should_maximize": false,
"show_help": false,
"suffix": ".gd",
"tests": [],
"unit_test_name": null
}

View File

@@ -0,0 +1,17 @@
[main]
run_all=Object(Shortcut,"resource_local_to_scene":false,"resource_name":"","events":[Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
],"script":null)
run_current_script=Object(Shortcut,"resource_local_to_scene":false,"resource_name":"","events":[Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
],"script":null)
run_current_inner=Object(Shortcut,"resource_local_to_scene":false,"resource_name":"","events":[Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
],"script":null)
run_current_test=Object(Shortcut,"resource_local_to_scene":false,"resource_name":"","events":[Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
],"script":null)
panel_button=Object(Shortcut,"resource_local_to_scene":false,"resource_name":"","events":[Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
],"script":null)

View File

@@ -0,0 +1,32 @@
{
"background_color": null,
"compact_mode": null,
"dirs": [
"res://test/integration"
],
"disable_colors": null,
"double_strategy": null,
"font_color": null,
"font_name": null,
"font_size": null,
"gut_on_top": null,
"hide_orphans": null,
"ignore_pause": null,
"include_subdirs": null,
"inner_class": null,
"junit_xml_file": null,
"junit_xml_timestamp": null,
"log_level": null,
"opacity": null,
"paint_after": null,
"post_run_script": null,
"pre_run_script": null,
"prefix": null,
"selected": null,
"should_exit": true,
"should_exit_on_success": null,
"should_maximize": null,
"suffix": null,
"tests": null,
"unit_test_name": null
}

View File

@@ -0,0 +1,16 @@
README.md
apis/DemoPetApi.gd
apis/DemoPetApi.md
apis/DemoStoreApi.gd
apis/DemoStoreApi.md
apis/DemoUserApi.gd
apis/DemoUserApi.md
core/DemoApiBee.gd
core/DemoApiConfig.gd
core/DemoApiError.gd
models/DemoApiResponse.gd
models/DemoCategory.gd
models/DemoOrder.gd
models/DemoPet.gd
models/DemoTag.gd
models/DemoUser.gd

View File

@@ -0,0 +1 @@
6.2.1-SNAPSHOT

View File

@@ -0,0 +1,36 @@
# Petstore GDScript Testing Client
## What
- [Godot] `4.x` project
- Made for `--headless` mode
- Returns non-zero exit code upon test failure
- Uses [GUT] as test engine
## Prepare the test server
See https://github.com/OpenAPITools/openapi-generator/wiki/Integration-Tests
> We are using the petstore docker, not the echo server for now.
> Feel free to refactor or duplicate the sample demo to use the new echo server.
> See `bin/configs/gdscript-petstore.yaml`.
## Run
godot --headless --debug --path samples/client/petstore/gdscript --script addons/gut/gut_cmdln.gd
The command should return a _zero_ exit code if all tests _passed_.
You may want to add `--verbose` for more logs when debugging.
## Update
Refresh the generated files, after modifying the gdscript templates or java generator:
bin/generate-samples.sh bin/configs/gdscript-petstore.yaml
[Godot]: https://godotengine.org
[GUT]: https://github.com/bitwes/Gut

View File

@@ -0,0 +1,124 @@
extends Node2D
# ##############################################################################
# This is a wrapper around the normal and compact gui controls and serves as
# the interface between gut.gd and the gui. The GutRunner creates an instance
# of this and then this takes care of managing the different GUI controls.
# ##############################################################################
@onready var _normal_gui = $Normal
@onready var _compact_gui = $Compact
var gut = null :
set(val):
gut = val
_set_gut(val)
func _ready():
_normal_gui.switch_modes.connect(use_compact_mode.bind(true))
_compact_gui.switch_modes.connect(use_compact_mode.bind(false))
_normal_gui.set_title("GUT")
_compact_gui.set_title("GUT")
_normal_gui.align_right()
_compact_gui.to_bottom_right()
use_compact_mode(false)
if(get_parent() == get_tree().root):
_test_running_setup()
func _test_running_setup():
_normal_gui.get_textbox().text = "hello world, how are you doing?"
# ------------------------
# Private
# ------------------------
func _set_gut(val):
_normal_gui.set_gut(val)
_compact_gui.set_gut(val)
val.start_run.connect(_on_gut_start_run)
val.end_run.connect(_on_gut_end_run)
val.start_pause_before_teardown.connect(_on_gut_pause)
val.end_pause_before_teardown.connect(_on_pause_end)
func _set_both_titles(text):
_normal_gui.set_title(text)
_compact_gui.set_title(text)
# ------------------------
# Events
# ------------------------
func _on_gut_start_run():
_set_both_titles('Running')
func _on_gut_end_run():
_set_both_titles('Finished')
func _on_gut_pause():
_set_both_titles('-- Paused --')
func _on_pause_end():
_set_both_titles('Running')
# ------------------------
# Public
# ------------------------
func get_textbox():
return _normal_gui.get_textbox()
func set_font_size(new_size):
return
var rtl = _normal_gui.get_textbox()
if(rtl.get('custom_fonts/normal_font') != null):
rtl.get('custom_fonts/bold_italics_font').size = new_size
rtl.get('custom_fonts/bold_font').size = new_size
rtl.get('custom_fonts/italics_font').size = new_size
rtl.get('custom_fonts/normal_font').size = new_size
func set_font(font_name):
_set_all_fonts_in_rtl(_normal_gui.get_textbox(), font_name)
func _set_font(rtl, font_name, custom_name):
if(font_name == null):
rtl.add_theme_font_override(custom_name, null)
else:
var dyn_font = FontFile.new()
dyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf')
rtl.add_theme_font_override(custom_name, dyn_font)
func _set_all_fonts_in_rtl(rtl, base_name):
if(base_name == 'Default'):
_set_font(rtl, null, 'normal_font')
_set_font(rtl, null, 'bold_font')
_set_font(rtl, null, 'italics_font')
_set_font(rtl, null, 'bold_italics_font')
else:
_set_font(rtl, base_name + '-Regular', 'normal_font')
_set_font(rtl, base_name + '-Bold', 'bold_font')
_set_font(rtl, base_name + '-Italic', 'italics_font')
_set_font(rtl, base_name + '-BoldItalic', 'bold_italics_font')
func set_default_font_color(color):
_normal_gui.get_textbox().set('custom_colors/default_color', color)
func set_background_color(color):
_normal_gui.set_bg_color(color)
func use_compact_mode(should=true):
_compact_gui.visible = should
_normal_gui.visible = !should
func set_opacity(val):
_normal_gui.modulate.a = val
_compact_gui.modulate.a = val

View File

@@ -0,0 +1,16 @@
[gd_scene load_steps=4 format=3 uid="uid://m28heqtswbuq"]
[ext_resource type="Script" path="res://addons/gut/GutScene.gd" id="1_b4m8y"]
[ext_resource type="PackedScene" uid="uid://duxblir3vu8x7" path="res://addons/gut/gui/NormalGui.tscn" id="2_j6ywb"]
[ext_resource type="PackedScene" uid="uid://cnqqdfsn80ise" path="res://addons/gut/gui/MinGui.tscn" id="3_3glw1"]
[node name="GutScene" type="Node2D"]
script = ExtResource("1_b4m8y")
[node name="Normal" parent="." instance=ExtResource("2_j6ywb")]
[node name="Compact" parent="." instance=ExtResource("3_3glw1")]
offset_left = 5.0
offset_top = 273.0
offset_right = 265.0
offset_bottom = 403.0

View File

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

View File

@@ -0,0 +1,52 @@
extends Window
@onready var rtl = $TextDisplay/RichTextLabel
func _get_file_as_text(path):
var to_return = null
var f = FileAccess.open(path, FileAccess.READ)
if(f != null):
to_return = f.get_as_text()
else:
to_return = str('ERROR: Could not open file. Error code ', FileAccess.get_open_error())
return to_return
func _ready():
rtl.clear()
func _on_OpenFile_pressed():
$FileDialog.popup_centered()
func _on_FileDialog_file_selected(path):
show_file(path)
func _on_Close_pressed():
self.hide()
func show_file(path):
var text = _get_file_as_text(path)
if(text == ''):
text = '<Empty File>'
rtl.set_text(text)
self.window_title = path
func show_open():
self.popup_centered()
$FileDialog.popup_centered()
func get_rich_text_label():
return $TextDisplay/RichTextLabel
func _on_Home_pressed():
rtl.scroll_to_line(0)
func _on_End_pressed():
rtl.scroll_to_line(rtl.get_line_count() -1)
func _on_Copy_pressed():
return
# OS.clipboard = rtl.text
func _on_file_dialog_visibility_changed():
if rtl.text.length() == 0 and not $FileDialog.visible:
self.hide()

View File

@@ -0,0 +1,92 @@
[gd_scene load_steps=2 format=3 uid="uid://bsm7wtt1gie4v"]
[ext_resource type="Script" path="res://addons/gut/UserFileViewer.gd" id="1"]
[node name="UserFileViewer" type="Window"]
exclusive = true
script = ExtResource("1")
[node name="FileDialog" type="FileDialog" parent="."]
access = 1
show_hidden_files = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TextDisplay" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_right = -10.0
offset_bottom = -65.0
color = Color(0.2, 0.188235, 0.188235, 1)
[node name="RichTextLabel" type="RichTextLabel" parent="TextDisplay"]
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 2
text = "In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used before final copy is available, but it may also be used to temporarily replace copy in a process called greeking, which allows designers to consider form without the meaning of the text influencing the design.
Lorem ipsum is typically a corrupted version of De finibus bonorum et malorum, a first-century BCE text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical, improper Latin.
Versions of the Lorem ipsum text have been used in typesetting at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. Lorem ipsum was introduced to the digital world in the mid-1980s when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word processors including Pages and Microsoft Word have since adopted Lorem ipsum as well."
selection_enabled = true
[node name="OpenFile" type="Button" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -158.0
offset_top = -50.0
offset_right = -84.0
offset_bottom = -30.0
text = "Open File"
[node name="Home" type="Button" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -478.0
offset_top = -50.0
offset_right = -404.0
offset_bottom = -30.0
text = "Home"
[node name="Copy" type="Button" parent="."]
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 160.0
offset_top = -50.0
offset_right = 234.0
offset_bottom = -30.0
text = "Copy"
[node name="End" type="Button" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -318.0
offset_top = -50.0
offset_right = -244.0
offset_bottom = -30.0
text = "End"
[node name="Close" type="Button" parent="."]
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 10.0
offset_top = -50.0
offset_right = 80.0
offset_bottom = -30.0
text = "Close"
[connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"]
[connection signal="visibility_changed" from="FileDialog" to="." method="_on_file_dialog_visibility_changed"]
[connection signal="pressed" from="OpenFile" to="." method="_on_OpenFile_pressed"]
[connection signal="pressed" from="Home" to="." method="_on_Home_pressed"]
[connection signal="pressed" from="Copy" to="." method="_on_Copy_pressed"]
[connection signal="pressed" from="End" to="." method="_on_End_pressed"]
[connection signal="pressed" from="Close" to="." method="_on_Close_pressed"]

View File

@@ -0,0 +1,59 @@
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2020 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.
#
# ##############################################################################
# Class used to keep track of objects to be freed and utilities to free them.
# ##############################################################################
var _to_free = []
var _to_queue_free = []
func add_free(thing):
if(typeof(thing) == TYPE_OBJECT):
if(!thing is RefCounted):
_to_free.append(thing)
func add_queue_free(thing):
_to_queue_free.append(thing)
func get_queue_free_count():
return _to_queue_free.size()
func get_free_count():
return _to_free.size()
func free_all():
for i in range(_to_free.size()):
if(is_instance_valid(_to_free[i])):
_to_free[i].free()
_to_free.clear()
for i in range(_to_queue_free.size()):
if(is_instance_valid(_to_queue_free[i])):
_to_queue_free[i].queue_free()
_to_queue_free.clear()

View File

@@ -0,0 +1,67 @@
extends Node
signal timeout
signal wait_started
var _wait_time = 0.0
var _wait_frames = 0
var _signal_to_wait_on = null
var _elapsed_time = 0.0
var _elapsed_frames = 0
func _physics_process(delta):
if(_wait_time != 0.0):
_elapsed_time += delta
if(_elapsed_time >= _wait_time):
_end_wait()
if(_wait_frames != 0):
_elapsed_frames += 1
if(_elapsed_frames >= _wait_frames):
_end_wait()
func _end_wait():
_wait_time = 0.0
_wait_frames = 0
_signal_to_wait_on = null
_elapsed_time = 0.0
_elapsed_frames = 0
timeout.emit()
const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_'
func _signal_callback(
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):
_signal_to_wait_on.disconnect(_signal_callback)
# DO NOT _end_wait here. For other parts of the test to get the signal that
# was waited on, we have to wait for a couple more frames. For example, the
# signal_watcher doesn't get the signal in time if we don't do this.
_wait_frames = 2
func wait_for(x):
_wait_time = x
wait_started.emit()
func wait_frames(x):
_wait_frames = x
wait_started.emit()
func wait_for_signal(the_signal, x):
the_signal.connect(_signal_callback)
_signal_to_wait_on = the_signal
_wait_time = x
wait_started.emit()
func is_waiting():
return _wait_time != 0.0 || _wait_frames != 0

View File

@@ -0,0 +1,123 @@
var _utils = load('res://addons/gut/utils.gd').get_instance()
var _strutils = _utils.Strutils.new()
var _max_length = 100
var _should_compare_int_to_float = true
const MISSING = '|__missing__gut__compare__value__|'
func _cannot_compare_text(v1, v2):
return str('Cannot compare ', _strutils.types[typeof(v1)], ' with ',
_strutils.types[typeof(v2)], '.')
func _make_missing_string(text):
return '<missing ' + text + '>'
func _create_missing_result(v1, v2, text):
var to_return = null
var v1_str = format_value(v1)
var v2_str = format_value(v2)
if(typeof(v1) == TYPE_STRING and v1 == MISSING):
v1_str = _make_missing_string(text)
to_return = _utils.CompareResult.new()
elif(typeof(v2) == TYPE_STRING and v2 == MISSING):
v2_str = _make_missing_string(text)
to_return = _utils.CompareResult.new()
if(to_return != null):
to_return.summary = str(v1_str, ' != ', v2_str)
to_return.are_equal = false
return to_return
func simple(v1, v2, missing_string=''):
var missing_result = _create_missing_result(v1, v2, missing_string)
if(missing_result != null):
return missing_result
var result = _utils.CompareResult.new()
var cmp_str = null
var extra = ''
var tv1 = typeof(v1)
var tv2 = typeof(v2)
# print(tv1, '::', tv2, ' ', _strutils.types[tv1], '::', _strutils.types[tv2])
if(_should_compare_int_to_float and [TYPE_INT, TYPE_FLOAT].has(tv1) and [TYPE_INT, TYPE_FLOAT].has(tv2)):
result.are_equal = v1 == v2
elif([TYPE_STRING, TYPE_STRING_NAME].has(tv1) and [TYPE_STRING, TYPE_STRING_NAME].has(tv2)):
result.are_equal = v1 == v2
elif(_utils.are_datatypes_same(v1, v2)):
result.are_equal = v1 == v2
if(typeof(v1) == TYPE_DICTIONARY or typeof(v1) == TYPE_ARRAY):
var sub_result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP)
result.summary = sub_result.get_short_summary()
if(!sub_result.are_equal):
extra = ".\n" + sub_result.get_short_summary()
else:
cmp_str = '!='
result.are_equal = false
extra = str('. ', _cannot_compare_text(v1, v2))
cmp_str = get_compare_symbol(result.are_equal)
result.summary = str(format_value(v1), ' ', cmp_str, ' ', format_value(v2), extra)
return result
func shallow(v1, v2):
var result = null
if(_utils.are_datatypes_same(v1, v2)):
if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):
result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP)
else:
result = simple(v1, v2)
else:
result = simple(v1, v2)
return result
func deep(v1, v2):
var result = null
if(_utils.are_datatypes_same(v1, v2)):
if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):
result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP)
else:
result = simple(v1, v2)
else:
result = simple(v1, v2)
return result
func format_value(val, max_val_length=_max_length):
return _strutils.truncate_string(_strutils.type2str(val), max_val_length)
func compare(v1, v2, diff_type=_utils.DIFF.SIMPLE):
var result = null
if(diff_type == _utils.DIFF.SIMPLE):
result = simple(v1, v2)
elif(diff_type == _utils.DIFF.DEEP):
result = deep(v1, v2)
return result
func get_should_compare_int_to_float():
return _should_compare_int_to_float
func set_should_compare_int_to_float(should_compare_int_float):
_should_compare_int_to_float = should_compare_int_float
func get_compare_symbol(is_equal):
if(is_equal):
return '=='
else:
return '!='

View File

@@ -0,0 +1,70 @@
var _are_equal = false
var are_equal = false :
get:
return get_are_equal()
set(val):
set_are_equal(val)
var _summary = null
var summary = null :
get:
return get_summary()
set(val):
set_summary(val)
var _max_differences = 30
var max_differences = 30 :
get:
return get_max_differences()
set(val):
set_max_differences(val)
var _differences = {}
var differences :
get:
return get_differences()
set(val):
set_differences(val)
func _block_set(which, val):
push_error(str('cannot set ', which, ', value [', val, '] ignored.'))
func _to_string():
return str(get_summary()) # could be null, gotta str it.
func get_are_equal():
return _are_equal
func set_are_equal(r_eq):
_are_equal = r_eq
func get_summary():
return _summary
func set_summary(smry):
_summary = smry
func get_total_count():
pass
func get_different_count():
pass
func get_short_summary():
return summary
func get_max_differences():
return _max_differences
func set_max_differences(max_diff):
_max_differences = max_diff
func get_differences():
return _differences
func set_differences(diffs):
_block_set('differences', diffs)
func get_brackets():
return null

View File

@@ -0,0 +1,64 @@
var _utils = load('res://addons/gut/utils.gd').get_instance()
var _strutils = _utils.Strutils.new()
const INDENT = ' '
var _max_to_display = 30
const ABSOLUTE_MAX_DISPLAYED = 10000
const UNLIMITED = -1
func _single_diff(diff, depth=0):
var to_return = ""
var brackets = diff.get_brackets()
if(brackets != null and !diff.are_equal):
to_return = ''
to_return += str(brackets.open, "\n",
_strutils.indent_text(differences_to_s(diff.differences, depth), depth+1, INDENT), "\n",
brackets.close)
else:
to_return = str(diff)
return to_return
func make_it(diff):
var to_return = ''
if(diff.are_equal):
to_return = diff.summary
else:
if(_max_to_display == ABSOLUTE_MAX_DISPLAYED):
to_return = str(diff.get_value_1(), ' != ', diff.get_value_2())
else:
to_return = diff.get_short_summary()
to_return += str("\n", _strutils.indent_text(_single_diff(diff, 0), 1, ' '))
return to_return
func differences_to_s(differences, depth=0):
var to_return = ''
var keys = differences.keys()
keys.sort()
var limit = min(_max_to_display, differences.size())
for i in range(limit):
var key = keys[i]
to_return += str(key, ": ", _single_diff(differences[key], depth))
if(i != limit -1):
to_return += "\n"
if(differences.size() > _max_to_display):
to_return += str("\n\n... ", differences.size() - _max_to_display, " more.")
return to_return
func get_max_to_display():
return _max_to_display
func set_max_to_display(max_to_display):
_max_to_display = max_to_display
if(_max_to_display == UNLIMITED):
_max_to_display = ABSOLUTE_MAX_DISPLAYED

View File

@@ -0,0 +1,158 @@
extends 'res://addons/gut/compare_result.gd'
const INDENT = ' '
enum {
DEEP,
SIMPLE
}
var _utils = load('res://addons/gut/utils.gd').get_instance()
var _strutils = _utils.Strutils.new()
var _compare = _utils.Comparator.new()
var DiffTool = load('res://addons/gut/diff_tool.gd')
var _value_1 = null
var _value_2 = null
var _total_count = 0
var _diff_type = null
var _brackets = null
var _valid = true
var _desc_things = 'somethings'
# -------- comapre_result.gd "interface" ---------------------
func set_are_equal(val):
_block_set('are_equal', val)
func get_are_equal():
if(!_valid):
return null
else:
return differences.size() == 0
func set_summary(val):
_block_set('summary', val)
func get_summary():
return summarize()
func get_different_count():
return differences.size()
func get_total_count():
return _total_count
func get_short_summary():
var text = str(_strutils.truncate_string(str(_value_1), 50),
' ', _compare.get_compare_symbol(are_equal), ' ',
_strutils.truncate_string(str(_value_2), 50))
if(!are_equal):
text += str(' ', get_different_count(), ' of ', get_total_count(),
' ', _desc_things, ' do not match.')
return text
func get_brackets():
return _brackets
# -------- comapre_result.gd "interface" ---------------------
func _invalidate():
_valid = false
differences = null
func _init(v1,v2,diff_type=DEEP):
_value_1 = v1
_value_2 = v2
_diff_type = diff_type
_compare.set_should_compare_int_to_float(false)
_find_differences(_value_1, _value_2)
func _find_differences(v1, v2):
if(_utils.are_datatypes_same(v1, v2)):
if(typeof(v1) == TYPE_ARRAY):
_brackets = {'open':'[', 'close':']'}
_desc_things = 'indexes'
_diff_array(v1, v2)
elif(typeof(v2) == TYPE_DICTIONARY):
_brackets = {'open':'{', 'close':'}'}
_desc_things = 'keys'
_diff_dictionary(v1, v2)
else:
_invalidate()
_utils.get_logger().error('Only Arrays and Dictionaries are supported.')
else:
_invalidate()
_utils.get_logger().error('Only Arrays and Dictionaries are supported.')
func _diff_array(a1, a2):
_total_count = max(a1.size(), a2.size())
for i in range(a1.size()):
var result = null
if(i < a2.size()):
if(_diff_type == DEEP):
result = _compare.deep(a1[i], a2[i])
else:
result = _compare.simple(a1[i], a2[i])
else:
result = _compare.simple(a1[i], _compare.MISSING, 'index')
if(!result.are_equal):
differences[i] = result
if(a1.size() < a2.size()):
for i in range(a1.size(), a2.size()):
differences[i] = _compare.simple(_compare.MISSING, a2[i], 'index')
func _diff_dictionary(d1, d2):
var d1_keys = d1.keys()
var d2_keys = d2.keys()
# Process all the keys in d1
_total_count += d1_keys.size()
for key in d1_keys:
if(!d2.has(key)):
differences[key] = _compare.simple(d1[key], _compare.MISSING, 'key')
else:
d2_keys.remove_at(d2_keys.find(key))
var result = null
if(_diff_type == DEEP):
result = _compare.deep(d1[key], d2[key])
else:
result = _compare.simple(d1[key], d2[key])
if(!result.are_equal):
differences[key] = result
# Process all the keys in d2 that didn't exist in d1
_total_count += d2_keys.size()
for i in range(d2_keys.size()):
differences[d2_keys[i]] = _compare.simple(_compare.MISSING, d2[d2_keys[i]], 'key')
func summarize():
var summary = ''
if(are_equal):
summary = get_short_summary()
else:
var formatter = load('res://addons/gut/diff_formatter.gd').new()
formatter.set_max_to_display(max_differences)
summary = formatter.make_it(self)
return summary
func get_diff_type():
return _diff_type
func get_value_1():
return _value_1
func get_value_2():
return _value_2

View File

@@ -0,0 +1,6 @@
{func_decleration}
__gutdbl.spy_on('{method_name}', {param_array})
if(__gutdbl.should_call_super('{method_name}', {param_array})):
return {super_call}
else:
return __gutdbl.get_stubbed_return('{method_name}', {param_array})

View File

@@ -0,0 +1,4 @@
{func_decleration}:
super({super_params})
__gutdbl.spy_on('{method_name}', {param_array})

View File

@@ -0,0 +1,31 @@
# ##############################################################################
# Gut Doubled Script
# ##############################################################################
{extends}
{constants}
{properties}
# ------------------------------------------------------------------------------
# GUT stuff
# ------------------------------------------------------------------------------
var __gutdbl_values = {
double = self,
thepath = '{path}',
subpath = '{subpath}',
stubber = {stubber_id},
spy = {spy_id},
gut = {gut_id},
from_singleton = '{singleton_name}',
is_partial = {is_partial},
}
var __gutdbl = load('res://addons/gut/double_tools.gd').new(__gutdbl_values)
# Here so other things can check for a method to know if this is a double.
func __gutdbl_check_method__():
pass
# ------------------------------------------------------------------------------
# Doubled Methods
# ------------------------------------------------------------------------------

View File

@@ -0,0 +1,52 @@
var thepath = ''
var subpath = ''
var stubber = null
var spy = null
var gut = null
var from_singleton = null
var is_partial = null
var double = null
const NO_DEFAULT_VALUE = '!__gut__no__default__value__!'
func from_id(inst_id):
if(inst_id == -1):
return null
else:
return instance_from_id(inst_id)
func should_call_super(method_name, called_with):
if(stubber != null):
return stubber.should_call_super(double, method_name, called_with)
else:
return false
func spy_on(method_name, called_with):
if(spy != null):
spy.add_call(double, method_name, called_with)
func get_stubbed_return(method_name, called_with):
if(stubber != null):
return stubber.get_return(double, method_name, called_with)
else:
return null
func default_val(method_name, p_index, default_val=NO_DEFAULT_VALUE):
if(stubber != null):
return stubber.get_default_value(double, method_name, p_index)
else:
return null
func _init(values=null):
if(values != null):
double = values.double
thepath = values.thepath
subpath = values.subpath
stubber = from_id(values.stubber)
spy = from_id(values.spy)
gut = from_id(values.gut)
from_singleton = values.from_singleton
is_partial = values.is_partial
if(gut != null):
gut.get_autofree().add_free(double)

View File

@@ -0,0 +1,321 @@
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2020 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
# -----------
# ##############################################################################
# ------------------------------------------------------------------------------
# A stroke of genius if I do say so. This allows for doubling a scene without
# having to write any files. By overloading the "instantiate" method we can
# make whatever we want.
# ------------------------------------------------------------------------------
class PackedSceneDouble:
extends PackedScene
var _script = null
var _scene = null
func set_script_obj(obj):
_script = obj
func instantiate(edit_state=0):
var inst = _scene.instantiate(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').get_instance()
var _base_script_text = _utils.get_file_as_text('res://addons/gut/double_templates/script_template.txt')
var _script_collector = _utils.ScriptCollector.new()
# used by tests for debugging purposes.
var print_source = false
var inner_class_registry = _utils.InnerClassRegistry.new()
# ###############
# Properties
# ###############
var _stubber = _utils.Stubber.new()
func get_stubber():
return _stubber
func set_stubber(stubber):
_stubber = stubber
var _lgr = _utils.get_logger()
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
_method_maker.set_logger(logger)
var _spy = null
func get_spy():
return _spy
func set_spy(spy):
_spy = spy
var _gut = null
func get_gut():
return _gut
func set_gut(gut):
_gut = gut
var _strategy = null
func get_strategy():
return _strategy
func set_strategy(strategy):
_strategy = strategy
var _method_maker = _utils.MethodMaker.new()
func get_method_maker():
return _method_maker
var _ignored_methods = _utils.OneToMany.new()
func get_ignored_methods():
return _ignored_methods
# ###############
# Private
# ###############
func _init(strategy=_utils.DOUBLE_STRATEGY.SCRIPT_ONLY):
set_logger(_utils.get_logger())
_strategy = strategy
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(parsed, method_name):
if(_utils.non_super_methods.has(method_name)):
return
var params = _utils.StubParams.new(parsed.script_path, method_name, parsed.subpath)
params.to_call_super()
_stubber.add_stub(params)
func _get_base_script_text(parsed, override_path, partial):
var path = parsed.script_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 gut_id = -1
if(_gut != null):
gut_id = _gut.get_instance_id()
var extends_text = parsed.get_extends_text()
var values = {
# Top sections
"extends":extends_text,
"constants":'',#obj_info.get_constants_text(),
"properties":'',#obj_info.get_properties_text(),
# metadata values
"path":path,
"subpath":_utils.nvl(parsed.subpath, ''),
"stubber_id":stubber_id,
"spy_id":spy_id,
"gut_id":gut_id,
"singleton_name":'',#_utils.nvl(obj_info.get_singleton_name(), ''),
"is_partial":partial,
}
return _base_script_text.format(values)
func _is_valid_double_method(parsed_script, parsed_method):
return !parsed_method.is_accessor() and \
!parsed_method.is_black_listed() and \
!_ignored_methods.has(parsed_script.resource, parsed_method.meta.name)
func _create_double(parsed, strategy, override_path, partial):
var base_script = _get_base_script_text(parsed, override_path, partial)
var super_name = ""
var path = ""
path = parsed.script_path
var dbl_src = ""
dbl_src += base_script
for method in parsed.get_local_methods():
if(_is_valid_double_method(parsed, method)):
var mthd = parsed.get_local_method(method.meta.name)
if(parsed.is_native):
dbl_src += _get_func_text(method.meta, parsed.resource, super_name)
else:
dbl_src += _get_func_text(method.meta, path, super_name)
if(strategy == _utils.DOUBLE_STRATEGY.INCLUDE_SUPER):
for method in parsed.get_super_methods():
if(_is_valid_double_method(parsed, method)):
_stub_to_call_super(parsed, method.meta.name)
if(parsed.is_native):
dbl_src += _get_func_text(method.meta, parsed.resource, super_name)
else:
dbl_src += _get_func_text(method.meta, path, super_name)
if(print_source):
print(_utils.add_line_numbers(dbl_src))
var DblClass = _utils.create_script_from_source(dbl_src)
if(_stubber != null):
_stub_method_default_values(DblClass, parsed, strategy)
return DblClass
func _stub_method_default_values(which, parsed, strategy):
for method in parsed.get_local_methods():
if(!method.is_black_listed() && !_ignored_methods.has(parsed.resource, method.meta.name)):
_stubber.stub_defaults_from_meta(parsed.script_path, method.meta)
func _double_scene_and_script(scene, strategy, partial):
var to_return = PackedSceneDouble.new()
to_return.load_scene(scene.get_path())
var script_obj = _utils.get_scene_script_object(scene)
if(script_obj != null):
var script_dbl = null
if(partial):
script_dbl = _partial_double(script_obj, strategy, scene.get_path())
else:
script_dbl = _double(script_obj, strategy, scene.get_path())
to_return.set_script_obj(script_dbl)
return to_return
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, path, super_=""):
var override_count = null;
if(_stubber != null):
override_count = _stubber.get_parameter_count(path, method_hash.name)
var text = _method_maker.get_function_text(method_hash, path, override_count, super_) + "\n"
return text
func _parse_script(obj):
var parsed = null
if(_utils.is_inner_class(obj)):
if(inner_class_registry.has(obj)):
parsed = _script_collector.parse(inner_class_registry.get_base_resource(obj), obj)
else:
_lgr.error('Doubling Inner Classes requires you register them first. Call register_inner_classes passing the script that contains the inner class.')
else:
parsed = _script_collector.parse(obj)
return parsed
# Override path is used with scenes.
func _double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, false)
func _partial_double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, true)
# -------------------------
# Public
# -------------------------
# double a script/object
func double(obj, strategy=_strategy):
return _double(obj, strategy)
func partial_double(obj, strategy=_strategy):
return _partial_double(obj, strategy)
# double a scene
func double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, false)
func partial_double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, true)
func double_gdnative(which):
return _double(which, _utils.DOUBLE_STRATEGY.INCLUDE_SUPER)
func partial_double_gdnative(which):
return _partial_double(which, _utils.DOUBLE_STRATEGY.INCLUDE_SUPER)
func double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, false)
func partial_double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, true)
func add_ignored_method(obj, method_name):
_ignored_methods.add(obj, method_name)

View File

@@ -0,0 +1,94 @@
Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com),
with Reserved Font Name Anonymous Pro.
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.

View File

@@ -0,0 +1,82 @@
@tool
extends Window
@onready var _ctrls = {
run_all = $Layout/CRunAll/ShortcutButton,
run_current_script = $Layout/CRunCurrentScript/ShortcutButton,
run_current_inner = $Layout/CRunCurrentInner/ShortcutButton,
run_current_test = $Layout/CRunCurrentTest/ShortcutButton,
panel_button = $Layout/CPanelButton/ShortcutButton,
}
func _ready():
for key in _ctrls:
var sc_button = _ctrls[key]
sc_button.connect('start_edit', _on_edit_start.bind(sc_button))
sc_button.connect('end_edit', _on_edit_end)
# show dialog when running scene from editor.
if(get_parent() == get_tree().root):
popup_centered()
# ------------
# Events
# ------------
func _on_Hide_pressed():
hide()
func _on_edit_start(which):
for key in _ctrls:
var sc_button = _ctrls[key]
if(sc_button != which):
sc_button.disable_set(true)
sc_button.disable_clear(true)
func _on_edit_end():
for key in _ctrls:
var sc_button = _ctrls[key]
sc_button.disable_set(false)
sc_button.disable_clear(false)
# ------------
# Public
# ------------
func get_run_all():
return _ctrls.run_all.get_shortcut()
func get_run_current_script():
return _ctrls.run_current_script.get_shortcut()
func get_run_current_inner():
return _ctrls.run_current_inner.get_shortcut()
func get_run_current_test():
return _ctrls.run_current_test.get_shortcut()
func get_panel_button():
return _ctrls.panel_button.get_shortcut()
func save_shortcuts(path):
var f = ConfigFile.new()
f.set_value('main', 'run_all', _ctrls.run_all.get_shortcut())
f.set_value('main', 'run_current_script', _ctrls.run_current_script.get_shortcut())
f.set_value('main', 'run_current_inner', _ctrls.run_current_inner.get_shortcut())
f.set_value('main', 'run_current_test', _ctrls.run_current_test.get_shortcut())
f.set_value('main', 'panel_button', _ctrls.panel_button.get_shortcut())
f.save(path)
func load_shortcuts(path):
var emptyShortcut = Shortcut.new()
var f = ConfigFile.new()
f.load(path)
_ctrls.run_all.set_shortcut(f.get_value('main', 'run_all', emptyShortcut))
_ctrls.run_current_script.set_shortcut(f.get_value('main', 'run_current_script', emptyShortcut))
_ctrls.run_current_inner.set_shortcut(f.get_value('main', 'run_current_inner', emptyShortcut))
_ctrls.run_current_test.set_shortcut(f.get_value('main', 'run_current_test', emptyShortcut))
_ctrls.panel_button.set_shortcut(f.get_value('main', 'panel_button', emptyShortcut))

View File

@@ -0,0 +1,150 @@
[gd_scene load_steps=3 format=3 uid="uid://bsk32dh41b4gs"]
[ext_resource type="PackedScene" uid="uid://sfb1fw8j6ufu" path="res://addons/gut/gui/ShortcutButton.tscn" id="1"]
[ext_resource type="Script" path="res://addons/gut/gui/BottomPanelShortcuts.gd" id="2"]
[node name="BottomPanelShortcuts" type="Popup"]
title = "Shortcuts"
size = Vector2i(500, 350)
visible = true
exclusive = true
script = ExtResource("2")
[node name="Layout" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 5.0
offset_right = -5.0
offset_bottom = 2.0
[node name="TopPad" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Label2" type="Label" parent="Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
text = "Always Active"
[node name="ColorRect" type="ColorRect" parent="Layout/Label2"]
show_behind_parent = true
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.196078)
[node name="CPanelButton" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CPanelButton"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Show/Hide GUT Panel"
[node name="ShortcutButton" parent="Layout/CPanelButton" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="GutPanelPad" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Label" type="Label" parent="Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
text = "Only Active When GUT Panel Shown"
[node name="ColorRect2" type="ColorRect" parent="Layout/Label"]
show_behind_parent = true
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.196078)
[node name="TopPad2" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="CRunAll" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunAll"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run All"
[node name="ShortcutButton" parent="Layout/CRunAll" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentScript" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentScript"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Script"
[node name="ShortcutButton" parent="Layout/CRunCurrentScript" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentInner" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentInner"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Inner Class"
[node name="ShortcutButton" parent="Layout/CRunCurrentInner" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentTest" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentTest"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Test"
[node name="ShortcutButton" parent="Layout/CRunCurrentTest" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CenterContainer2" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ShiftDisclaimer" type="Label" parent="Layout"]
layout_mode = 2
text = "\"Shift\" cannot be the only modifier for a shortcut."
[node name="HBoxContainer" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="Layout/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Hide" type="Button" parent="Layout/HBoxContainer"]
custom_minimum_size = Vector2(60, 30)
layout_mode = 2
text = "Close"
[node name="BottomPad" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 10)
layout_mode = 2
size_flags_horizontal = 3
[connection signal="pressed" from="Layout/HBoxContainer/Hide" to="." method="_on_Hide_pressed"]

View File

@@ -0,0 +1,367 @@
@tool
extends Control
const RUNNER_JSON_PATH = 'res://.gut_editor_config.json'
const RESULT_FILE = 'user://.gut_editor.bbcode'
const RESULT_JSON = 'user://.gut_editor.json'
const SHORTCUTS_PATH = 'res://.gut_editor_shortcuts.cfg'
var TestScript = load('res://addons/gut/test.gd')
var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')
var ScriptTextEditors = load('res://addons/gut/gui/script_text_editor_controls.gd')
var _interface = null;
var _is_running = false;
var _gut_config = load('res://addons/gut/gut_config.gd').new()
var _gut_config_gui = null
var _gut_plugin = null
var _light_color = Color(0, 0, 0, .5)
var _panel_button = null
var _last_selected_path = null
@onready var _ctrls = {
output = $layout/RSplit/CResults/TabBar/OutputText.get_rich_text_edit(),
output_ctrl = $layout/RSplit/CResults/TabBar/OutputText,
run_button = $layout/ControlBar/RunAll,
shortcuts_button = $layout/ControlBar/Shortcuts,
settings_button = $layout/ControlBar/Settings,
run_results_button = $layout/ControlBar/RunResultsBtn,
output_button = $layout/ControlBar/OutputBtn,
settings = $layout/RSplit/sc/Settings,
shortcut_dialog = $BottomPanelShortcuts,
light = $layout/RSplit/CResults/ControlBar/Light3D,
results = {
bar = $layout/RSplit/CResults/ControlBar,
passing = $layout/RSplit/CResults/ControlBar/Passing/value,
failing = $layout/RSplit/CResults/ControlBar/Failing/value,
pending = $layout/RSplit/CResults/ControlBar/Pending/value,
errors = $layout/RSplit/CResults/ControlBar/Errors/value,
warnings = $layout/RSplit/CResults/ControlBar/Warnings/value,
orphans = $layout/RSplit/CResults/ControlBar/Orphans/value
},
run_at_cursor = $layout/ControlBar/RunAtCursor,
run_results = $layout/RSplit/CResults/TabBar/RunResults
}
func _init():
_gut_config.load_panel_options(RUNNER_JSON_PATH)
func _ready():
_ctrls.results.bar.connect('draw', _on_results_bar_draw.bind(_ctrls.results.bar))
hide_settings(!_ctrls.settings_button.button_pressed)
_gut_config_gui = GutConfigGui.new(_ctrls.settings)
_gut_config_gui.set_options(_gut_config.options)
_apply_options_to_controls()
_ctrls.shortcuts_button.icon = get_theme_icon('Shortcut', 'EditorIcons')
_ctrls.settings_button.icon = get_theme_icon('Tools', 'EditorIcons')
_ctrls.run_results_button.icon = get_theme_icon('AnimationTrackGroup', 'EditorIcons') # Tree
_ctrls.output_button.icon = get_theme_icon('Font', 'EditorIcons')
_ctrls.run_results.set_output_control(_ctrls.output_ctrl)
_ctrls.run_results.set_font(
_gut_config.options.panel_options.font_name,
_gut_config.options.panel_options.font_size)
var check_import = load('res://addons/gut/images/red.png')
if(check_import == null):
_ctrls.run_results.add_centered_text("GUT got some new images that are not imported yet. Please restart Godot.")
print('GUT got some new images that are not imported yet. Please restart Godot.')
else:
_ctrls.run_results.add_centered_text("Let's run some tests!")
func _apply_options_to_controls():
hide_settings(_gut_config.options.panel_options.hide_settings)
hide_result_tree(_gut_config.options.panel_options.hide_result_tree)
hide_output_text(_gut_config.options.panel_options.hide_output_text)
_ctrls.output_ctrl.set_use_colors(_gut_config.options.panel_options.use_colors)
_ctrls.output_ctrl.set_all_fonts(_gut_config.options.panel_options.font_name)
_ctrls.output_ctrl.set_font_size(_gut_config.options.panel_options.font_size)
_ctrls.run_results.set_font(
_gut_config.options.panel_options.font_name,
_gut_config.options.panel_options.font_size)
_ctrls.run_results.set_show_orphans(!_gut_config.options.hide_orphans)
func _process(delta):
if(_is_running):
if(!_interface.is_playing_scene()):
_is_running = false
_ctrls.output_ctrl.add_text("\ndone")
load_result_output()
_gut_plugin.make_bottom_panel_item_visible(self)
# ---------------
# Private
# ---------------
func load_shortcuts():
_ctrls.shortcut_dialog.load_shortcuts(SHORTCUTS_PATH)
_apply_shortcuts()
func _is_test_script(script):
var from = script.get_base_script()
while(from and from.resource_path != 'res://addons/gut/test.gd'):
from = from.get_base_script()
return from != null
func _show_errors(errs):
_ctrls.output_ctrl.clear()
var text = "Cannot run tests, you have a configuration error:\n"
for e in errs:
text += str('* ', e, "\n")
text += "Check your settings ----->"
_ctrls.output_ctrl.add_text(text)
hide_output_text(false)
hide_settings(false)
func _save_config():
_gut_config.options = _gut_config_gui.get_options(_gut_config.options)
_gut_config.options.panel_options.hide_settings = !_ctrls.settings_button.button_pressed
_gut_config.options.panel_options.hide_result_tree = !_ctrls.run_results_button.button_pressed
_gut_config.options.panel_options.hide_output_text = !_ctrls.output_button.button_pressed
_gut_config.options.panel_options.use_colors = _ctrls.output_ctrl.get_use_colors()
var w_result = _gut_config.write_options(RUNNER_JSON_PATH)
if(w_result != OK):
push_error(str('Could not write options to ', RUNNER_JSON_PATH, ': ', w_result))
return;
func _run_tests():
var issues = _gut_config_gui.get_config_issues()
if(issues.size() > 0):
_show_errors(issues)
return
write_file(RESULT_FILE, 'Run in progress')
_save_config()
_apply_options_to_controls()
_ctrls.output_ctrl.clear()
_ctrls.run_results.clear()
_ctrls.run_results.add_centered_text('Running...')
_interface.play_custom_scene('res://addons/gut/gui/GutRunner.tscn')
_is_running = true
_ctrls.output_ctrl.add_text('Running...')
func _apply_shortcuts():
_ctrls.run_button.shortcut = _ctrls.shortcut_dialog.get_run_all()
_ctrls.run_at_cursor.get_script_button().shortcut = \
_ctrls.shortcut_dialog.get_run_current_script()
_ctrls.run_at_cursor.get_inner_button().shortcut = \
_ctrls.shortcut_dialog.get_run_current_inner()
_ctrls.run_at_cursor.get_test_button().shortcut = \
_ctrls.shortcut_dialog.get_run_current_test()
_panel_button.shortcut = _ctrls.shortcut_dialog.get_panel_button()
func _run_all():
_gut_config.options.selected = null
_gut_config.options.inner_class = null
_gut_config.options.unit_test_name = null
_run_tests()
# ---------------
# Events
# ---------------
func _on_results_bar_draw(bar):
bar.draw_rect(Rect2(Vector2(0, 0), bar.size), Color(0, 0, 0, .2))
func _on_Light_draw():
var l = _ctrls.light
l.draw_circle(Vector2(l.size.x / 2, l.size.y / 2), l.size.x / 2, _light_color)
func _on_editor_script_changed(script):
if(script):
set_current_script(script)
func _on_RunAll_pressed():
_run_all()
func _on_Shortcuts_pressed():
_ctrls.shortcut_dialog.popup_centered()
func _on_bottom_panel_shortcuts_visibility_changed():
_apply_shortcuts()
_ctrls.shortcut_dialog.save_shortcuts(SHORTCUTS_PATH)
func _on_RunAtCursor_run_tests(what):
_gut_config.options.selected = what.script
_gut_config.options.inner_class = what.inner_class
_gut_config.options.unit_test_name = what.test_method
_run_tests()
func _on_Settings_pressed():
hide_settings(!_ctrls.settings_button.button_pressed)
_save_config()
func _on_OutputBtn_pressed():
hide_output_text(!_ctrls.output_button.button_pressed)
_save_config()
func _on_RunResultsBtn_pressed():
hide_result_tree(! _ctrls.run_results_button.button_pressed)
_save_config()
# Currently not used, but will be when I figure out how to put
# colors into the text results
func _on_UseColors_pressed():
pass
# ---------------
# Public
# ---------------
func hide_result_tree(should):
_ctrls.run_results.visible = !should
_ctrls.run_results_button.button_pressed = !should
func hide_settings(should):
var s_scroll = _ctrls.settings.get_parent()
s_scroll.visible = !should
# collapse only collapses the first control, so we move
# settings around to be the collapsed one
if(should):
s_scroll.get_parent().move_child(s_scroll, 0)
else:
s_scroll.get_parent().move_child(s_scroll, 1)
$layout/RSplit.collapsed = should
_ctrls.settings_button.button_pressed = !should
func hide_output_text(should):
$layout/RSplit/CResults/TabBar/OutputText.visible = !should
_ctrls.output_button.button_pressed = !should
func load_result_output():
_ctrls.output_ctrl.load_file(RESULT_FILE)
var summary = get_file_as_text(RESULT_JSON)
var test_json_conv = JSON.new()
if (test_json_conv.parse(summary) != OK):
return
var results = test_json_conv.get_data()
_ctrls.run_results.load_json_results(results)
var summary_json = results['test_scripts']['props']
_ctrls.results.passing.text = str(summary_json.passing)
_ctrls.results.passing.get_parent().visible = true
_ctrls.results.failing.text = str(summary_json.failures)
_ctrls.results.failing.get_parent().visible = true
_ctrls.results.pending.text = str(summary_json.pending)
_ctrls.results.pending.get_parent().visible = _ctrls.results.pending.text != '0'
_ctrls.results.errors.text = str(summary_json.errors)
_ctrls.results.errors.get_parent().visible = _ctrls.results.errors.text != '0'
_ctrls.results.warnings.text = str(summary_json.warnings)
_ctrls.results.warnings.get_parent().visible = _ctrls.results.warnings.text != '0'
_ctrls.results.orphans.text = str(summary_json.orphans)
_ctrls.results.orphans.get_parent().visible = _ctrls.results.orphans.text != '0' and !_gut_config.options.hide_orphans
if(summary_json.tests == 0):
_light_color = Color(1, 0, 0, .75)
elif(summary_json.failures != 0):
_light_color = Color(1, 0, 0, .75)
elif(summary_json.pending != 0):
_light_color = Color(1, 1, 0, .75)
else:
_light_color = Color(0, 1, 0, .75)
_ctrls.light.visible = true
_ctrls.light.queue_redraw()
func set_current_script(script):
if(script):
if(_is_test_script(script)):
var file = script.resource_path.get_file()
_last_selected_path = script.resource_path.get_file()
_ctrls.run_at_cursor.activate_for_script(script.resource_path)
func set_interface(value):
_interface = value
_interface.get_script_editor().connect("editor_script_changed",Callable(self,'_on_editor_script_changed'))
var ste = ScriptTextEditors.new(_interface.get_script_editor())
_ctrls.run_results.set_interface(_interface)
_ctrls.run_results.set_script_text_editors(ste)
_ctrls.run_at_cursor.set_script_text_editors(ste)
set_current_script(_interface.get_script_editor().get_current_script())
func set_plugin(value):
_gut_plugin = value
func set_panel_button(value):
_panel_button = value
# ------------------------------------------------------------------------------
# Write a file.
# ------------------------------------------------------------------------------
func write_file(path, content):
var f = FileAccess.open(path, FileAccess.WRITE)
if(f != null):
f.store_string(content)
f = null;
return FileAccess.get_open_error()
# ------------------------------------------------------------------------------
# Returns the text of a file or an empty string if the file could not be opened.
# ------------------------------------------------------------------------------
func get_file_as_text(path):
var to_return = ''
var f = FileAccess.open(path, FileAccess.READ)
if(f != null):
to_return = f.get_as_text()
f = null
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

View File

@@ -0,0 +1,249 @@
[gd_scene load_steps=10 format=3 uid="uid://b3bostcslstem"]
[ext_resource type="Script" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://bsk32dh41b4gs" path="res://addons/gut/gui/BottomPanelShortcuts.tscn" id="2"]
[ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="4"]
[ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"]
[ext_resource type="PackedScene" uid="uid://bqmo4dj64c7yl" path="res://addons/gut/gui/OutputText.tscn" id="6"]
[sub_resource type="Shortcut" id="9"]
[sub_resource type="Image" id="Image_18d1g"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_8u17l"]
image = SubResource("Image_18d1g")
[node name="GutBottomPanel" type="Control"]
custom_minimum_size = Vector2(250, 250)
layout_mode = 3
anchor_left = -0.0025866
anchor_top = -0.00176575
anchor_right = 0.997413
anchor_bottom = 0.998234
offset_left = 2.64868
offset_top = 1.05945
offset_right = 2.64862
offset_bottom = 1.05945
script = ExtResource("1")
[node name="layout" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="ControlBar" type="HBoxContainer" parent="layout"]
layout_mode = 2
[node name="RunAll" type="Button" parent="layout/ControlBar"]
layout_mode = 2
size_flags_vertical = 11
shortcut = SubResource("9")
text = "Run All"
icon = ExtResource("4")
[node name="Label" type="Label" parent="layout/ControlBar"]
layout_mode = 2
mouse_filter = 1
text = "Current: "
[node name="RunAtCursor" parent="layout/ControlBar" instance=ExtResource("3")]
layout_mode = 2
[node name="CenterContainer2" type="CenterContainer" parent="layout/ControlBar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Sep1" type="ColorRect" parent="layout/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunResultsBtn" type="Button" parent="layout/ControlBar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_8u17l")
[node name="OutputBtn" type="Button" parent="layout/ControlBar"]
layout_mode = 2
toggle_mode = true
icon = SubResource("ImageTexture_8u17l")
[node name="Settings" type="Button" parent="layout/ControlBar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_8u17l")
[node name="Sep2" type="ColorRect" parent="layout/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Shortcuts" type="Button" parent="layout/ControlBar"]
layout_mode = 2
size_flags_vertical = 11
icon = SubResource("ImageTexture_8u17l")
[node name="RSplit" type="HSplitContainer" parent="layout"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="CResults" type="VBoxContainer" parent="layout/RSplit"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ControlBar" type="HBoxContainer" parent="layout/RSplit/CResults"]
layout_mode = 2
[node name="Sep2" type="ColorRect" parent="layout/RSplit/CResults/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Light3D" type="Control" parent="layout/RSplit/CResults/ControlBar"]
custom_minimum_size = Vector2(30, 30)
layout_mode = 2
[node name="Passing" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Passing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Passing"]
layout_mode = 2
text = "Passing"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Passing"]
layout_mode = 2
text = "---"
[node name="Failing" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Failing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Failing"]
layout_mode = 2
text = "Failing"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Failing"]
layout_mode = 2
text = "---"
[node name="Pending" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Pending"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Pending"]
layout_mode = 2
text = "Pending"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Pending"]
layout_mode = 2
text = "---"
[node name="Orphans" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Orphans"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Orphans"]
layout_mode = 2
text = "Orphans"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Orphans"]
layout_mode = 2
text = "---"
[node name="Errors" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Errors"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Errors"]
layout_mode = 2
text = "Errors"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Errors"]
layout_mode = 2
text = "---"
[node name="Warnings" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Warnings"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Warnings"]
layout_mode = 2
text = "Warnings"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Warnings"]
layout_mode = 2
text = "---"
[node name="CenterContainer" type="CenterContainer" parent="layout/RSplit/CResults/ControlBar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TabBar" type="HSplitContainer" parent="layout/RSplit/CResults"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="RunResults" parent="layout/RSplit/CResults/TabBar" instance=ExtResource("5")]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="OutputText" parent="layout/RSplit/CResults/TabBar" instance=ExtResource("6")]
visible = false
layout_mode = 2
[node name="sc" type="ScrollContainer" parent="layout/RSplit"]
custom_minimum_size = Vector2(500, 2.08165e-12)
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="BottomPanelShortcuts" parent="." instance=ExtResource("2")]
visible = false
[connection signal="pressed" from="layout/ControlBar/RunAll" to="." method="_on_RunAll_pressed"]
[connection signal="run_tests" from="layout/ControlBar/RunAtCursor" to="." method="_on_RunAtCursor_run_tests"]
[connection signal="pressed" from="layout/ControlBar/RunResultsBtn" to="." method="_on_RunResultsBtn_pressed"]
[connection signal="pressed" from="layout/ControlBar/OutputBtn" to="." method="_on_OutputBtn_pressed"]
[connection signal="pressed" from="layout/ControlBar/Settings" to="." method="_on_Settings_pressed"]
[connection signal="pressed" from="layout/ControlBar/Shortcuts" to="." method="_on_Shortcuts_pressed"]
[connection signal="draw" from="layout/RSplit/CResults/ControlBar/Light3D" to="." method="_on_Light_draw"]
[connection signal="visibility_changed" from="BottomPanelShortcuts" to="." method="_on_bottom_panel_shortcuts_visibility_changed"]

View File

@@ -0,0 +1,121 @@
extends Node2D
var Gut = load('res://addons/gut/gut.gd')
var ResultExporter = load('res://addons/gut/result_exporter.gd')
var GutConfig = load('res://addons/gut/gut_config.gd')
const RUNNER_JSON_PATH = 'res://.gut_editor_config.json'
const RESULT_FILE = 'user://.gut_editor.bbcode'
const RESULT_JSON = 'user://.gut_editor.json'
var _gut_config = null
var _gut = null;
var _wrote_results = false
# Flag for when this is being used at the command line. Otherwise it is
# assumed this is being used by the panel and being launched with
# play_custom_scene
var _cmdln_mode = false
@onready var _gut_layer = $GutLayer
@onready var _gui = $GutLayer/GutScene
var auto_run_tests = true
func _ready():
if(_gut_config == null):
_gut_config = GutConfig.new()
_gut_config.load_panel_options(RUNNER_JSON_PATH)
# The command line will call run_tests on its own. When used from the panel
# we have to kick off the tests ourselves b/c there's no way I know of to
# interact with the scene that was run via play_custom_scene.
if(!_cmdln_mode and auto_run_tests):
call_deferred('run_tests')
func run_tests(show_gui=true):
if(_gut == null):
get_gut()
_setup_gui(show_gui)
_gut.add_children_to = self
if(_gut_config.options.gut_on_top):
_gut_layer.add_child(_gut)
else:
add_child(_gut)
if(!_cmdln_mode):
_gut.end_run.connect(_on_tests_finished.bind(_gut_config.options.should_exit, _gut_config.options.should_exit_on_success))
_gut_config.config_gut(_gut)
var run_rest_of_scripts = _gut_config.options.unit_test_name == ''
_gut.test_scripts(run_rest_of_scripts)
func _setup_gui(show_gui):
if(show_gui):
_gui.gut = _gut
var printer = _gut.logger.get_printer('gui')
printer.set_textbox(_gui.get_textbox())
else:
_gut.logger.disable_printer('gui', true)
_gui.visible = false
var opts = _gut_config.options
_gui.set_font_size(opts.font_size)
_gui.set_font(opts.font_name)
if(opts.font_color != null and opts.font_color.is_valid_html_color()):
_gui.set_default_font_color(Color(opts.font_color))
if(opts.background_color != null and opts.background_color.is_valid_html_color()):
_gui.set_background_color(Color(opts.background_color))
_gui.set_opacity(min(1.0, float(opts.opacity) / 100))
# if(opts.should_maximize):
# _tester.maximize()
_gui.use_compact_mode(opts.compact_mode)
func _write_results():
var content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode()
var f = FileAccess.open(RESULT_FILE, FileAccess.WRITE)
if(f != null):
f.store_string(content)
f = null # closes file
else:
push_error('Could not save bbcode, result = ', FileAccess.get_open_error())
var exporter = ResultExporter.new()
var f_result = exporter.write_json_file(_gut, RESULT_JSON)
_wrote_results = true
func _exit_tree():
if(!_wrote_results and !_cmdln_mode):
_write_results()
func _on_tests_finished(should_exit, should_exit_on_success):
_write_results()
if(should_exit):
get_tree().quit()
elif(should_exit_on_success and _gut.get_fail_count() == 0):
get_tree().quit()
func get_gut():
if(_gut == null):
_gut = Gut.new()
return _gut
func set_gut_config(which):
_gut_config = which
func set_cmdln_mode(is_it):
_cmdln_mode = is_it

View File

@@ -0,0 +1,12 @@
[gd_scene load_steps=3 format=3 uid="uid://bqy3ikt6vu4b5"]
[ext_resource type="Script" path="res://addons/gut/gui/GutRunner.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://m28heqtswbuq" path="res://addons/gut/GutScene.tscn" id="2_6ruxb"]
[node name="GutRunner" type="Node2D"]
script = ExtResource("1")
[node name="GutLayer" type="CanvasLayer" parent="."]
layer = 128
[node name="GutScene" parent="GutLayer" instance=ExtResource("2_6ruxb")]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,161 @@
[gd_scene load_steps=5 format=3 uid="uid://cnqqdfsn80ise"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_farmq"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_a2e2l"]
[ext_resource type="Script" path="res://addons/gut/gui/gut_gui.gd" id="2_eokrf"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_xrhva"]
[node name="Min" type="Panel"]
clip_contents = true
custom_minimum_size = Vector2(280, 145)
offset_right = 280.0
offset_bottom = 145.0
theme = ExtResource("1_farmq")
script = ExtResource("2_eokrf")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "0.000s"
[node name="Body" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="LeftMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BodyRows" type="VBoxContainer" parent="MainBox/Body"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ProgressBars" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
layout_mode = 2
text = "T:"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 25.0
[node name="HBoxContainer2" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
layout_mode = 2
text = "S:"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/Body/BodyRows"]
clip_contents = true
layout_mode = 2
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/Body/BodyRows/PathDisplay"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
clip_text = true
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/PathDisplay"]
clip_contents = true
layout_mode = 2
[node name="S3" type="CenterContainer" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Footer" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
[node name="HandleLeft" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
orientation = 0
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="SwitchModes" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Expand"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Continue" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Continue
"
[node name="HandleRight" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="RightMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2

View File

@@ -0,0 +1,216 @@
[gd_scene load_steps=5 format=3 uid="uid://duxblir3vu8x7"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_5hlsm"]
[ext_resource type="Script" path="res://addons/gut/gui/gut_gui.gd" id="2_fue6q"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_u5uc1"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_2r8a8"]
[node name="Large" type="Panel"]
custom_minimum_size = Vector2(500, 150)
offset_right = 632.0
offset_bottom = 260.0
theme = ExtResource("1_5hlsm")
script = ExtResource("2_fue6q")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
custom_minimum_size = Vector2(90, 0)
layout_mode = 2
text = "999.999s"
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="MainBox/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="OutputBG" type="ColorRect" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
color = Color(0.0745098, 0.0705882, 0.0784314, 1)
metadata/_edit_layout_mode = 1
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="S2" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="TestOutput" type="RichTextLabel" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
bbcode_enabled = true
scroll_following = true
autowrap_mode = 0
selection_enabled = true
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ControlBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ProgressBars" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12)
layout_mode = 2
[node name="TestBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Tests"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 25.0
[node name="ScriptBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Scripts"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 6
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 3
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Spacer1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
visible = false
layout_mode = 2
size_flags_horizontal = 10
[node name="Continue" type="Button" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
size_flags_vertical = 4
text = "Continue
"
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BottomPad" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Footer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
[node name="SidePad1" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="ResizeHandle3" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
orientation = 0
resize_control = NodePath("../../..")
vertical_resize = null
[node name="SwitchModes" type="Button" parent="MainBox/Footer"]
layout_mode = 2
text = "Compact
"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ResizeHandle2" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
orientation = 1
resize_control = NodePath("../../..")
vertical_resize = null
[node name="SidePad2" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="BottomPad2" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2

View File

@@ -0,0 +1,341 @@
@tool
extends VBoxContainer
# ##############################################################################
# Keeps search results from the TextEdit
# ##############################################################################
class TextEditSearcher:
var te : TextEdit
var _last_term = ''
var _last_pos = Vector2(-1, -1)
var _ignore_caret_change = false
func set_text_edit(which):
te = which
te.caret_changed.connect(_on_caret_changed)
func _on_caret_changed():
if(_ignore_caret_change):
_ignore_caret_change = false
else:
_last_pos = _get_caret();
func _get_caret():
return Vector2(te.get_caret_column(), te.get_caret_line())
func _set_caret_and_sel(pos, len):
te.set_caret_line(pos.y)
te.set_caret_column(pos.x)
if(len > 0):
te.select(pos.y, pos.x, pos.y, pos.x + len)
func _find(term, search_flags):
var pos = _get_caret()
if(term == _last_term):
if(search_flags == 0):
pos = _last_pos
pos.x += 1
else:
pos = _last_pos
pos.x -= 1
var result = te.search(term, search_flags, pos.y, pos.x)
# print('searching from ', pos, ' for "', term, '" = ', result)
if(result.y != -1):
_ignore_caret_change = true
_set_caret_and_sel(result, term.length())
_last_pos = result
_last_term = term
func find_next(term):
_find(term, 0)
func find_prev(term):
_find(term, te.SEARCH_BACKWARDS)
# ##############################################################################
# Start OutputText control code
# ##############################################################################
@onready var _ctrls = {
output = $Output,
copy_button = $Toolbar/CopyButton,
use_colors = $Toolbar/UseColors,
clear_button = $Toolbar/ClearButton,
word_wrap = $Toolbar/WordWrap,
show_search = $Toolbar/ShowSearch,
caret_position = $Toolbar/LblPosition,
search_bar = {
bar = $Search,
search_term = $Search/SearchTerm,
}
}
var _sr = TextEditSearcher.new()
var _highlighter : CodeHighlighter
# Automatically used when running the OutputText scene from the editor. Changes
# to this method only affect test-running the control through the editor.
func _test_running_setup():
_ctrls.use_colors.text = 'use colors'
_ctrls.show_search.text = 'search'
_ctrls.word_wrap.text = 'ww'
set_all_fonts("CourierPrime")
set_font_size(5)
# print(_ctrls.output.get_theme_font_size("normal_font"))
_ctrls.output.queue_redraw()
load_file('user://.gut_editor.bbcode')
await get_tree().process_frame
show_search(true)
_ctrls.output.set_caret_line(0)
_ctrls.output.scroll_vertical = 0
_ctrls.output.caret_changed.connect(_on_caret_changed)
func _on_caret_changed():
var txt = str("line:",_ctrls.output.get_caret_line(), ' col:', _ctrls.output.get_caret_column())
_ctrls.caret_position.text = str(txt)
func _ready():
_sr.set_text_edit(_ctrls.output)
_ctrls.use_colors.icon = get_theme_icon('RichTextEffect', 'EditorIcons')
_ctrls.show_search.icon = get_theme_icon('Search', 'EditorIcons')
_ctrls.word_wrap.icon = get_theme_icon('Loop', 'EditorIcons')
_setup_colors()
_ctrls.use_colors.button_pressed = true
_use_highlighting(true)
if(get_parent() == get_tree().root):
_test_running_setup()
# ------------------
# Private
# ------------------
# Call this after changes in colors and the like to get them to apply. reloads
# the text of the output control.
func _refresh_output():
var orig_pos = _ctrls.output.scroll_vertical
var text = _ctrls.output.text
_ctrls.output.text = text
_ctrls.output.scroll_vertical = orig_pos
func _create_highlighter(default_color=Color(1, 1, 1, 1)):
var to_return = CodeHighlighter.new()
to_return.function_color = default_color
to_return.number_color = default_color
to_return.symbol_color = default_color
to_return.member_variable_color = default_color
var keywords = [
['Failed', Color.RED],
['Passed', Color.GREEN],
['Pending', Color.YELLOW],
['Orphans', Color.YELLOW],
['WARNING', Color.YELLOW],
['ERROR', Color.RED]
]
for keyword in keywords:
to_return.add_keyword_color(keyword[0], keyword[1])
return to_return
func _setup_colors():
_ctrls.output.clear()
var f_color = null
if (_ctrls.output.theme == null) :
f_color = get_theme_color("font_color")
else :
f_color = _ctrls.output.theme.font_color
_highlighter = _create_highlighter()
_ctrls.output.queue_redraw()
func _set_font(font_name, custom_name):
var rtl = _ctrls.output
if(font_name == null):
rtl.add_theme_font_override(custom_name, null)
else:
var dyn_font = FontFile.new()
dyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf')
rtl.add_theme_font_override(custom_name, dyn_font)
func _use_highlighting(should):
if(should):
_ctrls.output.syntax_highlighter = _highlighter
else:
_ctrls.output.syntax_highlighter = null
_refresh_output()
# ------------------
# Events
# ------------------
func _on_CopyButton_pressed():
copy_to_clipboard()
func _on_UseColors_pressed():
_use_highlighting(_ctrls.use_colors.button_pressed)
func _on_ClearButton_pressed():
clear()
func _on_ShowSearch_pressed():
show_search(_ctrls.show_search.button_pressed)
func _on_SearchTerm_focus_entered():
_ctrls.search_bar.search_term.call_deferred('select_all')
func _on_SearchNext_pressed():
_sr.find_next(_ctrls.search_bar.search_term.text)
func _on_SearchPrev_pressed():
_sr.find_prev(_ctrls.search_bar.search_term.text)
func _on_SearchTerm_text_changed(new_text):
if(new_text == ''):
_ctrls.output.deselect()
else:
_sr.find_next(new_text)
func _on_SearchTerm_text_entered(new_text):
if(Input.is_physical_key_pressed(KEY_SHIFT)):
_sr.find_prev(new_text)
else:
_sr.find_next(new_text)
func _on_SearchTerm_gui_input(event):
if(event is InputEventKey and !event.pressed and event.keycode == KEY_ESCAPE):
show_search(false)
func _on_WordWrap_pressed():
if(_ctrls.word_wrap.button_pressed):
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
else:
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_NONE
_ctrls.output.queue_redraw()
# ------------------
# Public
# ------------------
func show_search(should):
_ctrls.search_bar.bar.visible = should
if(should):
_ctrls.search_bar.search_term.grab_focus()
_ctrls.search_bar.search_term.select_all()
_ctrls.show_search.button_pressed = should
func search(text, start_pos, highlight=true):
return _sr.find_next(text)
func copy_to_clipboard():
var selected = _ctrls.output.get_selected_text()
if(selected != ''):
DisplayServer.clipboard_set(selected)
else:
DisplayServer.clipboard_set(_ctrls.output.text)
func clear():
_ctrls.output.text = ''
func set_all_fonts(base_name):
if(base_name == 'Default'):
_set_font(null, 'font')
_set_font(null, 'normal_font')
_set_font(null, 'bold_font')
_set_font(null, 'italics_font')
_set_font(null, 'bold_italics_font')
else:
_set_font(base_name + '-Regular', 'font')
_set_font(base_name + '-Regular', 'normal_font')
_set_font(base_name + '-Bold', 'bold_font')
_set_font(base_name + '-Italic', 'italics_font')
_set_font(base_name + '-BoldItalic', 'bold_italics_font')
func set_font_size(new_size):
return # this isn't working.
var rtl = _ctrls.output
# rtl.add_theme_font_size_override("font", new_size)
# rtl.add_theme_font_size_override("normal_font", new_size)
# rtl.add_theme_font_size_override("bold_font", new_size)
# rtl.add_theme_font_size_override("italics_font", new_size)
# rtl.add_theme_font_size_override("bold_italics_font", new_size)
rtl.set("theme_override_font_sizes/size", new_size)
# print(rtl.get("theme_override_font_sizes/size"))
# if(rtl.get('custom_fonts/font') != null):
# rtl.get('custom_fonts/font').size = new_size
# rtl.get('custom_fonts/bold_italics_font').size = new_size
# rtl.get('custom_fonts/bold_font').size = new_size
# rtl.get('custom_fonts/italics_font').size = new_size
# rtl.get('custom_fonts/normal_font').size = new_size
func set_use_colors(value):
pass
func get_use_colors():
return false;
func get_rich_text_edit():
return _ctrls.output
func load_file(path):
var f = FileAccess.open(path, FileAccess.READ)
if(f == null):
return
var t = f.get_as_text()
f = null # closes file
_ctrls.output.text = t
_ctrls.output.scroll_vertical = _ctrls.output.get_line_count()
_ctrls.output.set_deferred('scroll_vertical', _ctrls.output.get_line_count())
func add_text(text):
if(is_inside_tree()):
_ctrls.output.text += text
func scroll_to_line(line):
_ctrls.output.scroll_vertical = line
_ctrls.output.set_caret_line(line)

View File

@@ -0,0 +1,114 @@
[gd_scene load_steps=5 format=3 uid="uid://bqmo4dj64c7yl"]
[ext_resource type="Script" path="res://addons/gut/gui/OutputText.gd" id="1"]
[sub_resource type="Image" id="Image_o4jv5"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_uk57o"]
image = SubResource("Image_o4jv5")
[sub_resource type="CodeHighlighter" id="CodeHighlighter_sv352"]
number_color = Color(1, 1, 1, 1)
symbol_color = Color(1, 1, 1, 1)
function_color = Color(1, 1, 1, 1)
member_variable_color = Color(1, 1, 1, 1)
keyword_colors = {
"ERROR": Color(1, 0, 0, 1),
"Failed": Color(1, 0, 0, 1),
"Orphans": Color(1, 1, 0, 1),
"Passed": Color(0, 1, 0, 1),
"Pending": Color(1, 1, 0, 1),
"WARNING": Color(1, 1, 0, 1)
}
[node name="OutputText" type="VBoxContainer"]
offset_right = 862.0
offset_bottom = 523.0
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1")
[node name="Toolbar" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShowSearch" type="Button" parent="Toolbar"]
layout_mode = 2
tooltip_text = "Search"
toggle_mode = true
icon = SubResource("ImageTexture_uk57o")
[node name="UseColors" type="Button" parent="Toolbar"]
layout_mode = 2
tooltip_text = "Colorized Text"
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_uk57o")
[node name="WordWrap" type="Button" parent="Toolbar"]
layout_mode = 2
tooltip_text = "Word Wrap"
toggle_mode = true
icon = SubResource("ImageTexture_uk57o")
[node name="CenterContainer" type="CenterContainer" parent="Toolbar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="LblPosition" type="Label" parent="Toolbar"]
layout_mode = 2
[node name="CopyButton" type="Button" parent="Toolbar"]
layout_mode = 2
text = " Copy "
[node name="ClearButton" type="Button" parent="Toolbar"]
layout_mode = 2
text = " Clear "
[node name="Output" type="TextEdit" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
deselect_on_focus_loss_enabled = false
virtual_keyboard_enabled = false
middle_mouse_paste_enabled = false
highlight_all_occurrences = true
highlight_current_line = true
syntax_highlighter = SubResource("CodeHighlighter_sv352")
scroll_smooth = true
[node name="Search" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="SearchTerm" type="LineEdit" parent="Search"]
layout_mode = 2
size_flags_horizontal = 3
[node name="SearchNext" type="Button" parent="Search"]
layout_mode = 2
text = "Next"
[node name="SearchPrev" type="Button" parent="Search"]
layout_mode = 2
text = "Prev"
[connection signal="pressed" from="Toolbar/ShowSearch" to="." method="_on_ShowSearch_pressed"]
[connection signal="pressed" from="Toolbar/UseColors" to="." method="_on_UseColors_pressed"]
[connection signal="pressed" from="Toolbar/WordWrap" to="." method="_on_WordWrap_pressed"]
[connection signal="pressed" from="Toolbar/CopyButton" to="." method="_on_CopyButton_pressed"]
[connection signal="pressed" from="Toolbar/ClearButton" to="." method="_on_ClearButton_pressed"]
[connection signal="focus_entered" from="Search/SearchTerm" to="." method="_on_SearchTerm_focus_entered"]
[connection signal="gui_input" from="Search/SearchTerm" to="." method="_on_SearchTerm_gui_input"]
[connection signal="text_changed" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_changed"]
[connection signal="text_submitted" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_entered"]
[connection signal="pressed" from="Search/SearchNext" to="." method="_on_SearchNext_pressed"]
[connection signal="pressed" from="Search/SearchPrev" to="." method="_on_SearchPrev_pressed"]

View File

@@ -0,0 +1,108 @@
@tool
extends ColorRect
# #############################################################################
# Resize Handle control. Place onto a control. Set the orientation, then
# set the control that this should resize. Then you can resize the control
# by dragging this thing around. It's pretty neat.
# #############################################################################
enum ORIENTATION {
LEFT,
RIGHT
}
@export var orientation := ORIENTATION.RIGHT :
get: return orientation
set(val):
orientation = val
queue_redraw()
@export var resize_control : Control = null
@export var vertical_resize := true
var _line_width = .5
var _line_color = Color(.4, .4, .4)
var _active_line_color = Color(.3, .3, .3)
var _invalid_line_color = Color(1, 0, 0)
var _grab_margin = 2
var _line_space = 3
var _num_lines = 8
var _mouse_down = false
# Called when the node enters the scene tree for the first time.
func _draw():
var c = _line_color
if(resize_control == null):
c = _invalid_line_color
elif(_mouse_down):
c = _active_line_color
if(orientation == ORIENTATION.LEFT):
_draw_resize_handle_left(c)
else:
_draw_resize_handle_right(c)
func _gui_input(event):
if(resize_control == null):
return
if(orientation == ORIENTATION.LEFT):
_handle_left_input(event)
else:
_handle_right_input(event)
# Draw the lines in the corner to show where you can
# drag to resize the dialog
func _draw_resize_handle_right(color):
var br = size
for i in range(_num_lines):
var start = br - Vector2(i * _line_space, 0)
var end = br - Vector2(0, i * _line_space)
draw_line(start, end, color, _line_width, true)
func _draw_resize_handle_left(color):
var bl = Vector2(0, size.y)
for i in range(_num_lines):
var start = bl + Vector2(i * _line_space, 0)
var end = bl - Vector2(0, i * _line_space)
draw_line(start, end, color, _line_width, true)
func _handle_right_input(event : InputEvent):
if(event is InputEventMouseMotion):
if(_mouse_down and
event.global_position.x > 0 and
event.global_position.y < DisplayServer.window_get_size().y):
if(vertical_resize):
resize_control.size.y += event.relative.y
resize_control.size.x += event.relative.x
elif(event is InputEventMouseButton):
if(event.button_index == MOUSE_BUTTON_LEFT):
_mouse_down = event.pressed
queue_redraw()
func _handle_left_input(event : InputEvent):
if(event is InputEventMouseMotion):
if(_mouse_down and
event.global_position.x > 0 and
event.global_position.y < DisplayServer.window_get_size().y):
var start_size = resize_control.size
resize_control.size.x -= event.relative.x
if(resize_control.size.x != start_size.x):
resize_control.global_position.x += event.relative.x
if(vertical_resize):
resize_control.size.y += event.relative.y
elif(event is InputEventMouseButton):
if(event.button_index == MOUSE_BUTTON_LEFT):
_mouse_down = event.pressed
queue_redraw()

View File

@@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://bvrqqgjpyouse"]
[ext_resource type="Script" path="res://addons/gut/gui/ResizeHandle.gd" id="1_oi5ed"]
[node name="ResizeHandle" type="ColorRect"]
custom_minimum_size = Vector2(20, 20)
color = Color(1, 1, 1, 0)
script = ExtResource("1_oi5ed")

View File

@@ -0,0 +1,348 @@
@tool
extends Control
var _show_orphans = true
var show_orphans = true :
get: return _show_orphans
set(val): _show_orphans = val
var _hide_passing = true
var hide_passing = true :
get: return _hide_passing
set(val): _hide_passing = val
var _icons = {
red = load('res://addons/gut/images/red.png'),
green = load('res://addons/gut/images/green.png'),
yellow = load('res://addons/gut/images/yellow.png'),
}
const _col_1_bg_color = Color(0, 0, 0, .1)
var _max_icon_width = 10
var _root : TreeItem
@onready var _ctrls = {
tree = $Tree,
lbl_overlay = $Tree/TextOverlay
}
signal item_selected(script_path, inner_class, test_name, line_number)
# -------------------
# Private
# -------------------
func _ready():
_root = _ctrls.tree.create_item()
_root = _ctrls.tree.create_item()
_ctrls.tree.set_hide_root(true)
_ctrls.tree.columns = 2
_ctrls.tree.set_column_expand(0, true)
_ctrls.tree.set_column_expand(1, false)
_ctrls.tree.set_column_clip_content(0, true)
$Tree.item_selected.connect(_on_tree_item_selected)
if(get_parent() == get_tree().root):
_test_running_setup()
func _test_running_setup():
load_json_file('user://.gut_editor.json')
func _on_tree_item_selected():
var item = _ctrls.tree.get_selected()
var item_meta = item.get_metadata(0)
var item_type = null
# Only select the left side of the tree item, cause I like that better.
# you can still click the right, but only the left gets highlighted.
if(item.is_selected(1)):
item.deselect(1)
item.select(0)
if(item_meta == null):
return
else:
item_type = item_meta.type
var script_path = '';
var line = -1;
var test_name = ''
var inner_class = ''
if(item_type == 'test'):
var s_item = item.get_parent()
script_path = s_item.get_metadata(0)['path']
inner_class = s_item.get_metadata(0)['inner_class']
line = -1
test_name = item.get_text(0)
elif(item_type == 'assert'):
var s_item = item.get_parent().get_parent()
script_path = s_item.get_metadata(0)['path']
inner_class = s_item.get_metadata(0)['inner_class']
line = _get_line_number_from_assert_msg(item.get_text(0))
test_name = item.get_parent().get_text(0)
elif(item_type == 'script'):
script_path = item.get_metadata(0)['path']
if(item.get_parent() != _root):
inner_class = item.get_text(0)
line = -1
test_name = ''
else:
return
item_selected.emit(script_path, inner_class, test_name, line)
func _get_line_number_from_assert_msg(msg):
var line = -1
if(msg.find('at line') > 0):
line = msg.split("at line")[-1].split(" ")[-1].to_int()
return line
func _get_path_and_inner_class_name_from_test_path(path):
var to_return = {
path = '',
inner_class = ''
}
to_return.path = path
if !path.ends_with('.gd'):
var loc = path.find('.gd')
to_return.inner_class = path.split('.')[-1]
to_return.path = path.substr(0, loc + 3)
return to_return
func _find_script_item_with_path(path):
var items = _root.get_children()
var to_return = null
var idx = 0
while(idx < items.size() and to_return == null):
var item = items[idx]
if(item.get_metadata(0).path == path):
to_return = item
else:
idx += 1
return to_return
func _add_script_tree_item(script_path, script_json):
var path_info = _get_path_and_inner_class_name_from_test_path(script_path)
var item_text = script_path
var parent = _root
if(path_info.inner_class != ''):
parent = _find_script_item_with_path(path_info.path)
item_text = path_info.inner_class
if(parent == null):
parent = _add_script_tree_item(path_info.path, {})
parent.get_metadata(0).inner_tests += script_json['props']['tests']
parent.get_metadata(0).inner_passing += script_json['props']['tests']
parent.get_metadata(0).inner_passing -= script_json['props']['failures']
parent.get_metadata(0).inner_passing -= script_json['props']['pending']
var total_text = str("All ", parent.get_metadata(0).inner_tests, " passed")
if(parent.get_metadata(0).inner_passing != parent.get_metadata(0).inner_tests):
total_text = str(parent.get_metadata(0).inner_passing, '/', parent.get_metadata(0).inner_tests, ' passed.')
parent.set_text(1, total_text)
var item = _ctrls.tree.create_item(parent)
item.set_text(0, item_text)
var meta = {
"type":"script",
"path":path_info.path,
"inner_class":path_info.inner_class,
"json":script_json,
"inner_passing":0,
"inner_tests":0
}
item.set_metadata(0, meta)
item.set_custom_bg_color(1, _col_1_bg_color)
return item
func _add_assert_item(text, icon, parent_item):
# print(' * adding assert')
var assert_item = _ctrls.tree.create_item(parent_item)
assert_item.set_icon_max_width(0, _max_icon_width)
assert_item.set_text(0, text)
assert_item.set_metadata(0, {"type":"assert"})
assert_item.set_icon(0, icon)
assert_item.set_custom_bg_color(1, _col_1_bg_color)
return assert_item
func _add_test_tree_item(test_name, test_json, script_item):
# print(' * adding test ', test_name)
var no_orphans_to_show = !_show_orphans or (_show_orphans and test_json.orphans == 0)
if(_hide_passing and test_json['status'] == 'pass' and no_orphans_to_show):
return
var item = _ctrls.tree.create_item(script_item)
var status = test_json['status']
var meta = {"type":"test", "json":test_json}
item.set_text(0, test_name)
item.set_text(1, status)
item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
item.set_custom_bg_color(1, _col_1_bg_color)
item.set_metadata(0, meta)
item.set_icon_max_width(0, _max_icon_width)
var orphan_text = 'orphans'
if(test_json.orphans == 1):
orphan_text = 'orphan'
orphan_text = str(test_json.orphans, ' ', orphan_text)
if(status == 'pass' and no_orphans_to_show):
item.set_icon(0, _icons.green)
elif(status == 'pass' and !no_orphans_to_show):
item.set_icon(0, _icons.yellow)
item.set_text(1, orphan_text)
elif(status == 'fail'):
item.set_icon(0, _icons.red)
else:
item.set_icon(0, _icons.yellow)
if(!_hide_passing):
for passing in test_json.passing:
_add_assert_item('pass: ' + passing, _icons.green, item)
for failure in test_json.failing:
_add_assert_item("fail: " + failure.replace("\n", ''), _icons.red, item)
for pending in test_json.pending:
_add_assert_item("pending: " + pending.replace("\n", ''), _icons.yellow, item)
if(status != 'pass' and !no_orphans_to_show):
_add_assert_item(orphan_text, _icons.yellow, item)
return item
func _add_script_to_tree(key, script_json):
var tests = script_json['tests']
var test_keys = tests.keys()
var s_item = _add_script_tree_item(key, script_json)
var bad_count = 0
for test_key in test_keys:
var t_item = _add_test_tree_item(test_key, tests[test_key], s_item)
if(tests[test_key].status != 'pass'):
bad_count += 1
elif(t_item != null):
t_item.collapsed = true
if(s_item.get_children().size() == 0):
s_item.free()
else:
var total_text = str('All ', test_keys.size(), ' passed')
if(bad_count == 0):
s_item.collapsed = true
else:
total_text = str(test_keys.size() - bad_count, '/', test_keys.size(), ' passed')
s_item.set_text(1, total_text)
func _free_childless_scripts():
var items = _root.get_children()
for item in items:
var next_item = item.get_next()
if(item.get_children().size() == 0):
item.free()
item = next_item
func _show_all_passed():
if(_root.get_children() == null):
add_centered_text('Everything passed!')
func _load_result_tree(j):
var scripts = j['test_scripts']['scripts']
var script_keys = scripts.keys()
# if we made it here, the json is valid and we did something, otherwise the
# 'nothing to see here' should be visible.
clear_centered_text()
for key in script_keys:
if(scripts[key]['props']['tests'] > 0):
_add_script_to_tree(key, scripts[key])
_free_childless_scripts()
_show_all_passed()
# -------------------
# Public
# -------------------
func load_json_file(path):
var file = FileAccess.open(path, FileAccess.READ)
var text = ''
if(file != null):
text = file.get_as_text()
if(text != ''):
var test_json_conv = JSON.new()
var result = test_json_conv.parse(text)
if(result != OK):
add_centered_text(str(path, " has invalid json in it \n",
'Error ', result, "@", test_json_conv.get_error_line(), "\n",
test_json_conv.get_error_message()))
return
var data = test_json_conv.get_data()
load_json_results(data)
else:
add_centered_text(str(path, ' was empty or does not exist.'))
func load_json_results(j):
clear()
_load_result_tree(j)
func clear():
_ctrls.tree.clear()
_root = _ctrls.tree.create_item()
func set_summary_min_width(width):
_ctrls.tree.set_column_custom_minimum_width(1, width)
func add_centered_text(t):
_ctrls.lbl_overlay.visible = true
_ctrls.lbl_overlay.text = t
func clear_centered_text():
_ctrls.lbl_overlay.visible = false
_ctrls.lbl_overlay.text = ''
func collapse_all():
set_collapsed_on_all(_root, true)
func expand_all():
set_collapsed_on_all(_root, false)
func set_collapsed_on_all(item, value):
item.set_collapsed_recursive(value)
if(item == _root and value):
item.set_collapsed(false)
func get_selected():
return _ctrls.tree.get_selected()

View File

@@ -0,0 +1,32 @@
[gd_scene load_steps=2 format=3 uid="uid://dls5r5f6157nq"]
[ext_resource type="Script" path="res://addons/gut/gui/ResultsTree.gd" id="1_b4uub"]
[node name="ResultsTree" type="VBoxContainer"]
custom_minimum_size = Vector2(10, 10)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -70.0
offset_bottom = -104.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_b4uub")
[node name="Tree" type="Tree" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
hide_root = true
[node name="TextOverlay" type="Label" parent="Tree"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2

View File

@@ -0,0 +1,158 @@
@tool
extends Control
var ScriptTextEditors = load('res://addons/gut/gui/script_text_editor_controls.gd')
@onready var _ctrls = {
btn_script = $HBox/BtnRunScript,
btn_inner = $HBox/BtnRunInnerClass,
btn_method = $HBox/BtnRunMethod,
lbl_none = $HBox/LblNoneSelected,
arrow_1 = $HBox/Arrow1,
arrow_2 = $HBox/Arrow2
}
var _editors = null
var _cur_editor = null
var _last_line = -1
var _cur_script_path = null
var _last_info = {
script = null,
inner_class = null,
test_method = null
}
signal run_tests(what)
func _ready():
_ctrls.lbl_none.visible = true
_ctrls.btn_script.visible = false
_ctrls.btn_inner.visible = false
_ctrls.btn_method.visible = false
_ctrls.arrow_1.visible = false
_ctrls.arrow_2.visible = false
# ----------------
# Private
# ----------------
func _set_editor(which):
_last_line = -1
if(_cur_editor != null and _cur_editor.get_ref()):
# _cur_editor.get_ref().disconnect('cursor_changed',Callable(self,'_on_cursor_changed'))
_cur_editor.get_ref().caret_changed.disconnect(_on_cursor_changed)
if(which != null):
_cur_editor = weakref(which)
which.caret_changed.connect(_on_cursor_changed.bind(which))
# which.connect('cursor_changed',Callable(self,'_on_cursor_changed'),[which])
_last_line = which.get_caret_line()
_last_info = _editors.get_line_info()
_update_buttons(_last_info)
func _update_buttons(info):
_ctrls.lbl_none.visible = _cur_script_path == null
_ctrls.btn_script.visible = _cur_script_path != null
_ctrls.btn_inner.visible = info.inner_class != null
_ctrls.arrow_1.visible = info.inner_class != null
_ctrls.btn_inner.text = str(info.inner_class)
_ctrls.btn_inner.tooltip_text = str("Run all tests in Inner-Test-Class ", info.inner_class)
_ctrls.btn_method.visible = info.test_method != null
_ctrls.arrow_2.visible = info.test_method != null
_ctrls.btn_method.text = str(info.test_method)
_ctrls.btn_method.tooltip_text = str("Run test ", info.test_method)
# The button's new size won't take effect until the next frame.
# This appears to be what was causing the button to not be clickable the
# first time.
call_deferred("_update_size")
func _update_size():
custom_minimum_size.x = _ctrls.btn_method.size.x + _ctrls.btn_method.position.x
# ----------------
# Events
# ----------------
func _on_cursor_changed(which):
if(which.get_caret_line() != _last_line):
_last_line = which.get_caret_line()
_last_info = _editors.get_line_info()
_update_buttons(_last_info)
func _on_BtnRunScript_pressed():
var info = _last_info.duplicate()
info.script = _cur_script_path.get_file()
info.inner_class = null
info.test_method = null
emit_signal("run_tests", info)
func _on_BtnRunInnerClass_pressed():
var info = _last_info.duplicate()
info.script = _cur_script_path.get_file()
info.test_method = null
emit_signal("run_tests", info)
func _on_BtnRunMethod_pressed():
var info = _last_info.duplicate()
info.script = _cur_script_path.get_file()
emit_signal("run_tests", info)
# ----------------
# Public
# ----------------
func set_script_text_editors(value):
_editors = value
func activate_for_script(path):
_ctrls.btn_script.visible = true
_ctrls.btn_script.text = path.get_file()
_ctrls.btn_script.tooltip_text = str("Run all tests in script ", path)
_cur_script_path = path
_editors.refresh()
# We have to wait a beat for the visibility to change on
# the editors, otherwise we always get the first one.
await get_tree().process_frame
_set_editor(_editors.get_current_text_edit())
func get_script_button():
return _ctrls.btn_script
func get_inner_button():
return _ctrls.btn_inner
func get_test_button():
return _ctrls.btn_method
# not used, thought was configurable but it's just the script prefix
func set_method_prefix(value):
_editors.set_method_prefix(value)
# not used, thought was configurable but it's just the script prefix
func set_inner_class_prefix(value):
_editors.set_inner_class_prefix(value)
# Mashed this function in here b/c it has _editors. Probably should be
# somewhere else (possibly in script_text_editor_controls).
func search_current_editor_for_text(txt):
var te = _editors.get_current_text_edit()
var result = te.search(txt, 0, 0, 0)
var to_return = -1
return to_return

View File

@@ -0,0 +1,65 @@
[gd_scene load_steps=4 format=3 uid="uid://0yunjxtaa8iw"]
[ext_resource type="Script" path="res://addons/gut/gui/RunAtCursor.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="2"]
[ext_resource type="Texture2D" uid="uid://6wra5rxmfsrl" path="res://addons/gut/gui/arrow.png" id="3"]
[node name="RunAtCursor" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 1.0
offset_bottom = -527.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1")
[node name="HBox" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="LblNoneSelected" type="Label" parent="HBox"]
layout_mode = 2
text = "<None>"
[node name="BtnRunScript" type="Button" parent="HBox"]
visible = false
layout_mode = 2
text = "<script>"
icon = ExtResource("2")
[node name="Arrow1" type="TextureButton" parent="HBox"]
visible = false
custom_minimum_size = Vector2(24, 0)
layout_mode = 2
texture_normal = ExtResource("3")
stretch_mode = 3
[node name="BtnRunInnerClass" type="Button" parent="HBox"]
visible = false
layout_mode = 2
text = "<inner class>"
icon = ExtResource("2")
[node name="Arrow2" type="TextureButton" parent="HBox"]
visible = false
custom_minimum_size = Vector2(24, 0)
layout_mode = 2
texture_normal = ExtResource("3")
stretch_mode = 3
[node name="BtnRunMethod" type="Button" parent="HBox"]
visible = false
layout_mode = 2
text = "<method>"
icon = ExtResource("2")
[connection signal="pressed" from="HBox/BtnRunScript" to="." method="_on_BtnRunScript_pressed"]
[connection signal="pressed" from="HBox/BtnRunInnerClass" to="." method="_on_BtnRunInnerClass_pressed"]
[connection signal="pressed" from="HBox/BtnRunMethod" to="." method="_on_BtnRunMethod_pressed"]

View File

@@ -0,0 +1,255 @@
@tool
extends Control
var _interface = null
var _font = null
var _font_size = null
var _editors = null # script_text_editor_controls.gd
var _output_control = null
@onready var _ctrls = {
tree = $VBox/Output/Scroll/Tree,
toolbar = {
toolbar = $VBox/Toolbar,
collapse = $VBox/Toolbar/Collapse,
collapse_all = $VBox/Toolbar/CollapseAll,
expand = $VBox/Toolbar/Expand,
expand_all = $VBox/Toolbar/ExpandAll,
hide_passing = $VBox/Toolbar/HidePassing,
show_script = $VBox/Toolbar/ShowScript,
scroll_output = $VBox/Toolbar/ScrollOutput
}
}
func _ready():
var f = null
if ($FontSampler.get_label_settings() == null) :
f = get_theme_default_font()
else :
f = $FontSampler.get_label_settings().font
var s_size = f.get_string_size("000 of 000 passed")
_ctrls.tree.set_summary_min_width(s_size.x)
_set_toolbutton_icon(_ctrls.toolbar.collapse, 'CollapseTree', 'c')
_set_toolbutton_icon(_ctrls.toolbar.collapse_all, 'CollapseTree', 'c')
_set_toolbutton_icon(_ctrls.toolbar.expand, 'ExpandTree', 'e')
_set_toolbutton_icon(_ctrls.toolbar.expand_all, 'ExpandTree', 'e')
_set_toolbutton_icon(_ctrls.toolbar.show_script, 'Script', 'ss')
_set_toolbutton_icon(_ctrls.toolbar.scroll_output, 'Font', 'so')
_ctrls.tree.hide_passing = true
_ctrls.toolbar.hide_passing.button_pressed = false
_ctrls.tree.show_orphans = true
_ctrls.tree.item_selected.connect(_on_item_selected)
if(get_parent() == get_tree().root):
_test_running_setup()
call_deferred('_update_min_width')
func _test_running_setup():
_ctrls.tree.hide_passing = true
_ctrls.tree.show_orphans = true
var _gut_config = load('res://addons/gut/gut_config.gd').new()
_gut_config.load_panel_options('res://.gut_editor_config.json')
set_font(
_gut_config.options.panel_options.font_name,
_gut_config.options.panel_options.font_size)
_ctrls.toolbar.hide_passing.text = '[hp]'
_ctrls.tree.load_json_file('user://.gut_editor.json')
func _set_toolbutton_icon(btn, icon_name, text):
if(Engine.is_editor_hint()):
btn.icon = get_theme_icon(icon_name, 'EditorIcons')
else:
btn.text = str('[', text, ']')
func _update_min_width():
custom_minimum_size.x = _ctrls.toolbar.toolbar.size.x
func _open_script_in_editor(path, line_number):
if(_interface == null):
print('Too soon, wait a bit and try again.')
return
var r = load(path)
if(line_number != null and line_number != -1):
_interface.edit_script(r, line_number)
else:
_interface.edit_script(r)
if(_ctrls.toolbar.show_script.pressed):
_interface.set_main_screen_editor('Script')
# starts at beginning of text edit and searches for each search term, moving
# through the text as it goes; ensuring that, when done, it found the first
# occurance of the last srting that happend after the first occurance of
# each string before it. (Generic way of searching for a method name in an
# inner class that may have be a duplicate of a method name in a different
# inner class)
func _get_line_number_for_seq_search(search_strings, te):
if(te == null):
print("No Text editor to get line number for")
return 0;
var result = null
var line = Vector2i(0, 0)
var s_flags = 0
var i = 0
var string_found = true
while(i < search_strings.size() and string_found):
result = te.search(search_strings[i], s_flags, line.y, line.x)
if(result.x != -1):
line = result
else:
string_found = false
i += 1
return line.y
func _goto_code(path, line, method_name='', inner_class =''):
if(_interface == null):
print('going to ', [path, line, method_name, inner_class])
return
_open_script_in_editor(path, line)
if(line == -1):
var search_strings = []
if(inner_class != ''):
search_strings.append(inner_class)
if(method_name != ''):
search_strings.append(method_name)
line = _get_line_number_for_seq_search(search_strings, _editors.get_current_text_edit())
if(line != null and line != -1):
_interface.get_script_editor().goto_line(line)
func _goto_output(path, method_name, inner_class):
if(_output_control == null):
return
var search_strings = [path]
if(inner_class != ''):
search_strings.append(inner_class)
if(method_name != ''):
search_strings.append(method_name)
var line = _get_line_number_for_seq_search(search_strings, _output_control.get_rich_text_edit())
if(line != null and line != -1):
_output_control.scroll_to_line(line)
# --------------
# Events
# --------------
func _on_Collapse_pressed():
collapse_selected()
func _on_Expand_pressed():
expand_selected()
func _on_CollapseAll_pressed():
collapse_all()
func _on_ExpandAll_pressed():
expand_all()
func _on_Hide_Passing_pressed():
_ctrls.tree.hide_passing = !_ctrls.toolbar.hide_passing.button_pressed
_ctrls.tree.load_json_file('user://.gut_editor.json')
func _on_item_selected(script_path, inner_class, test_name, line):
if(_ctrls.toolbar.show_script.button_pressed):
_goto_code(script_path, line, test_name, inner_class)
if(_ctrls.toolbar.scroll_output.button_pressed):
_goto_output(script_path, test_name, inner_class)
# --------------
# Public
# --------------
func add_centered_text(t):
_ctrls.tree.add_centered_text(t)
func clear_centered_text():
_ctrls.tree.clear_centered_text()
func clear():
_ctrls.tree.clear()
clear_centered_text()
func set_interface(which):
_interface = which
func set_script_text_editors(value):
_editors = value
func collapse_all():
_ctrls.tree.collapse_all()
func expand_all():
_ctrls.tree.expand_all()
func collapse_selected():
var item = _ctrls.tree.get_selected()
if(item != null):
_ctrls.tree.set_collapsed_on_all(item, true)
func expand_selected():
var item = _ctrls.tree.get_selected()
if(item != null):
_ctrls.tree.set_collapsed_on_all(item, false)
func set_show_orphans(should):
_ctrls.tree.show_orphans = should
func set_font(font_name, size):
pass
# var dyn_font = FontFile.new()
# var font_data = FontFile.new()
# font_data.font_path = 'res://addons/gut/fonts/' + font_name + '-Regular.ttf'
# font_data.antialiased = true
# dyn_font.font_data = font_data
#
# _font = dyn_font
# _font.size = size
# _font_size = size
func set_output_control(value):
_output_control = value
func load_json_results(j):
_ctrls.tree.load_json_results(j)

View File

@@ -0,0 +1,116 @@
[gd_scene load_steps=5 format=3 uid="uid://4gyyn12um08h"]
[ext_resource type="Script" path="res://addons/gut/gui/RunResults.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://dls5r5f6157nq" path="res://addons/gut/gui/ResultsTree.tscn" id="2_o808v"]
[sub_resource type="Image" id="Image_18d1g"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_8u17l"]
image = SubResource("Image_18d1g")
[node name="RunResults" type="Control"]
custom_minimum_size = Vector2(345, 0)
layout_mode = 3
anchors_preset = 0
offset_right = 709.0
offset_bottom = 321.0
script = ExtResource("1")
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Toolbar" type="HBoxContainer" parent="VBox"]
layout_mode = 2
size_flags_horizontal = 0
[node name="Expand" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
icon = SubResource("ImageTexture_8u17l")
[node name="Collapse" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
icon = SubResource("ImageTexture_8u17l")
[node name="Sep" type="ColorRect" parent="VBox/Toolbar"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="LblAll" type="Label" parent="VBox/Toolbar"]
layout_mode = 2
text = "All:"
[node name="ExpandAll" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
icon = SubResource("ImageTexture_8u17l")
[node name="CollapseAll" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
icon = SubResource("ImageTexture_8u17l")
[node name="Sep2" type="ColorRect" parent="VBox/Toolbar"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="HidePassing" type="CheckBox" parent="VBox/Toolbar"]
layout_mode = 2
size_flags_horizontal = 4
text = "Passing"
[node name="Sep3" type="ColorRect" parent="VBox/Toolbar"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="LblSync" type="Label" parent="VBox/Toolbar"]
layout_mode = 2
text = "Sync:"
[node name="ShowScript" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_8u17l")
[node name="ScrollOutput" type="Button" parent="VBox/Toolbar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_8u17l")
[node name="Output" type="Panel" parent="VBox"]
self_modulate = Color(1, 1, 1, 0.541176)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Scroll" type="ScrollContainer" parent="VBox/Output"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Tree" parent="VBox/Output/Scroll" instance=ExtResource("2_o808v")]
layout_mode = 2
[node name="FontSampler" type="Label" parent="."]
visible = false
layout_mode = 0
offset_right = 40.0
offset_bottom = 14.0
text = "000 of 000 passed"
[connection signal="pressed" from="VBox/Toolbar/Expand" to="." method="_on_Expand_pressed"]
[connection signal="pressed" from="VBox/Toolbar/Collapse" to="." method="_on_Collapse_pressed"]
[connection signal="pressed" from="VBox/Toolbar/ExpandAll" to="." method="_on_ExpandAll_pressed"]
[connection signal="pressed" from="VBox/Toolbar/CollapseAll" to="." method="_on_CollapseAll_pressed"]
[connection signal="pressed" from="VBox/Toolbar/HidePassing" to="." method="_on_Hide_Passing_pressed"]

View File

@@ -0,0 +1,7 @@
[gd_scene format=3 uid="uid://cvvvtsah38l0e"]
[node name="Settings" type="VBoxContainer"]
offset_right = 388.0
offset_bottom = 586.0
size_flags_horizontal = 3
size_flags_vertical = 3

View File

@@ -0,0 +1,144 @@
@tool
extends Control
@onready var _ctrls = {
shortcut_label = $Layout/lblShortcut,
set_button = $Layout/SetButton,
save_button = $Layout/SaveButton,
cancel_button = $Layout/CancelButton,
clear_button = $Layout/ClearButton
}
signal changed
signal start_edit
signal end_edit
const NO_SHORTCUT = '<None>'
var _source_event = InputEventKey.new()
var _pre_edit_event = null
var _key_disp = NO_SHORTCUT
var _modifier_keys = [KEY_ALT, KEY_CTRL, KEY_META, KEY_SHIFT]
# Called when the node enters the scene tree for the first time.
func _ready():
set_process_unhandled_key_input(false)
func _display_shortcut():
if(_key_disp == ''):
_key_disp = NO_SHORTCUT
_ctrls.shortcut_label.text = _key_disp
func _is_shift_only_modifier():
return _source_event.shift_pressed and \
!(_source_event.alt_pressed or \
_source_event.ctrl_pressed or \
_source_event.meta_pressed) \
and !_is_modifier(_source_event.keycode)
func _has_modifier(event):
return event.alt_pressed or event.ctrl_pressed or \
event.meta_pressed or event.shift_pressed
func _is_modifier(keycode):
return _modifier_keys.has(keycode)
func _edit_mode(should):
set_process_unhandled_key_input(should)
_ctrls.set_button.visible = !should
_ctrls.save_button.visible = should
_ctrls.save_button.disabled = should
_ctrls.cancel_button.visible = should
_ctrls.clear_button.visible = !should
if(should and to_s() == ''):
_ctrls.shortcut_label.text = 'press buttons'
else:
_ctrls.shortcut_label.text = to_s()
if(should):
emit_signal("start_edit")
else:
emit_signal("end_edit")
# ---------------
# Events
# ---------------
func _unhandled_key_input(event):
if(event is InputEventKey):
if(event.pressed):
if(_has_modifier(event) and !_is_modifier(event.get_keycode_with_modifiers())):
_source_event = event
_key_disp = OS.get_keycode_string(event.get_keycode_with_modifiers())
else:
_source_event = InputEventKey.new()
_key_disp = NO_SHORTCUT
_display_shortcut()
_ctrls.save_button.disabled = !is_valid()
func _on_SetButton_pressed():
_pre_edit_event = _source_event.duplicate(true)
_edit_mode(true)
func _on_SaveButton_pressed():
_edit_mode(false)
_pre_edit_event = null
emit_signal('changed')
func _on_CancelButton_pressed():
_edit_mode(false)
_source_event = _pre_edit_event
_key_disp = to_s()
_display_shortcut()
func _on_ClearButton_pressed():
clear_shortcut()
# ---------------
# Public
# ---------------
func to_s():
return OS.get_keycode_string(_source_event.get_keycode_with_modifiers())
func is_valid():
return _has_modifier(_source_event) and !_is_shift_only_modifier()
func get_shortcut():
var to_return = Shortcut.new()
to_return.events.append(_source_event)
return to_return
func set_shortcut(sc):
if(sc == null or sc.events == null || sc.events.size() <= 0):
clear_shortcut()
else:
_source_event = sc.events[0]
_key_disp = to_s()
_display_shortcut()
func clear_shortcut():
_source_event = InputEventKey.new()
_key_disp = NO_SHORTCUT
_display_shortcut()
func disable_set(should):
_ctrls.set_button.disabled = should
func disable_clear(should):
_ctrls.clear_button.disabled = should

Some files were not shown because too many files have changed in this diff Show More