Home

Awesome

Straw Banner

straw

Straw is a juce automation framework for integration testing.

Download and install the python framework

Go to https://www.python.org/downloads/ and download a python install. The version 3.11 is reccommended. After installing it, make sure you write down the location of the install. For example on macOS, typically it is installed in /Library/Frameworks/Python.framework/Versions/3.11 but you could use the /Library/Frameworks/Python.framework/Versions/Current symbolic link for simplicity.

Setup the dependency using cmake

In order to configure straw in the juce project, is necessary to fetch the code for it together with specifying and finding the Python install. Once both are setup they need to be passed to the list of libraries to link.

# Fetch the dependency using FetchContent
Include (FetchContent)
FetchContent_Declare (straw
  GIT_REPOSITORY https://github.com/kunitoki/straw.git
  GIT_TAG origin/main
  SOURCE_DIR ${CMAKE_CURRENT_BINARY_DIR}/straw)
FetchContent_MakeAvailable (straw)

# Setup the Python_ROOT_DIR where python has been installed and python options
set(Python_ROOT_DIR "/Library/Frameworks/Python.framework/Versions/Current")
set(Python_USE_STATIC_LIBS TRUE) # Optional for static builds
find_package(Python REQUIRED Development.Embed)

# Add libraries to the linking libraries of your target
target_link_libraries(${TARGET_NAME} PRIVATE
    ...
    straw::juce_straw_recommended_warning_flags
    straw::juce_straw
    Python::Python)

Declare and instantiate the server

In your application, add a new dependency to straw::AutomationServer which will start listening on port 8001.

class Application : public juce::JUCEApplication
{
public:
    Application() = default;

    void initialise (const String&) override
    {
        // Initialise your windows and components here...

        // Create the animation server instance
        automationServer = std::make_unique<straw::AutomationServer>();

        // Register default endpoints (optional)
        automationServer->registerDefaultEndpoints();

        // Start the HTTP server
        auto result = automationServer->start();
        if (result.failed())
            DBG(result.getErrorMessage());
    }

    void shutdown() override
    {
        // Server is stopped together with background connections
        automationServer.reset();
    }

private:
    std::unique_ptr<straw::AutomationServer> automationServer;
};

At this point when the app has started, it is able to respond to requests from the same machine:

curl -X GET http://localhost:8001/straw/component/exists -H 'Content-Type: application/json' -d '{"id":"MyComponentID"}'
# Will return a json with the result of the query { "result": true }

Registering custom endpoints

It is possible to register custom endpoints:

// Initialise your windows and components here...
mainComponent = std::make_unique<MainComponent>();

// Create the animation server instance
automationServer = std::make_unique<straw::AutomationServer>();

// Register a custom endpoint
auto weakMainComponent = juce::Component::SafePointer<MainComponent>(mainComponent.get());
automationServer.registerEndpoint ("/change_background_colour", [weakMainComponent](straw::Request request)
{
    // Parse the data from the request
    auto colour = juce::Colour::fromString (request.data.getProperty ("colour", "FF00FF00").toString());

    juce::MessageManager::callAsync ([weakMainComponent, colour, connection = std::move (request.connection)]
    {
        if (auto mainComponent = weakMainComponent.getComponent())
        {
            mainComponent->setColour (juce::DocumentWindow::backgroundColourId, colour);
            mainComponent->repaint();

            // Return a 200 OK HTTP response with a JSON payload of { "result": true }
            straw::sendHttpResultResponse (true, 200, *connection);
        }
        else
        {
            // Return a 404 Not Found HTTP response with a JSON payload of { "error": "..." }
            straw::sendHttpErrorMessage("Unable to find the component!", 404, *connection);
        }
    });
});

// Start the HTTP server
auto result = automationServer->start();

This way your application can execute a remote JSON RPC callback:

# Execute custom defined callback
curl -X GET http://localhost:8001/change_background_colour -H 'Content-Type: application/json' -d '{"colour":"FFFF0000"}

Setting up and executing python test suites

To execute a test suite defined as a remote python script, all is necessary to do is to first register the default juce and straw python bindings.


// Create the animation server instance
automationServer = std::make_unique<straw::AutomationServer>();

// Register default endpoints (needed for the remote test suite script execution)
automationServer->registerDefaultEndpoints();

// Register default bindings (juce classes and straw helper methods)
automationServer->registerDefaultComponents();

// Start the HTTP server
auto result = automationServer->start();

Then it's possible to send a text/x-python script to the automation server and it will be executed, content of MyTestSuite.py:

import straw

straw.log ("Testing findComponent ====================")

comp = straw.findComponentById ("non-existing")
straw.assertTrue (comp is None)

comp = straw.findComponentById ("button")
straw.assertEqual (comp.typeName(), "juce::TextButton")
straw.assertEqual (comp.getComponentID(), "button")
straw.assertTrue (comp is not None)
straw.assertTrue (comp.isVisible())
straw.assertTrue (comp.isShowing())

properties = comp.getProperties()
straw.assertEqual (len (properties), 1)
straw.assertTrue ("example" in properties)
straw.assertEqual (properties["example"], 1337)

comp = straw.findComponentById ("slider")
straw.assertEqual (comp.typeName(), "juce::Slider")
straw.assertGreaterThan (comp.getValue(), 0.0)

comp = straw.findComponentById ("animation")
children = comp.getChildren()
straw.assertEqual (len (children), 2)
straw.assertTrue ([x for x in children if x.getComponentID() == "button"])
straw.assertTrue ([x for x in children if x.getComponentID() == "slider"])

And send it to the automation server for evaluation:

curl --data-binary '@MyTestSuite.py' http://localhost:8001 -H 'Content-Type: text/x-python'

If all is good, a result JSON object is returned { "result": true }

Expose custom components and custom methods to the python scripts

TODO

Example of REST API

# Test if a component exists
curl -X GET http://localhost:8001/straw/component/exists -H 'Content-Type: application/json' -d '{"id":"animation"}'

# Return the informations from a component (recursive as well)
curl -X GET http://localhost:8001/straw/component/info -H 'Content-Type: application/json' -d '{"id":"animation", "recursive": true}'

# Click a component
curl -X GET http://localhost:8001/straw/component/click -H 'Content-Type: application/json' -d '{"id":"button"}'

# Render a component (with or without children and return a png)
curl -X GET http://localhost:8001/straw/component/render -H 'Content-Type: application/json' -d '{"id":"animation", "withChildren":true}' > test.png

# Execute custom defined callback
curl -X GET http://localhost:8001/change_background_colour -H 'Content-Type: application/json' -d '{"colour":"FFFF0000"}'

Example of Python API

curl --data-binary '@./Demo/Scripts/clickComponent.py' http://localhost:8001 -H 'Content-Type: text/x-python'
curl --data-binary '@./Demo/Scripts/findComponent.py' http://localhost:8001 -H 'Content-Type: text/x-python'
curl --data-binary '@./Demo/Scripts/log.py' http://localhost:8001 -H 'Content-Type: text/x-python'
curl --data-binary '@./Demo/Scripts/raise.py' http://localhost:8001 -H 'Content-Type: text/x-python'
curl --data-binary '@./Demo/Scripts/test.py' http://localhost:8001 -H 'Content-Type: text/x-python'