Tests: Improve drop-in snapshot and regenerate baseline

The drop-in baseline preserves framework backward-compatibility contracts by recording expected defaults and states.
Explicit value comparisons are enforced and omitted snapshot entries are reported to detect unintended default modifications.

* DropInSnapshot:
  * Record booleans, numbers and strings by value.
  * Serialize plain objects with sorted keys, and record functions
    and circular references without raising an exception.
  * Extract DropInViewState, and isolate each getter so that an
    exception leaves the remaining state intact.
  * Add throwIfError, so that a generator does not save an exception
    as expected state.
  * Add a skipped list that names each class or window style left out
    of a snapshot.
  * Add DropInCibTree, with view, menu and object descriptions and a
    cib owner, to describe the objects a cib produces.
* DropInBaselineGenerator: Pass throwIfError and print each skipped
  entry.
* DropInBaseline: Regenerate with recorded values and sorted keys.
This commit is contained in:
David Richardson committed 2026-10-31 19:45:45 -06:00
1 parent fcf94a5df2
commit e3bb38134b
3 files changed
+811 -626

No files matched your search

+3 -1
View File
@@ -35,7 +35,8 @@ function main(args, namedArgs)
{
[[CPApplication alloc] init];
var snapshot = DropInCreateSnapshot(),
var skipped = [],
snapshot = DropInCreateSnapshot(YES, skipped),
baselinePath = path.join(process.cwd(), "Tests", "AppKit", "Resources", "DropInBaseline.json");
fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 1) + "\n");
@@ -45,4 +46,5 @@ function main(args, namedArgs)
Object.keys(snapshot.themeAttributes).length + " themed classes, " +
Object.keys(snapshot.construction).length + " constructed views, " +
Object.keys(snapshot.windows).length + " window styles");
skipped.forEach(function(aLine) { print("skipped " + aLine); });
}
+261 -78
View File
@@ -23,58 +23,112 @@
@import <AppKit/AppKit.j>
/*
An application that updates its frameworks relies on more than documented
behavior. It relies on the defaults a class supplies when no theme overrides
them, on the public methods it calls and overrides, and on the state a control
has when it is created. Together these form the drop-in contract.
An application that updates its frameworks relies on more than documented
behavior. It relies on the defaults a class supplies when no theme overrides
them, on the public methods it calls and overrides, and on the state a control
has when it is created. Together these form the drop-in contract.
This file records that contract as plain data. DropInBaselineGenerator.j saves
a snapshot as the baseline, and DropInBaselineTest.j compares the running
frameworks with it. Additions are allowed. Changes and removals are not.
*/
This file records that contract as plain data. DropInBaselineGenerator.j saves
a snapshot as the baseline, and DropInBaselineTest.j compares the running
frameworks with it. Additions are allowed. Changes and removals are not.
*/
var DropInConstructionGetters = [
@"acceptsFirstResponder",
@"alignment",
@"autoresizingMask",
@"backgroundColor",
@"bezelStyle",
@"controlSize",
@"drawsBackground",
@"isBordered",
@"isEditable",
@"isEnabled",
@"isHidden",
@"isSelectable",
@"lineBreakMode",
@"verticalAlignment"
],
@"acceptsFirstResponder",
@"alignment",
@"autoresizingMask",
@"backgroundColor",
@"bezelStyle",
@"controlSize",
@"drawsBackground",
@"isBordered",
@"isEditable",
@"isEnabled",
@"isHidden",
@"isSelectable",
@"lineBreakMode",
@"verticalAlignment"
],
DropInWindowStyles = {
@"borderless": CPBorderlessWindowMask,
@"titled": CPTitledWindowMask,
@"closable": CPClosableWindowMask,
@"resizable": CPClosableWindowMask | CPResizableWindowMask,
@"miniaturizable": CPClosableWindowMask | CPMiniaturizableWindowMask,
@"full": CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask,
@"hud": CPHUDBackgroundWindowMask,
@"hudClosable": CPHUDBackgroundWindowMask | CPClosableWindowMask
};
DropInWindowStyles = {
@"borderless": CPBorderlessWindowMask,
@"titled": CPTitledWindowMask,
@"closable": CPClosableWindowMask,
@"resizable": CPClosableWindowMask | CPResizableWindowMask,
@"miniaturizable": CPClosableWindowMask | CPMiniaturizableWindowMask,
@"full": CPClosableWindowMask | CPMiniaturizableWindowMask | CPResizableWindowMask,
@"hud": CPHUDBackgroundWindowMask,
@"hudClosable": CPHUDBackgroundWindowMask | CPClosableWindowMask
};
function DropInIsPublicName(aName)
{
return aName.charAt(0) !== "_" && aName.charAt(0) !== "$";
}
function DropInSerializeObject(val, seen)
{
if (val === null || val === undefined)
return nil;
if (typeof val === "function")
return "[Function]";
if (typeof val !== "object")
return val;
if (seen.indexOf(val) !== -1)
return "[Circular]";
seen.push(val);
var result;
if (Array.isArray(val))
{
result = [];
for (var i = 0; i < val.length; i++)
{
result[i] = DropInSerializeObject(val[i], seen);
}
}
else
{
result = {};
var keys = Object.keys(val).sort();
for (var i = 0; i < keys.length; i++)
{
var k = keys[i];
result[k] = DropInSerializeObject(val[k], seen);
}
}
seen.pop();
return result;
}
function DropInSafeJSONStringify(aValue)
{
return JSON.stringify(DropInSerializeObject(aValue, []));
}
/*
Reduces a value to a string that does not depend on object identity, so that
two runs of the same framework produce the same snapshot.
*/
Reduces a value to a string that does not depend on object identity, so that
two runs of the same framework produce the same snapshot.
*/
function DropInDescribeValue(aValue)
{
if (aValue === nil || aValue === undefined)
return "nil";
/*
Booleans, numbers and strings answer to isa as well, so they must be handled
first, or they are recorded by class and their values are never compared.
*/
if (typeof aValue === "boolean" || typeof aValue === "number" || typeof aValue === "string")
return String(aValue);
if (aValue === [CPNull null])
return "CPNull";
@@ -84,9 +138,9 @@ function DropInDescribeValue(aValue)
return "CPColor:" + [aValue cssString];
/*
The system font is a global setting that applications and tests change,
so a default that uses it records only that fact.
*/
The system font is a global setting that applications and tests change,
so a default that uses it records only that fact.
*/
if ([aValue isKindOfClass:[CPFont class]])
return [aValue isSystem] ? "CPFont:system" + ([aValue isBold] ? " bold" : "") : "CPFont:" + [aValue cssString];
@@ -96,16 +150,13 @@ function DropInDescribeValue(aValue)
return aValue.isa.name;
}
if (typeof aValue === "object")
return JSON.stringify(aValue);
return String(aValue);
return DropInSafeJSONStringify(aValue);
}
function DropInMethodNames(aClass)
{
return class_copyMethodList(aClass).map(function(aMethod)
{
{
return method_getName(aMethod);
}).filter(DropInIsPublicName).sort();
}
@@ -121,10 +172,10 @@ function DropInThemeAttributes(aClass)
return nil;
var result = {},
keys = [[attributes allKeys] sortedArrayUsingSelector:@selector(compare:)];
keys = [[attributes allKeys] sortedArrayUsingSelector:@selector(compare:)];
keys.forEach(function(aKey)
{
{
result[aKey] = DropInDescribeValue([attributes objectForKey:aKey]);
});
@@ -134,28 +185,144 @@ function DropInThemeAttributes(aClass)
function DropInSubviewClasses(aView)
{
return [aView subviews].map(function(aSubview)
{
{
return aSubview.isa.name;
}).filter(DropInIsPublicName).sort();
}
function DropInDefaultConstruction(aClass)
function DropInViewState(aView, throwIfError)
{
var view = [[aClass alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)],
result = {
@"frameSize": DropInDescribeValue([view frameSize]),
@"subviews": DropInSubviewClasses(view)
};
var result = {
@"frameSize": DropInDescribeValue([aView frameSize]),
@"subviews": DropInSubviewClasses(aView)
};
DropInConstructionGetters.forEach(function(aGetter)
{
if ([view respondsToSelector:CPSelectorFromString(aGetter)])
result[aGetter] = DropInDescribeValue([view performSelector:CPSelectorFromString(aGetter)]);
{
var selector = CPSelectorFromString(aGetter);
if ([aView respondsToSelector:selector])
{
try
{
result[aGetter] = DropInDescribeValue([aView performSelector:selector]);
}
catch (anException)
{
if (throwIfError)
throw anException;
result[aGetter] = "[Exception: " + String(anException) + "]";
}
}
});
return result;
}
function DropInDefaultConstruction(aClass, throwIfError)
{
return DropInViewState([[aClass alloc] initWithFrame:CGRectMake(0.0, 0.0, 100.0, 100.0)], throwIfError);
}
/*
Describes a view and its public subviews, in order. Private views are left out,
because they are the class's own concern.
*/
function DropInViewTree(aView)
{
return {
@"class": aView.isa.name,
@"frame": DropInDescribeValue([aView frame]),
@"subviews": [aView subviews].filter(function(aSubview)
{
return DropInIsPublicName(aSubview.isa.name);
}).map(DropInViewTree)
};
}
function DropInMenuTree(aMenu)
{
return [aMenu itemArray].map(function(anItem)
{
return {
@"title": [anItem title],
@"keyEquivalent": [anItem keyEquivalent],
@"submenu": [anItem hasSubmenu] ? DropInMenuTree([anItem submenu]) : nil
};
});
}
/*
Describes what a cib produced. Objects other than windows, views and menus are
recorded by class alone.
*/
function DropInObjectTree(anObject)
{
if ([anObject isKindOfClass:[CPWindow class]])
return { @"class": anObject.isa.name, @"title": [anObject title], @"contentView": DropInViewTree([anObject contentView]) };
if ([anObject isKindOfClass:[CPView class]])
return DropInViewTree(anObject);
if ([anObject isKindOfClass:[CPMenu class]])
return { @"class": anObject.isa.name, @"items": DropInMenuTree(anObject) };
return { @"class": anObject.isa.name };
}
/*
The owner of the reference cibs. It has the outlets and actions that those cibs
connect to it, so that loading them gives no warnings.
*/
@implementation _DropInCibOwner : CPObject
{
id folderNameField;
id predicateEditor;
id predicateSheet;
id textView;
id window;
}
- (void)closeSheet:(id)aSender
{
}
@end
/*
Loads a cib and describes its top level objects, sorted so that the order in
which the cib lists them does not matter.
A scroll view takes its scroller style from a global setting when it is decoded,
and that style changes the frames inside it. Applications and tests change the
setting, so each cib loads with the same style. Layout also changes frames and
adds subviews, so it is complete before anything is described.
*/
function DropInCibTree(aCibName, aRawString)
{
var topLevelObjects = [],
cib = [[CPCib alloc] _initWithData:[CPData dataWithRawString:aRawString] bundle:nil cibName:aCibName],
scrollerStyle = [CPScrollView globalScrollerStyle];
[CPScrollView setGlobalScrollerStyle:CPScrollerStyleLegacy];
try
{
[cib instantiateCibWithExternalNameTable:@{ CPCibOwner: [[_DropInCibOwner alloc] init], CPCibTopLevelObjects: topLevelObjects }];
[_CPDisplayServer run];
return topLevelObjects.map(DropInObjectTree).map(function(aTree)
{
return JSON.stringify(aTree);
}).sort();
}
finally
{
[CPScrollView setGlobalScrollerStyle:scrollerStyle];
}
}
function DropInClassIsView(aClass)
{
var viewClass = [CPView class];
@@ -170,38 +337,42 @@ function DropInClassIsView(aClass)
function DropInSortedClasses()
{
var classes = [],
count = objj_getClassList(classes, 1 << 16);
count = objj_getClassList(classes, 1 << 16);
return classes.slice(0, count).filter(function(aClass)
{
{
return aClass && aClass.name.indexOf("$") !== 0;
}).sort(function(lhs, rhs)
{
{
return lhs.name < rhs.name ? -1 : (lhs.name > rhs.name ? 1 : 0);
});
}
/*
Some classes cannot be created without a surrounding object, or without a DOM.
A class that cannot be created is left out of the construction section, and the
comparison does not report it. This reduces coverage, but it cannot report a
false difference.
*/
function DropInCreateSnapshot()
Some classes cannot be created without a surrounding object, or without a DOM.
A class that cannot be created is left out of the construction section, and the
comparison does not report it. This reduces coverage, but it cannot report a
false difference.
A generator passes throwIfError, so that a getter that throws is not saved as
expected state, and passes skipped, to list each class or window style that is
left out.
*/
function DropInCreateSnapshot(throwIfError, skipped)
{
var snapshot = {
@"publicClasses": [],
@"themeAttributes": {},
@"instanceMethods": {},
@"classMethods": {},
@"construction": {},
@"windows": {}
};
@"publicClasses": [],
@"themeAttributes": {},
@"instanceMethods": {},
@"classMethods": {},
@"construction": {},
@"windows": {}
};
DropInSortedClasses().forEach(function(aClass)
{
{
var name = aClass.name,
isPublic = DropInIsPublicName(name);
isPublic = DropInIsPublicName(name);
try
{
@@ -212,6 +383,8 @@ function DropInCreateSnapshot()
}
catch (anException)
{
if (skipped)
skipped.push(name + " theme attributes: " + String(anException).slice(0, 80));
}
if (!isPublic)
@@ -226,19 +399,29 @@ function DropInCreateSnapshot()
try
{
snapshot.construction[name] = DropInDefaultConstruction(aClass);
snapshot.construction[name] = DropInDefaultConstruction(aClass, throwIfError);
}
catch (anException)
{
if (skipped)
skipped.push(name + " construction: " + String(anException).slice(0, 80));
}
});
for (var style in DropInWindowStyles)
{
var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0)
styleMask:DropInWindowStyles[style]];
try
{
var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 400.0, 400.0)
styleMask:DropInWindowStyles[style]];
snapshot.windows[style] = DropInSubviewClasses([aWindow _windowView]);
snapshot.windows[style] = DropInSubviewClasses([aWindow _windowView]);
}
catch (anException)
{
if (skipped)
skipped.push(style + " window style: " + String(anException).slice(0, 80));
}
}
return snapshot;
File diff suppressed because it is too large. Load diff