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 (e.g. 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?