简体   繁体   中英

How to use/test NSProgress userInfo changes of a child NSProgress instance

I'm implementing NSProgress support in a library, and I wrote some unit tests to test that everything's working correctly. While ideally I'd like to be able to pass some additional metadata ( userInfo keys not used by NSProgress itself, but for users of my API to consume), for now I'm just trying to get localizedDescription and localizedAdditionalDescription to work like the documentation says they should. Since the method I'm testing extracts files from an archive, I set the kind to NSProgressKindFile and set the various keys associated with file operations (eg NSProgressFileCompletedCountKey ).

I expect when I observe changes to localizedDescription with KVO, that I'll see updates like this:

Processing “Test File A.txt”

Processing “Test File B.jpg”

Processing “Test File C.m4a”

When I stop at a breakpoint and po the localizedDescription on the worker NSProgress instance ( childProgress below), that is in fact what I see. But when my tests run, all they see is the following, implying it's not seeing any of the userInfo keys I set:

0% completed

0% completed

53% completed

100% completed

100% completed

It looks like the userInfo keys I set on a child NSProgress instance are not getting passed on to its parent, even though fractionCompleted does. Am I doing something wrong?

I give some abstract code snippets below, but you can also download the commit with these changes from GitHub . If you'd like to reproduce this behavior, run the -[ProgressReportingTests testProgressReporting_ExtractFiles_Description] and -[ProgressReportingTests testProgressReporting_ExtractFiles_AdditionalDescription] test cases.

In my test case class:

static void *ProgressContext = &ProgressContext;

...

- (void)testProgressReporting {
    NSProgress *parentProgress = [NSProgress progressWithTotalUnitCount:1];
    [parentProgress becomeCurrentWithPendingUnitCount:1];

    [parentProgress addObserver:self
                     forKeyPath:NSStringFromSelector(@selector(localizedDescription))
                        options:NSKeyValueObservingOptionInitial
                        context:ProgressContext];

    MyAPIClass *apiObject = // initialize
    [apiObject doLongRunningThing];

    [parentProgress resignCurrent];
    [parentProgress removeObserver:self
                        forKeyPath:NSStringFromSelector(@selector(localizedDescription))];
}


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                       context:(void *)context
{
    if (context == ProgressContext) {
        // Should refer to parentProgress from above
        NSProgress *notificationProgress = object;

        [self.descriptionArray addObject:notificationProgress.localizedDescription];
    }
}

Then, in my class under test:

- (void) doLongRunningThing {
    ...
    NSProgress *childProgress = [NSProgress progressWithTotalUnitCount:/* bytes calculated above */];
    progress.kind = NSProgressKindFile;
    [childProgress setUserInfoObject:@0
                              forKey:NSProgressFileCompletedCountKey];
    [childProgress setUserInfoObject:@(/*array count from above*/)
                              forKey:NSProgressFileTotalCountKey];

    int counter = 0;

    for /* Long-running loop */ {
        [childProgress setUserInfoObject: // a file URL
                                  forKey:NSProgressFileURLKey];

        // Do stuff

        [childProgress setUserInfoObject:@(++counter)
                                  forKey:NSProgressFileCompletedCountKey];
        childProgress.completedUnitCount += myIncrement;
    }
}

At the time I increment childProgress.completedUnitCount , this is what the userInfo looks like in the debugger. The fields I set are all represented:

> po childProgress.userInfo

{
    NSProgressFileCompletedCountKey = 2,
    NSProgressFileTotalCountKey = 3,
    NSProgressFileURLKey = "file:///...Test%20File%20B.jpg"; // chunk elided from URL
}

When each KVO notification comes back, this is how notificationProgress.userInfo looks:

> po notificationProgress.userInfo

{
}

Ok, I had a chance to look at the code again with more coffee in my system and more time on my hands. I'm actually seeing it working.

In your testProgressReporting_ExtractFiles_AdditionalDescription method, I changed the code to this:

NSProgress *extractFilesProgress = [NSProgress progressWithTotalUnitCount:1];
[extractFilesProgress setUserInfoObject:@10 forKey:NSProgressEstimatedTimeRemainingKey];
[extractFilesProgress setUserInfoObject:@"Test" forKey:@"TestKey"];

And then in observeValueForKeyPath, I printed these objects:

po progress.userInfo {
NSProgressEstimatedTimeRemainingKey = 10;
TestKey = Test;
}

po progress.localizedAdditionalDescription
0 of 1 — About 10 seconds remaining

You can see the key-values I added, and the localizedAdditionalDescription was created based on those entries (notice the time remaining). So, this all looks like it's working correctly.

I think one point of confusion might be around the NSProgress properties and their effect on the key-values in the userInfo dict. Setting the properties doesn't add key-values to the userInfo dict, and setting the key-values doesn't set the properties. For example, setting the progress kind doesn't add the NSProgressFileOperationKindKey to the userInfo dict. The value in the userInfo dict, if present, is more of an override of the property that's only used when creating the localizedAdditionalDescription.

You can also see the custom key-value I added. So, this all looks like it's working right. Can you point me to something that still looks off?

I wanted to comment on @clarus's answer, but SO won't let me do readable formatting in a comment. TL;DR - their take has always been my understanding and it's something that bit me when I started working with NSProgress a few years back.

For stuff like this, I like to check the Swift Foundation code for implementation hints. It's maybe not 100% authoritative if stuff's not done yet, but I like seeing the general thinking.

If you look at the implementation of setUserInfoObject(: forKey:) , you can see that the implementation simply sets the user info dict without propagating anything up to the parent.

Conversely, updates that impact the child's fraction completed explicitly call back to the (private) _parent property to indicate its state should update in response to a child change.

That private _updateChild(: from: to: portion:) only seems concerned updating the fraction completed and not anything related to the user info dictionary.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM