Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Sources/Classes/Internal/SLTestState.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// SLTestState.h
// Subliminal
//
// Created by Jacob Relkin on 8/22/14.
// Copyright (c) 2014 Inkling. All rights reserved.
//

#import <Foundation/Foundation.h>

@class SLTestFailure;

/**
SLTestState objects define the state of a Subliminal test or test case.
*/

@interface SLTestState : NSObject

/**
Denotes whether the test or test case failed
*/
@property (nonatomic, readonly) BOOL failed;

/**
If the test or test case failure was expected.
*/
@property (nonatomic, readonly) BOOL failureWasExpected;

/**
Sets the properties above, from the failure's description.
The value of `failureWasExpected` is determined by the first failure that was encountered.

@param failure The test failure to record.
*/
- (void)recordFailure:(SLTestFailure *)failure;

@end
31 changes: 31 additions & 0 deletions Sources/Classes/Internal/SLTestState.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// SLTestState.m
// Subliminal
//
// Created by Jacob Relkin on 8/22/14.
// Copyright (c) 2014 Inkling. All rights reserved.
//

#import "SLTestState.h"
#import "SLTestFailure.h"

@interface SLTestState ()

@property (nonatomic, readwrite) BOOL failed;
@property (nonatomic, readwrite) BOOL failureWasExpected;

@end

@implementation SLTestState

- (void)recordFailure:(SLTestFailure *)failure {
NSParameterAssert(failure);

if (!self.failed) {
self.failureWasExpected = failure.isExpected;
}

self.failed = YES;
}

@end
11 changes: 11 additions & 0 deletions Sources/Classes/SLTest.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#import "SLTestController+AppHooks.h"
#import "SLStringUtilities.h"

@class SLTestFailure;

/**
`SLTest` is the abstract superclass of Subliminal integration tests.

Expand Down Expand Up @@ -366,6 +368,15 @@
*/
+ (NSUInteger)runGroup;

/**
If overridden, provides a hook into test and test case failures.

@param failure An object which describes the failure.
@warning This method will be invoked for each exception that is handled by the test framework.
@see -[SLTestFailure failureWithException:phase:testCaseSelector:]
*/
- (void)testDidEncounterFailure:(SLTestFailure *)failure;

@end


Expand Down
80 changes: 47 additions & 33 deletions Sources/Classes/SLTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@
#import <objc/runtime.h>
#import <objc/message.h>

#import "SLTestFailure.h"
#import "SLTestState.h"

// All exceptions thrown by SLTest must have names beginning with this prefix
// so that `-[SLTest exceptionByAddingFileInfo:]` can determine whether to attach
// call site information to exceptions.
static NSString *const SLTestExceptionNamePrefix = @"SLTest";



@implementation SLTest

static NSString *__lastKnownFilename;
Expand Down Expand Up @@ -330,26 +331,38 @@ + (NSString *)unfocusedTestCaseName:(NSString *)testCase {
return testCase;
}

- (void)reportFailureInPhase:(SLTestFailurePhase)phase toState:(SLTestState *)state exception:(NSException *)exception testCaseSelector:(SEL)testCaseSelector {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admire the impulse to keep SLTest as stateless as possible though it does make naming this method tricky, haha.

What do you think about -reportFailureWithException:inPhase:withTestCaseSelector:state:? The idea being to keep the test state parameters contiguous. This mirrors SLTestFailure's initializer too.

SLTestFailure *failure = [SLTestFailure failureWithException:exception phase:phase testCaseSelector:testCaseSelector];
NSException *exceptionToLog = [self exceptionByAddingFileInfo:exception];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this should be the exception that's recorded in the failure, as the most complete record of the event.

[[SLLogger sharedLogger] logException:exceptionToLog
expected:[failure isExpected]];
[state recordFailure:failure];
[self testDidEncounterFailure:failure];
}

- (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted
failed:(NSUInteger *)numCasesFailed
failedUnexpectedly:(NSUInteger *)numCasesFailedUnexpectedly {
NSUInteger numberOfCasesExecuted = 0, numberOfCasesFailed = 0, numberOfCasesFailedUnexpectedly = 0;
SLTestState *testState = [SLTestState new];

BOOL testDidFailInSetUpOrTearDown = NO;
@try {
[self setUpTest];
}
@catch (NSException *exception) {
[[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception]
expected:[[self class] exceptionWasExpected:exception]];
testDidFailInSetUpOrTearDown = YES;
[self reportFailureInPhase:SLTestFailurePhaseTestSetup
toState:testState
exception:exception
testCaseSelector:NULL];
}

// if setUpTest failed, skip the test cases
if (!testDidFailInSetUpOrTearDown) {
if (!testState.failed) {
NSString *test = NSStringFromClass([self class]);
for (NSString *testCaseName in [[self class] testCasesToRun]) {
@autoreleasepool {
SLTestState *testCaseState = [SLTestState new];

// all logs below use the focused name, so that the logs are consistent
// with what's actually running
[[SLLogger sharedLogger] logTest:test caseStart:testCaseName];
Expand All @@ -362,29 +375,29 @@ - (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted
// (though we can't guarantee it won't be reused within a test case)
[SLTest clearLastKnownCallSite];

BOOL caseFailed = NO, failureWasExpected = NO;
@try {
[self setUpTestCaseWithSelector:unfocusedTestCaseSelector];
}
@catch (NSException *exception) {
caseFailed = YES;
failureWasExpected = [[self class] exceptionWasExpected:exception];
[[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception]
expected:failureWasExpected];
[self reportFailureInPhase:SLTestFailurePhaseTestCaseSetup
toState:testCaseState
exception:exception
testCaseSelector:unfocusedTestCaseSelector];
}

// Only execute the test case if set-up succeeded.
if (!caseFailed) {
if (!testCaseState.failed) {
@try {
// We use objc_msgSend so that Clang won't complain about performSelector leaks
// Make sure to send the actual test case selector
((void(*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(testCaseName));
}
@catch (NSException *exception) {
caseFailed = YES;
failureWasExpected = [[self class] exceptionWasExpected:exception];
[[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception]
expected:failureWasExpected];
[self reportFailureInPhase:SLTestFailurePhaseTestCaseExecution
toState:testCaseState
exception:exception
testCaseSelector:unfocusedTestCaseSelector];

}
}

Expand All @@ -394,22 +407,22 @@ - (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted
[self tearDownTestCaseWithSelector:unfocusedTestCaseSelector];
}
@catch (NSException *exception) {
BOOL caseHadFailed = caseFailed;
caseFailed = YES;
// don't override `failureWasExpected` if we had already failed
BOOL exceptionWasExpected = [[self class] exceptionWasExpected:exception];
if (!caseHadFailed) failureWasExpected = exceptionWasExpected;
[[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception]
expected:exceptionWasExpected];
[self reportFailureInPhase:SLTestFailurePhaseTestCaseTeardown
toState:testCaseState
exception:exception
testCaseSelector:unfocusedTestCaseSelector];
}

if (caseFailed) {
[[SLLogger sharedLogger] logTest:test caseFail:testCaseName expected:failureWasExpected];
if (testCaseState.failed) {
[[SLLogger sharedLogger] logTest:test caseFail:testCaseName expected:testCaseState.failureWasExpected];
numberOfCasesFailed++;
if (!failureWasExpected) numberOfCasesFailedUnexpectedly++;
if (!testCaseState.failureWasExpected) {
numberOfCasesFailedUnexpectedly++;
}
} else {
[[SLLogger sharedLogger] logTest:test casePass:testCaseName];
}

numberOfCasesExecuted++;
}
}
Expand All @@ -420,16 +433,17 @@ - (BOOL)runAndReportNumExecuted:(NSUInteger *)numCasesExecuted
[self tearDownTest];
}
@catch (NSException *exception) {
[[SLLogger sharedLogger] logException:[self exceptionByAddingFileInfo:exception]
expected:[[self class] exceptionWasExpected:exception]];
testDidFailInSetUpOrTearDown = YES;
[self reportFailureInPhase:SLTestFailurePhaseTestTeardown
toState:testState
exception:exception
testCaseSelector:NULL];
}

if (numCasesExecuted) *numCasesExecuted = numberOfCasesExecuted;
if (numCasesFailed) *numCasesFailed = numberOfCasesFailed;
if (numCasesFailedUnexpectedly) *numCasesFailedUnexpectedly = numberOfCasesFailedUnexpectedly;

return !testDidFailInSetUpOrTearDown;
return !testState.failed;
}

- (void)wait:(NSTimeInterval)interval {
Expand Down Expand Up @@ -468,9 +482,9 @@ - (NSException *)exceptionByAddingFileInfo:(NSException *)exception {
return exception;
}

+ (BOOL)exceptionWasExpected:(NSException *)exception {
return [[exception name] isEqualToString:SLTestAssertionFailedException];
}
// Abstract
- (void)testDidEncounterFailure:(SLTestFailure *)failure {}


@end

53 changes: 53 additions & 0 deletions Sources/Classes/SLTestFailure.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// SLTestCaseFailure.h
// Subliminal
//
// Created by Jacob Relkin on 6/20/14.
// Copyright (c) 2014 Inkling. All rights reserved.
//

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, SLTestFailurePhase) {
SLTestFailurePhaseTestSetup,
SLTestFailurePhaseTestCaseSetup,
SLTestFailurePhaseTestCaseExecution,
SLTestFailurePhaseTestCaseTeardown,
SLTestFailurePhaseTestTeardown
};

/**
SLTestFailure objects hold failure information for Subliminal test and test cases.
*/

@interface SLTestFailure : NSObject

/**
@param exception The exception that was thrown to cause this failure.
@param phase The phase in the test lifecycle in which the failure happened.
@param testCaseSelector The failed test case's selector. (can be NULL)
@return A new SLTestFailure object.
*/
+ (instancetype)failureWithException:(NSException *)exception phase:(SLTestFailurePhase)phase testCaseSelector:(SEL)testCaseSelector;

/**
The phase in the test lifecycle in which the failure happened.
*/
@property (nonatomic, readonly, assign) SLTestFailurePhase phase;

/**
The failed test case's selector. (can be NULL)
*/
@property (nonatomic, readonly, assign) SEL testCaseSelector;

/**
The exception that was thrown to cause this failure.
*/
@property (nonatomic, readonly, strong) NSException *exception;

/**
If the failure was expected.
*/
@property (nonatomic, readonly, getter = isExpected) BOOL expected;

@end
47 changes: 47 additions & 0 deletions Sources/Classes/SLTestFailure.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// SLTestCaseFailure.m
// Subliminal
//
// Created by Jacob Relkin on 6/20/14.
// Copyright (c) 2014 Inkling. All rights reserved.
//

#import "SLTestFailure.h"
#import "SLTest.h"

@interface SLTestFailure ()

@property (nonatomic, readwrite, strong) NSException *exception;
@property (nonatomic, readwrite, assign) SEL testCaseSelector;
@property (nonatomic, readwrite, assign) SLTestFailurePhase phase;

@end

@implementation SLTestFailure

+ (instancetype)failureWithException:(NSException *)exception phase:(SLTestFailurePhase)phase testCaseSelector:(SEL)testCaseSelector {
SLTestFailure *failure = [self new];
failure.phase = phase;
failure.exception = exception;
failure.testCaseSelector = testCaseSelector;
return failure;
}

- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[SLTestFailure class]]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-isEqual:'s docs say that you've got to override -hash too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call.

return NO;
}

SLTestFailure *otherObject = object;
return (otherObject.phase == self.phase &&
otherObject.testCaseSelector == self.testCaseSelector &&
otherObject.isExpected == self.isExpected &&
[otherObject.exception.name isEqualToString:self.exception.name] &&
[otherObject.exception.reason isEqualToString:self.exception.reason]);
}

- (BOOL)isExpected {
return [self.exception.name isEqualToString:SLTestAssertionFailedException];
}

@end
1 change: 1 addition & 0 deletions Sources/Subliminal.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#import "SLTestController+AppHooks.h"
#import "SLTest.h"
#import "SLTestAssertions.h"
#import "SLTestFailure.h"

#import "SLDevice.h"
#import "SLElement.h"
Expand Down
Loading