-
Notifications
You must be signed in to change notification settings - Fork 8
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
separating startup and logic phases #1
Comments
I think something like that would be a great addition, for several reasons. The other reason to support something like this is to be able to see the code that deals with the client, and ignore the server. Your concrete idea would help with that a bit, but not entirely: if both the server and the client run in-process, the code between It would be great to be able to take it even further: having a way to isolate the coverage between client and server. I think the problem with both approaches is how to achieve it. Currently discover just parses the code coverage file; I think the best way to support this would be to take over the code rewriting that cmd/cover does. With that, we could collect much richer data which would allow discover to infer more about the code relationships. Looking at cmd/cover this does not look terribly difficult, but I have not yet figured out how to incorporate running tests with the rewritten code. If you have any ideas on how to accomplish that, that would be great. |
I've made a fair bit of progress on this. I think it should be workable. The current design I am thinking of is the following. Let me know what you think.
As far as implementation goes, it will be done by annotating source just like the cover package, but due to the additional tracing richness (conditional tracing and having different traces) it will be a bit slower than the test coverage. I think it will just end up being a constant per-function overhead (to do a one-time "trace id" lookup), and otherwise identical to -covermode=atomic. Thoughts? |
What (schematically) would that look like for a simple client-server thing (or something that has a setup phase and then does the actual test)? |
Something like this for net/http: func DiscoverHTTPGet(d *discover.D) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world")
}))
defer ts.Close()
d.Trace(func() {
res, err := http.Get(ts.URL)
if err != nil {
d.Fatal(err)
}
res.Body.Close()
})
} The output would be a trace showing only the code that gets run for the HTTP client, not any of the server code, even though both get executed concurrently. |
That seems really useful. For other tests where you're interacting with a client (potentially over the network) "snapshotting" should still be interesting. Say I'm doing a client request as in your example, but I'm really interested in what that request triggers on the server. Then maybe the API that I would want is: func DiscoverHTTPGet(d *discover.D) {
var ts http.Server // keep in scope for later teardown
// Trace the setup. This isn't collected, but `bg` captures the state so that we can diff on it.
bg := d.Trace(func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world")
})
}
// Execute the client request, but trace only the server portion. This should be achievable
// by internally creating a trace for the `func() {}` below (which gives the "isolated" code touched
// by the client until it hits the network) and removing that from all of the code touched while the
// code runs (relative to the code the init step touched).
bg.Trace(func() {
res, err := http.Get(ts.URL)
if err != nil {
d.Fatal(err)
}
res.Body.Close()
})
// More teardown here if desired
} Your (simpler) example above in this API would look the same. Some more options are presumably needed for more obscure tests, for example whether to halt the global clock (so that timers don't fire), but that doesn't seem important enough to consider, and there may always remain some amount of randomness (unless we also make the scheduler, RNG, etc deterministic) for these tests. That seems ok though. |
Interesting idea. I am not sure how easy it is to accomplish :). Your example seems to hint at exposing some kind of set operation API, where any piece of code that was executed was part of zero or more traces, and then having control of the output by reasoning about those sets. I wonder to what extent it's necessary though. The implementation I am working on links together any started goroutines with the trace that started them, which would enable the HTTP server code to be captured by your "bg" trace above (the code that runs It's possible to circumvent this by passing control to some background goroutine which was started for example in |
If I am right, this would trace the HTTP server part: func DiscoverHTTPServer(d *discover.D) {
var ts http.Server // keep in scope for later teardown
d.BGTrace(func() {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world")
})
}
defer ts.Close()
res, err := http.Get(ts.URL)
if err != nil {
d.Fatal(err)
}
res.Body.Close()
} Where |
How would that avoid mixing the initialization part of the server with server-side code tickled by the client request? That's ultimately what motivated the above. |
Yeah, I see what you mean. I am not sure there is a good way to avoid that. For atomicity I think the number of traces needs to be computable at "compile time", so I think it will be hard to enable having multiple different traces for a single The goal is ultimately to provide useful output, not perfect output. If we can get 80% or 90% of the way there, perhaps the little bit of initialization that will creep in sometimes can be disregarded or manually deleted if you want to use the output for illustration/teaching purposes. |
In the cases that I was thinking, you're looking at a pretty complicated system whose internals you may not have access to, so it'd be hard to manually delete the bootstrap code (because that's most of the code). Is it really that difficult to implement though? I was thinking to do something similar to what
package discover
var mu sync.RWMutex
func Trace(name string, f func()) {
mu.Lock()
defer mu.Unlock()
Active[name] = struct{}{} // should check for existence
defer func() { delete(Active, name) } // BGTrace would omit this
f()
}
mu.RLock()
for s := range Active {
// Do the stuff cmd/cover does for each counter, but key it.
// The variable GoCover would by a map[string]<type_of_original_GoCover>,
// with keys populated as they first occur.
}
mu.RUnlock() I think that this should be a fairly easy fork of |
Your proposed implementation is very similar to the one I have been working on. You can see the progress in the tracing branch here on GitHub, which implements most of the Like you said, I think the approach is quite flexible and with the ability to turn on and off coverage profiles at runtime we should be able to iterate on the API to come up with something useful. If I understand you correctly your initial example should indeed be pretty easy to implement ( |
Cool, glad to hear! I'll keep my eyes peeled for the prototype. Let me know if you need a review. |
I was playing with this on https://github.com/cockroachdb/cockroach (more concretely
(cd storage; discover test TestStoreSend)
). It works fine, but since these tests have to actually start a server, there's a lot of code being run that isn't actually "logic".I suspect that that's often going to be the case for complicated systems, and that the high level view is really where you'd want to use this tool most (as opposed to looking at a focused unit test, where reading the logic is easier).
So maybe this is crazy, but is there a way of setting "checkpoints" in tests that separate the setup- and logic phase? Something like
Maybe this could be achieved naively by running the test multiple times, panicking first at the first breakpoint, then at the second (and so on), and computing diffs from the accumulated coverage profiles?
The text was updated successfully, but these errors were encountered: