5

I've just read the following post and have tried to implement the approach described there:

Writing iOS acceptance tests using Kiwi - Being Agile

All the stuff described there does work perfectly. But! there is one thing that breaks determinism when I am running my acceptance tests.

Here is the repo on Github where author of the post pushed his experiments (it can be found on the bottom of the page in the comments): https://github.com/moredip/2012-Olympics-iOS--iPad-and-iPhone--source-code/tree/kiwi-acceptance-mk1

Consider this code he uses for tapping a view:

- (void) tapViewViaSelector:(NSString *)viewSelector{
    [UIAutomationBridge tapView:[self viewViaSelector:viewSelector]];
    sleepFor(0.1); //ugh
}

...where sleepFor has the following definition behind itself:

#define sleepFor(interval) (CFRunLoopRunInMode(kCFRunLoopDefaultMode, interval, false))

It is a naive attempt ('naive' is not about the author, but about the fact that it is the first thing that comes into a head) to wait for a tiny period of time until all the animations are processed and soak all the possible events that were(or could be) scheduled to a main run loop (see also this comment).

The problem is that this naive code does not work in a deterministic way. There are a bunches of UI interactions which cause fx next button tap to be pressed before the current edited textfield's keyboard is disappeared and so on...

If I just increase the time from 0.1 to fx 1 all the problems disappear, but this leads to that every single interaction like "fill in textfield with a text..." or "tap button with title..." become to cost One second!

So I don't mean just increasing a wait time here, but rather a way to make such artificial waits guarantee that I do can proceed my test case with a next step.

I hope that it should be a more reliable way to wait enough until all the stuff caused by current action (all the transitions/animations or whatever main run loop stuff) are done.

To summarize it all to be a question:

Is there a way to exhaust/drain/soak all the stuff scheduled to a main thread and its run loop to be sure that main thread is idle and its run loop is "empty"?

This was my initial solution:

// DON'T like it
static inline void runLoopIfNeeded() {
    // https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html

    while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);

    // DON'T like it
    if (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource) runLoopIfNeeded();
}
4

3 に答える 3

5

あなたはこれを試すことができます

while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource);

これは、実行ループに何もなくなるまで実行されます。0 が機能しない場合は、時間間隔を 0.1 に変更してみてください。

于 2013-06-07T02:28:37.657 に答える
4

スレッドに関連付けられた実行ループのステータスを確認し、個別のフェーズのコールバックを登録するには、CFRunLoopObserverRef. これにより、コールバックが呼び出されるタイミングを非常にきめ細かく制御できます。また、ハッキーなタイムアウトなどに依存する必要はありません。

そのように1つ追加できます(メインの実行ループに1つ追加していることに注意してください)

CFRunLoopObserverRef obs = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0 /* order */, handler);
CFRunLoopAddObserver([NSRunLoop mainRunLoop].getCFRunLoop, obs, kCFRunLoopCommonModes);
CFRelease(obs);

登録するアクティビティに応じて、ハンドラーが適切に呼び出されます。上記のサンプルでは、​​オブザーバーはすべてのアクティビティをリッスンします。おそらく必要なのはkCFRunLoopBeforeWaiting

ハンドラーは次のようになります

id handler = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry:
            // About to enter the processing loop. Happens
            // once per `CFRunLoopRun` or `CFRunLoopRunInMode` call
            break;
        case kCFRunLoopBeforeTimers:
        case kCFRunLoopBeforeSources:
            // Happens before timers or sources are about to be handled
            break;
        case kCFRunLoopBeforeWaiting:
            // All timers and sources are handled and loop is about to go
            // to sleep. This is most likely what you are looking for :)
            break;
        case kCFRunLoopAfterWaiting:
            // About to process a timer or source
            break;
        case kCFRunLoopExit:
            // The `CFRunLoopRun` or `CFRunLoopRunInMode` call is about to
            // return
            break;
    }
};
于 2014-10-24T03:53:48.717 に答える
1

これが私の現在の解決策です。誰も私が間違っていると言わなかったり、最初により良い答えを提案したりしない場合は、少し後でコードにコメントと説明を追加します。

// It is much better, than it was, but still unsure
static inline void runLoopIfNeeded() {
    // https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html

    __block BOOL flag = NO;

    // http://stackoverflow.com/questions/7356820/specify-to-call-someting-when-main-thread-is-idle
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            flag = YES;
        });
    });

    while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);

    if (flag == NO) runLoopIfNeeded();
}

今のところ、これをより効果的にする方法については考えがありません。

于 2013-06-07T02:55:08.947 に答える