Last active
August 29, 2015 14:02
-
-
Save landonf/ae56368de6037fc8856a to your computer and use it in GitHub Desktop.
A much nicer single-header XCTest-compatible testing DSL, all in a single header.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#import "XSmallTests.h" | |
xsm_given("an integer value") { | |
int v; | |
xsm_when("the value is 42") { | |
v = 42; | |
xsm_then("the value is the answer to the life, the universe, and everything") { | |
XCTAssertEqual(42, v); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2014 Landon Fuller <[email protected]> | |
* All rights reserved. | |
* | |
* Permission is hereby granted, free of charge, to any person | |
* obtaining a copy of this software and associated documentation | |
* files (the "Software"), to deal in the Software without | |
* restriction, including without limitation the rights to use, | |
* copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the | |
* Software is furnished to do so, subject to the following | |
* conditions: | |
* | |
* The above copyright notice and this permission notice shall be | |
* included in all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT | |
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR | |
* OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
#import <XCTest/XCTest.h> | |
#import <inttypes.h> | |
#import <objc/runtime.h> | |
#import <objc/message.h> | |
#import <mach-o/getsect.h> | |
#import <dlfcn.h> | |
/* | |
* The pseudo root section tag; references the last previously declared section; any section referencing this | |
* parent is either 1) a root XSM_SECTION_TYPE_TEST_CASE, or 2) a XSM_SECTION_TYPE_NESTED that should be connected | |
* with a previously declared XSM_SECTION_TYPE_TEST_CASE. | |
* | |
* This tag is reserved via the XSM_TEST_PARENT_RESERVED_ definition below. | |
*/ | |
#define XSM_INT_ORPHAN_TAG 0 | |
/* The pseudo root section tag; references the last previously declared section. Since __COUNTER__ is gauranteed to start | |
* at 0 and increment monotonically, we declare this field first to ensure that no other __COUNTER__-derived tag value | |
* will be 0. */ | |
static const int XSM_TEST_PARENT_TAG __attribute__((used)) = XSM_INT_ORPHAN_TAG; | |
/* Ensures that XSM_TEST_PARENT_TAG's '0' value is reserved by allocating it (or a greater value). If __COUNTER__ is already greater than 0, | |
* our gaurantees still hold. */ | |
static const int XSM_TEST_PARENT_RESERVED_ __attribute__((unused)) = __COUNTER__; | |
/** | |
* Define a new test case with the given description. All tests must first be defined via xsm_given(). | |
* | |
* Example: | |
@code | |
xsm_given("an integer value") { | |
// Assertions or additional sections | |
} | |
@endcode | |
* | |
* @param desc The clause's description. | |
*/ | |
#define xsm_given(description) int_xsm_given_(description, __COUNTER__) // indirection required to ensure __COUNTER__ is expanded | |
#define int_xsm_given_(description, tag) int_xsm_given__(description, tag) | |
#define int_xsm_given__(__xsm_description, __xsm_tag) \ | |
/* Declare our test case's method IMP */ \ | |
static void xsm_int_test_func_ ## __xsm_tag (id self, SEL _cmd, NSMutableSet *XSM_INT_TEST_TAGS); \ | |
\ | |
/* Record the section */ \ | |
xsm_write_section_record(XSM_SECTION_TYPE_TEST_CASE, XSM_CLAUSE_TYPE_GIVEN, __xsm_description, XSM_TEST_PARENT_TAG, __xsm_tag, &xsm_int_test_func_ ## __xsm_tag); \ | |
\ | |
/* Define a constructor to perform test case class registration */ \ | |
__attribute__((constructor)) static void xsm_int_parse_test_records_ ## __xst_case_name (void) { \ | |
xsm_int_parse_test_records(); \ | |
} \ | |
\ | |
/* Define the test case method IMP (sans body, which the user must provide. We provide 'self' and _cmd to support | |
* Objective-C-based invocation from our custom XCTestCase */ \ | |
static void xsm_int_test_func_ ## __xsm_tag (id self, SEL _cmd, NSMutableSet *XSM_INT_TEST_TAGS) | |
/** | |
* Define a new test `when' clause with the given description. All tests must first be defined via xsm_given(). | |
* | |
* Example: | |
@code | |
xsm_given("an integer value") { | |
int v; | |
xsm_where("the value is 42") { | |
v = 42; | |
// Additional sections or tests here | |
} | |
} | |
@endcode | |
* | |
* @param desc The clause's description. | |
*/ | |
#define xsm_when(description) int_xsm_generic_sect(description, XSM_CLAUSE_TYPE_WHEN, XSM_TEST_PARENT_TAG, __COUNTER__) | |
/** | |
* Define a new `then' clause with the given description. All tests must first be defined via xsm_given(). | |
* | |
* Example: | |
@code | |
xsm_given("an integer value") { | |
int v; | |
xsm_when("the value is 42") { | |
v = 42; | |
xsm_then("the value is the answer to the life, the universe, and everything") { | |
XCTestAssertEquals(42, LIFE_UNIVERSE_EVERYTHING); | |
} | |
} | |
xsm_then("this test should fail") { | |
XCTFail("ints are smelly"); | |
} | |
} | |
@endcode | |
* | |
* @param desc The clause's description. | |
*/ | |
#define xsm_then(description) int_xsm_generic_sect(description, XSM_CLAUSE_TYPE_THEN, XSM_TEST_PARENT_TAG, __COUNTER__) | |
/* Generic nested section declaration */ | |
#define int_xsm_generic_sect(description, clause_type, parent_tag, tag) int_xsm_generic_sect_(description, clause_type, parent_tag, tag) // indirection required to ensure __COUNTER__ is expanded | |
#define int_xsm_generic_sect_(__xsm_description, __xsm_clause_type, __xsm_parent_tag, __xsm_tag) \ | |
/* Record the section */ \ | |
xsm_write_section_record(XSM_SECTION_TYPE_NESTED, __xsm_clause_type, __xsm_description, __xsm_parent_tag, __xsm_tag, NULL /* always NULL for our sub-sections */); \ | |
\ | |
/* Create a new XSM_TEST_PARENT_TAG for this scope and check the scope against XSM_INT_TEST_TAGS */ \ | |
for (const int XSM_TEST_PARENT_TAG __attribute__((unused)) = __xsm_tag; xsm_int_sect_tag_check(XSM_INT_TEST_TAGS, __xsm_tag);) | |
/** | |
* @internal | |
* Record a test case section with the given section type, clause type, description, and file-scoped order. | |
* | |
* @param __xsm_type The section type (xsm_section_type_t) to declare. | |
* @param __xsm_clause The clause type (xsm_clause_type_t) to declare. | |
* @param __xsm_description The section's description, as a constant string. | |
* @param __xsm_parent_tag The section's parent's order tag, or 0 if this is a top-level or second-level section (in which case, parentage has to be | |
* reconstructed from the order tags) | |
* @param __xsm_tag The section's translation-unit-scoped order tag. | |
* @param __xsm_sect_func The section's implementation function (xsm_int_test_section_function_t). | |
*/ | |
#define xsm_write_section_record(__xsm_type, __xsm_clause, __xsm_description, __xsm_parent_tag, __xsm_tag, __xsm_sect_func) \ | |
/* Try to provide a decent error message if a non-constant description is provided */ \ | |
__attribute__((unused)) char xsm_description_messages_must_be_constant_strings_ ## __xsm_tag[__builtin_constant_p(__xsm_description) ? 0 : -1]; \ | |
\ | |
XSM_INT_MAKE_SECT_RECORD(__xsm_type, __xsm_clause, __xsm_description, __xsm_parent_tag, __xsm_tag, __xsm_sect_func) | |
/* Write a section record to __DATA,xsm_tests */ | |
#define XSM_INT_MAKE_SECT_RECORD(__xsm_type, __xsm_clause, __xsm_description, __xsm_parent_tag, __xsm_tag, __xsm_sect_func) \ | |
struct int_xsm_sect_record_record_ ## __xsm_tag ## _t { \ | |
xsm_int_section_record record; \ | |
char description[sizeof(__xsm_description)]; \ | |
} __attribute__((packed)); \ | |
\ | |
static struct int_xsm_sect_record_record_ ## __xsm_tag ## _t int_xsm_sect_record_record_ ## __xsm_tag __attribute__((section(XSM_SEG_NAME "," XSM_SECT_NAME))) __attribute__((used)) = { \ | |
.record = { \ | |
.size = sizeof(struct int_xsm_sect_record_record_ ## __xsm_tag ## _t), \ | |
.version = 0, \ | |
.type = (uint8_t) __xsm_type, \ | |
.clause = (uint8_t) __xsm_clause, \ | |
.tu = &xsm_int_local_translation_unit, \ | |
.parent_tag = __xsm_parent_tag, \ | |
.tag = __xsm_tag, \ | |
.description = (char *) 0 /* description directly follows */, \ | |
.section_class = nil, \ | |
.section_imp = __xsm_sect_func \ | |
}, \ | |
.description = __xsm_description \ | |
}; | |
/** | |
* @internal | |
* The segment to which XSM's section records are written | |
*/ | |
#define XSM_SEG_NAME "__DATA" | |
/** | |
* @internal | |
* The section (within XSM_SEG_NAME) to which XSM's section records are written | |
*/ | |
#define XSM_SECT_NAME "xsm_tests_v2" | |
/** | |
* @internal | |
* The section (within XSM_SEG_NAME) to which XSM's translation unit records are written. | |
*/ | |
#define XSM_TU_SECT_NAME "xsm_txu" | |
/* Internal ARC compatibility macros */ | |
#if __has_feature(objc_arc) | |
# define XSM_INT_RETAIN(obj) | |
# define XSM_INT_RELEASE(__xsmall_obj) __xsmall_obj = nil; | |
# define XSM_INT_MRC_ONLY(expr) | |
#else | |
# define XSM_INT_RETAIN(__xsmall_obj) [__xsmall_obj retain]; | |
# define XSM_INT_RELEASE(__xsmall_obj) ([__xsmall_obj release] && __xsmall_obj = nil); | |
# define XSM_INT_MRC_ONLY(expr) expr | |
#endif | |
/** | |
* XSM Clause Types | |
*/ | |
typedef NS_ENUM(uint8_t, xsm_clause_type_t) { | |
/** A top-level test case. In the BDD unit test style, this would be equivalent to a 'Given' declaration. */ | |
XSM_CLAUSE_TYPE_GIVEN = 0, | |
/** An inner 'when' test clause. */ | |
XSM_CLAUSE_TYPE_WHEN = 1, | |
/** An inner 'then' test clause. */ | |
XSM_CLAUSE_TYPE_THEN = 2, | |
}; | |
/** | |
* XSM Hierarchical Section Types | |
*/ | |
typedef NS_ENUM(uint8_t, xsm_section_type_t) { | |
/** A top-level test case. */ | |
XSM_SECTION_TYPE_TEST_CASE = 0, | |
/** An inner (nested) section. */ | |
XSM_SECTION_TYPE_NESTED = 1 | |
}; | |
/** | |
* @internal | |
* XSM translation unit record. | |
*/ | |
typedef struct xsm_int_translation_unit { | |
/** | |
* Record version. The current version is 0; if an incompatible change is made to this data format, this value should be incremented. | |
*/ | |
uint8_t version; | |
} xsm_int_translation_unit; | |
/** | |
* @iternal | |
* A test section implementation. | |
* | |
* @param self An XCTestCase instance. | |
* @param _cmd The selector allocated for this test function. | |
* @param tags The section tags enabled for this test run. | |
*/ | |
typedef void (xsm_int_test_section_function_t)(id self, SEL _cmd, NSMutableSet *tags); | |
/** | |
* @internal | |
* XSM test record. Test records are written to the __DATA,xsm_tests section when declared via the | |
* XSM test macros. | |
*/ | |
typedef struct xsm_int_section_record { | |
/** Size of this record (including the size field). */ | |
uint16_t size; | |
uint8_t _padding; | |
/** | |
* Record version. The current version is 0; if an incompatible change is made to this data format, this value | |
* should be incremented. To avoid incompatibilities, -always- add additional fields to the -end- of this | |
* structure. | |
*/ | |
uint8_t version; | |
/** The type of this section (xsm_section_type_t). */ | |
uint8_t type; | |
/** The clause to use when presenting this test section (xsm_clause_type_t). */ | |
uint8_t clause; | |
/** The section's description. When serialized, this will be the offset from the end of this structure element. */ | |
char *description; | |
/** A symbol-based reference to the containing translation unit */ | |
xsm_int_translation_unit *tu; | |
/** The section parent's order tag. For top-level and second-level sections, this will be 0 due to implementation | |
* constraints; the actual parent reference must be reconstructed by ordering all the parsed sections within | |
* a translation unit, and inserting orphaned sections into the previously parsed XSM_SECTION_TYPE_TEST_CASE test | |
* case. */ | |
uint32_t parent_tag; | |
/** The section's order tag. This can be used to reconstruct the in-source ordering of sections within | |
* a single file */ | |
uint32_t tag; | |
/** The runtime-created test case class for this test section. This is initialized at runtime */ | |
Class section_class; | |
/** The method IMP for this test section, or NULL if the parent's test function should be used. */ | |
xsm_int_test_section_function_t *section_imp; | |
} xsm_int_section_record; | |
/** | |
* @internal | |
* A common translation unit record, used by all test sections defined within | |
* the current translation unit | |
*/ | |
static xsm_int_translation_unit xsm_int_local_translation_unit __attribute__((section(XSM_SEG_NAME "," XSM_TU_SECT_NAME))) = { | |
.version = 0 | |
}; | |
/* Function foward-declarations */ | |
static inline BOOL xsm_int_sect_tag_check (NSMutableSet *tags, int expected); | |
static inline id xsm_int_meth_impl_cls_defaultTestSuite (id self, SEL _cmd); | |
static inline xsm_int_test_section_function_t *xsm_int_meth_impl_testEntry (id self, SEL _cmd); | |
static inline void xsm_int_meth_impl_xsmTestSection (id self, SEL _cmd); | |
static inline id xsm_int_meth_impl_name (id self, SEL _cmd); | |
static inline SEL xsm_int_register_test_selector (Class cls, const char *description, void (*imp)(id self, SEL _cmd)); | |
static inline Class xsm_int_register_test_case_class (const char *description); | |
/* Provide method selectors for our dynamically registered XCTestCase subclass */ | |
@protocol XSmallTestCase | |
- (void) xsmTestSection; | |
@end | |
/* | |
* Implements runtime parsing and registration of XSM test cases. | |
*/ | |
static inline void xsm_int_parse_test_records (void) { | |
/* Find the __DATA,xsm_tests section; this contains our registered test cases and sections. */ | |
struct { | |
uint8_t *data; | |
const uint8_t *end; | |
unsigned long size; | |
} xsm_sect; | |
{ | |
/* Fetch the mach header */ | |
Dl_info dli; | |
if (dladdr((const void *) &xsm_int_parse_test_records, &dli) == 0) { | |
[NSException raise: NSInternalInconsistencyException format: @"Could not find our own image!"]; | |
} | |
/* Fetch the section data */ | |
#ifdef __LP64__ | |
xsm_sect.data = getsectiondata((const struct mach_header_64 *) dli.dli_fbase, XSM_SEG_NAME, XSM_SECT_NAME, &xsm_sect.size); | |
#else | |
xsm_sect.data = getsectiondata((const struct mach_header_32 *) dli.dli_fbase, XSM_SEG_NAME, XSM_SECT_NAME, &xsm_sect.size); | |
#endif | |
/* This should never happen; our constructor is only called if at least a test case has been registered */ | |
if (xsm_sect.data == NULL) { | |
[NSException raise: NSInternalInconsistencyException format: @"Could not find __DATA,xsm_tests in %s", dli.dli_fname]; | |
__builtin_trap(); | |
} | |
xsm_sect.end = xsm_sect.data + xsm_sect.size; | |
} | |
/* Extract all registered tests */ | |
@autoreleasepool { | |
uint8_t *ptr = xsm_sect.data; | |
uint16_t record_size; | |
/* Parse all records for this translation unit */ | |
NSMutableArray *tagOrder = [NSMutableArray array]; /* tags */ | |
NSMutableDictionary *records = [NSMutableDictionary dictionary]; /* tag -> NSValue -> xsm_int_section_record ptr; */ | |
for (record_size = *(uint16_t *) ptr; ptr != xsm_sect.end; ptr += (record_size = *(uint16_t *) ptr)) { | |
assert(ptr < xsm_sect.end); | |
/* Skip invalid versions */ | |
uint8_t version = *(ptr + sizeof(uint16_t)); | |
if (version != 0) { | |
NSLog(@"Skipping invlid XSM test record version (%hhu)", version); | |
continue; | |
} | |
/* Read the full record */ | |
xsm_int_section_record *rec = (xsm_int_section_record *) ptr; | |
/* If the node isn't within our translation unit, punt */ | |
if (rec->tu != &xsm_int_local_translation_unit) | |
continue; | |
/* Skip records that have already been registered */ | |
if (rec->section_class != nil) | |
continue; | |
/* Adjust relative data pointers */ | |
rec->description = (char *) ((uint8_t *) rec) + sizeof(*rec); | |
/* Save the record */ | |
[tagOrder addObject: @(rec->tag)]; | |
records[@(rec->tag)] = [NSValue valueWithPointer: rec]; | |
#ifdef XSM_INT_DEBUG | |
NSLog(@"Found record %p (size=%hu, version=%hhu) with type=%hhu, clause=%hhu, order=%u, desc=%s, parent=%u, imp=%p", | |
rec, rec->size, rec->version, rec->type, rec->clause, rec->tag, rec->description, rec->parent_tag, rec->section_imp); | |
#endif | |
} | |
/* Re-order the section's according to their tag order; this corresponds to the order they were declared | |
* in this translation unit. */ | |
[tagOrder sortWithOptions: 0 usingComparator: ^NSComparisonResult(id obj1, id obj2) { | |
return [(NSNumber *)obj1 compare: obj2]; | |
}]; | |
/* Fix up any parent references to the pseudo-test case tag (0). These are references to the most recently | |
* defined test case. */ | |
{ | |
xsm_int_section_record *currentRoot = NULL; | |
for (NSNumber *tag in tagOrder) { | |
xsm_int_section_record *rec = (xsm_int_section_record *) [(NSValue *) records[tag] pointerValue]; | |
/* Record new roots -- we leave their parent_tag set to XSM_INT_ORPHAN_TAG */ | |
if (rec->type == XSM_SECTION_TYPE_TEST_CASE) { | |
assert(rec->parent_tag == XSM_INT_ORPHAN_TAG); | |
currentRoot = rec; | |
continue; | |
} | |
/* Skip non-orphan references */ | |
if (rec->parent_tag != XSM_INT_ORPHAN_TAG) { | |
continue; | |
} | |
/* There must already be a test case defined; the API makes anything else impossible */ | |
if (currentRoot == NULL) | |
[NSException raise: NSInternalInconsistencyException format: @"No parent test case was defined for section '%s'", rec->description]; | |
/* Fix up the ophan's parent tag reference. */ | |
rec->parent_tag = currentRoot->tag; | |
} | |
} | |
/* Generate test cases for all tags */ | |
NSMutableDictionary *testCases = [NSMutableDictionary dictionary]; /* tag -> XCTestCase */ | |
NSMutableSet *allParentTags = [NSMutableSet set]; /* tag -- includes all test cases that have defined children */ | |
{ | |
NSMutableArray *tagStack = [NSMutableArray array]; | |
xsm_int_section_record *rootRecord = NULL; | |
for (NSNumber *tag in tagOrder) { | |
xsm_int_section_record *rec = (xsm_int_section_record *) [(NSValue *) records[tag] pointerValue]; | |
/* Adjust our parse state for the section type. */ | |
switch ((xsm_section_type_t) rec->type) { | |
case XSM_SECTION_TYPE_TEST_CASE: | |
/* This is a new root node; drop our previous tag stack, and set the new root node value */ | |
[tagStack removeAllObjects]; | |
rootRecord = rec; | |
break; | |
case XSM_SECTION_TYPE_NESTED: | |
/* This is a child node; pop the stack until we hit this child's parent's tag. */ | |
while ([tagStack count] > 0 && ![[tagStack lastObject] isEqual: @(rec->parent_tag)]) { | |
[tagStack removeLastObject]; | |
assert([tagStack count] != 0); | |
} | |
break; | |
} | |
/* At this point, there *must* be a root record in well-formed data */ | |
assert(rootRecord != NULL); | |
/* Register this section on the tag stack. */ | |
[tagStack addObject: @(rec->tag)]; | |
/* Register this sections' XCTestCase class, if necessary. */ | |
if (rec->section_class == nil) { | |
/* Nested sections get the root record's test case class -- which may also have to be registered on-demand. */ | |
if (rootRecord->section_class == nil) { | |
rootRecord->section_class = xsm_int_register_test_case_class(rootRecord->description); | |
} | |
rec->section_class = rootRecord->section_class; | |
} | |
/* Register this sections' test IMP, if necessary. */ | |
if (rec->section_imp == NULL) { | |
assert(rootRecord->section_imp != NULL); | |
rec->section_imp = rootRecord->section_imp; | |
} | |
/* Format the section's description */ | |
NSString *description; | |
switch ((xsm_clause_type_t) rec->clause) { | |
case XSM_CLAUSE_TYPE_GIVEN: | |
description = [NSString stringWithFormat: @"Given %s", rec->description]; | |
break; | |
case XSM_CLAUSE_TYPE_WHEN: | |
description = [NSString stringWithFormat: @"when %s", rec->description]; | |
break; | |
case XSM_CLAUSE_TYPE_THEN: | |
description = [NSString stringWithFormat: @"then %s", rec->description]; | |
break; | |
} | |
/* Create a test case instance for this section */ | |
SEL testSel = xsm_int_register_test_selector(rec->section_class, rec->description, &xsm_int_meth_impl_xsmTestSection); | |
XCTestCase *tc = [rec->section_class testCaseWithSelector: testSel]; | |
objc_setAssociatedObject(tc, (const void *) &xsm_int_meth_impl_name, description, OBJC_ASSOCIATION_RETAIN); | |
objc_setAssociatedObject(tc, (const void *) &xsm_int_meth_impl_xsmTestSection, [NSSet setWithArray: tagStack], OBJC_ASSOCIATION_RETAIN); | |
objc_setAssociatedObject(tc, (const void *) &xsm_int_meth_impl_testEntry, [NSValue valueWithPointer: (const void *) rec->section_imp], OBJC_ASSOCIATION_RETAIN); | |
testCases[@(rec->tag)] = tc; | |
/* Mark our parent as having a child */ | |
if (rec->parent_tag != XSM_INT_ORPHAN_TAG) { | |
[allParentTags addObject: @(rec->parent_tag)]; | |
} | |
} | |
} | |
/* Export the parsed entries out to a test suite/test case instance tree. */ | |
{ | |
/* Convert all non-leaf nodes to test suites */ | |
for (NSNumber *tag in tagOrder) { | |
if ([allParentTags containsObject: tag]) { | |
/* This is a non-leaf node; convert to a suite */ | |
XCTestCase *tc = testCases[tag]; | |
XCTestSuite *replacement = [XCTestSuite testSuiteWithName: tc.name]; | |
testCases[tag] = replacement; | |
} | |
} | |
/* Iterate over the test cases and combine them into test suite(s) */ | |
xsm_int_section_record *rootRecord = NULL; | |
NSMutableArray *testCaseTags = [NSMutableArray array]; | |
XCTestSuite *rootSuite = nil; | |
for (NSUInteger i = 0; i < tagOrder.count; i++) { | |
NSNumber *tag = tagOrder[i]; | |
xsm_int_section_record *rec = (xsm_int_section_record *) [(NSValue *) records[tag] pointerValue]; | |
/* Root node, reset our state */ | |
if (rec->parent_tag == XSM_INT_ORPHAN_TAG) { | |
XCTest *test = testCases[@(rec->tag)]; | |
rootRecord = rec; | |
rootSuite = [XCTestSuite testSuiteWithName: test.name]; | |
[testCaseTags removeAllObjects]; | |
/* If the root node has children, its children should be directly attached to the containing test suite */ | |
if ([allParentTags containsObject: @(rec->tag)]) | |
testCases[@(rec->tag)] = rootSuite; | |
} else { | |
assert(rootRecord != NULL); | |
assert(rootSuite != nil); | |
} | |
/* Include this tag in the output for the current root test case */ | |
[testCaseTags addObject: tag]; | |
/* If this is not the final entry in this test case, nothing left to do */ | |
if (i + 1 < tagOrder.count) { | |
NSNumber *nextTag = tagOrder[i + 1]; | |
xsm_int_section_record *next = (xsm_int_section_record *) [(NSValue *) records[nextTag] pointerValue]; | |
if (next->parent_tag != XSM_INT_ORPHAN_TAG) | |
continue; | |
} | |
/* We've iterated over the entire test case; nwo we can attach all children to their parent test suite. */ | |
for (NSNumber *tag in testCaseTags) { | |
xsm_int_section_record *rec = (xsm_int_section_record *) [(NSValue *) records[tag] pointerValue]; | |
/* If this is the root node, we exclude it; it will either be included directly below if it's leaf node, | |
* or if it's not a leaf node, it simply provides an extraneous level of nesting below the root */ | |
if (rec->tag == rootRecord->tag) | |
continue; | |
XCTest *child = testCases[@(rec->tag)]; | |
XCTestSuite *parent = testCases[@(rec->parent_tag)]; | |
[parent addTest: child]; | |
} | |
/* Attach the new test suite to the test case's class. */ | |
XCTest *defaultTestSuite; | |
if (![allParentTags containsObject: @(rootRecord->tag)]) { | |
/* If the root node has no children, it should be used directly as the top-level suite */ | |
defaultTestSuite = testCases[@(rootRecord->tag)]; | |
} else { | |
defaultTestSuite = rootSuite; | |
} | |
objc_setAssociatedObject(rootRecord->section_class, (const void *) &xsm_int_meth_impl_cls_defaultTestSuite, defaultTestSuite, OBJC_ASSOCIATION_RETAIN); | |
#ifdef XSM_INT_DEBUG | |
NSLog(@"Registered test cases: %@", testCases); | |
#endif | |
/* Clean up state */ | |
rootRecord = NULL; | |
rootSuite = nil; | |
} | |
} | |
} | |
}; | |
/* Check whether our tag is enabled, and whether we've already been run. */ | |
static inline BOOL xsm_int_sect_tag_check (NSMutableSet *tags, int expected) { | |
NSNumber *expectedObj = [NSNumber numberWithInt: expected]; | |
if (tags.count == 0 || ![tags containsObject: expectedObj]) | |
return NO; | |
[tags removeObject: expectedObj]; | |
return YES; | |
} | |
/* Custom XCTestCase method implementations. These implementations simply return whatever values have been | |
* associated with the receiving object, using the actual method function address as a key */ | |
static inline id xsm_int_meth_impl_name (id self, SEL _cmd) { | |
return objc_getAssociatedObject(self, (const void *) &xsm_int_meth_impl_name); | |
} | |
static inline id xsm_int_meth_impl_cls_defaultTestSuite (id self, SEL _cmd) { | |
return objc_getAssociatedObject(self, (const void *) &xsm_int_meth_impl_cls_defaultTestSuite); | |
} | |
static inline xsm_int_test_section_function_t *xsm_int_meth_impl_testEntry (id self, SEL _cmd) { | |
return (xsm_int_test_section_function_t *) [(NSValue *) objc_getAssociatedObject(self, (const void *) &xsm_int_meth_impl_testEntry) pointerValue]; | |
} | |
static inline void xsm_int_meth_impl_xsmTestSection (id self, SEL _cmd) { | |
NSSet *tags = objc_getAssociatedObject(self, (const void *) &xsm_int_meth_impl_xsmTestSection); | |
xsm_int_test_section_function_t *target = xsm_int_meth_impl_testEntry(self, _cmd); | |
target(self, _cmd, [tags mutableCopy]); | |
} | |
/* Given a test case description and a target class, derive a unique selector selector from the camel cased description, and register it with the target class and imp. */ | |
static inline SEL xsm_int_register_test_selector (Class cls, const char *description, void (*imp)(id self, SEL _cmd)) { | |
/* Split the description into individual components */ | |
NSArray *components = [[NSString stringWithUTF8String: description] componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
/* Camel case the string, removing invalid characters */ | |
NSMutableArray *cleanedComponents = [NSMutableArray arrayWithCapacity: [components count]]; | |
NSCharacterSet *allowedChars = [NSCharacterSet characterSetWithCharactersInString: @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"]; | |
for (NSUInteger i = 0; i < [components count]; i++) { | |
NSString *component = components[i]; | |
NSString *newComponent = [[component componentsSeparatedByCharactersInSet: [allowedChars invertedSet]] componentsJoinedByString: @""]; | |
if (i == 0) { | |
newComponent = [newComponent lowercaseString]; | |
} else { | |
newComponent = [newComponent capitalizedString]; | |
} | |
[cleanedComponents addObject: newComponent]; | |
} | |
/* Strip any leading numbers */ | |
NSString *selectorName = [cleanedComponents componentsJoinedByString: @""]; | |
{ | |
NSRange range = [selectorName rangeOfCharacterFromSet: [NSCharacterSet decimalDigitCharacterSet]]; | |
if (range.location == 0) | |
selectorName = [selectorName substringFromIndex: range.length]; | |
} | |
/* If the selector is already in use, loop until we have a unique name */ | |
while (class_getInstanceMethod(cls, NSSelectorFromString(selectorName)) != NULL) { | |
for (NSUInteger i = 0; i < NSUIntegerMax; i++) { | |
if (i == NSUIntegerMax) { | |
[NSException raise: NSInternalInconsistencyException format: @"Couldn't find a unique selector name for %s. You must have an impressive number of tests.", description]; | |
__builtin_trap(); | |
} | |
selectorName = [NSString stringWithFormat:@"%@%" PRIu64, selectorName, (uint64_t) i]; | |
if (class_getInstanceMethod(cls, NSSelectorFromString(selectorName)) == NULL) | |
break; | |
} | |
} | |
/* Register and return the SEL */ | |
SEL newSel = NSSelectorFromString(selectorName); | |
{ // -xsmTestSection | |
NSString *typeEnc = [NSString stringWithFormat: @"%s%s%s", @encode(void), @encode(id), @encode(SEL)]; | |
class_addMethod(cls, newSel, (IMP) imp, [typeEnc UTF8String]); | |
} | |
return newSel; | |
} | |
/* Register a new class, automatically selecting a valid and unique class name based on the given description. */ | |
static inline Class xsm_int_register_test_case_class (const char *description) { | |
NSCharacterSet *allowedChars = [NSCharacterSet characterSetWithCharactersInString: @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"]; | |
/* Camel case the string, removing invalid characters */ | |
NSArray *words = [[NSString stringWithUTF8String: description] componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]; | |
NSMutableString *className = [NSMutableString string]; | |
for (NSUInteger i = 0; i < [words count]; i++) { | |
NSString *word = words[i]; | |
/* Strip unsupported characters */ | |
word = [[word componentsSeparatedByCharactersInSet: [allowedChars invertedSet]] componentsJoinedByString: @""]; | |
/* Apply casing */ | |
word = [word capitalizedString]; | |
[className appendString: word]; | |
} | |
/* Strip any leading numbers */ | |
if (className != nil) { | |
NSRange range = [className rangeOfCharacterFromSet: [NSCharacterSet decimalDigitCharacterSet]]; | |
if (range.location == 0) | |
[className deleteCharactersInRange: range];; | |
} | |
/* If the class names are already in use, loop until we've got a unique name */ | |
if (NSClassFromString(className) != nil) { | |
for (NSUInteger i = 0; i < NSUIntegerMax; i++) { | |
if (i == NSUIntegerMax) { | |
[NSException raise: NSInternalInconsistencyException format: @"Couldn't find a unique test name for %@. You must have an impressive number of tests.", className]; | |
__builtin_trap(); | |
} | |
NSMutableString *proposedName = [NSMutableString stringWithFormat:@"%@%" PRIu64, className, (uint64_t) i]; | |
if (NSClassFromString(proposedName) == nil) { | |
className = proposedName; | |
break; | |
} | |
} | |
} | |
/* Allocate the new class */ | |
Class cls = objc_allocateClassPair([XCTestCase class], [className UTF8String], 0); | |
if (cls == nil) { | |
[NSException raise: NSInternalInconsistencyException format: @"Could not allocate test class: %@", className]; | |
__builtin_trap(); | |
} | |
/* Add XCTestCase methods */ | |
{ // +defaultTestSuite | |
Method m = class_getInstanceMethod([[XCTestCase class] class], @selector(defaultTestSuite)); | |
class_addMethod(object_getClass(cls), @selector(defaultTestSuite), (IMP) xsm_int_meth_impl_cls_defaultTestSuite, method_getTypeEncoding(m)); | |
} | |
/* Add XCTest methods */ | |
{ // -name | |
Method m = class_getInstanceMethod([[XCTestCase class] class], @selector(name)); | |
class_addMethod(cls, @selector(name), (IMP) xsm_int_meth_impl_name, method_getTypeEncoding(m)); | |
} | |
/* Register the new class */ | |
objc_registerClassPair(cls); | |
return cls; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment