From 2a330ef4ff876970a8193515ce422cd2a4938868 Mon Sep 17 00:00:00 2001 From: Rob Blau Date: Thu, 23 Jun 2011 18:19:24 -0700 Subject: [PATCH] Updated doxygen docs. Fixed passing a JSON string to batch. Fixed returning attachment ID from upload. Added support for local file paths. Added MACROs to control logging to different levels. Uses ASL for Console logging. Added NSDate subclasses to tell the difference between Dates and DateTimes. More complete unit test coverage. Updated config defaults to values that work better for ASIHTTPRequests. Return the response from startSynchronous. Fixed queue management logic. Limit to 4 concurrent requests by default. --- Classes/ClientCapabilities.h | 3 +- Classes/ClientCapabilities.m | 1 + Classes/ServerCapabilities.h | 3 +- Classes/ServerCapabilities.m | 1 + Classes/Shotgun.h | 214 +-------------- Classes/Shotgun.m | 61 +++-- Classes/ShotgunConfig.h | 3 +- Classes/ShotgunConfig.m | 5 +- Classes/ShotgunDate.h | 37 +++ Classes/ShotgunDate.m | 84 ++++++ Classes/ShotgunEntity.h | 3 +- Classes/ShotgunEntity.m | 1 + Classes/ShotgunLogging.h | 39 +++ Classes/ShotgunRequest.h | 14 +- Classes/ShotgunRequest.m | 55 +++- Classes/ShotgunRequestPrivate.h | 3 +- .../SG API UnitTests/ShotgunApiLongTest.m | 137 ++++++---- UnitTests/SG API UnitTests/ShotgunApiTest.m | 248 +++++++++++++++--- .../SG API UnitTests/ShotgunApiTestBase.h | 12 +- .../SG API UnitTests/ShotgunApiTestBase.m | 75 +++++- 20 files changed, 653 insertions(+), 346 deletions(-) create mode 100644 Classes/ShotgunDate.h create mode 100644 Classes/ShotgunDate.m create mode 100644 Classes/ShotgunLogging.h diff --git a/Classes/ClientCapabilities.h b/Classes/ClientCapabilities.h index 8bab28f..3b4fd73 100644 --- a/Classes/ClientCapabilities.h +++ b/Classes/ClientCapabilities.h @@ -5,10 +5,11 @@ // Created by Rob Blau on 6/11/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ClientCapabilities.h A structure for storing information about the client. #import -@interface ClientCapabilities : NSObject; +@interface ClientCapabilities : NSObject @property (retain, readwrite, nonatomic) NSString *platform; @property (retain, readwrite, nonatomic) NSString *localPathField; diff --git a/Classes/ClientCapabilities.m b/Classes/ClientCapabilities.m index ec98bba..65d6d7f 100644 --- a/Classes/ClientCapabilities.m +++ b/Classes/ClientCapabilities.m @@ -5,6 +5,7 @@ // Created by Rob Blau on 6/11/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ClientCapabilities.m Implementation of ClientCapabilities #import "ClientCapabilities.h" diff --git a/Classes/ServerCapabilities.h b/Classes/ServerCapabilities.h index 3c8ed18..5e0b737 100644 --- a/Classes/ServerCapabilities.h +++ b/Classes/ServerCapabilities.h @@ -5,10 +5,11 @@ // Created by Rob Blau on 6/11/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ServerCapabilities.h A structure for storing information about the server. #import -@interface ServerCapabilities : NSObject; +@interface ServerCapabilities : NSObject @property (assign, readonly, nonatomic) BOOL isDev; @property (assign, readonly, nonatomic) BOOL hasPaging; diff --git a/Classes/ServerCapabilities.m b/Classes/ServerCapabilities.m index 69cafd2..0fcf229 100644 --- a/Classes/ServerCapabilities.m +++ b/Classes/ServerCapabilities.m @@ -5,6 +5,7 @@ // Created by Rob Blau on 6/11/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ServerCapabilities.m Implementation of ServerCapabilities #import "ServerCapabilities.h" diff --git a/Classes/Shotgun.h b/Classes/Shotgun.h index 5df7c04..4362760 100644 --- a/Classes/Shotgun.h +++ b/Classes/Shotgun.h @@ -5,217 +5,24 @@ // Created by Rob Blau on 6/8/11. // Copyright 2011 Laika. All rights reserved. // - -#pragma mark - Main Page Documentation - -/*! - * - * @mainpage %Shotgun API - * - * @section toc Table of Contents - *
    - *
  • @ref Introduction
  • - *
  • @ref Installation
  • - *
  • @ref Notes
  • - *
  • @ref details
  • - *
      - *
    • @ref connecting
    • - *
    • @ref finding
    • - *
    • @ref modifying
    • - *
    • @ref batch
    • - *
    • @ref schema
    • - *
    • @ref files
    • - *
    • @ref requests
    • - *
    - *
  • @ref TODOs
  • - *
  • @ref Dependencies
  • - *
  • @ref Links
  • - *
- * - * @section Introduction - * This is an objective c port of the python shotgun api. - * - * For complete documentation of how the %Shotgun API works along with - * tutorials, examples, and other details see:\n - * https://github.com/shotgunsoftware/python-api/wiki - * - * @section Installation - *
    - *
  • Start by downloading the code from https://github.com/shotgunsoftware/objc-api
  • - *
  • Copy the files in Classes directory into your project.
  • - *
  • Copy the dependencies into your project.\n\n - * All the dependencies are included as git submodules in the Dependencies directory.\n - * You can either download the dependencies yourself or (if you cloned the project via\n - * git rather than downloading the tarball) run 'git submodule init' followed by\n - * 'git submodule update' to download the projects at the revision as of the writing of\n - * this library.\n\n - *
      - *
    • json-framework - Copy the files in Classes into your project.
    • - *
    • asi-http-request (See the official docs for more in depth instructions). - *
        - *
      • Copy the files in Classes into your project (just the files, the directories are not needed)
      • - *
      • Copy the files in External/Reachability into your project.
      • - *
      • Link against CFNetwork, SystemConfiguration, MobileCoreServices, CoreGraphics and zlib
      • - *
      - *
    • - *
    - *
  • - *
- * See the example project under Examples to see what the resulting setup should look like. - * - * @section Notes - * \li All NSDate objects are assumed to be in UTC (This is the default NSDate behavior). - * - * @section details API Details - * @subsection connecting Connecting to Shotgun - * \code - * NSString *url = @"http://mysite.shotgunsoftware.com"; - * NSString *script = @"example_script"; - * NSString *key = @"abcdefghijklmnopqrstuvwxyz"; - * Shotgun *shotgun = [[[Shotgun alloc] initWithUrl:url scriptName:script andKey:key] autorelease]; - * \endcode - * - * @subsection finding Finding entities - * \code - * ShotgunRequest *request = - * [shotgun findEntityOfType:@"Version" - * withFilters:@"[[\"code\", \"starts_with\", \"100\"]]" - * andFields:@"[\"code\", \"image\"]"]; - * [request startSynchronous]; - * NSArray *results = [request response]; - * \endcode - * - * @subsection modifying Creating, modifying, deleting, and reviving entities - * \code - * ShotgunRequest *request = [shotgun createEntityOfType:@"Shot" - * withData:@"{\"code\": \"s10\", \"description\": \"Shot 10\"}"]; - * [request startSynchronous]; - * ShotgunEntity *shot = [request response]; - * \endcode - * \code - * ShotgunRequest *request = [shotgun updateEntityOfType:@"Shot" - * withId:[NSNumber numberWithInt:23] - * withData:@"{\"description\": \"Shot 20 - More Info\"}"]; - * [request startSynchronous]; - * ShotgunEntity *shot = [request response]; - * \endcode - * \code - * ShotgunRequest *request = [shotgun deleteEntityOfType:@"Shot" withId:[NSNumber numberWithInt:23]]; - * [request startSynchronous]; - * BOOL success = [[request response] boolValue]; - * \endcode - * \code - * ShotgunRequest *request = [shotgun reviveEntityOfType:@"Shot" withId:[NSNumber numberWithInt:23]]; - * [request startSynchronous]; - * BOOL success = [[request response] boolValue]; - * \endcode - * - * @subsection batch Batch operations - * \code - * ShotgunRequest *request = [shotgun batch:@"[" \ - * "{ " \ - * " \"request_type\": \"create\", " \ - * " \"entity_type\": \"Shot\", " \ - * " \"data\": { " \ - * " \"code\": \"s10\", " \ - * " \"description\": \"Shot 10\" " \ - * " } " \ - * "}, " \ - * "{\"request_type\": \"delete\", \"entity_type\": \"Shot\", \"entity_id\": 23}" \ - * ]"]; - * [request startSynchronous]; - * NSArray *results = [request response]; - * \endcode - * - * @subsection schema Meta-Schema queries - * \code - * ShotgunRequest *request = [shotgun schemaEntityRead]; - * [request startSynchronous]; - * NSDictionary *schemaInfo = [request response]; - * \endcode - * \code - * ShotgunRequest *request = [shotgun schemaRead]; - * [request startSynchronous]; - * NSDictionary *schema = [request response]; - * \endcode - * \code - * ShotgunRequest *request = [shotgun schemaFieldReadForEntityOfType:@"Shot" forField:@"sg_status_list"]; - * [request startSynchronous]; - * NSDictionary *entitySchema = [request response]; - * \endcode - * - * @subsection files Uploading and downloading files - * \code - * NSNumber *attachmentId = [shotgun uploadThumbnailForEntityOfType:@"Shot" - * withId:[NSNumber numberWithInt:23] - * fromPath:@"/path/to/the/file.jpg"]; - * \endcode - * \code - * NSData *imageData = downloadAttachmentWithId:[NSNumber numberWithInt:201]; - * \endcode - * - * @subsection requests Using ShotgunRequest Objects - * ShotgunRequests can be run either synchronously or asynchronously. - * - * To run a request syncronously simply call startSyncronously: - * \code - * [request startSyncronous]; - * \endcode - * The request will block the current thread until it is finished and its response is ready. - * - * To run a request asynchronously call startAsynchronous: - * \code - * [request startAsynchronous]; - * \endcode - * Control will return to the current thread right away. To process the response to the - * request, register callback blocks with request before starting it: - * \code - * [request setCompletionBlock:^{ - * id response = [request response]; - * // Do Stuff with the response - * }]; - * \endcode - * - * The currently supported callbacks are: - * \li startedBlock - Called when the request is started. - * \li completionBlock - Called when the request has finished. - * \li failedBlock - Called when the request failed. - * - * The postProcessBlock is used internally to the API and should not be overridden. - * - * @section TODOs - * @li Switch from Exceptions to NSErrors - * @li Add support for responding to events via delegate SELs - * @li Add support for asychronous image field resolution - * @li Better API around paging - * @li Finish documentation - * @li Round out unit tets. Use OCMock. - * @li Switch to a decent logging system - * @li \ref todo "Other inline TODOs" - * - * @section Dependencies - * \li ASIHTTPRequest: http://allseeing-i.com/ASIHTTPRequest/ - * \li SBJson: http://stig.github.com/json-framework/ - * \li GHUnit (only needed to run the unit tests): https://github.com/gabriel/gh-unit - * - * @section Links - * \li Python API: https://github.com/shotgunsoftware/python-api - * \li Mailing List: https://groups.google.com/group/shotgun-objc-api - * \li Issues: https://github.com/shotgunsoftware/objc-api/issues - * - * Rob Blau - * - */ +/// @file Shotgun.h The main import for the API. All public Shotgun functionality is defined. #pragma mark - Interface #import +#import "ShotgunDate.h" #import "ShotgunEntity.h" #import "ShotgunRequest.h" -/** Represents a connection to a shotgun server. */ -@interface Shotgun : NSObject; +/** + * Represents a connection to a shotgun server. + * + * @todo Implement Authentication + * @todo Finish file upload/download asynchronous option + * @todo Switch from NSException to NSError + */ +@interface Shotgun : NSObject #pragma mark - Initialize @@ -291,7 +98,6 @@ * @param entityType An NSString specifying the type of entity to return. * @param entityId An NSNumber with the id of the entity to update. * @param data An NSDictionary specifying values for fields on the new entity (or an NSString that is well formed JSON describing the same value). - * @param returnFields An NSArray of NSStrings specifying what fields to return (or an NSString that is well formed JSON describing the same value). * * @return A ShotgunRequest whose response is a ShotgunEntity representing the created entity populated with the specified @p returnFields. */ diff --git a/Classes/Shotgun.m b/Classes/Shotgun.m index d4554d8..c95be7b 100644 --- a/Classes/Shotgun.m +++ b/Classes/Shotgun.m @@ -5,19 +5,13 @@ // Created by Rob Blau on 6/8/11. // Copyright 2011 Laika. All rights reserved. // - -/*! - * @todo Implement Authentication - * @todo Figure out a way to do image url lookup in the background - * @todo Figure out how to handle date fields - * @todo Finish support for local paths - * @todo Finish file upload/download asynchronous option - */ +/// @file Shotgun.m The implementation of the main Shotgun API. #import "SBJson.h" #import "ASIHTTPRequest.h" #import "ASIFormDataRequest.h" +#import "ShotgunLogging.h" #import "ShotgunConfig.h" #import "ServerCapabilities.h" #import "ClientCapabilities.h" @@ -177,7 +171,6 @@ - (ShotgunRequest *)findEntitiesOfType:(NSString *)entityType withFilters:(id)fi [newFilters setObject:@"and" forKey:@"logical_operator"]; else [newFilters setObject:@"or" forKey:@"logical_operator"]; - NSMutableArray *conditions = [NSMutableArray array]; for (NSArray *filter in checkedFilters) [conditions addObject:[NSDictionary dictionaryWithObjectsAndKeys: @@ -359,8 +352,13 @@ - (ShotgunRequest *)reviveEntityOfType:(NSString *)entityType withId:(NSNumber * - (ShotgunRequest *)batch:(id)requests { + // Convert JSON arg to object + id checkedRequests = requests; + if ([checkedRequests isKindOfClass:[NSString class]]) + checkedRequests = [requests JSONValue]; + NSMutableArray *calls = [NSMutableArray array]; - for (NSDictionary *request in requests) { + for (NSDictionary *request in checkedRequests) { NSString *requestType = [request objectForKey:@"request_type"]; if ([requestType isEqualToString:@"create"]) { NSSet *requiredKeys = [NSSet setWithObjects:@"entity_type", @"data", nil]; @@ -541,13 +539,18 @@ - (NSNumber *)uploadForEntityOfType:(NSString *)entityType withId:(NSNumber *)en "%@: %@", path, error]; } NSString *response = [request responseString]; - NSLog(@"Response: %@", response); + SG_INFO(@"Upload Response: %@", response); if ([response characterAtIndex:0] != '1') [NSException raise:@"File upload error" format:@"Could not upload file successfully, " \ "but not sure why.\nPath: %@\nUrl: %@\nError: %@", path, url, response]; - NSNumber *resultId = [NSNumber numberWithInt:0]; - return resultId; + NSArray *splitResponse = [response componentsSeparatedByString:@":"]; + if ([splitResponse count] <= 1) + return [NSNumber numberWithInt:0]; + NSNumberFormatter *formatter = [[[NSNumberFormatter alloc] init] autorelease]; + NSArray *splitValue = [[splitResponse objectAtIndex:1] componentsSeparatedByString:@"\n"]; + [formatter setNumberStyle:NSNumberFormatterDecimalStyle]; + return [formatter numberFromString:[splitValue objectAtIndex:0]]; } - (NSData *)downloadAttachmentWithId:(NSNumber *)attachmentId @@ -672,7 +675,6 @@ - (NSArray *)parseRecords_:(id)records NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease]; [queue setMaxConcurrentOperationCount:4]; queue.name = @"Thumbnail Converting Queue"; - NSLog(@"Converting thumbs on queue: %@", queue.name); if (![records isKindOfClass:[NSArray class]]) iteratee = [NSArray arrayWithObject:records]; else @@ -698,6 +700,15 @@ - (NSArray *)parseRecords_:(id)records }]; continue; } + + if ([value isKindOfClass:[NSDictionary class]] && + [[value objectForKey:@"link_type"] isEqualToString:@"local"]) { + NSString *localPath = [value objectForKey:self.clientCaps.localPathField]; + if (localPath != Nil) { + [value setObject:localPath forKey:@"local_path"]; + [value setObject:[NSString stringWithFormat:@"file://%@", localPath] forKey:@"url"]; + } + } } } [queue waitUntilAllOperationsAreFinished]; @@ -717,7 +728,7 @@ - (NSString *)buildThumbUrlForEntity_:(ShotgunEntity *)entity NSArray *parts = [body componentsSeparatedByString:@"\n"]; NSInteger code = [(NSString *)[parts objectAtIndex:0] integerValue]; if (code == 0) - NSLog(@"Error getting thumbnail url for entity %@ response was '%@'", entity, body); + SG_ERROR(@"Error getting thumbnail url for entity %@ response was '%@'", entity, body); if (code == 1) { NSString *path = [parts objectAtIndex:1]; if ([path length] == 0) @@ -726,7 +737,7 @@ - (NSString *)buildThumbUrlForEntity_:(ShotgunEntity *)entity return [url absoluteString]; } - NSLog(@"Error getting thumbnail url: Unknown code %d %@", code, parts); + SG_ERROR(@"Error getting thumbnail url: Unknown code %d %@", code, parts); return Nil; } @@ -778,9 +789,18 @@ - (id)transformOutboundData_:(id)data { id(^outboundVisitor)(id) = ^(id value) { if ([value isKindOfClass:[NSDate class]]) { - NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; - NSString *ret = [formatter stringFromDate:value]; + NSString *ret = Nil; + if ([value isKindOfClass:[ShotgunDateTime class]]) { + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; + ret = [formatter stringFromDate:value]; + } else if ([value isKindOfClass:[ShotgunDate class]]) { + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setDateFormat:@"yyyy-MM-dd"]; + ret = [formatter stringFromDate:value]; + } else { + [NSException raise:@"Shotgun Error" format:@"Cannot pass in a NSDate, must be ShotgunDate or ShotgunDateTime"]; + } return ret ? ret : value; } return value; @@ -796,7 +816,8 @@ - (id)transformInboundData_:(id)data if([value length] == 20) { NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; - NSDate *date = [formatter dateFromString:value]; + NSDate *convertedDate = [formatter dateFromString:value]; + ShotgunDateTime *date = [ShotgunDateTime dateTimeWithDate:convertedDate]; return date ? date : value; } } diff --git a/Classes/ShotgunConfig.h b/Classes/ShotgunConfig.h index 912e990..944a7a0 100644 --- a/Classes/ShotgunConfig.h +++ b/Classes/ShotgunConfig.h @@ -5,11 +5,12 @@ // Created by Rob Blau on 6/8/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ShotgunConfig.h A structure for storing information about how to interact with the server. #import -@interface ShotgunConfig : NSObject; +@interface ShotgunConfig : NSObject @property (assign, readwrite, nonatomic) NSUInteger maxRpcAttempts; @property (assign, readwrite, nonatomic) NSUInteger timeoutSecs; diff --git a/Classes/ShotgunConfig.m b/Classes/ShotgunConfig.m index e70797d..cf4a8de 100644 --- a/Classes/ShotgunConfig.m +++ b/Classes/ShotgunConfig.m @@ -5,6 +5,7 @@ // Created by Rob Blau on 6/8/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ShotgunConfig.m Implementation of ShotgunConfig #import "ShotgunConfig.h" @@ -32,9 +33,9 @@ - (id)init self = [super init]; if (self) { self.maxRpcAttempts = 3; - self.timeoutSecs = 3; + self.timeoutSecs = 20; self.apiVer = @"api3"; - self.recordsPerPage = 500; + self.recordsPerPage = 100; self.apiKey = Nil; self.scheme = Nil; self.server = Nil; diff --git a/Classes/ShotgunDate.h b/Classes/ShotgunDate.h new file mode 100644 index 0000000..7f9fd9a --- /dev/null +++ b/Classes/ShotgunDate.h @@ -0,0 +1,37 @@ +// ShotgunDate.h +// UnitTests +// +// Created by Rob Blau on 6/23/11. +// Copyright 2011 Laika. All rights reserved. +// +/// @file ShotgunDate.h Classes implementing Date handling. + +#import + +/** A thin wrapper around NSDate to be able to differentiate date objects from datetime objects. */ +@interface ShotgunDate : NSDate + +/*! Create a ShotgunDate object + * + * @param date An NSDate object to convert to a ShotgunDate + */ ++ (id)dateWithDate:(NSDate *)date; + +/** Format the date to be compatible with a value in a JSON string. */ +- (NSString *)descriptionWithLocale:(id)locale; + +@end + +/** A thin wrapper around NSDate to be able to differentiate date objects from datetime objects. */ +@interface ShotgunDateTime : NSDate + +/*! Create a ShotgunDateTime object + * + * @param date An NSDate object to convert to a ShotgunDateTime + */ ++ (id)dateTimeWithDate:(NSDate *)date; + +/** Format the date to be compatible with a value in a JSON string. */ +- (NSString *)descriptionWithLocale:(id)locale; + +@end \ No newline at end of file diff --git a/Classes/ShotgunDate.m b/Classes/ShotgunDate.m new file mode 100644 index 0000000..09e0e27 --- /dev/null +++ b/Classes/ShotgunDate.m @@ -0,0 +1,84 @@ +// +// ShotgunDate.m +// UnitTests +// +// Created by Rob Blau on 6/23/11. +// Copyright 2011 Laika. All rights reserved. +// +/// @file ShotgunDate.m Date and DateTime implementation. + +#import "ShotgunDate.h" + +@interface ShotgunDate () +@property (assign, readwrite, nonatomic) NSTimeInterval ti; +@end + +@implementation ShotgunDate + +@synthesize ti = ti_; + ++ (id)dateWithDate:(NSDate *)date +{ + return [[[ShotgunDate alloc] initWithTimeIntervalSince1970:[date timeIntervalSince1970]] autorelease]; +} + +- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)secsToBeAdded +{ + self = [super init]; + if (self) { + self.ti = [[NSString stringWithFormat:@"%,0f", secsToBeAdded] floatValue]; + } + return self; +} + +- (NSTimeInterval)timeIntervalSinceReferenceDate +{ + return self.ti; +} + +- (NSString *)descriptionWithLocale:(id)locale +{ + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setDateFormat:@"'\"'yyyy-MM-dd'\"'"]; + NSString *ret = [formatter stringFromDate:self]; + return ret; +} + +@end + +@interface ShotgunDateTime () +@property (assign, readwrite, nonatomic) NSTimeInterval ti; +@end + +@implementation ShotgunDateTime + +@synthesize ti = ti_; + ++ (id)dateTimeWithDate:(NSDate *)date +{ + return [[[ShotgunDateTime alloc] initWithTimeIntervalSince1970:[date timeIntervalSince1970]] autorelease]; +} + +- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)secsToBeAdded +{ + self = [super init]; + if (self) { + self.ti = [[NSString stringWithFormat:@"%,0f", secsToBeAdded] floatValue]; + } + return self; +} + +- (NSTimeInterval)timeIntervalSinceReferenceDate +{ + return self.ti; +} + +- (NSString *)descriptionWithLocale:(id)locale +{ + NSDateFormatter *formatter = [[[NSDateFormatter alloc] init] autorelease]; + [formatter setDateFormat:@"'\"'yyyy-MM-dd'T'HH:mm:ss'Z\"'"]; + NSString *ret = [formatter stringFromDate:self]; + return ret; +} + +@end diff --git a/Classes/ShotgunEntity.h b/Classes/ShotgunEntity.h index 57c8e8b..d731564 100644 --- a/Classes/ShotgunEntity.h +++ b/Classes/ShotgunEntity.h @@ -5,6 +5,7 @@ // Created by Rob Blau on 6/15/11. // Copyright 2011 __MyCompanyName__. All rights reserved. // +/// @file ShotgunEntity.h Interface for a wrapper around dictionaries for Shotgun logic. #import @@ -13,7 +14,7 @@ * @details * This is a thin wrapper around NSMutableDictionary. */ -@interface ShotgunEntity : NSMutableDictionary; +@interface ShotgunEntity : NSMutableDictionary @property (retain, readonly, nonatomic) NSNumber *entityId; ///< The id of the entity in %Shotgun @property (retain, readonly, nonatomic) NSString *entityType; ///< The type of the entity in %Shotgun diff --git a/Classes/ShotgunEntity.m b/Classes/ShotgunEntity.m index 7295f4a..71ebbef 100644 --- a/Classes/ShotgunEntity.m +++ b/Classes/ShotgunEntity.m @@ -5,6 +5,7 @@ // Created by Rob Blau on 6/15/11. // Copyright 2011 __MyCompanyName__. All rights reserved. // +/// @file ShotgunEntity.m Implementation of ShotgunEntity objects. #import "SBJson.h" diff --git a/Classes/ShotgunLogging.h b/Classes/ShotgunLogging.h new file mode 100644 index 0000000..b811c59 --- /dev/null +++ b/Classes/ShotgunLogging.h @@ -0,0 +1,39 @@ +// +// ShotgunLogging.h +// ShotgunApi +// +// Created by Rob Blau on 6/22/11. +// Copyright 2011 Laika. All rights reserved. +// +// From: +// http://wranglingmacs.blogspot.com/2009/04/improving-on-nslog.html +// http://icodesnip.com/snippet/objective-c/a-better-logging-solution +// +/// @file ShotgunLogging.h Macros to make logging easier. + +#include + +#ifndef ASL_KEY_FACILITY + #define ASL_KEY_FACILITY "com.laika.objc-ShotgunApi" +#endif + +#ifdef DEBUG + #define SG_NSLOG_LEVEL ASL_LEVEL_INFO +#else + #define SG_NSLOG_LEVEL ASL_LEVEL_WARNING +#endif + +#define SGLOG_LEVEL(log_level, format, ...) { \ + asl_log(NULL, NULL, log_level, "[%s:%d%s] %s", __FILE__, __LINE__, __FUNCTION__, [[NSString stringWithFormat:format, ##__VA_ARGS__] UTF8String]); \ + if (log_level <= SG_NSLOG_LEVEL) \ + NSLog(@"[%@:%d%s] %@", [[NSString stringWithUTF8String:__FILE__] lastPathComponent], __LINE__, __FUNCTION__, [NSString stringWithFormat:format, ##__VA_ARGS__]); \ +} + +#define SG_EMERG(format, ...) SGLOG_LEVEL(ASL_LEVEL_EMERG, format, ##__VA_ARGS__) +#define SG_ALERT(format, ...) SGLOG_LEVEL(ASL_LEVEL_ALERT, format, ##__VA_ARGS__) +#define SG_CRIT(format, ...) SGLOG_LEVEL(ASL_LEVEL_CRIT, format, ##__VA_ARGS__) +#define SG_ERROR(format, ...) SGLOG_LEVEL(ASL_LEVEL_ERR, format, ##__VA_ARGS__) +#define SG_WARN(format, ...) SGLOG_LEVEL(ASL_LEVEL_WARNING, format, ##__VA_ARGS__) +#define SG_NOTICE(format, ...) SGLOG_LEVEL(ASL_LEVEL_NOTICE, format, ##__VA_ARGS__) +#define SG_INFO(format, ...) SGLOG_LEVEL(ASL_LEVEL_INFO, format, ##__VA_ARGS__) +#define SG_DEBUG(format, ...) SGLOG_LEVEL(ASL_LEVEL_DEBUG, format, ##__VA_ARGS__) \ No newline at end of file diff --git a/Classes/ShotgunRequest.h b/Classes/ShotgunRequest.h index 937877e..fc3ada8 100644 --- a/Classes/ShotgunRequest.h +++ b/Classes/ShotgunRequest.h @@ -5,6 +5,7 @@ // Created by Rob Blau on 6/15/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ShotgunRequest.h Interface for an actual request to Shotgun #import @@ -20,7 +21,7 @@ typedef id (^ShotgunPostProcessBlock)(NSDictionary *, NSString *); #pragma mark ShotgunRequest /** Represents a simple request being made to a %Shotgun instance. */ -@interface ShotgunRequest : NSOperation ; +@interface ShotgunRequest : NSOperation /*! Initialize a request * @@ -40,8 +41,11 @@ typedef id (^ShotgunPostProcessBlock)(NSDictionary *, NSString *); */ - (id)initWithConfig:(ShotgunConfig *)config path:(NSString *)path body:(NSString *)body headers:(NSDictionary *)headers andHTTPMethod:(NSString *)method; -/** Start the connection blocking the current thread until the request is finished */ -- (void)startSynchronous; +/** Start the connection blocking the current thread until the request is finished + * + * @return The equivalent of calling [request response] + */ +- (id)startSynchronous; /** Start the connection */ - (void)startAsynchronous; @@ -56,7 +60,7 @@ typedef id (^ShotgunPostProcessBlock)(NSDictionary *, NSString *); @property (copy, readwrite, nonatomic) ShotgunRequestBlock completionBlock; ///< The block called when the request completes @property (copy, readwrite, nonatomic) ShotgunRequestBlock failedBlock; ///< The block called when the request errors -@property (assign, readonly, nonatomic) BOOL isFinished; -@property (assign, readonly, nonatomic) BOOL isExecuting; +@property (assign, readonly, nonatomic) BOOL isFinished; ///< Whether the Operation is finished or not. +@property (assign, readonly, nonatomic) BOOL isExecuting; ///< Whether the Operation is running or not. @end \ No newline at end of file diff --git a/Classes/ShotgunRequest.m b/Classes/ShotgunRequest.m index 1a1a565..8421039 100644 --- a/Classes/ShotgunRequest.m +++ b/Classes/ShotgunRequest.m @@ -5,6 +5,8 @@ // Created by Rob Blau on 6/15/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ShotgunRequest.m Implementation of Request handling. +/// @todo Make requests copyable #import "SBJson.h" #import "ASIHTTPRequest.h" @@ -12,9 +14,12 @@ #import "ShotgunConfig.h" #import "ShotgunEntity.h" +#import "ShotgunLogging.h" #import "ShotgunRequest.h" #import "ShotgunRequestPrivate.h" +static NSOperationQueue *sharedQueue = Nil; + @interface ShotgunRequest() @property (retain, readwrite, nonatomic) id response; @@ -65,6 +70,15 @@ @implementation ShotgunRequest @synthesize headers = headers_; @synthesize method = method_; ++ (void)initialize +{ + if (self == [ShotgunRequest class]) { + sharedQueue = [[NSOperationQueue alloc] init]; + [sharedQueue setName:@"Shotgun Request Default Queue"]; + [sharedQueue setMaxConcurrentOperationCount:4]; + } +} + + (id)shotgunRequestWithConfig:(ShotgunConfig *)config path:(NSString *)path body:(NSString *)body headers:(NSDictionary *)headers andHTTPMethod:(NSString *)method { return [[[ShotgunRequest alloc] initWithConfig:config path:path body:body headers:headers andHTTPMethod:method] autorelease]; @@ -74,19 +88,20 @@ - (id)initWithConfig:(ShotgunConfig *)config path:(NSString *)path body:(NSStrin { self = [super init]; if (self) { - self.queue = [NSOperationQueue mainQueue]; self.config = config; self.path = path; self.body = body; self.headers = headers; self.method = method; + self.queue = sharedQueue; } return self; } -- (void)startSynchronous +- (id)startSynchronous { [self startSynchronous:YES]; + return [self response]; } - (void)startSynchronous:(BOOL)synchronous @@ -98,16 +113,18 @@ - (void)startSynchronous:(BOOL)synchronous self.currentAttempt = 0; self.maxAttempts = self.config.maxRpcAttempts; + self.timeout = self.config.timeoutSecs; self.request = [self makeRequest]; - NSLog(@"Request is %@:%@", [self.request requestMethod], [self.request url]); - NSLog(@"Request headers are %@", [self.request requestHeaders]); - NSLog(@"Request body is %@", [NSString stringWithUTF8String:[[self.request postBody] bytes]]); + NSString *body = [[[NSString alloc] initWithData:[self.request postBody] encoding:NSUTF8StringEncoding] autorelease]; + SG_INFO(@"\nStarting %@ request %@ with body\n-----------------------\n%@\n-----------------------", + synchronous ? @"synchronous" : @"asynchronous", + [self.request requestID], + body); if (synchronous == YES) { [self.request startSynchronous]; [self finishSynchronous:YES]; } else { - NSLog(@"Started on queue: %@", [self.queue name]); [self willChangeValueForKey:@"isExecuting"]; self.isExecuting = YES; [self didChangeValueForKey:@"isExecuting"]; @@ -120,6 +137,11 @@ - (void)startSynchronous:(BOOL)synchronous - (void)startAsynchronous { + int runningCount = 0; + NSArray *ops = [self.queue operations]; + for (NSOperation *op in ops) + runningCount += ([op isExecuting]) ? 1 : 0; + SG_INFO(@"ASYNC Starting: Queue count (%d/%d running)", runningCount, [ops count]); [self.queue addOperation:self]; } @@ -133,6 +155,7 @@ - (void)continueSynchronous:(BOOL)synchronous if (synchronous == YES) { while (self.currentAttempt < self.maxAttempts) { self.currentAttempt += 1; + SG_INFO(@"Request failed, trying again (try %d). %@", self.currentAttempt, [self.request error]); self.request = [self makeRequest]; [self.request startSynchronous]; NSData *data = [self.request responseData]; @@ -150,6 +173,7 @@ - (void)finishSynchronous:(BOOL)synchronous { NSData *data = [self.request responseData]; if ((data == Nil) && (self.currentAttempt < self.maxAttempts)) { + SG_INFO(@"Request failed, trying again."); [self continueSynchronous:synchronous]; return; } @@ -157,10 +181,10 @@ - (void)finishSynchronous:(BOOL)synchronous self.response = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease]; if (self.postProcessBlock) self.response = [self.postProcessBlock([self.request responseHeaders], self.response) retain]; - NSLog(@"Response status is %d %@", [self.request responseStatusCode], [self.request responseStatusMessage]); - NSLog(@"Response headers are %@", [self.request responseHeaders]); - NSLog(@"Response is (rc %d) %@", [self.response retainCount], self.response); - NSLog(@"Completed rpc call to %@", [self.request requestMethod]); + SG_INFO(@"Finished request %@", [self.request requestID]); + SG_DEBUG(@"Response status is %d %@", [self.request responseStatusCode], [self.request responseStatusMessage]); + SG_DEBUG(@"Response headers are %@", [self.request responseHeaders]); + SG_DEBUG(@"Response is %@", self.response); if (synchronous == NO) { [self willChangeValueForKey:@"isExecuting"]; @@ -175,13 +199,21 @@ - (void)finishSynchronous:(BOOL)synchronous #pragma mark ASIHTTPRequest Delegate +- (void)requestStarted:(ASIHTTPRequest *)request +{ + SG_INFO(@"ASYNC Started: %@", [request requestID]); +} + - (void)requestFinished:(ASIHTTPRequest *)request { + SG_INFO(@"ASYNC Finished: %@", [request requestID]); [self finishSynchronous:NO]; } - (void)requestFailed:(ASIHTTPRequest *)request { + NSString *body = [[[NSString alloc] initWithData:[self.request postBody] encoding:NSUTF8StringEncoding] autorelease]; + SG_INFO(@"ASYNC Failed (current %d): %@\n%@\n-----------\n%@\n----------", self.currentAttempt, [request requestID], [request error], body); /// @todo Set class error if (self.currentAttempt < self.maxAttempts) { [self continueSynchronous:NO]; @@ -217,7 +249,8 @@ - (ASIHTTPRequest *)makeRequest [aRequest setRequestMethod:self.method]; [aRequest setRequestHeaders:[NSMutableDictionary dictionaryWithDictionary:self.headers]]; [aRequest setShouldAttemptPersistentConnection:YES]; - [aRequest setTimeOutSeconds:self.config.timeoutSecs]; + [aRequest setTimeOutSeconds:self.timeout]; + [aRequest setNumberOfTimesToRetryOnTimeout:self.config.maxRpcAttempts]; return aRequest; } diff --git a/Classes/ShotgunRequestPrivate.h b/Classes/ShotgunRequestPrivate.h index 8343580..7eee98e 100644 --- a/Classes/ShotgunRequestPrivate.h +++ b/Classes/ShotgunRequestPrivate.h @@ -1,10 +1,11 @@ // -// ShotgunRequest.m +// ShotgunRequestPrivate.m // UnitTests // // Created by Rob Blau on 6/15/11. // Copyright 2011 Laika. All rights reserved. // +/// @file ShotgunRequestPrivate.h Interface used by friend classes. #import "ShotgunRequest.h" diff --git a/UnitTests/SG API UnitTests/ShotgunApiLongTest.m b/UnitTests/SG API UnitTests/ShotgunApiLongTest.m index 78a27b7..f78d8cb 100644 --- a/UnitTests/SG API UnitTests/ShotgunApiLongTest.m +++ b/UnitTests/SG API UnitTests/ShotgunApiLongTest.m @@ -13,14 +13,15 @@ @interface ShotgunApiLongTest : ShotgunApiTestBase {} @implementation ShotgunApiLongTest -// Test Schema +#pragma mark Test Schema + - (NSArray *)schemaTestRequests { - ShotgunRequest *req1 = [shotgun schemaEntityRead]; - ShotgunRequest *req2 = [shotgun schemaRead]; - ShotgunRequest *req3 = [shotgun schemaFieldReadForEntityOfType:@"Version"]; - ShotgunRequest *req4 = [shotgun schemaFieldReadForEntityOfType:@"Version" forField:@"user"]; - ShotgunRequest *req5 = [shotgun schemaFieldCreateForEntityOfType:@"Version" + ShotgunRequest *req1 = [self.shotgun schemaEntityRead]; + ShotgunRequest *req2 = [self.shotgun schemaRead]; + ShotgunRequest *req3 = [self.shotgun schemaFieldReadForEntityOfType:@"Version"]; + ShotgunRequest *req4 = [self.shotgun schemaFieldReadForEntityOfType:@"Version" forField:@"user"]; + ShotgunRequest *req5 = [self.shotgun schemaFieldCreateForEntityOfType:@"Version" ofDataType:@"number" withDisplayName:@"Monkey Count" andProperties:@"{\"description\": \"How many monkeys were needed\"}"]; @@ -35,11 +36,6 @@ - (void)schemaTestResponses:(NSArray *)responses id res3 = [responses objectAtIndex:2]; id res4 = [responses objectAtIndex:3]; id res5 = [responses objectAtIndex:4]; - GHTestLog(@"RES1: %@", res1); - GHTestLog(@"RES2: %@", res2); - GHTestLog(@"RES3: %@", res3); - GHTestLog(@"RES4: %@", res4); - GHTestLog(@"RES5: %@", res5); GHAssertTrue([res1 isKindOfClass:[NSDictionary class]], @"SchemaEntityRead was not an NSDictionary"); GHAssertTrue([res1 count]>0, @"SchemaEntityRead count was zero"); GHAssertTrue([res2 isKindOfClass:[NSDictionary class]], @"SchemaRead was not an NSDictionary"); @@ -51,14 +47,14 @@ - (void)schemaTestResponses:(NSArray *)responses GHAssertTrue([res4 objectForKey:@"user"] != Nil, @"SchemaFieldRead did not have user"); GHAssertTrue([res5 isKindOfClass:[NSString class]], @"SchemaFieldCreate was not an NSString"); - ShotgunRequest *update = [shotgun schemaFieldUpdateForEntityOfType:@"Version" + ShotgunRequest *update = [self.shotgun schemaFieldUpdateForEntityOfType:@"Version" forField:res5 withProperties:@"{\"description\": \"How many monkeys turned up\"}"]; [update startSynchronous]; GHAssertTrue([[update response] isKindOfClass:[NSNumber class]], @"SchemaFieldUpdate was not an NSNumber"); GHAssertTrue([[update response] boolValue] == YES, @"SchemaFieldUpdate did not return true."); - ShotgunRequest *delete = [shotgun schemaFieldDeleteForEntityOfType:@"Version" forField:res5]; + ShotgunRequest *delete = [self.shotgun schemaFieldDeleteForEntityOfType:@"Version" forField:res5]; [delete startSynchronous]; GHAssertTrue([[delete response] isKindOfClass:[NSNumber class]], @"SchemaFieldUpdate was not an NSNumber"); GHAssertTrue([[delete response] boolValue] == YES, @"SchemaFieldUpdate did not return true."); @@ -66,49 +62,90 @@ - (void)schemaTestResponses:(NSArray *)responses - (void)testSchemaSync { - NSArray *requests = [self schemaTestRequests]; - NSMutableArray *responses = [NSMutableArray arrayWithCapacity:[requests count]]; - for (ShotgunRequest *request in requests) { - [request startSynchronous]; - [responses addObject:[request response]]; - } - [self schemaTestResponses:responses]; + [self runSyncWith:@selector(schemaTestRequests) + andCheckWith:@selector(schemaTestResponses:)]; } - (void)testSchemaAsync { - [self prepare:@selector(testSchemaAsync)]; - NSArray *requests = [self schemaTestRequests]; - __block NSMutableArray *successes = [[NSMutableArray alloc] initWithCapacity:[requests count]]; - __block NSMutableArray *failures = [[NSMutableArray alloc] initWithCapacity:[requests count]]; - for (NSUInteger index=0; index<[requests count]; index++) { - ShotgunRequest *request = [requests objectAtIndex:index]; - [request setCompletionBlock:^{ - @synchronized(self) { - [successes addObject:[NSNumber numberWithInt:index]]; - if ([successes count] == [requests count]) - [self notify:kGHUnitWaitStatusSuccess forSelector:@selector(testSchemaAsync)]; - } - }]; - [request setFailedBlock:^{ - @synchronized(self) { - [failures addObject:[NSNumber numberWithInt:index]]; - [self notify:kGHUnitWaitStatusFailure forSelector:@selector(testSchemaAsync)]; - } - }]; - [request startAsynchronous]; + [self runAsyncWith:@selector(schemaTestRequests) + checkWith:@selector(schemaTestResponses:) + timeout:60.0]; +} + +#pragma mark Test Automated Find + +- (NSArray *)automatedFindRequests +{ + ShotgunRequest *request = [self.shotgun schemaEntityRead]; + [request startSynchronous]; + NSDictionary *entityInfo = [request response]; + NSMutableArray *requests = [NSMutableArray array]; + + NSString *direction = @"asc"; + NSString *filterOperator = @"all"; + NSUInteger limit = 1; + NSUInteger page = 1; + + for (NSString *entityType in entityInfo) { + request = [self.shotgun schemaFieldReadForEntityOfType:entityType]; + [request startSynchronous]; + NSDictionary *fields = [request response]; + if ([fields count] == 0) { + GHTestLog(@"Entity %@ has no fields, skipping", entityType); + continue; + } + + NSString *order = [NSString stringWithFormat:@"[{\"field_name\": \"%@\", \"direction\": \"%@\"}]", + [[fields keyEnumerator] nextObject], direction]; + NSString *filters; + if ([fields objectForKey:@"project"] != Nil) + filters = [NSString stringWithFormat:@"[[\"project\", \"is\", "\ + "{\"type\": \"Project\", \"id\": %@}]]", self.projectId]; + else + filters = @"[]"; + + request = [self.shotgun findEntitiesOfType:entityType + withFilters:filters + andFields:[fields allKeys] + andOrder:order + andFilterOperator:filterOperator + andLimit:limit + andPage:page + retiredOnly:NO]; + [requests addObject:request]; + + filterOperator = [filterOperator isEqualToString:@"all"] ? @"any" : @"all"; + direction = [direction isEqualToString:@"desc"] ? @"asc" : @"desc"; + limit = (limit % 5) + 1; + page = (page % 3) + 1; } - [self waitForStatus:kGHUnitWaitStatusSuccess timeout:10.0]; - GHAssertTrue([successes count] == [requests count], @"Success count not equal to the number of requests: %@ of %d", successes, [requests count]); - GHAssertTrue([failures count] == 0, @"Failure count is non-zero: %@", failures); - NSMutableArray *responses = [NSMutableArray arrayWithCapacity:[requests count]]; - for (ShotgunRequest *request in requests) { - id response = [request response]; - [responses addObject:response ? response : [NSNull null]]; + return requests; +} + +- (void)automatedFindTestResponses:(NSArray *)responses +{ + for (id response in responses) { + GHAssertTrue([response isKindOfClass:[NSArray class]], @"TestAutomatedFind response was not a list."); + if ([response count] != 0) { + ShotgunEntity *entity = [response objectAtIndex:0]; + GHAssertTrue([entity isKindOfClass:[ShotgunEntity class]], @"TestAutomatedFind returned a non ShotgunEntity"); + GHTestLog(@"%@: %@", entity.entityType, entity); + } } - [self schemaTestResponses:responses]; - [failures release]; - [successes release]; +} + +- (void)testAutomatedFindSync +{ + [self runSyncWith:@selector(automatedFindRequests) + andCheckWith:@selector(automatedFindTestResponses:)]; +} + +- (void)testAutomatedFindAsync +{ + [self runAsyncWith:@selector(automatedFindRequests) + checkWith:@selector(automatedFindTestResponses:) + timeout:120.0]; } @end \ No newline at end of file diff --git a/UnitTests/SG API UnitTests/ShotgunApiTest.m b/UnitTests/SG API UnitTests/ShotgunApiTest.m index a49d1ae..6da3a3c 100644 --- a/UnitTests/SG API UnitTests/ShotgunApiTest.m +++ b/UnitTests/SG API UnitTests/ShotgunApiTest.m @@ -6,79 +6,247 @@ // Copyright 2011 Laika. All rights reserved. // +#import "SBJson.h" + #import "ShotgunApiTestBase.h" -@interface ShotgunApiTest : ShotgunApiTestBase {} +@interface ShotgunApiTest : ShotgunApiTestBase; +@end + +// Get rid of warning that we are calling a private method off of a Shotgun instance +@interface Shotgun () +- (NSString *)getSessionToken_; @end @implementation ShotgunApiTest -- (void)testConnection +- (void)testInfo { - GHAssertNotNil(shotgun, @"initWithUrl returned nil"); + ShotgunRequest *req = [self.shotgun info]; + [req startSynchronous]; + NSDictionary *info = [req response]; + GHAssertNotNil([info objectForKey:@"version"], @"version key not found in info dict"); } -- (void)testSchemaEntityRead +- (void)testDates { - ShotgunRequest *req = [shotgun schemaEntityRead]; - [req startSynchronous]; - NSDictionary *entities = [req response]; - GHAssertNotNil([entities objectForKey:@"Shot"], @"Shot entity not found in schemaEntityRead"); + ShotgunEntity *task; + ShotgunEntity *queried; + ShotgunEntity *playlist; + BOOL rv; + NSDate *now = [NSDate date]; + ShotgunDate *date = [ShotgunDate dateWithDate:now]; + ShotgunDateTime *datetime = [ShotgunDateTime dateTimeWithDate:now]; + + // Test Date + NSString *taskData = [NSString stringWithFormat: + @"{ " \ + " \"project\": {\"type\": \"Project\", \"id\": %@}, " \ + " \"content\": \"ObjC Test Task\", " \ + " \"start_date\": %@ " \ + "} ", self.projectId, date]; + NSMutableDictionary *taskDict = [taskData JSONValue]; + + task = [[self.shotgun createEntityOfType:@"Task" withData:taskData] startSynchronous]; + + queried = [[self.shotgun findEntityOfType:task.entityType + withFilters:[NSString stringWithFormat:@"[[\"id\", \"is\", %@]]", task.entityId] + andFields:@"[\"start_date\"]"] startSynchronous]; + GHAssertTrue([[queried objectForKey:@"start_date"] isEqualToString:[taskDict objectForKey:@"start_date"]], + @"Returned entity start_date did not match upload: %@", [queried objectForKey:@"start_date"]); + + rv = [[[self.shotgun deleteEntityOfType:task.entityType withId:task.entityId] startSynchronous] boolValue]; + GHAssertTrue(rv == TRUE, @"Failed to delete created Task: %@", task.entityId); + + [taskDict setObject:date forKey:@"start_date"]; + task = [[self.shotgun createEntityOfType:@"Task" withData:taskDict] startSynchronous]; + rv = [[[self.shotgun deleteEntityOfType:task.entityType withId:task.entityId] startSynchronous] boolValue]; + GHAssertTrue(rv == TRUE, @"Failed to delete created Task: %@", task.entityId); + + [taskDict setObject:datetime forKey:@"start_date"]; + GHAssertThrows([[self.shotgun createEntityOfType:@"Task" withData:taskDict] startSynchronous], + @"Passing datetime for a date field did not raise an exception"); + + [taskDict setObject:now forKey:@"start_date"]; + GHAssertThrows([[self.shotgun createEntityOfType:@"Task" withData:taskDict] startSynchronous], + @"Passing NSDate for a date field did not raise an exception"); + + // Test DateTime + NSString *playlistData = [NSString stringWithFormat: + @"{ " \ + " \"project\": {\"type\": \"Project\", \"id\": %@}, " \ + " \"code\": \"ObjC Test Playlist\", " \ + " \"sg_date_and_time\": %@ " \ + "} ", self.projectId, datetime]; + NSMutableDictionary *playlistDict = [playlistData JSONValue]; + + playlist = [[self.shotgun createEntityOfType:@"Playlist" withData:playlistData] startSynchronous]; + + queried = [[self.shotgun findEntityOfType:playlist.entityType + withFilters:[NSString stringWithFormat:@"[[\"id\", \"is\", %@]]", playlist.entityId] + andFields:@"[\"sg_date_and_time\"]"] startSynchronous]; + id field = [queried objectForKey:@"sg_date_and_time"]; + GHAssertTrue([field isKindOfClass:[ShotgunDateTime class]], + @"Returned entity sg_date_and_time was not a ShotgunDateTime object"); + GHAssertTrue([field isEqualToDate:datetime], @"Returned sg_date_and_time did not match uploaded value: %f vrs %f.", + [field timeIntervalSinceReferenceDate], [datetime timeIntervalSinceReferenceDate]); + rv = [[[self.shotgun deleteEntityOfType:playlist.entityType withId:playlist.entityId] startSynchronous] boolValue]; + GHAssertTrue(rv == TRUE, @"Failed to delete created Playlist: %@", playlist.entityId); + + [playlistDict setObject:datetime forKey:@"sg_date_and_time"]; + playlist = [[self.shotgun createEntityOfType:@"Playlist" withData:playlistDict] startSynchronous]; + rv = [[[self.shotgun deleteEntityOfType:playlist.entityType withId:playlist.entityId] startSynchronous] boolValue]; + GHAssertTrue(rv == TRUE, @"Failed to delete created Playlist: %@", playlist.entityId); + + [playlistDict setObject:date forKey:@"sg_date_and_time"]; + GHAssertThrows([[self.shotgun createEntityOfType:@"Playlist" withData:playlistDict] startSynchronous], + @"Passing date for a datetime field did not raise an exception"); + + [playlistDict setObject:now forKey:@"sg_date_and_time"]; + GHAssertThrows([[self.shotgun createEntityOfType:@"Playlist" withData:playlistDict] startSynchronous], + @"Passing NSDate for a date field did not raise an exception"); } -- (void)testInfo +- (void)testBatch { - ShotgunRequest *req = [shotgun info]; - [req startSynchronous]; - NSDictionary *info = [req response]; - GHAssertNotNil([info objectForKey:@"version"], @"version key not found in info dict"); + NSString *batch = [NSString stringWithFormat:@"[" \ + "{ " \ + " \"request_type\": \"create\", " \ + " \"entity_type\": \"Shot\", " \ + " \"data\": { " \ + " \"code\": \"New Shot\", " \ + " \"project\": {\"type\": \"Project\", \"id\": %@} " \ + " } " \ + "}, { " \ + " \"request_type\": \"update\", " \ + " \"entity_type\": \"Shot\", " \ + " \"entity_id\": %@, " \ + " \"data\": { " \ + " \"code\": \"Changed\" " \ + " } " \ + "}] ", self.projectId, self.shotId]; + ShotgunRequest *request = [self.shotgun batch:batch]; + [request startSynchronous]; + NSArray *responses = [request response]; + GHAssertTrue([responses isKindOfClass:[NSArray class]], @"Batch did not return an NSArray"); + + NSNumber *createdId = [[responses objectAtIndex:0] objectForKey:@"id"]; + NSNumber *updatedId = [[responses objectAtIndex:1] objectForKey:@"id"]; + GHAssertTrue([createdId intValue] != 0, @"Batch create returned 0 for id"); + GHAssertTrue([updatedId isEqualToNumber:self.shotId], @"Batch updated returned id other than config."); + + batch = [NSString stringWithFormat:@"[ " \ + "{ " \ + " \"request_type\": \"delete\", " \ + " \"entity_type\": \"Shot\", " \ + " \"entity_id\": %@ " \ + "}]", createdId]; + request = [self.shotgun batch:batch]; + [request startSynchronous]; + responses = [request response]; + GHAssertTrue([[responses objectAtIndex:0] boolValue] == TRUE, @"Batch delete did not return True"); } -- (void)testCreateUpdateDelete +- (void)testCreateUpdateDeleteRevive { // Create - NSString *data = @"{ \ - \"code\": \"ObjC Unit Test Template\", \ - \"description\": \"This template should be retired by the unit tests if everything goes well.\", \ - \"entity_type\": \"Shot\" \ - }"; - ShotgunRequest *request1 = [shotgun createEntityOfType:@"TaskTemplate" withData:data]; - [request1 startSynchronous]; - ShotgunEntity *template = [request1 response]; - GHAssertTrue([template entityId] != 0, @"return id of Task Template was Nil"); + NSDictionary *data = [[NSString stringWithFormat:@"{ " \ + " \"project\": {\"type\": \"Project\", \"id\": %@}, " \ + " \"code\": \"ObjC Unit Test Version\", " \ + " \"description\": \"This version should be retired by the unit tests if everything goes well.\", " \ + " \"entity\": {\"type\": \"Shot\", \"id\": %@} " \ + "}", self.projectId, self.shotId] JSONValue]; + ShotgunRequest *request = [self.shotgun createEntityOfType:@"Version" withData:data]; + [request startSynchronous]; + ShotgunEntity *version = [request response]; + GHAssertTrue([version.entityId intValue] != 0, @"return id of Create was 0"); + GHTestLog(@"Created version with ID: %@", version.entityId); + GHAssertTrue([[version objectForKey:@"description"] isEqualToString:[data objectForKey:@"description"]], @"return description did not match"); + GHAssertTrue([[version objectForKey:@"code"] isEqualToString:[data objectForKey:@"code"]], @"return code did not match"); + // Update NSString *updateData = @"{ \ \"description\": \"Updated description. Delete Next.\" \ }"; - ShotgunRequest *request2 = [shotgun updateEntityOfType:@"TaskTemplate" withId:[template entityId] withData:updateData]; - [request2 startSynchronous]; - ShotgunEntity *updatedTemplate = [request2 response]; - GHAssertEqualStrings([updatedTemplate valueForKey:@"description"], @"Updated description. Delete Next.", @"Description not updated"); + request = [self.shotgun updateEntityOfType:@"Version" withId:version.entityId withData:updateData]; + [request startSynchronous]; + ShotgunEntity *updatedVersion = [request response]; + GHAssertEqualStrings([updatedVersion valueForKey:@"description"], @"Updated description. Delete Next.", @"Description not updated"); + GHAssertTrue([updatedVersion.entityId isEqualToNumber:version.entityId] != 0, @"return id of update does not match create."); + // Delete - ShotgunRequest *request3 = [shotgun deleteEntityOfType:@"TaskTemplate" withId:[template entityId]]; - [request3 startSynchronous]; - BOOL result = [[request3 response] boolValue]; - GHAssertTrue(result, @"delete for Task Template returned False"); + request = [self.shotgun deleteEntityOfType:@"Version" withId:version.entityId]; + [request startSynchronous]; + BOOL result = [[request response] boolValue]; + GHAssertTrue(result == TRUE, @"delete returned False"); + GHTestLog(@"Deleted version with ID: %@", version.entityId); + request = [self.shotgun deleteEntityOfType:@"Version" withId:version.entityId]; + [request startSynchronous]; + result = [[request response] boolValue]; + GHAssertTrue(result == FALSE, @"second delete returned True"); + + // Revive + request = [self.shotgun reviveEntityOfType:@"Version" withId:version.entityId]; + [request startSynchronous]; + result = [[request response] boolValue]; + GHAssertTrue(result == TRUE, @"revive returned False"); + GHTestLog(@"Revived version with ID: %@", version.entityId); + request = [self.shotgun reviveEntityOfType:@"Version" withId:version.entityId]; + [request startSynchronous]; + result = [[request response] boolValue]; + GHAssertTrue(result == FALSE, @"second revive returned True"); + + // Final Delete + request = [self.shotgun deleteEntityOfType:@"Version" withId:version.entityId]; + [request startSynchronous]; + result = [[request response] boolValue]; + GHAssertTrue(result == TRUE, @"delete returned False"); + GHTestLog(@"Final delete of version with ID: %@", version.entityId); } - (void)testFind { - ShotgunRequest *request = [shotgun findEntitiesOfType:@"Shot" - withFilters:@"[[\"sg_status_list\", \"is\", \"ip\"]]" - andFields:@"[\"sg_status_list\", \"code\", \"created_at\", \"image\", \"sg_status_list\"]"]; + NSString *filters = [NSString stringWithFormat:@"[" \ + " [\"project\", \"is\", {\"type\": \"Project\", \"id\": %@}], " \ + " [\"id\", \"is\", %@] " \ + "]", self.projectId, self.versionId]; + NSString *fields = @"[\"id\"]"; + + ShotgunRequest *request = [self.shotgun findEntitiesOfType:@"Version" withFilters:filters andFields:fields]; [request startSynchronous]; NSArray *results = [request response]; - GHTestLog(@"Find returned: %@", results); + GHAssertTrue([results isKindOfClass:[NSArray class]], @"Find did not return an NSArray"); + ShotgunEntity *version = [results objectAtIndex:0]; + GHAssertTrue([version.entityType isEqualToString:@"Version"], @"Find entity was not a Version"); + GHAssertTrue([version.entityId isEqualToNumber:self.versionId], @"Find entity id did not match config"); + + request = [self.shotgun findEntityOfType:@"Version" withFilters:filters andFields:fields]; + [request startSynchronous]; + version = [request response]; + GHAssertTrue([version isKindOfClass:[ShotgunEntity class]], @"Find one did not return a ShotgunEntity"); + GHAssertTrue([version.entityType isEqualToString:@"Version"], @"Find one entity was not a Version"); + GHAssertTrue([version.entityId isEqualToNumber:self.versionId], @"Find one entity id did not match config"); } -- (void)testUpload { - NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"jpg"]; - [shotgun uploadThumbnailForEntityOfType:@"Asset" withId:[NSNumber numberWithInt:826] fromPath:path]; +- (void)testGetSessionToken +{ + NSString *uuid = [self.shotgun getSessionToken_]; + GHAssertTrue([uuid length] > 0, @"Get Session Token did not return a valid token"); + GHTestLog(@"Get Session Token returned: %@", uuid); } -- (void)testDownload { - NSData *data = [shotgun downloadAttachmentWithId:[NSNumber numberWithInt:6]]; +- (void)testUploadDownload +{ + NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"jpg"]; + + NSNumber *attachId = [self.shotgun uploadThumbnailForEntityOfType:@"Version" withId:self.versionId fromPath:path]; + GHAssertTrue([attachId intValue] != 0, @"upload returned zero for attachment id"); + + NSData *data = [self.shotgun downloadAttachmentWithId:attachId]; GHAssertTrue([data length] != 0, @"download for attachment returned no data."); + + NSData *originalData = [NSData dataWithContentsOfFile:path]; + GHAssertTrue([originalData isEqualToData:data], @"Downloaded data did not match uploaded."); } @end diff --git a/UnitTests/SG API UnitTests/ShotgunApiTestBase.h b/UnitTests/SG API UnitTests/ShotgunApiTestBase.h index 52e39ad..1975ba2 100644 --- a/UnitTests/SG API UnitTests/ShotgunApiTestBase.h +++ b/UnitTests/SG API UnitTests/ShotgunApiTestBase.h @@ -10,8 +10,14 @@ #import "Shotgun.h" -@interface ShotgunApiTestBase : GHAsyncTestCase { - Shotgun *shotgun; -} +@interface ShotgunApiTestBase : GHAsyncTestCase; + +@property (retain, readwrite, nonatomic) Shotgun *shotgun; +@property (retain, readwrite, nonatomic) NSNumber *projectId; +@property (retain, readwrite, nonatomic) NSNumber *shotId; +@property (retain, readwrite, nonatomic) NSNumber *versionId; + +- (void)runSyncWith:(SEL)requestsSelector andCheckWith:(SEL)checkSelector; +- (void)runAsyncWith:(SEL)requestsSelector checkWith:(SEL)checkSelector timeout:(NSUInteger)timeout; @end diff --git a/UnitTests/SG API UnitTests/ShotgunApiTestBase.m b/UnitTests/SG API UnitTests/ShotgunApiTestBase.m index 7f243b3..5692bc8 100644 --- a/UnitTests/SG API UnitTests/ShotgunApiTestBase.m +++ b/UnitTests/SG API UnitTests/ShotgunApiTestBase.m @@ -8,22 +8,85 @@ #import "ShotgunApiTestBase.h" - @implementation ShotgunApiTestBase +@synthesize shotgun = shotgun_; +@synthesize projectId = projectId_; +@synthesize shotId = shotId_; +@synthesize versionId = versionId_; + - (void)setUp { NSString *path = [[NSBundle mainBundle] pathForResource:@"Config" ofType:@"plist"]; NSDictionary *config = [[[NSDictionary alloc] initWithContentsOfFile:path] autorelease]; - shotgun = [[Shotgun alloc] initWithUrl:[config objectForKey:@"url"] - scriptName:[config objectForKey:@"script"] - andKey:[config objectForKey:@"key"]]; + self.shotgun = [[Shotgun alloc] initWithUrl:[config objectForKey:@"url"] + scriptName:[config objectForKey:@"script"] + andKey:[config objectForKey:@"key"]]; + self.projectId = [config objectForKey:@"projectId"]; + self.shotId = [config objectForKey:@"shotId"]; + self.versionId = [config objectForKey:@"versionId"]; } - (void)tearDown { - [shotgun release]; - shotgun = Nil; + self.shotgun = Nil; + self.projectId = Nil; + self.shotId = Nil; + self.versionId = Nil; +} + +- (void)runSyncWith:(SEL)requestsSelector andCheckWith:(SEL)checkSelector +{ + NSArray *requests = [self performSelector:requestsSelector]; + NSMutableArray *responses = [NSMutableArray arrayWithCapacity:[requests count]]; + for (ShotgunRequest *request in requests) { + [request startSynchronous]; + [responses addObject:[request response]]; + } + [self performSelector:checkSelector withObject:responses]; +} + +- (void)runAsyncWith:(SEL)requestsSelector checkWith:(SEL)checkSelector timeout:(NSUInteger)timeout +{ + [self prepare:requestsSelector]; + NSArray *requests = [self performSelector:requestsSelector]; + __block NSMutableArray *successes = [[NSMutableArray alloc] initWithCapacity:[requests count]]; + __block NSMutableArray *failures = [[NSMutableArray alloc] initWithCapacity:[requests count]]; + for (NSUInteger index=0; index<[requests count]; index++) { + ShotgunRequest *request = [requests objectAtIndex:index]; + [request setCompletionBlock:^{ + @synchronized(self) { + [successes addObject:[NSNumber numberWithInt:index]]; + if ([successes count] == [requests count]) + [self notify:kGHUnitWaitStatusSuccess forSelector:requestsSelector]; + } + }]; + [request setFailedBlock:^{ + @synchronized(self) { + [failures addObject:[NSNumber numberWithInt:index]]; + [self notify:kGHUnitWaitStatusFailure forSelector:requestsSelector]; + } + }]; + [request startAsynchronous]; + } + @try { + [self waitForStatus:kGHUnitWaitStatusSuccess timeout:timeout]; + } + @catch (NSException *exception) { + GHTestLog(@"Successes (%d of %d): %@", [successes count], [requests count], successes); + GHTestLog(@"Failures (%d): %@", [failures count], failures); + @throw; + } + GHAssertTrue([successes count] == [requests count], @"Success count not equal to the number of requests: %@ of %d", successes, [requests count]); + GHAssertTrue([failures count] == 0, @"Failure count is non-zero: %@", failures); + NSMutableArray *responses = [NSMutableArray arrayWithCapacity:[requests count]]; + for (ShotgunRequest *request in requests) { + id response = [request response]; + [responses addObject:response ? response : [NSNull null]]; + } + [self performSelector:checkSelector withObject:responses]; + [failures release]; + [successes release]; } @end