Skip to content

add graphql websocket subsciption tests#768

Open
dwsutherland wants to merge 9 commits into
cylc:masterfrom
dwsutherland:graphql-ws-tests
Open

add graphql websocket subsciption tests#768
dwsutherland wants to merge 9 commits into
cylc:masterfrom
dwsutherland:graphql-ws-tests

Conversation

@dwsutherland
Copy link
Copy Markdown
Member

Adds an integration test for testing the graphql subscriptions via websocket connection.

Check List

  • I have read CONTRIBUTING.md and added my name as a Code Contributor.
  • Contains logically grouped changes (else tidy your branch by rebase).
  • Does not contain off-topic changes (use other PRs for other changes).
  • Applied any dependency changes to both setup.cfg (and conda-environment.yml if present).
  • Tests are included (or explain why tests are not needed).
  • Changelog entry included if this is a change that can affect users
  • Cylc-Doc pull request opened if required at cylc/cylc-doc/pull/XXXX.
  • If this is a bug fix, PR should be raised against the relevant ?.?.x branch.

@dwsutherland dwsutherland added this to the 1.8.2 milestone Dec 20, 2025
@dwsutherland dwsutherland self-assigned this Dec 20, 2025
@dwsutherland dwsutherland force-pushed the graphql-ws-tests branch 2 times, most recently from a0653ed to 8738722 Compare December 20, 2025 10:55
@dwsutherland dwsutherland changed the title add graphql websocket subsciption test add graphql websocket subsciption tests Dec 20, 2025
Copy link
Copy Markdown
Member

@oliver-sanders oliver-sanders left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, one question (see comment).

This pushes code coverage in the right direction 👍:

Screenshot from 2026-01-05 10-58-11

There are still a few gaps:

  • Would be good to cover error handling, e.g, GraphQL validation errors.
  • Would be good to cover more of the websocket subprocotocol (though note that we will change subprotocol soon)
  • Would be good to cover connection termination (as this is something we've had issues with).

Comment thread cylc/uiserver/graphql/tornado.py
Comment thread cylc/uiserver/graphql/tornado.py
Comment thread cylc/uiserver/tests/test_graphql.py Outdated
Comment on lines +341 to +356
response = json.loads(await ws.read_message())

assert response == {
'id': sub_id,
'type': 'data',
'payload': {
'data': {
'workflows': [
{
'id': '~me/foo',
'status': 'stopped'
}
]
}
}
}
Copy link
Copy Markdown
Member

@oliver-sanders oliver-sanders Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to test that the subscription can yield more than one result, i.e:

for _ in range(3):
    assert json.loads(await ws.read_message()) == { ...

    # poke UIS subscriptions as though something changed to make them yield again
    await do_something()

Not sure what do_something is though?

@oliver-sanders oliver-sanders modified the milestones: 1.8.2, 1.8.3 Jan 7, 2026
@dwsutherland
Copy link
Copy Markdown
Member Author

There are still a few gaps:

Yes, this is a work in process, I'll keep expanding the tests here (hopefully until 85% project coverage at min)

Comment thread cylc/uiserver/graphql/tornado.py
@dwsutherland dwsutherland force-pushed the graphql-ws-tests branch 4 times, most recently from 1627744 to 5710e81 Compare January 30, 2026 08:07
@oliver-sanders
Copy link
Copy Markdown
Member

oliver-sanders commented Jan 30, 2026

83% project coverage 🥇

Ping me when you want a review.

@dwsutherland
Copy link
Copy Markdown
Member Author

83% project coverage 🥇

Ping me when you want a review.

Just need to bump up the websocket/subscription side of things first.

@dwsutherland
Copy link
Copy Markdown
Member Author

dwsutherland commented Feb 17, 2026

@oliver-sanders - I think this will do for now, there's some more that could be done with the subscription handler.. But we might as well get this in as a first step for people to build on.

@oliver-sanders oliver-sanders changed the base branch from 1.8.x to master February 19, 2026 11:14
@oliver-sanders oliver-sanders modified the milestones: 1.8.4, 1.9.0 Feb 19, 2026
@oliver-sanders
Copy link
Copy Markdown
Member

(shifting this onto the 1.9.0 milestone as there are functional code changes)

Comment on lines +148 to +150
# test against same query different method
post_response = await gql_query(*('cylc', 'graphql'), query=query)
assert get_response.body == post_response.body
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this right?

Isn't GET for queries and POST for mutations?

Copy link
Copy Markdown
Member Author

@dwsutherland dwsutherland Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite right, a little more work to do here, it's not being caught by the thing that should catch it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it appears that the only restriction is mutations have to be POST, it's the same with Django:
https://github.com/graphql-python/graphene-django/blob/f02ea337a23df06d166d463d6f92cb7505fbb435/graphene_django/views.py#L322-L337

I do test this..

    # try a mutation with GET method
    response = await gql_query(
        *('cylc', 'graphql'),
        headers={'Content-Type': 'application/x-www-form-urlencoded'},
        query='''
            mutation {
                stop(workflows: ["*"]) {
                    result
                }
            }
        ''',
        method='GET',
        raise_error=False
    )
    # Should be rejected for incorrect method.
    assert response.code == 405
    assert response.reason == 'Method Not Allowed'

So GET or POST for queries, only POST for mutations.. The POST just means we package the query into the body instead of URL..

So, "nothing to see here.."

@oliver-sanders
Copy link
Copy Markdown
Member

(shifting this onto the 1.9.0 milestone as there are functional code changes)

FYI: We'll be looking to release 1.9.0 shortly once the new Cylc Review is in, would be great to merge this pre-release.

@oliver-sanders oliver-sanders modified the milestones: 1.9.0, 1.10.0 May 6, 2026
@dwsutherland dwsutherland modified the milestones: 1.10.0, 1.9.x May 14, 2026
@dwsutherland dwsutherland changed the base branch from master to 1.9.x May 14, 2026 04:50
@dwsutherland
Copy link
Copy Markdown
Member Author

dwsutherland commented May 14, 2026

(shifting this onto the 1.9.0 milestone as there are functional code changes)

FYI: We'll be looking to release 1.9.0 shortly once the new Cylc Review is in, would be great to merge this pre-release.

@oliver-sanders - This is ready to go.. (was a while ago but I thought the whole POST query would be more narly, turned out it was the norm amongst framework implementations of graphql)

(
seems coverage has dropped with Cylc Review and/or something else..
image

Maybe #797 and others ... But this PR does what it says and 100% on this patch... So follow-ons to get coverage up.
)

@oliver-sanders
Copy link
Copy Markdown
Member

(project coverage is just a target, you don't have to hit that number in this PR!)

@dwsutherland
Copy link
Copy Markdown
Member Author

dwsutherland commented May 14, 2026

(project coverage is just a target, you don't have to hit that number in this PR!)

Yeah, there is one thing annoying me though.. The async gens are not being served:
image

(which doesn't seem to make sense, because how can the stuff at the beginning of on_start not be covered if the try: is hit ??)

I think I need to try push the subscriptions a little further.. Like you mentioned:
#768 (comment)

So will try get that covered at least

@dwsutherland
Copy link
Copy Markdown
Member Author

Not sure what's with coverage, that block is hit:

    async def on_start(self, connection_context, op_id, params):
        # Attempt to unsubscribe first in case we already have a subscription
        # with this id.
        await connection_context.unsubscribe(op_id)

        print('HELLO')
        params['kwargs']['root_value'] = op_id
        execution_result = await self.execute(params)
        iterator = None
        try:
            if isawaitable(execution_result):
                execution_result = await execution_result
            if not hasattr(execution_result, '__aiter__'):
                await self.send_execution_result(
                    connection_context, op_id, execution_result)
            else:
                print('THERE')

then forcing an assert error shows it in the stdout:

$ pytest cylc/uiserver/tests/test_graphql.py::test_subscription
.
.
.
--------------------------------- Captured stdout call ---------------------------------
HELLO
THERE

🤷

@oliver-sanders
Copy link
Copy Markdown
Member

(re-running CI on fresh merge to see if coverage reports change)

@dwsutherland
Copy link
Copy Markdown
Member Author

(re-running CI on fresh merge to see if coverage reports change)

Looks like it's showing as covered now.. I don't want to be too finicky about tornado_ws.py with #821
in bound..

So good to go as is, then I can build on it.. i.e. with #597

@oliver-sanders
Copy link
Copy Markdown
Member

(Codecov sometimes jitters, I think test re-runs can cause it, a fresh run fixes it)

@oliver-sanders oliver-sanders modified the milestones: 1.9.x, 1.10.0 May 26, 2026
@oliver-sanders oliver-sanders changed the base branch from 1.9.x to master May 26, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants