How do you test forever blocking routines?

xuanbao · · 564 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>Hi!</p> <p><strong>EDIT</strong>: Thank you everyone for the massive help you guys provided! I agree with all of you that I should rather try and change the code than shoehorn a test to it. It is the right approach to this problem. </p> <p>So, I have a code that I&#39;m writing a test for, which has a go routine call in it, that blocks forever. This code is not something I&#39;ve written, it&#39;s an outside function I have no control off. I&#39;m just writing a test for it at the moment. So altering that code is right now, out of the question.</p> <p>Something like this: </p> <pre><code>func TheBlockingRoutine() { var wg sync.WaitGroup wg.Add(1) ... // do some stuff here... go serve(port) ...// do some more stuff here... wg.Wait() return nil } </code></pre> <p>So it never does wg.Done() at any point, so it will block. And this is a function I can&#39;t alter at this time.</p> <p>I&#39;ve come up with something like this, hoping that it would kill the encoposing routine:</p> <pre><code> go launchRoutine(stop) defer stopRoutine(stop) ... // do some testing here... </code></pre> <p>And the launch and stop are just like this: </p> <pre><code>func stopRoutine(stop chan bool) { stop &lt;- true } func launchRoutine(stop chan bool) { go TheBlockingRoutine(53765) &lt;-stop } </code></pre> <p>But this, if I run it with go test -race, still causes a race condition warning. Idealy I would alter the source to make it testable, but that&#39;s not an option right now. I know killing a go routine is not possible, but I thought I could somehow encapsulate it. </p> <p>Note: I don&#39;t have access to the Wait Group. That&#39;s defined in the blocking function. So I can&#39;t call wg.Done() from the test.</p> <p>Any ideas on how to proceed with this? I would much appreciate it!! </p> <p>Thank you!</p> <hr/>**评论:**<br/><br/>earthboundkid: <pre><p>A couple of things:</p> <ul> <li>The race detector finds unsynchronized memory access, not goroutine scheduling problems.</li> <li>You need to call wg.Done() somewhere. If you can&#39;t modify the function you&#39;re waiting on, wrap it in a closure and call it there. </li> <li>Research done channels. I&#39;m on my phone but I can add a link later. </li> </ul></pre>skarlso: <pre><p>That&#39;s awesome, the wrapping is what I was thinking, but my test is in another castle. I mean, another file. :) So I can&#39;t access the wg that its using internaly. So, calling wg.Done() in a wrapper function would not work, no? That sounds like something I would like to do, but how do you suppose I could do it? </p> <p>Wg is declared inside the function which is executing the routine. Not outside.</p> <p>And yes, the race occurs, because that go routine inside is still running after the test is finished and it&#39;s using a map which then the next test is trying to use. I could go on and use a lock on the map in the test, but that wouldn&#39;t help to other routine which is using it without shielding. That&#39;s another poor implementation in there. </p></pre>earthboundkid: <pre><p>Change your code to something like this:</p> <pre><code>wg.Add(1) go func() { serve(port) wg.Done() }() wg.Wait() return nil </code></pre></pre>skarlso: <pre><p>That is the code IN the blocking function. :) Not an option to change. :)</p></pre>earthboundkid: <pre><p>It&#39;s not clear to me what your constraints are. Do you control <code>wg</code> or not? If you control <code>wg</code>, your responsibility is to call <code>Done</code> after you finish doing whatever else. If you&#39;re not controlling <code>wg</code>, then whoever creates it should have had some plan for when <code>Done</code> will be called.</p></pre>skarlso: <pre><p>I edited the question to clarify this. I do NOT control the wg unfortunately.</p> <p>Yeah, I came to the same concolusion. :( I can&#39;t test this without creating a race condition. :(</p></pre>earthboundkid: <pre><p>If the blocking routine really doesn&#39;t have a plan for how to call <code>Done</code>, it&#39;s poorly written and you should probably just fork the code and rewrite it. Don&#39;t fix the test; fix the code! :-)</p></pre>skarlso: <pre><p>Yepp. I agree with you on this. :) Thanks. :)</p></pre>intermernet: <pre><p>Is this a collaborative coding effort? The person who created the code that runs in the goroutine needs to provide some way for it to exit cleanly.</p> <p>If the inability to modify the goroutine code is due to it being in a 3rd party package, can you give us some details of which package so we can perhaps provide better advice?</p> <p>If you don&#39;t have access to the <code>wg</code>, you can&#39;t call <code>Done</code> on it and the goroutine will always block.</p></pre>sethammons: <pre><p>Without more details, it is hard to give a suggestion. My first two thoughts are: (1) Don&#39;t test other&#39;s code, test yours. Maybe you can wrap their code in an interface, and provide a testable interface into your tests (think fake/mock). Or (2), fork the offending code and make it more friendly. Without more information, I&#39;m leaning towards (1). Cheers!</p></pre>anacrolix: <pre><p>If the test never returns it must be working. Lol</p></pre>TheMerovius: <pre><blockquote> <p>Any ideas on how to proceed with this? I would much appreciate it!!</p> </blockquote> <p>The correct answer is: Change the code. Fork it, if you must, but you have read-access to it, so you <em>can</em> definitely create a testable version of it.</p> <p>The ugly answer is: Use process-isolation. Set an environment variable that indicates which test you want to run and use <a href="http://godoc.org/os/exec" rel="nofollow">os/exec</a> to execute the test-binary. Something like this (untested):</p> <pre><code>func TestFrobnicate(t *testing.T) { switch os.GetEnv(&#34;TEST_FROBNICATE&#34;) { case &#34;&#34;: cmd := exec.Command(os.Args[0], os.Args[1:]...) cmd.Env = append(os.Environ(), &#34;TEST_FROBNICATE=1&#34;) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if _, err := cmd.Run(); err != nil { t.FailNow(); } return case &#34;1&#34;: break default: return } // do your tests here } </code></pre> <p>There is probably quite a bit of ugliness around here, that you need to work around. But, as I said, the correct solution is to <em>change that code and make it testable</em>.</p> <p>[edit] meh, that obviously doesn&#39;t work like that. You can&#39;t properly decide between tests like this. Lemme try again:</p> <pre><code>func TestFrobnicate(t *testing.T) { switch os.GetEnv(&#34;TEST_FUNCTION&#34;) { case &#34;&#34;: cmd := exec.Command(os.Args[0], os.Args[1:]...) cmd.Env = append(os.Environ(), &#34;TEST_FUNCTION=Frobnicate&#34;) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if _, err := cmd.Run(); err != nil { t.FailNow(); } return case &#34;Frobnicate&#34;: break default: return } // do your tests here } </code></pre></pre>skarlso: <pre><p>Wow dude! That&#39;s some amazing stuff right there! :-D I shall save this for later research, however I absolutely agree with you that I should rather change the code. </p></pre>izuriel: <pre><p>My questions for you: Why can&#39;t you modify this? If it&#39;s pet of someone else&#39;s library then really it&#39;s not in the scope of the test. What are you trying to test? Perhaps isolating and testing each small piece individually outside of that function is your best route. It looks like a lot is happening there. Tests are supposed to cover smaller isolated pieces. If those pieces encompass a lot then the parts should be tested individually. In this case your saying it&#39;s impossible to test this function so obviously test its parts then. </p></pre>skarlso: <pre><p>I agree with you dude. I didn&#39;t want to change it because how things look right now, it would be a major refactor. And it&#39;s huge. But I agree with everyone here, that I should fix the code rather than shoehorn a test into it. </p></pre>intermernet: <pre><p>I think you want to have the goroutines exit when they receive a message on a &#34;done&#34; channel (as mentioned by <a href="/u/earthboundkid" rel="nofollow">/u/earthboundkid</a> ). Have a look at <a href="https://blog.golang.org/pipelines" rel="nofollow">https://blog.golang.org/pipelines</a> , specifically the bit about &#34;Explicit cancellation&#34;.</p> <blockquote> <p>When main decides to exit without receiving all the values from out, it must tell the goroutines in the upstream stages to abandon the values they&#39;re trying it send. It does so by sending values on a channel called done.</p> </blockquote> <p>...</p> <blockquote> <p>The sending goroutines replace their send operation with a select statement that proceeds either when the send on out happens or when they receive a value from done. The value type of done is the empty struct because the value doesn&#39;t matter: it is the receive event that indicates the send on out should be abandoned.</p> </blockquote></pre>skarlso: <pre><p>Sorry, but nope. That&#39;s not what I&#39;m looking for. :) But thanks. </p> <p>I know how to cancel, close, discrad routines when you have control over them. But this is testing a foreign code where there is a routine which does not finish. Ever. And on top of that, you don&#39;t have access to the Wait Group it uses, so you can&#39;t call done arbiterarly unless you do some reflection magic, which I do not want to fall back on. :)</p> <p>Though I&#39;m starting to think that&#39;s plainly not possible to do so, and that I should wonder on.</p></pre>

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

564 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传