Create new track for GDScript

Hi!

I’d like to suggest adding a new track for GDScript, the default programming language of Godot Engine.

The syntax is similar to Python, but with enough differences (in my opinion) to justify a dedicated learning track.

The main advantages of adding this track to Exercism would be:

  • Godot Engine is a popular open source tool for making different types of applications (not only games, but also simulations, visualizations etc.). The community is already big and grows fast, and I’m sure it would benefit from a place to exchange good coding practices.
  • Godot Engine is also a great way to start an adventure with programming. It contains of a small editor (the binary is ~80 MB) with no dependencies, so it’s very easy to install. Moreover, the output of the program can be quickly visualized (using 2D or 3D graphics), which might make it easier for beginners to grasp some programming concepts.
  • Adding a track for a gamedev oriented language would make Exercism an even more diverse tool :)

I understand that coding challenges on Exercism focus on algorithms and will not explore the 2D/3D/audio parts of Godot Engine. But I believe that a new track could still help many people to discover Godot and get familiar with GDScript, and use that knowledge to make their own ideas come true :)

Best,
Paweł Fertyk, a.k.a. Miskatonic Studio

3 Likes

Omg that would be so cool, I have actually had an interest in gdscript and personally would love that language on exercism. I could even help with starting the track if needed.

The only problem I see is that we would need to separate the language from the engine and I am unsure if it is possible to run gdscript without the engine.

2 Likes

There is a headless version of the engine, which I believe would be helpful here ;) A simple script could look like this:

extends SceneTree


func _init():
    print("Hello!")
    quit()

There is a command line tutorial in the official docs:

1 Like

Yeah I see that now, I also checked on github and there I know see the headless version.

Are you interested on building this track on exercism?

I am up to help you with building the track if you would like, but as you probably notice am I not an expert at gdscript, but I am willing to learn to get better at it. And I do have quite a bit of experience with exercism I can bring.

@pfertyk If you’d like to lead on the track, the first step is to decide on the set up for your hello-world exercise and test suite. Hello world is a standardised first exercise in all languages. You can see it’s specification here. Creating this helps us determine what work needs to go into building the track from the Exercism staff’s perspective. Once we’ve got that agreed, we’ll create you a repository and can start adding code into that on GitHub.

Could you provide a sample Hello World solution and test suite for it in this forum thread pls?

I’m guessing using Python as your basis is best, in which case look at these two files:

Thank you!

3 Likes

@iHiD Thank you for a fast response! I’ll take a look at the specs and get back to you in the next few days. I would be very happy to see a GDScript track on Exercism :)

@Meatball What a coincidence: I have quite a bit of experience with GDScript, but very little with Exercism :P I am very interested in building this track, and any help you can provide along the way would be much appreciated :+1:

3 Likes

Hi again @iHiD !

I designed the following “Hello, World!” solution (the return value, of course, should be changed as a part of this exercise):

func hello():
    return "Goodbye, Mars!"

The test suite would look like this:

extends SceneTree

# Godot's default error message is 'Assertion failed', this will be more informative
const ERROR_MESSAGE = "Expected output was '{expected}', actual output was '{output}'."

# For multiple tests, it might be easier to iterate over a list of test cases
const TEST_CASES = [
    {"method": "hello", "args": [], "expected": "Goodbye, Mars!"},
    {"method": "hello", "args": [], "expected": "Hello, World!"},
]


# Simple test for a specific output of a specific method
func run_tests(script):
    var expected = "Hello, World!"
    var output = script.hello()

    assert(
        output == expected,
        ERROR_MESSAGE.format({"expected": expected, "output": output})
    )


# Parametrized test case
func run_tests_parametrized(script):
    for test_case in TEST_CASES:
        var expected = test_case["expected"]
        var output = script.callv(test_case["method"], test_case["args"])

        assert(
            output == expected,
            ERROR_MESSAGE.format({"expected": expected, "output": output})
        )


func _init():
    var script = preload("user_script.gd").new()
    run_tests(script)
    run_tests_parametrized(script)
    quit()

A command used to run these tests would be:

./Godot_v3.5.1-stable_linux_headless.64 -s test_suite.gd 

where Godot_v3.5.1-stable_linux_headless.64 is a headless Godot Engine binary (available in the official download repository).

The output will look like this:

Godot Engine v3.5.1.stable.official.6fed1ffa3 - https://godotengine.org
 
SCRIPT ERROR: Assertion failed: Expected output was 'Hello, World!', actual output was 'Goodbye, Mars!'.
          at: run_tests (res://test_suite.gd:18)
SCRIPT ERROR: Assertion failed: Expected output was 'Hello, World!', actual output was 'Goodbye, Mars!'.
          at: run_tests_parametrized (res://test_suite.gd:30)

I’ve added 2 options in this tests suite: a simple test for a specific value, and a list of test cases with a loop. I assume that the latter is preferred (emulating e.g. pytest.parametrize), but I wanted to cover all bases ;)

There is also a unit test framework for Godot called Godot Unit Test (GUT). It is not a part of the engine itself, but rather a plugin. If Exercism allows to run a full Godot project (instead of a single script), we might use that. If not, I believe a solution based on a loop and assertions will do fine ;)

Please let me know what you think ;)

Best,
Paweł

2 Likes

I’ve created a repository for you here:

I gave both @pfertyk and @Meatball maintainer-access to the track.

Let me know if/when I should add anyone else!

3 Likes

@pfertyk Awesome. In addition to @meatball’s support, I’ll leave you in the capable hands of:

  • Our docs - which should tell you everything you need (pls improve them if not! :slightly_smiling_face: )
  • The lovely community on these forums - lots of whom have built tracks.
  • @ErikSchierboom, our Head of Open Source - your go-to person if you have technical or Exercism questions.
  • @kytrinyx, my co-founder, if you’d like to explore using her exercise generator to create exercises from problem specs.
  • @jonathanmiddleton our community manager, who always likes to chat to new contributors :slight_smile:
2 Likes

I think a good start would be to have some communication method, I am open to most stuff but the easiest could be on exercisms slack, my name on slack is meatball. If you are not on slack, I think @iHiD could give you a link.

I am up to having calls and working seasons, or working separately and discussing via chat or call. My timezone is cet btw.

A good start I think would be to setup configlet on your machine. I am up for showing you how to set it up, although I have to know your os in beforehand if I am supposed to have a tutorial since I am a bit unsure how to set it up on windows.

Then some decisions on how to setup the track has to take place, and I am here to help you guide through them. Such a question could for example be if we should use godot test unit or not.

But that godot test unit can Export results in standard JUnit XML format.
Makes me a bit tempted to use it

1 Like

@kytrinyx @iHiD Thank you very much! I will review the docs and set up all the necessary tools over the next few days. I’ll let you know if I have any questions :heart:

@Meatball Thanks for sharing this responsibility with me :smiley: My timezone is also CET, my OS is Ubuntu, I’m ok with either this forum or with Slack. In case of Slack, I will need an invitation though.

I think I prefer async work, with periodic updates about the progress, if that’s ok with you. Since I’ve never created an Exercism track before, I will start with the docs to just get an overview of the process. Then I would love to discuss the details with you.

There is one question that I’d like to ask now: how many exercises does it take to launch a track? I imagine that we can add more later, but how many do we need at the beginning?

Best,
Paweł

1 Like

Minimum is 20 and a test runner is also needed for miniumum requirement, but filling out doc is a good way to start. If we end up using that libary, so will there need to be docs on how to use it aswell.

And async work, works fine by me

1 Like

I’ve started reading the documentation and wow, is there a lot! ;) Great job on describing the whole process in such detail! :heart:

I need a bit more time to go through all the topics. Meanwhile, I’ve added a WIP pull requests for the “Hello, World!” exercise. It’s incomplete now, but I’ll update it step-by-step:

For the language itself, I would suggest 3.5.1, being the latest stable release. Godot 4 will introduce a lot of changes, but for now only RC version is available. Once it becomes stable, we can update the exercises (although I would imagine Godot 3 being more popular for some time).

The main question for me is the choice of the testing framework. I’ve tried Godot Unit Test today and it seem to work fine. I haven’t checked yet how to set up a testing environment (but I’ll get there ;) ). If it’s OK to have a full testing project (with plugin code) then I think we could use GUT. Otherwise, as I mentioned before, creating a custom solution based on assertions should be entirely possible :slight_smile:

@Meatball should the discussion about the testing framework be moved to GitHub, or should it stay here? You mentioned before that JUnit XML format is tempting, is that a requirement for Exercism unit tests, or can they work with standard output as well?

Best,
Paweł

Nice!

I agree 3.5.1, is good.

With testing, is a big question the test runner, the test runner is what makes a track, able to be solved using the online editor. I only have experience with 1 test runner, and that is crystals, the crystal one is pretty amazingly written (I didn’t write it), as far as I can see so is the design pretty bulletproof and I have actually not really had many issues with it.

The crystal test runner uses Junit XML format and it really helps, although crystal is a general-purpose language, which can help in some aspects. The crystal test runner only utilizes the standard library.

I think it could be good to start prototyping a test runner with that library before writing 20 exercises with it and then running into issues.

The test runner should output json, but sometimes JUnit xml can help build the json file.

I think this discussion could be on github or here, the forum is more “public” while github is a bit more “private” I guess, so it is up to you.

I have got that libary working with my headless version of godot atleast

Although I have some issues running tests with it.

Thanks for the info @Meatball ! I will be traveling for a week, so my time will be limited, but I’ll try to solve the issues we might have with Godot’s test runner. Next week I will be more engaged in this task ;)

Best,
Paweł

1 Like

Hi again!

Sorry for a long break, there’s a lot going on right now for me, but I’m still interested in this track and I’ll keep working on it whenever possible.

I started adding the GitHub action for tests, only to realize that the Test Runner has to be implemented differently ;) I’ve found the docs for Test Runner Interface and I’ll be working on it next.

Good news is: there is already an action for setting up Godot headless, and it seems to work as expected. I’ve added my handmade test example, and next I’ll check if it’s possible to run Godot Unit Test plugin (might be useful for the Test Runner as well). I’ll keep you posted ;)

Could you please clarify something @Meatball ? I think all the test examples I’ve found for other languages in Exercism (at least for “Hello, World”) seem to have a code sample that’s wrong and a test that’s correct (e.g. the sample returning “Goodbye, Mars” and the test checking for “Hello, World”). That would mean that GitHub tests should be failing, but it doesn’t seem to be the case … Did I miss something?

Best,
Paweł

1 Like

Exercise directories contain a .meta directory with at least one valid example solution. Also, the track bin directory contains a script to test against these examples. The ‘wrong’ solution is the stub that is given to students when they start an exercise.

Examples: Hello World solution, test script.

Yeah as Matthijs said, there are 2 files always, one of which is the one you give the student, and that one will always fail. And then one in the .meta directory that is an example of to solve it.

My plan is to get working on some stuff for the track as well. But if you want me to review any content just let me know.

Hi again!

I created a very simple tests runner for GDScript. I decided to create a custom solution based on assertions, not Godot Unit Test, as I’m not sure how many benefits the plugin would bring. The code should be refactored and cleaned, I just wanted to present my initial idea:

extends SceneTree

const ERROR_MESSAGE = "Expected output was '{}', actual output was '{}'."


func run_tests(solution, method_name, test_cases):
    var test_results = []

    for test_case in test_cases:
        var test_name = test_case["test_name"]
        var args = test_case["args"]
        var expected = test_case["expected"]

        # TODO: check for errors
        var output = solution.callv(method_name, args)

        var status = run_single_test(solution, method_name, args, expected)
        var message = ERROR_MESSAGE.format([expected, output], "{}")

        test_results.append({
            "name": test_name,
            "status": "pass" if status == OK else "fail",
            "message": null if status == OK else message,
        })

    return test_results


func run_single_test(solution, method_name, args, expected):
    var output = solution.callv(method_name, args)
    assert(output == expected)
    return OK


func _init():
    # TODO: read names from params
    var solution = preload("user_solution.gd").new()
    var test_suite = preload("test_suite.gd").new()

    var method_name = test_suite.METHOD_NAME
    var test_cases = test_suite.TEST_CASES

    var test_results = run_tests(solution, method_name, test_cases)

    var results = {
        "version": 2,
        "tests": test_results,
        "status": "pass"
    }
    # TODO: check for errors
    for test_result in test_results:
        if test_result["status"] == "fail":
            results["status"] = "fail"
            break

    # TODO: save to results.json instead of printing
    var pretty_results = JSON.print(results, "  ")
    print(pretty_results)
    quit()

A single test suite could look like this:

# Assuming that we always test the same method in a given test suite
# If not, this can be moved to TEST_CASES
const METHOD_NAME = "add_2_numbers"

const TEST_CASES = [
    {"test_name": "Test One", "args": [1, 2], "expected": 3},
    {"test_name": "Test Two", "args": [10, 20], "expected": 30},
]

And an example of the user solution would be:

func add_2_numbers(a, b):
    return 3

In this case, the solution is wrong, so the output from the test runner looks like this:

➜  test_runner ./Godot_v3.5.1-stable_linux_headless.64 -s test_runner.gd
Godot Engine v3.5.1.stable.official.6fed1ffa3 - https://godotengine.org

SCRIPT ERROR: Assertion failed.
          at: run_single_test (res://test_runner.gd:31)
{
  "version": 2,
  "tests": [
    {
      "name": "Test One",
      "status": "pass",
      "message": null
    },
    {
      "name": "Test Two",
      "status": "fail",
      "message": "Expected output was '30', actual output was '3'."
    }
  ],
  "status": "fail"
}

Godot Engine is oriented toward games, so it’s unusual to do things like running scripts in separation from 2D/3D scenes or running unit tests ;) It also doesn’t have a try ... catch mechanism, so my example lacks error detection (it only checks failures). I think the closest in GDScript I can get to detecting errors is splitting the code into many methods with assert() inside and checking if the results are ERR* or OK. I’ve tested this on a simple method and I think this approach could work. There is also a discussion here.

I’ve seen that Test Runner Interface also includes things like test_code and output. For the former, I think we could use Script.source_code, although in this parametrized version we can also build the test code from available pieces. For the latter, I will have to do some digging. There are things like Expression.execute but I’m not sure if they will work for custom methods.

Please let me know what do you think @Meatball . If this looks like a good approach, I think the next step will be to create a Docker version in a new repository. If you have any remarks or notice any issues, let’s discuss ;)

I will be traveling this weekend, so I will only get back to my laptop on Monday. My responses might be delayed until then ;)