@@ -62,11 +62,19 @@ static void Await(dispatch_semaphore_t gate, const char *description) {
6262static int64_t waitBudget;
6363static NSString *root;
6464
65- // In-memory NSUserDefaults collaborator. Every production defaults mutation must
66- // still hold the REAL production os_unfair_lock. Pausing one write inside that
67- // lock lets the opposing operation start without relying on OS scheduling.
68- @interface TestDefaults : NSUserDefaults
69- @property (nonatomic , strong ) NSMutableDictionary <NSString *, id> *values;
65+ // In-memory collaborator, deliberately NOT an NSUserDefaults subclass: inherited
66+ // accessors could otherwise reach real preferences without ObserveWrite(). Only
67+ // explicitly modelled selectors are supported. Every mutation still holds the
68+ // REAL production lock; pausing one write fixes the competing operation's order.
69+ @interface TestDefaults : NSObject {
70+ NSMutableDictionary <NSString *, id > *_values;
71+ }
72+ - (void )setObject : (id )value forKey : (NSString *)key ;
73+ - (void )removeObjectForKey : (NSString *)key ;
74+ - (id )objectForKey : (NSString *)key ;
75+ - (NSString *)stringForKey : (NSString *)key ;
76+ - (NSDictionary *)dictionaryForKey : (NSString *)key ;
77+ - (NSDictionary <NSString *, id> *)dictionaryRepresentation ;
7078@end
7179
7280static void ObserveWrite (void ) {
@@ -83,20 +91,46 @@ - (instancetype)init {
8391 if (self) { _values = [NSMutableDictionary new ]; }
8492 return self;
8593}
86- - (void )setObject : (id )value forKey : (NSString *)key { ObserveWrite (); self. values [key] = value; }
87- - (void )removeObjectForKey : (NSString *)key { ObserveWrite (); [self .values removeObjectForKey: key]; }
88- - (id )objectForKey : (NSString *)key { return self. values [key]; }
94+ - (void )setObject : (id )value forKey : (NSString *)key { ObserveWrite (); _values [key] = value; }
95+ - (void )removeObjectForKey : (NSString *)key { ObserveWrite (); [_values removeObjectForKey: key]; }
96+ - (id )objectForKey : (NSString *)key { return _values [key]; }
8997- (NSString *)stringForKey : (NSString *)key {
90- id value = self. values [key];
98+ id value = _values [key];
9199 return [value isKindOfClass: NSString .class ] ? value : nil ;
92100}
93101- (NSDictionary *)dictionaryForKey : (NSString *)key {
94- id value = self. values [key];
102+ id value = _values [key];
95103 return [value isKindOfClass: NSDictionary .class ] ? value : nil ;
96104}
97- - (NSDictionary <NSString *, id> *)dictionaryRepresentation { return [self .values copy ]; }
105+ - (NSDictionary <NSString *, id> *)dictionaryRepresentation { return [_values copy ]; }
106+ - (void )doesNotRecognizeSelector : (SEL )selector {
107+ // Fail even if product code catches Objective-C exceptions. Dynamic sends
108+ // must not fall through to a real defaults domain or turn into a passed test.
109+ std::string message = " unsupported TestDefaults selector: " ;
110+ message += NSStringFromSelector (selector).UTF8String ;
111+ Expect (false , message.c_str ());
112+ }
98113@end
99114
115+ // Type substitution is confined to this test translation unit, AFTER Foundation
116+ // is imported. The extracted bodies stay unchanged, but a newly used defaults
117+ // selector must be explicitly implemented here rather than inherited silently.
118+ #define NSUserDefaults TestDefaults
119+
120+ // Compile-only contract probes, enabled individually by the runner. Ordinary
121+ // warnings must remain non-fatal; unmodelled selectors must fail compilation.
122+ #if defined(TEST_DEFAULTS_READ_PROBE)
123+ static BOOL DefaultsReadProbe (NSUserDefaults *defaults) {
124+ return [defaults boolForKey: @" probe" ];
125+ }
126+ #elif defined(TEST_DEFAULTS_WRITE_PROBE)
127+ static void DefaultsWriteProbe (NSUserDefaults *defaults) {
128+ [defaults setBool: YES forKey: @" probe" ];
129+ }
130+ #elif defined(TEST_HARMLESS_WARNING_PROBE)
131+ #warning PUSHY_TEST_HARMLESS_WARNING
132+ #endif
133+
100134static TestDefaults *testDefaults;
101135static NSUserDefaults *PushyDefaults (void ) { return testDefaults; }
102136static NSTimeInterval PushyMonotonicNow (void ) { return 100 ; }
@@ -413,8 +447,45 @@ static void CompleteInTime(void) {
413447 Finish ();
414448}
415449
450+ // This contract test deliberately does not call SetUp: a new collaborator must
451+ // start empty on its own, not only because the fixture reset some known keys.
452+ static void DefaultsIsolation (void ) {
453+ TestDefaults *first = [TestDefaults new ];
454+ TestDefaults *second = [TestDefaults new ];
455+ Expect (![first isKindOfClass: NSClassFromString (@" NSUserDefaults" )],
456+ " test defaults must not inherit real preferences storage" );
457+ PushyWithStateLock (^{
458+ [first setObject: @" value" forKey: @" key" ];
459+ [first setObject: @{@" nested" : @YES } forKey: @" dictionary" ];
460+ Expect ([[first stringForKey: @" key" ] isEqual: @" value" ], " string accessor uses memory" );
461+ Expect ([[first dictionaryForKey: @" dictionary" ][@" nested" ] boolValue ],
462+ " dictionary accessor uses memory" );
463+ NSDictionary *snapshot = [first dictionaryRepresentation ];
464+ [first removeObjectForKey: @" key" ];
465+ Expect ([first objectForKey: @" key" ] == nil , " remove accessor uses memory" );
466+ Expect ([snapshot[@" key" ] isEqual: @" value" ], " dictionary snapshot is independent" );
467+ Expect ([second dictionaryRepresentation ].count == 0 , " instances must not share defaults" );
468+ });
469+ }
470+
416471int main (int argc, char **argv) {
417472 @autoreleasepool {
473+ // These intentional failures run in separate processes. Erasing the
474+ // static type also checks the runtime backstop, not just the compiler.
475+ if (argc > 1 && (std::string (argv[1 ]) == " unsupported_defaults_read" ||
476+ std::string (argv[1 ]) == " unsupported_defaults_write" )) {
477+ testName = argv[1 ];
478+ id defaults = [TestDefaults new ];
479+ if (std::string (argv[1 ]) == " unsupported_defaults_read" ) {
480+ (void )[defaults boolForKey: @" probe" ];
481+ } else {
482+ PushyWithStateLock (^{ [defaults setBool: YES forKey: @" probe" ]; });
483+ }
484+ Expect (false , " unsupported defaults call unexpectedly returned" );
485+ }
486+ testName = " defaults_isolation" ;
487+ DefaultsIsolation ();
488+ std::printf (" [PASS] defaults_isolation\n " );
418489 struct Case { const char *name; void (*run)(); };
419490 const Case cases[] = {
420491 {" late_commit" , LateCommit},
0 commit comments