-
Notifications
You must be signed in to change notification settings - Fork 94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: integration testing battery #3616
Conversation
After a quick skim through (all I have time for today 😬 ) - sounds brilliant 👍 |
It seems sane as a concept and the examples look reasonable. I'm not sure I could dive in and use it to write a test of the scheduler straight out, although it'd be fun to try. Would you like me to give that a go? |
Not quite ready for general use yet, the interface may well change, need to get logging working, etc. |
I've managed to get to the point where It seems robust and I'm confident it can handle the existing use cases. Working my way through the existing unit tests and translating them across. For me a simple test takes less than 2 seconds, using pytest scoping for sharing objects between test functions that's approx 2 seconds for a small battery of tests. |
4051e56
to
1617ac2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this is now a working system, not quite ready for review but good enough for discussion. I've converted two of the unittests over as a POC.
Here are some examples of the sort of thing that is now possible.
import logging
from pathlib import Path
import pytest
# no more gripping the log file, log tuples can be obtained returned by run_flow()
@pytest.mark.asyncio
async def test_cylc_version(flow, run_flow, simple_conf):
"""Ensure the flow logs the cylc version 8.0a1."""
scheduler = flow(simple_conf)
async with run_flow(scheduler) as log:
assert (
('cylc', logging.INFO, 'Cylc version: 8.0a1')
in log.record_tuples
)
# command line options can be provided to flow() using their "dest" names
@pytest.mark.asyncio
async def test_hold_start(flow, run_flow, simple_conf):
"""Ensure the flow starts in held mode when run with hold_start=True."""
scheduler = flow(simple_conf, hold_start=True)
async with run_flow(scheduler):
assert scheduler.paused()
# when the flow stops the scheduler object is still there for us to poke
@pytest.mark.asyncio
async def test_shutdown(flow, run_flow, simple_conf):
"""Ensure the server shutsdown with the flow."""
scheduler = flow(simple_conf)
async with run_flow(scheduler):
await scheduler.shutdown('because i said so')
assert scheduler.server.socket.closed
# you don't have to run suites, infact we should avoid it when possible
@pytest.mark.asyncio
async def test_install(flow, run_flow, simple_conf, run_dir):
"""Ensure the flow starts in held mode when run with hold_start=True."""
scheduler = flow(simple_conf)
jobscript = Path(run_dir, scheduler.suite, '.service', 'etc', 'job.sh')
assert jobscript.exists()
And the best bit:
[gw0] [ 25%] PASSED itests/test_foo.py::test_cylc_version
itests/test_foo.py::test_hold_start
[gw0] [ 50%] PASSED itests/test_foo.py::test_hold_start
itests/test_foo.py::test_shutdown
[gw0] [ 75%] PASSED itests/test_foo.py::test_shutdown
itests/test_foo.py::test_install
[gw0] [100%] PASSED itests/test_foo.py::test_install
itests/__init__.py
Outdated
success = True | ||
contact = (run_dir / scheduler.suite / '.service' / 'contact') | ||
try: | ||
asyncio.get_event_loop().create_task(scheduler.start()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can now import cylc.flow.scheduler.Scheduler
, initiate and run it from Python, all you need to do is to call Scheduler.start
from an event loop. You can start multiple schedulers in the same event loop.
itests/__init__.py
Outdated
success = False | ||
raise exc from None # raise the exception so the test fails | ||
finally: | ||
await scheduler.shutdown(SchedulerStop(StopMode.AUTO.value)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All schedulers start in a single process which makes tidying up afterwards a lot easier, just call the shutdown method.
itests/conftest.py
Outdated
|
||
|
||
@pytest.fixture | ||
def run_flow(run_dir): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The basic fixtures are:
make_flow
- writes thesuite.rc
to disk.make_scheduler
- initiates theScheduler
.flow
- shorthand formake_scheduler(make_flow)
because I'm lazy.run_flow
- callsScheduler.start
.
There are mod_*
and ses_*
variants of each of these to allow you to create Pytest fixtures with module or session level scoping. This allows us to run a workflow once and use it in multiple tests for efficiency reasons.
This is parallel safe!
Pytest has been configured to run tests from the same module together, so all the tests in a module can share the same scheduler, no problem.
caveat the exception is you can't run workflows in the session scope, but that would be crazy anyway.
itests/test_client.py
Outdated
@pytest.mark.asyncio | ||
async def test_ping(flow_a_w_client): | ||
"""It should return True if running.""" | ||
scheduler, client = flow_a_w_client | ||
assert await client.async_request('ping_suite') | ||
assert not client.socket.closed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a cylc ping
example, the bash equivalent would be:
set_tests 2
mkdir "$HOME/cylc-run/$REG" -p
cat > "$HOME/cylc-run/$REG" <<<__SUITERC__
[scheduling
[[dependencies]]
graph = foo
__SUITERC__
run_ok cylc run "$REG"
_poll_suite_started
run_ok cylc ping "$REG"
cylc stop "$REG"
_poll_suite_stopped
purge_suite
exit
Other thoughts:
|
I'm already overwhelmed by the existing great ideas here 😁 |
3d2b2f2
to
671930d
Compare
* move cli stuff into the cli * move functional stuff out of the cli * add an interface for creating scheduler options objects * tidy the --format argument * move daemonise logic to scheudler_cli * move event loop logic to scheduler_cli * move logging into scheduler_cli * move start message to scheduler_cli * store id as top-level attr
aaba326
to
85397a6
Compare
955724a
to
6a0be64
Compare
* use the new integration battery instead
6a0be64
to
58e08ec
Compare
58e08ec
to
fc04f2d
Compare
No one screamed loud enough so I'm going to close this proposal and re-raise as a PR. |
tldr
Here is a POC Python test framework which could be used to re-implement our flaky unit tests which run workflows (an alternative option to #3611 which opens more doors).
This writeup was probably more work than the code so I'm happy to drop it if not desired. Might cherry pick a few commits onto master if dropped.
The Testing Situation
At present we have two test batteries:
tests/
) written inbash
and run withprove
.cylc/flow/tests
) written inpython
and run withpytest
.We are missing a layer.
(note our functional tests are muddled in with some unit tests and quite a lot of integration tests).
CylcWorkflowTestCase
In the unit tests we have a framework called
CylcWorkflowTestCase
, a number of tests are already implemented using it:These aren't unit tests, they are really more like integration tests. They have been implemented as unittests because it is more convenient to test Python from Python.
Unfortunately running Cylc from Python is not really possible at the moment, consequently
CylcWorkflowTestCase
works by mocking something that looks like a scheduler and mounting the necessary parts onto it. This makes the tests very complex to write and fragile to changes to the scheduler.Here's an excerpt from
cylc/flow/tests/network/test_client.py
so you can see what I'm going on about:There are 100 lines of boilerplate but all the test actually wants to do is to call a ZMQ endpoint.
These tests would be a lot simpler if written in bash (see #3611) but there is more to it than that...
Testing And Python
I think there is a strong argument for running cylc tests in Python, a few quick examples:
Unfortunately
CylcWorkflowTestCase
does not yet have the benefit of the same evolution as the functional test harness, for example it uses the user/site configuration rather than the test configuration and doesn't tidy up afterwards.A New Framework
By making some small changes to the Scheduler, namely moving command-line logic back into the command line we can make it possible to run suites by invoking the
Scheduler
directly.This makes it possible to write a Python test harness for workflows without re-inventing the wheel.
This PR shows a proof of concept of how that can be done.
A New Battery?
We could just bung this code into the existing unit tests, however, I'm keen to keep integration tests (ones that run suites) out of the unit tests:
cylc run
, but we might still want to run the unit tests.tests/
makes it hard to flush out the simple errors.This PR puts the integration test framework in its own directory, this forces us to maintain a distinction between unit and functional tests. (And yes the integration framework is unit tested but "A Foolish Consistency is the Hobgoblin of Little Minds".)
This POC
itests/__init__
)Going Forward
This PR isn't ready to roll, there are a few major issues:
And some minor features yet to be implemented:
no_detach=False
Oh, and by the way, the tests in this PR take ~2s each, which includes suite installation, execution and shutdown so this could be quicker to run than the bash tests :)
Questions
@cylc/core
Makes it easy to strip them from distribution.
Makes more sense if we manage to get the functional tests running under pytest too.
Requirements check-list
CONTRIBUTING.md
and added my name as a Code Contributor.