-
Notifications
You must be signed in to change notification settings - Fork 97
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
CDP replacement candidate: direct python API for BiDi #934
Conversation
@jelly @allisonkarlitskaya At this point it seems pretty clear to me that this approach is much better than #926 (where all of the required JS → Python glue isn't even written, it uses the outdated webdriver protocol, and requires umpteen new dependencies, and it's also slower). This is demo code, so no need for a minutely review, but I'd like to hear your coarse-grained opinion about the approach. Thanks! |
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 very cool piece of work.
I would prefer to see more separation between driver/session/context. It might be useful to think about sharing these things (but my experiments also show problems there, particularly with respect to each fresh pytest getting its own new asyncio event loop). So maybe the added complication is not helpful there.
I think the next thing I'd like to see is a version of this integrated into test/common/
in the cockpit repo....
asyncio.run_coroutine_threadsafe(self.driver.start_session(), self.loop).result() | ||
|
||
def close(self): | ||
asyncio.run_coroutine_threadsafe(self.driver.close(), self.loop).result() |
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 run_coroutine_threadsafe()
approach is very cool. Much better than the custom-built machinery I did in my version of this, and probably more efficient, too.
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.
Thanks! I'm surprised that it surprised you 😉
driver: bidi.WebdriverBidi | ||
|
||
@staticmethod | ||
def asyncio_loop_thread(loop: asyncio.AbstractEventLoop) -> None: |
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.
I generally like the approach of binding the bidi driver and the asyncio loop thread directly to the Browser
object. I think that's probably right. Once we start interacting with the websocket in the asyncio loop running in that thread, we really ought not to touch it from any other context.
We can dream about sharing the session some day, but for now, this seems right.
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.
Wrt. sharing the session: I share the sentiment. Starting up chromium is lighting fast, but starting firefox takes ~ 5 seconds annoyingly. It would be cool to leave the process running and just instantiate sessions. I didn't test how isolated they are from one another though -- standard "tab" isolation isn't enough (we need separate cookie/localStorage for each tab), but if it's more like "private window" that'd be great.
But that isn't a regression -- our current CDP library also starts firefox with each Browser
instance, so for now I'd like to keep that in the backlog. This project is huge enough even without doing additional structural changes at the same time 😅
However, for your pytest work the picture is different -- you don't nearly need so much machinery there (no frame tracking, cookies, mouse clicks, all the testlib API, no firefox, no coverage, etc.). While we can certainly look at uniting the bidi driver stuff at some point, it may be easier to just keep it separate for the first version. WDYT?
args: list[object] | ||
text: str | ||
|
||
def __init__(self, message_params): |
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.
How do you feel about the cockpit json helpers? I notice you have your own JsonObject
defined above, and indeed, we can't import jsonutil
from starter-kit
. I wonder if we should move those helpers to a more neutral location where they can also get used by tests.
It's a bit of a weird situation. We might end up with two copies... or maybe we don't have to care about typing too much here.
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.
Heh yes, had the same "wanna but can't" feeling. But TBH we just need it at exactly one place, I feel it's probably not worth sharing bridge with testlib code for that. Maybe if we run into more of these, but I don't see that coming -- the protocol side is basically "done", the remaining work is all on the "outwards" testlib side.
if data["type"] == "event": | ||
if data["method"] == "log.entryAdded": | ||
log = LogMessage(data["params"]) | ||
self.logs.append(log) |
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.
I think this isn't useful enough for qunit tests. This needs to be at least an async queue or something else that can be awaited.
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.
Ah, sure -- cf. my question above with "does it already make sense at this point to use that exact API for the unit tests"? Do you have a pointer for what a unit test is trying to do that relies on waiting for log messages? There could certainly be one method for "set up a log watch future", and resolve it here if it's set up; is that enough?
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.
See ./test/common/tap-cdp and cdp.read_log()
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.
Right, that's the plan. This is strictly "draft", mostly because it's a convenient place for a demo, and we wanted to compare different approaches. |
8d9eca3
to
0f31b7f
Compare
Unfortunately the "interesting" commands like "click" don't have a BiDi command, and need classic webdriver.
That's what we do in CDP as well
Re-eliminate geckodriver. Turns out we can do everything through BiDi.
Thanks Lis for the idea!
Too small window sizes make the test unstable. Now it works reliably in a loop.
218f7fa
to
ff3cb22
Compare
This makes waiting for text robust. script.addPreloadScript() doesn't export the declared functions, so we need to attach them to `window`. We also don't need all of them.
This is all sync code, with sketch of what the updated Browser class looks like.
This better lives in the sync Browser class.
This works fine with Firefox, and conforms to the spec. However, Chromium gets confused and clicks on the wrong position. Work around that for now by keeping our old `ph_mouse()` event synthesizer for Chromium.
Use the same `document` tracking fix/hack as in our CDP driver.
Wow, it's very straightforward to talk to CDP. chromedriver already enables CDP and gives you the address in the returned capabilities. And looks like not only the bidi sessions/tabs appear there, they even have the exact same session IDs as bidi/webdriver:
Unfortunately,
That's pretty much exactly what I am currently trying to only connect to the CDP websocket when necessary, it seems a big waste to let it run all the time -- then we have to read all the messages and discard them sigh. But that's my next attempt, |
This is not accessible via BiDi or webdriver, but fortunately the CDP and BiDi sessions are compatible.
Look ma, CDP Profiler (aka coverage) support! That was my final TODO item that I could think of. Now I think it's time to aim higher and put it into actual cockpit. |
This was just a demo and has fulfilled its purpose. cockpit-project/cockpit#20832 is Ze Real Zing. |
This is another candidate for https://issues.redhat.com/browse/COCKPIT-1139 , in the similar spirit as #926
This talks to https://w3c.github.io/webdriver-bidi/ directly, requires zero new npm dependencies (in fact, we can drop chrome-remote-interface), and only a new package (
chromedriver
) in the tasks container (installed locally for this demoadded to official container now).This is much further ahead than #926, in the sense of that it actually is a Python test, in our usual testlib shape. We won't need any extra
node
process to talk to a JS webdriver module any more, and they aren't any good ones for BiDi anyway.Compared to CDP this has the following advantages:
{firefox,chromium}-cdp-driver.js
forksMouseEvent
, while this actually goes through the browser.Hiccups:
input.performActions
does not understand key names like "Return" or "ArrowUp". input.Key{Down,Up}Actionvalue
is undefined w3c/webdriver-bidi#757https://issues.redhat.com/browse/COCKPIT-1154