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
introduce and encourage use of an interface that is more easily testable #1159
Comments
Okay, this sounds good. I agree with your "drawbacks" at the top. I also agree that while there are unit-tests in Autobahn they tend to be of the second variety (and thus lean on There also isn't a great story for users of Autobahn: how should they test their websocket-using application? So, I'm certainly curious to see what a better approach looks like. What would an |
I think that's right... I had imagine an "open" which I bet does exactly what you are imagining for "connect". I am a rank WebSocket newb though, so input from folks who are more familiar with its capabilities would be really great. I think |
Also, yea, I guess I should be more clear about my perspective: I am thinking about all of this as a developer writing an application with Autobahn. For development of Autobahn I think you would certainly want and need more testing strategies than just this one. |
Yes, that sounds approximately correct. The way Autobahn is split up is: there's "framework agnostic" code, where we try to make most stuff live and "framework specific" code (twisted vs asyncio), the details of which are MOSTLY hidden behind
Yes, so this is very interesting to me. We HAVE gone down this path with the WAMP stuff, taking a "listener" approach to many of these things. There's also a decorator-based approach behind Bringing a similar approach down to WebSocket too isn't conceptually that hard at this point. This would look like: replace a method like ...but that aside, I do like the idea of (at some point) extending this approach. In WAMP-land, the important thing is the Session -- in WebSocket land the important thing is a Protocol instance. So that would point to For the "options we might want to give to connect" I think those would have to be part of the agent or So, the life-cycle would look like this:
If we mimicked the This might look something like this, then:
Needs some more thought probably ;) |
Yes, absolutely. However, I think if we have a good story for users of Autobahn, we can improve our own unit-tests using similar machinery :) |
yep, a WebSocket opening handshake always begins with a HTTP/GET and Upgrade header - POST/PUT/DELETE/WHATEVER is not valid. also: the requested URL in the HTTP/GET can contain query parameters, but must not contain a fragment identifier (AB server is checking that) for completeness: the WebSocket spec in principle allows a client to talk regular HTTP (all methods) first, and then (without closing the connection) can once upgrade to WebSocket - but cannot go back to the former (doing regular requests). Autobahn does not implement that ... and I am not aware of any library or server that does. But the spec has it. Not sure if it's in the text explicitly, but it was discuess on the IETF WG - and I could dig out the posts .. though I'd prefer not to, since the WG mailing list history is huge;) rgd unit testing: I definitely agree that the unit tests (at the websocket level) in AB could be improved, and I am curious to learn here. rgd functional testing at the websocket protocol level: guess it's no news that Autobahn pioneered the so-called "Autobahn testsuite" which has >600 fuzzing tests - but this is different from unit tests. It tests blackbox via network and is trying to fuzz the testee with various invalid stuff. https://github.com/crossbario/autobahn-testsuite/ the testsuite itself is using a version pinned AB and twisted we don't currently have that fuzzing tests automated in CI here in AB - which we should, but never found time (we run it "occasionally sometimes" .. which isn't the right way of course) another "defect": the testsuite has over 600 tests, that really are I would claim quite exhaustive rgd coverage of websocket protocol spec compliance - BUT: the tests are only for anything after the initial websocket opening handshake has finished (we never had time to add tests for that .. and that would probably been a rabbit hole, since eg testing only the HTTP request line in the same rigorosity as the testsuite does for the actual websocket protocol (after the opening HS) would be quite some work. HTTP is surprisingly complex when magnified to the finest detail level .. |
A possible answer to this less-clearness could be that the I'm not super clear on when the current I wonder about the other one-off events not just being futures. What's the reasoning behind |
Hrmm, I might have been getting confused by server vs client code. It looks like the client I do think the most-clear approach is to pass in any "options needed for the handshake" instead of "something gets them via a callback, at some point". I'm not sure if we need two methods, though? Could we just pass in connections options to |
Well, you'd at least need |
Oh, I remember another reason: if you want to be able to use the API from |
Maybe different APIs for basically different things (one-time vs n-time) is okay? Also, a direction to explore for message delivery might be tubes (serious? or not? I don't know. 😄) Anyhow, I don't feel too strongly about this. I think anything that gives you composability without subclassing is already going to be a huge step forward. Also, I guess it's all wandering a little far from the original topic. |
So, I think I'm proposing: |
Well, I think if we're proposing "a new thing that sets up websocket connections" this is all relevant. We don't have to implement it all right now ;) I don't see Autobahn growing |
Okay, so lets see. If you want to run some code "when the websocket closes", one re-factoring of the above example would look like this:
However, that example doesn't include re-connections. If this "agent" just doesn't handle re-connections (i.e. something higher-level does) then the above looks pretty good. It might make sense, then, to have a A "higher-level thing that does re-connections" could provide a listener-based API instead (because "connected" and "closed" would happen multiple times in that case). I guess the question: is it an advantage to have the same API in the "higher level thing" and the protocol, or not? |
Oh, haha, there's already |
So I guess approximately (from Twisted perspective):
Note: "loop" can be reactor, or event-loop depending on which framework we're using |
rgd For servers, this is called during the opening handshake (from the HTTP/GET the client sent) with an instance of
Then, when the request comes back to the client, For servers, For clients, The state machine for the WebSocket protocol state goes like this:
Thus, there is also a closing handshake. A WebSocket peer (a server or a client) can send "close" (websocket level message). The other peer must reply with close to that (it cannot continue regular websocket messaging). Finally, a server must then drop the connection. Hence, we have 2 timeouts for the closing handshake:
|
Ah, okay so maybe (based on @oberstet's comment above) the lifecycle should be:
|
I guess practically speaking I'd want to encourage |
I'm not sure I completely understand this message. Maybe that means the rest of this comment is tilting at windmills... Nevertheless...
I think you want to group all of the "here's how to connect" stuff into agent state and "here's what to do with the connection" somewhere else. So, the reactor/loop is part of agent state. All message handling state is part of some per-connection thing (eg a protocol). I agree you don't want to accidentally miss any messages but I'm think there are lots of ways to deal with this. a. Guarantee that no messages can be delivered until after the loop regains control after the open() future has a result. This means a pattern like:
is perfectly safe. Implementation-wise, this might mean a buffer but it also just might mean taking care to dispatch the opened-and-handshaked-and-stuff event before delivering messages, which is pretty much naturally what the implementation would be, I would think (can't parse and dispatch messages if you haven't figured out the handshake succeeded, can you?). b. Just buffer up some messages until the first call to Alternatively, support an api like:
and you're definitely guaranteed to have the callback before any messages arrive. So, adapting your interface proposal:
Or, this but with an With |
Okay, yeah this makes sense I think -- I was taking the "this looks like this other WAMP thing" analogy too far in my mind. So, I'm convinced that I think your first pattern makes sense, and will work correctly if we say "a message cannot arrive until we do
It might be that we'd need to insert a |
@exarkun Added a "proof-of-concept" branch which I think represents what's been discussed up to here |
Was this closed by #1186 or are there residual improvements to be implemented? |
I've had some difficulty writing good unit tests for WebSocketClientProtocol-based implementations. The tools available seem to encourage one of two paths:
The first of these options has the drawback that there's no guarantee the tests will actually drive the application code correctly (and even if it is initially correct the chances of it diverging from reality increase as Autobahn is maintained and changed).
The second of these options has the drawback that it is prone to many different kinds of spurious failure (most code that takes this approach doesn't even manage to get the part where you start listening on a port correct 😢 ). It also draws in a lot more code and so failures can become harder to spot. Failures can also end up being caused by other code that's not the main focus of the test. It's also a lot slower/more expensive as a result of running so much more code.
I think it would be very useful if Autobahn itself offered and encouraged the use of a higher-level interface that allows details about connections/sockets/networks to be contained in an object that can easily be substituted for a testing fake. A model for this which appears to be working well is the regular HTTP client interface in Twisted,
twisted.web.client.IAgent
. This has one primary network-enabled implementation in Twisted,twisted.web.client.Agent
and the third-party(-ish) treq library provides a test-focused implementation,treq.testing.RequestTraversalAgent
.Application code written against
IAgent
can accept an implementation and do what it needs to do. Real-world use of this application code passes in anAgent
instance (or possibly a wrapper but that's not directly relevant here). Test use of this same application code passes in a correctly initializedRequestTraversalAgent
. The application code doesn't know or care which implementation it gets.Agent
goes off and makes real requests over a real network.RequestTraversalAgent
decomposes the request and serves up a response to it using atwisted.web.resource.IResource
provider - with no network, only Python method calls and correct copying of bytes from one place to another in memory.The text was updated successfully, but these errors were encountered: