mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-11-03 13:49:45 +00:00
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:
1 parent
fcf94a5df2
commit
e3bb38134b
3 files changed
+811
-626
No files matched your search
@@ -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
@@ -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
Reference in new issue
Block a user