diff --git a/.gitignore b/.gitignore
index 64048d3..f61574d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,12 @@
-Carthage
-xcuserdata
.DS_Store
-report.xml
-test_output
\ No newline at end of file
+
+*.xcscmblueprint
+xcuserdata
+
+/archive
+/build
+
+/Carthage
+
+/fastlane/report.xml
+/fastlane/test_output
\ No newline at end of file
diff --git a/Framework/Resources/Info.plist b/Framework/Resources/Info.plist
new file mode 100644
index 0000000..1f571be
--- /dev/null
+++ b/Framework/Resources/Info.plist
@@ -0,0 +1,20 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ ${PRODUCT_NAME}
+ CFBundlePackageType
+ BNDL
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+
+
diff --git a/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.h b/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.h
index 67b5de8..e949026 100644
--- a/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.h
+++ b/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.h
@@ -11,9 +11,9 @@ NS_ASSUME_NONNULL_BEGIN
@interface NSBundle (SRGDiagnostics)
/**
- * The SRGDiagnostics resource bundle.
+ * The SRG Diagnostics resource bundle.
*/
-+ (NSBundle *)srg_diagnosticsBundle;
+@property (class, nonatomic, readonly) NSBundle *srg_diagnosticsBundle;
@end
diff --git a/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.m b/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.m
index e2f0eb4..1fbab50 100644
--- a/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.m
+++ b/Framework/Sources/Helpers/NSBundle+SRGDiagnostics.m
@@ -17,7 +17,9 @@ + (instancetype)srg_diagnosticsBundle
static NSBundle *s_bundle;
static dispatch_once_t s_onceToken;
dispatch_once(&s_onceToken, ^{
- s_bundle = [NSBundle bundleForClass:[SRGDiagnosticsService class]];
+ NSString *bundlePath = [[NSBundle bundleForClass:SRGDiagnosticsService.class].bundlePath stringByAppendingPathComponent:@"SRGDiagnostics.bundle"];
+ s_bundle = [NSBundle bundleWithPath:bundlePath];
+ NSAssert(s_bundle, @"Please add SRGDiagnostics.bundle to your project resources");
});
return s_bundle;
}
diff --git a/Framework/Sources/Helpers/NSTimer+SRGDiagnostics.h b/Framework/Sources/Helpers/NSTimer+SRGDiagnostics.h
new file mode 100644
index 0000000..d29fb04
--- /dev/null
+++ b/Framework/Sources/Helpers/NSTimer+SRGDiagnostics.h
@@ -0,0 +1,20 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface NSTimer (SRGDiagnostics)
+
+/**
+ * Create a block-based timer (a feature only available since iOS 10), scheduled with common run loop modes.
+ */
++ (NSTimer *)srgdiagnostics_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/Helpers/NSTimer+SRGDiagnostics.m b/Framework/Sources/Helpers/NSTimer+SRGDiagnostics.m
new file mode 100644
index 0000000..471fbe8
--- /dev/null
+++ b/Framework/Sources/Helpers/NSTimer+SRGDiagnostics.m
@@ -0,0 +1,30 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "NSTimer+SRGDiagnostics.h"
+
+#import "SRGDiagnosticsTimerTarget.h"
+
+@implementation NSTimer (SRGDiagnostics)
+
++ (NSTimer *)srgdiagnostics_timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer * _Nonnull timer))block
+{
+ NSTimer *timer = nil;
+
+ if (@available(iOS 10, *)) {
+ timer = [self timerWithTimeInterval:interval repeats:repeats block:block];
+ }
+ else {
+ // Do not use self as target, since this would lead to subtle issues when the timer is deallocated
+ SRGDiagnosticsTimerTarget *target = [[SRGDiagnosticsTimerTarget alloc] initWithBlock:block];
+ timer = [self timerWithTimeInterval:interval target:target selector:@selector(fire:) userInfo:nil repeats:repeats];
+ }
+
+ [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
+ return timer;
+}
+
+@end
diff --git a/Framework/Sources/Helpers/SRGDiagnosticsTimerTarget.h b/Framework/Sources/Helpers/SRGDiagnosticsTimerTarget.h
new file mode 100644
index 0000000..6745c42
--- /dev/null
+++ b/Framework/Sources/Helpers/SRGDiagnosticsTimerTarget.h
@@ -0,0 +1,29 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Helper class used as target for a timer.
+ */
+// TODO: Remove when iOS 10 is the minimum required version
+@interface SRGDiagnosticsTimerTarget : NSObject
+
+/**
+ * Create the target with the specified to be executed when `-fire:` is called.
+ */
+- (instancetype)initWithBlock:(nullable void (^)(NSTimer *timer))block;
+
+/**
+ * Execute the attached block on behalf of the specified timer.
+ */
+- (void)fire:(NSTimer *)timer;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/Helpers/SRGDiagnosticsTimerTarget.m b/Framework/Sources/Helpers/SRGDiagnosticsTimerTarget.m
new file mode 100644
index 0000000..94e4223
--- /dev/null
+++ b/Framework/Sources/Helpers/SRGDiagnosticsTimerTarget.m
@@ -0,0 +1,30 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticsTimerTarget.h"
+
+@interface SRGDiagnosticsTimerTarget ()
+
+@property (nonatomic, copy) void (^block)(NSTimer *);
+
+@end
+
+@implementation SRGDiagnosticsTimerTarget
+
+- (instancetype)initWithBlock:(void (^)(NSTimer * _Nonnull))block
+{
+ if (self = [super init]) {
+ self.block = block;
+ }
+ return self;
+}
+
+- (void)fire:(NSTimer *)timer
+{
+ self.block ? self.block(timer) : nil;
+}
+
+@end
diff --git a/Framework/Sources/Player/SRGDiagnosticsService.h b/Framework/Sources/Player/SRGDiagnosticsService.h
deleted file mode 100644
index 036ddb5..0000000
--- a/Framework/Sources/Player/SRGDiagnosticsService.h
+++ /dev/null
@@ -1,15 +0,0 @@
-//
-// Copyright (c) SRG SSR. All rights reserved.
-//
-// License information is available from the LICENSE file.
-//
-
-#import
-
-NS_ASSUME_NONNULL_BEGIN
-
-@interface SRGDiagnosticsService : NSObject
-
-@end
-
-NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/Player/SRGDiagnosticsService.m b/Framework/Sources/Player/SRGDiagnosticsService.m
deleted file mode 100644
index 5bad4e7..0000000
--- a/Framework/Sources/Player/SRGDiagnosticsService.m
+++ /dev/null
@@ -1,11 +0,0 @@
-//
-// Copyright (c) SRG SSR. All rights reserved.
-//
-// License information is available from the LICENSE file.
-//
-
-#import "SRGDiagnosticsService.h"
-
-@implementation SRGDiagnosticsService
-
-@end
diff --git a/Framework/Sources/SRGDiagnosticInformation.h b/Framework/Sources/SRGDiagnosticInformation.h
new file mode 100644
index 0000000..f7fea0a
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticInformation.h
@@ -0,0 +1,51 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Diagnostic information.
+ */
+@interface SRGDiagnosticInformation : NSObject
+
+/**
+ * Associate primitive values with keys.
+ */
+- (void)setBool:(BOOL)value forKey:(NSString *)key;
+- (void)setInteger:(NSInteger)value forKey:(NSString *)key;
+- (void)setFloat:(float)value forKey:(NSString *)key;
+- (void)setDouble:(double)value forKey:(NSString *)key;
+
+/**
+ * Associate objects with keys. Setting `nil` removes the associated entry, if any.
+ */
+- (void)setString:(nullable NSString *)string forKey:(NSString *)key;
+- (void)setNumber:(nullable NSNumber *)number forKey:(NSString *)key;
+- (void)setURL:(nullable NSURL *)URL forKey:(NSString *)key;
+
+/**
+ * Start / stop a time measurement, saving the associated value under the specified key.
+ *
+ * @discussion If a measurement is not stopped, it will be ignored when the report is submitted.
+ */
+- (void)startTimeMeasurementForKey:(NSString *)key;
+- (void)stopTimeMeasurementForKey:(NSString *)key;
+
+/**
+ * Return nested information under the specified key.
+ */
+- (SRGDiagnosticInformation *)informationForKey:(NSString *)key;
+
+/**
+ * Return report information as a dictionary serializable to JSON.
+ */
+- (NSDictionary *)JSONDictionary;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/SRGDiagnosticInformation.m b/Framework/Sources/SRGDiagnosticInformation.m
new file mode 100644
index 0000000..5c74e82
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticInformation.m
@@ -0,0 +1,178 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticInformation.h"
+
+#import "SRGTimeMeasurement.h"
+
+@interface SRGDiagnosticInformation ()
+
+@property (nonatomic) NSMutableDictionary *values;
+@property (nonatomic) NSMutableDictionary *timeMeasurements;
+@property (nonatomic) NSMutableDictionary *informationEntries;
+
+@end
+
+@implementation SRGDiagnosticInformation
+
+#pragma mark Object lifecycle
+
+- (instancetype)init
+{
+ if (self = [super init]) {
+ self.values = [NSMutableDictionary dictionary];
+ self.timeMeasurements = [NSMutableDictionary dictionary];
+ self.informationEntries = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+#pragma mark Associated data
+
+- (void)setBool:(BOOL)value forKey:(NSString *)key
+{
+ @synchronized(self) {
+ self.values[key] = @(value);
+ }
+}
+
+- (void)setInteger:(NSInteger)value forKey:(NSString *)key
+{
+ @synchronized(self) {
+ self.values[key] = @(value);
+ }
+}
+
+- (void)setFloat:(float)value forKey:(NSString *)key
+{
+ @synchronized(self) {
+ self.values[key] = @(value);
+ }
+}
+
+- (void)setDouble:(double)value forKey:(NSString *)key
+{
+ @synchronized(self) {
+ self.values[key] = @(value);
+ }
+}
+
+- (void)setString:(NSString *)string forKey:(NSString *)key
+{
+ @synchronized(self) {
+ self.values[key] = string;
+ }
+}
+
+- (void)setNumber:(NSNumber *)number forKey:(NSString *)key
+{
+ @synchronized(self) {
+ self.values[key] = number;
+ }
+}
+
+- (void)setURL:(NSURL *)URL forKey:(NSString *)key
+{
+ @synchronized(self) {
+ self.values[key] = URL.absoluteString;
+ }
+}
+
+- (void)startTimeMeasurementForKey:(NSString *)key
+{
+ [[self timeMeasurementForKey:key] start];
+}
+
+- (void)stopTimeMeasurementForKey:(NSString *)key
+{
+ [[self timeMeasurementForKey:key] stop];
+}
+
+#pragma mark Time measurements
+
+- (SRGTimeMeasurement *)timeMeasurementForKey:(NSString *)key
+{
+ @synchronized(self) {
+ SRGTimeMeasurement *timeMeasurement = self.timeMeasurements[key];
+ if (! timeMeasurement) {
+ timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ self.timeMeasurements[key] = timeMeasurement;
+ }
+ return timeMeasurement;
+ }
+}
+
+- (SRGDiagnosticInformation *)informationForKey:(NSString *)key
+{
+ @synchronized(self) {
+ SRGDiagnosticInformation *information = self.informationEntries[key];
+ if (! information) {
+ information = [[SRGDiagnosticInformation alloc] init];
+ self.informationEntries[key] = information;
+ }
+ return information;
+ }
+}
+
+#pragma mark JSON serialization
+
+- (NSDictionary *)timeMeasurementsDictionary
+{
+ @synchronized(self) {
+ NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
+ [self.timeMeasurements enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, SRGTimeMeasurement * _Nonnull timeMeasurement, BOOL * _Nonnull stop) {
+ NSTimeInterval timeInterval = timeMeasurement.timeInterval;
+ if (timeInterval != SRGTimeMeasurementUndefined) {
+ dictionary[key] = @(round(timeInterval * 1000.));
+ }
+ }];
+ return dictionary;
+ }
+}
+
+- (NSDictionary *)JSONDictionary
+{
+ @synchronized(self) {
+ NSMutableDictionary *dictionary = [self.values mutableCopy];
+ [dictionary addEntriesFromDictionary:[self timeMeasurementsDictionary]];
+ [self.informationEntries enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, SRGDiagnosticInformation * _Nonnull information, BOOL * _Nonnull stop) {
+ dictionary[key] = [information JSONDictionary];
+ }];
+ return [dictionary copy];
+ }
+}
+
+#pragma mark NSCopying protocol
+
+- (id)copyWithZone:(NSZone *)zone
+{
+ @synchronized(self) {
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ information.values = [self.values mutableCopy];
+ information.timeMeasurements = [self.timeMeasurements mutableCopy];
+
+ NSMutableDictionary *informationEntries = [NSMutableDictionary dictionary];
+ [self.informationEntries enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, SRGDiagnosticInformation * _Nonnull information, BOOL * _Nonnull stop) {
+ informationEntries[key] = [information copy];
+ }];
+ information.informationEntries = informationEntries;
+ return information;
+ }
+}
+
+#pragma mark Description
+
+- (NSString *)description
+{
+ return [NSString stringWithFormat:@"<%@: %p; values = %@; timeMeasurements = %@; informationEntries: %@>",
+ self.class,
+ self,
+ self.values,
+ self.timeMeasurements,
+ self.informationEntries];
+}
+
+@end
diff --git a/Framework/Sources/SRGDiagnosticReport+Private.h b/Framework/Sources/SRGDiagnosticReport+Private.h
new file mode 100644
index 0000000..8ed1695
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticReport+Private.h
@@ -0,0 +1,26 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticReport.h"
+#import "SRGDiagnosticsService.h"
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Private interface for internal use.
+ */
+@interface SRGDiagnosticReport (Private)
+
+/**
+ * Create a report and associate it for submission by the specified service.
+ */
+- (instancetype)initWithDiagnosticsService:(SRGDiagnosticsService *)diagnosticsService;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/SRGDiagnosticReport.h b/Framework/Sources/SRGDiagnosticReport.h
new file mode 100644
index 0000000..ef1f012
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticReport.h
@@ -0,0 +1,25 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticInformation.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * A diagnostic report. Call the report `-finish` method when a diagnostic report is complete and ready for submission.
+ */
+@interface SRGDiagnosticReport : SRGDiagnosticInformation
+
+/**
+ * Finish the report.
+ *
+ * @discussion A finished report cannot be changed afterwards.
+ */
+- (void)finish;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/SRGDiagnosticReport.m b/Framework/Sources/SRGDiagnosticReport.m
new file mode 100644
index 0000000..d9f530d
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticReport.m
@@ -0,0 +1,49 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticReport.h"
+
+#import "SRGDiagnosticsService.h"
+#import "SRGDiagnosticsService+Private.h"
+#import "SRGTimeMeasurement.h"
+
+@interface SRGDiagnosticReport ()
+
+@property (nonatomic, weak) SRGDiagnosticsService *diagnosticsService;
+
+@end
+
+@implementation SRGDiagnosticReport
+
+#pragma mark Object lifecycle
+
+- (instancetype)initWithDiagnosticsService:(SRGDiagnosticsService *)diagnosticsService;
+{
+ if (self = [super init]) {
+ self.diagnosticsService = diagnosticsService;
+ }
+ return self;
+}
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-implementations"
+
+- (instancetype)init
+{
+ [self doesNotRecognizeSelector:_cmd];
+ return [self initWithDiagnosticsService:[SRGDiagnosticsService new]];
+}
+
+#pragma clang diagnostic pop
+
+#pragma mark Submission
+
+- (void)finish
+{
+ [self.diagnosticsService prepareToSubmitReport:self];
+}
+
+@end
diff --git a/Framework/Sources/SRGDiagnostics.h b/Framework/Sources/SRGDiagnostics.h
index ef1c740..c34397c 100644
--- a/Framework/Sources/SRGDiagnostics.h
+++ b/Framework/Sources/SRGDiagnostics.h
@@ -6,5 +6,10 @@
#import
+// Public headers.
+#import "SRGDiagnosticInformation.h"
+#import "SRGDiagnosticReport.h"
+#import "SRGDiagnosticsService.h"
+
// Official version number.
FOUNDATION_EXPORT NSString *SRGDiagnosticsMarketingVersion(void);
diff --git a/Framework/Sources/SRGDiagnostics.m b/Framework/Sources/SRGDiagnostics.m
index b5cbb15..6d37a82 100644
--- a/Framework/Sources/SRGDiagnostics.m
+++ b/Framework/Sources/SRGDiagnostics.m
@@ -10,5 +10,5 @@
NSString *SRGDiagnosticsMarketingVersion(void)
{
- return [NSBundle srg_diagnosticsBundle].infoDictionary[@"CFBundleShortVersionString"];
+ return NSBundle.srg_diagnosticsBundle.infoDictionary[@"CFBundleShortVersionString"];
}
diff --git a/Framework/Sources/SRGDiagnosticsService+Private.h b/Framework/Sources/SRGDiagnosticsService+Private.h
new file mode 100644
index 0000000..16257b3
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticsService+Private.h
@@ -0,0 +1,23 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticsService.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Private interface for internal use.
+ */
+@interface SRGDiagnosticsService (Private)
+
+/**
+ * Submit the specified report.
+ */
+- (void)prepareToSubmitReport:(SRGDiagnosticReport *)report;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/SRGDiagnosticsService.h b/Framework/Sources/SRGDiagnosticsService.h
new file mode 100644
index 0000000..0b0ef39
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticsService.h
@@ -0,0 +1,75 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticReport.h"
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Standard time intervals for automatic report submission.
+ */
+static const NSTimeInterval SRGDiagnosticsDefaultSubmissionInterval = 30.;
+static const NSTimeInterval SRGDiagnosticsMinimumSubmissionInterval = 10.;
+static const NSTimeInterval SRGDiagnosticsDisabledSubmissionInterval = DBL_MAX;
+
+/**
+ * A diagnostics service provides a way to create and submit diagnostic reports. A service, identified by a name,
+ * is created on the fly when accessed for the first time, and remains in existence for the lifetime of the application.
+ * Several services can coexist in an application, each submitting diagnostic reports in a different way. The submission
+ * process itself can take various forms, from webservice request to file logging, for example.
+ *
+ * Once a service has been retrieved, a report can be created or retrived by its name. This makes it possible to access
+ * and fill report information from any part of the application in a decentralized way. Once a report is complete, marking
+ * it as finished informs the service that it can submit it when possible.
+ *
+ * Submission is made on a periodic basis. For each finished report which has not been successfully submitted yet, the
+ * service will attempt to process it, calling a block letting you customize how submission actually occurs. Once it
+ * has been successfully submitted, a report is automatically discarded.
+ *
+ * Diagnostics service and reports can be created and accessed from arbitrary threads.
+ */
+@interface SRGDiagnosticsService : NSObject
+
+/**
+ * Retrieve a service for the specified name (creating the service if it did not exist yet).
+ */
++ (SRGDiagnosticsService *)serviceWithName:(NSString *)name;
+
+/**
+ * Return a report with the specified name. An empty report is created if none existed for the specified name.
+ *
+ * @discussion Call the report `-finish` method to finish the report and let the service submit it.
+ */
+- (SRGDiagnosticReport *)reportWithName:(NSString *)name;
+
+/**
+ * Block which gets called when a report needs to be submitted. Implementations (which might be asynchronous) must call
+ * the provided completion block when done. If the associated `success` boolean is set to `YES`, the report is discarded,
+ * otherwise the service will attempt submitting it again until it succeeds.
+ *
+ * @discussion If no block has been assigned, reports will simply be discarded when submitted.
+ */
+@property (nonatomic, copy, nullable) void (^submissionBlock)(NSDictionary *JSONDictionary, void (^completionBlock)(BOOL success));
+
+/**
+ * The interval at which finished reports are submitted. Default is `SRGDiagnosticsDefaultSubmissionInterval`. Use
+ * `SRGDiagnosticsDisabledSubmissionInterval` to disable periodic submission, in which case you are responsible of
+ * calling the `-submitFinishedReports` method when reports must be submitted.
+ */
+@property (nonatomic) NSTimeInterval submissionInterval;
+
+/**
+ * Trigger submission of finished reports.
+ *
+ * @discussion Reports are automatically submitted on a regular basis. Call this method to trigger submission earlier.
+ */
+- (void)submitFinishedReports;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/SRGDiagnosticsService.m b/Framework/Sources/SRGDiagnosticsService.m
new file mode 100644
index 0000000..1b11173
--- /dev/null
+++ b/Framework/Sources/SRGDiagnosticsService.m
@@ -0,0 +1,152 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGDiagnosticsService.h"
+
+#import "NSTimer+SRGDiagnostics.h"
+#import "SRGDiagnosticReport+Private.h"
+
+static NSMutableDictionary *s_diagnosticsServices;
+
+@interface SRGDiagnosticsService ()
+
+@property (nonatomic) NSMutableDictionary *reports;
+@property (nonatomic) NSMutableArray *finishedReports;
+
+@property (nonatomic) NSTimer *timer;
+@property (nonatomic, getter=isSubmitting) BOOL submitting;
+
+@end
+
+@implementation SRGDiagnosticsService
+
+#pragma mark Class methods
+
++ (SRGDiagnosticsService *)serviceWithName:(NSString *)name
+{
+ @synchronized(s_diagnosticsServices) {
+ static dispatch_once_t s_onceToken;
+ dispatch_once(&s_onceToken, ^{
+ s_diagnosticsServices = [NSMutableDictionary dictionary];
+ });
+
+ SRGDiagnosticsService *diagnosticsService = s_diagnosticsServices[name];
+ if (! diagnosticsService) {
+ diagnosticsService = [[SRGDiagnosticsService alloc] init];
+ s_diagnosticsServices[name] = diagnosticsService;
+ }
+ return diagnosticsService;
+ }
+}
+
+#pragma mark Object lifecycle
+
+- (instancetype)init
+{
+ if (self = [super init]) {
+ self.reports = [NSMutableDictionary dictionary];
+ self.finishedReports = [NSMutableArray array];
+ self.submissionInterval = SRGDiagnosticsDefaultSubmissionInterval;
+ }
+ return self;
+}
+
+#pragma mark Getters and setters
+
+- (void)setSubmissionBlock:(void (^)(NSDictionary * _Nonnull, void (^ _Nonnull)(BOOL)))submissionBlock
+{
+ @synchronized(self) {
+ _submissionBlock = [submissionBlock copy];
+ }
+}
+
+- (void)setSubmissionInterval:(NSTimeInterval)submissionInterval
+{
+ if (submissionInterval < SRGDiagnosticsMinimumSubmissionInterval) {
+ submissionInterval = SRGDiagnosticsMinimumSubmissionInterval;
+ }
+
+ _submissionInterval = submissionInterval;
+
+ __weak __typeof(self) weakSelf = self;
+ self.timer = [NSTimer srgdiagnostics_timerWithTimeInterval:submissionInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
+ [weakSelf submitFinishedReports];
+ }];
+}
+
+- (void)setTimer:(NSTimer *)timer
+{
+ [_timer invalidate];
+ _timer = timer;
+}
+
+#pragma mark Reports
+
+- (SRGDiagnosticReport *)reportWithName:(NSString *)name
+{
+ @synchronized(self) {
+ SRGDiagnosticReport *report = self.reports[name];
+ if (! report) {
+ report = [[SRGDiagnosticReport alloc] initWithDiagnosticsService:self];
+ self.reports[name] = report;
+ }
+ return report;
+ }
+}
+
+#pragma mark Submission
+
+- (void)prepareToSubmitReport:(SRGDiagnosticReport *)report
+{
+ @synchronized(self) {
+ NSString *identifier = [self.reports allKeysForObject:report].firstObject;
+ if (identifier) {
+ [self.reports removeObjectForKey:identifier];
+ [self.finishedReports addObject:[report copy]];
+ }
+ }
+}
+
+- (void)submitFinishedReports
+{
+ @synchronized(self) {
+ if (self.submitting) {
+ return;
+ }
+
+ if (self.finishedReports.count == 0) {
+ return;
+ }
+
+ self.submitting = YES;
+
+ __block NSUInteger processedReports = 0;
+ NSArray *finishedReports = [self.finishedReports copy];
+ for (SRGDiagnosticReport *report in finishedReports) {
+ void (^completionBlock)(BOOL) = ^(BOOL success) {
+ @synchronized(self) {
+ if (success) {
+ [self.finishedReports removeObject:report];
+ }
+
+ ++processedReports;
+ if (processedReports == finishedReports.count) {
+ self.submitting = NO;
+ }
+ }
+ };
+
+ if (self.submissionBlock) {
+ self.submissionBlock([report JSONDictionary], completionBlock);
+ }
+ else {
+ completionBlock(YES);
+ }
+ }
+ }
+}
+
+@end
diff --git a/Framework/Sources/SRGTimeMeasurement.h b/Framework/Sources/SRGTimeMeasurement.h
new file mode 100644
index 0000000..a66fcb2
--- /dev/null
+++ b/Framework/Sources/SRGTimeMeasurement.h
@@ -0,0 +1,43 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Undefined time interval (measurement not started or not finished yet).
+ */
+static NSTimeInterval const SRGTimeMeasurementUndefined = -1.;
+
+/**
+ * Internal class for time measurements. Valid measurements start with a `-start` and end with a `-stop`.
+ */
+@interface SRGTimeMeasurement : NSObject
+
+/**
+ * Start a time measurement.
+ *
+ * @discussion Until stopped, the measured time is `SRGTimeMeasurementUndefined`. Attempting to start an already started
+ * measurement does nothing.
+ */
+- (void)start;
+
+/**
+ * Stop a time measurement.
+ *
+ * @discussion Attempting to stop a non-started measurement does noting.
+ */
+- (void)stop;
+
+/**
+ * The time measurement, or `SRGTimeMeasurementUndefined` if not determined yet.
+ */
+- (NSTimeInterval)timeInterval;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Framework/Sources/SRGTimeMeasurement.m b/Framework/Sources/SRGTimeMeasurement.m
new file mode 100644
index 0000000..38c3151
--- /dev/null
+++ b/Framework/Sources/SRGTimeMeasurement.m
@@ -0,0 +1,64 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGTimeMeasurement.h"
+
+@interface SRGTimeMeasurement ()
+
+@property (nonatomic) NSDate *startDate;
+@property (nonatomic) NSTimeInterval timeInterval;
+
+@end
+
+@implementation SRGTimeMeasurement
+
+#pragma mark Object lifecycle
+
+- (instancetype)init
+{
+ if (self = [super init]) {
+ self.timeInterval = SRGTimeMeasurementUndefined;
+ }
+ return self;
+}
+
+#pragma mark Measurement
+
+- (void)start
+{
+ @synchronized(self) {
+ if (self.startDate) {
+ return;
+ }
+
+ self.timeInterval = SRGTimeMeasurementUndefined;
+ self.startDate = NSDate.date;
+ }
+}
+
+- (void)stop
+{
+ @synchronized(self) {
+ if (! self.startDate) {
+ return;
+ }
+
+ self.timeInterval = [NSDate.date timeIntervalSinceDate:self.startDate];
+ self.startDate = nil;
+ }
+}
+
+#pragma mark Description
+
+- (NSString *)description
+{
+ return [NSString stringWithFormat:@"<%@: %p; timeInterval = %@>",
+ self.class,
+ self,
+ @(self.timeInterval)];
+}
+
+@end
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8c8be76
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+#!/usr/bin/xcrun make -f
+
+.PHONY: all
+all:
+ @echo "Building the project..."
+ @xcodebuild build
+ @echo "... done.\n"
+
+.PHONY: package
+package:
+ @echo "Packaging binaries..."
+ @mkdir -p archive
+ @carthage build --no-skip-current
+ @carthage archive --output archive
+ @echo "... done.\n"
+
+.PHONY: clean
+clean:
+ @echo "Cleaning up build products..."
+ @xcodebuild clean
+ @rm -rf $(CARTHAGE_FOLDER)
+ @echo "... done.\n"
+
+.PHONY: help
+help:
+ @echo "The following targets are available:"
+ @echo " all Build project dependencies and the project"
+ @echo " package Build and package the framework for attaching to github releases"
+ @echo " clean Clean the project and its dependencies"
+ @echo " help Display this message"
diff --git a/SRGDiagnostics.xcodeproj/project.pbxproj b/SRGDiagnostics.xcodeproj/project.pbxproj
index 964e71c..cc2e7e1 100644
--- a/SRGDiagnostics.xcodeproj/project.pbxproj
+++ b/SRGDiagnostics.xcodeproj/project.pbxproj
@@ -11,10 +11,42 @@
6F0EB54320FC7FF4009C02CF /* SRGDiagnostics.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0EB54220FC7FF4009C02CF /* SRGDiagnostics.m */; };
6F0EB54720FC8049009C02CF /* NSBundle+SRGDiagnostics.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0EB54520FC8049009C02CF /* NSBundle+SRGDiagnostics.m */; };
6F0EB54820FC8049009C02CF /* NSBundle+SRGDiagnostics.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F0EB54620FC8049009C02CF /* NSBundle+SRGDiagnostics.h */; };
- 6F0EB54C20FC80E2009C02CF /* SRGDiagnosticsService.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F0EB54A20FC80E2009C02CF /* SRGDiagnosticsService.h */; };
- 6F0EB54D20FC80E2009C02CF /* SRGDiagnosticsService.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F0EB54B20FC80E2009C02CF /* SRGDiagnosticsService.m */; };
+ 6F126F422126C9E700158646 /* SRGDiagnosticReport.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F126F402126C9E700158646 /* SRGDiagnosticReport.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 6F126F432126C9E700158646 /* SRGDiagnosticReport.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F126F412126C9E700158646 /* SRGDiagnosticReport.m */; };
+ 6F126F462126CF2900158646 /* SRGTimeMeasurement.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F126F442126CF2900158646 /* SRGTimeMeasurement.h */; };
+ 6F126F472126CF2900158646 /* SRGTimeMeasurement.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F126F452126CF2900158646 /* SRGTimeMeasurement.m */; };
+ 6F126F512126E34300158646 /* SRGDiagnostics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F0EB53120FC7F58009C02CF /* SRGDiagnostics.framework */; };
+ 6F126F672126E7F800158646 /* SRGDiagnostics.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 6F126F5D2126E79E00158646 /* SRGDiagnostics.bundle */; };
+ 6F1EA3082126C92E0095820B /* SRGDiagnosticsService.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F1EA3062126C92E0095820B /* SRGDiagnosticsService.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 6F1EA3092126C92E0095820B /* SRGDiagnosticsService.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F1EA3072126C92E0095820B /* SRGDiagnosticsService.m */; };
+ 6F21945C212BD481000449AC /* NSTimer+SRGDiagnostics.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F21945A212BD481000449AC /* NSTimer+SRGDiagnostics.h */; };
+ 6F21945D212BD481000449AC /* NSTimer+SRGDiagnostics.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F21945B212BD481000449AC /* NSTimer+SRGDiagnostics.m */; };
+ 6F219460212BD63E000449AC /* SRGDiagnosticsTimerTarget.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F21945E212BD63D000449AC /* SRGDiagnosticsTimerTarget.h */; };
+ 6F219461212BD63E000449AC /* SRGDiagnosticsTimerTarget.m in Sources */ = {isa = PBXBuildFile; fileRef = 6F21945F212BD63E000449AC /* SRGDiagnosticsTimerTarget.m */; };
+ 6FC45897212AA3CD007CAA97 /* SRGDiagnosticsServiceTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FC45895212AA3CD007CAA97 /* SRGDiagnosticsServiceTestCase.m */; };
+ 6FC45898212AA3CD007CAA97 /* SRGDiagnosticInformationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FC45896212AA3CD007CAA97 /* SRGDiagnosticInformationTestCase.m */; };
+ 6FC4589A212AB5E8007CAA97 /* SRGTimeMeasurementTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FC45899212AB5E8007CAA97 /* SRGTimeMeasurementTestCase.m */; };
+ 6FC4589D212AD5DC007CAA97 /* SRGDiagnosticInformation.h in Headers */ = {isa = PBXBuildFile; fileRef = 6FC4589B212AD5DC007CAA97 /* SRGDiagnosticInformation.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 6FC4589E212AD5DC007CAA97 /* SRGDiagnosticInformation.m in Sources */ = {isa = PBXBuildFile; fileRef = 6FC4589C212AD5DC007CAA97 /* SRGDiagnosticInformation.m */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ 6F126F522126E34300158646 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6F0EB52820FC7F58009C02CF /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6F0EB53020FC7F58009C02CF;
+ remoteInfo = SRGDiagnostics;
+ };
+ 6F126F652126E7F300158646 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6F0EB52820FC7F58009C02CF /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6F126F5C2126E79E00158646;
+ remoteInfo = "SRGDiagnostics-resources";
+ };
+/* End PBXContainerItemProxy section */
+
/* Begin PBXFileReference section */
6F0EB53120FC7F58009C02CF /* SRGDiagnostics.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SRGDiagnostics.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6F0EB53E20FC7FA9009C02CF /* SRGDiagnostics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRGDiagnostics.h; sourceTree = ""; };
@@ -22,8 +54,27 @@
6F0EB54220FC7FF4009C02CF /* SRGDiagnostics.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnostics.m; sourceTree = ""; };
6F0EB54520FC8049009C02CF /* NSBundle+SRGDiagnostics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+SRGDiagnostics.m"; sourceTree = ""; };
6F0EB54620FC8049009C02CF /* NSBundle+SRGDiagnostics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSBundle+SRGDiagnostics.h"; sourceTree = ""; };
- 6F0EB54A20FC80E2009C02CF /* SRGDiagnosticsService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SRGDiagnosticsService.h; sourceTree = ""; };
- 6F0EB54B20FC80E2009C02CF /* SRGDiagnosticsService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnosticsService.m; sourceTree = ""; };
+ 6F126F402126C9E700158646 /* SRGDiagnosticReport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SRGDiagnosticReport.h; sourceTree = ""; };
+ 6F126F412126C9E700158646 /* SRGDiagnosticReport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnosticReport.m; sourceTree = ""; };
+ 6F126F442126CF2900158646 /* SRGTimeMeasurement.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SRGTimeMeasurement.h; sourceTree = ""; };
+ 6F126F452126CF2900158646 /* SRGTimeMeasurement.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SRGTimeMeasurement.m; sourceTree = ""; };
+ 6F126F4C2126E34300158646 /* SRGDiagnostics-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SRGDiagnostics-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6F126F502126E34300158646 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 6F126F5D2126E79E00158646 /* SRGDiagnostics.bundle */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SRGDiagnostics.bundle; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6F126F682126FB9900158646 /* SRGDiagnosticReport+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SRGDiagnosticReport+Private.h"; sourceTree = ""; };
+ 6F1EA3062126C92E0095820B /* SRGDiagnosticsService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRGDiagnosticsService.h; sourceTree = ""; };
+ 6F1EA3072126C92E0095820B /* SRGDiagnosticsService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnosticsService.m; sourceTree = ""; };
+ 6F21945A212BD481000449AC /* NSTimer+SRGDiagnostics.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSTimer+SRGDiagnostics.h"; sourceTree = ""; };
+ 6F21945B212BD481000449AC /* NSTimer+SRGDiagnostics.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSTimer+SRGDiagnostics.m"; sourceTree = ""; };
+ 6F21945E212BD63D000449AC /* SRGDiagnosticsTimerTarget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRGDiagnosticsTimerTarget.h; sourceTree = ""; };
+ 6F21945F212BD63E000449AC /* SRGDiagnosticsTimerTarget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnosticsTimerTarget.m; sourceTree = ""; };
+ 6F673C8E212A86D5006C42AE /* SRGDiagnosticsService+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SRGDiagnosticsService+Private.h"; sourceTree = ""; };
+ 6FC45895212AA3CD007CAA97 /* SRGDiagnosticsServiceTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnosticsServiceTestCase.m; sourceTree = ""; };
+ 6FC45896212AA3CD007CAA97 /* SRGDiagnosticInformationTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnosticInformationTestCase.m; sourceTree = ""; };
+ 6FC45899212AB5E8007CAA97 /* SRGTimeMeasurementTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRGTimeMeasurementTestCase.m; sourceTree = ""; };
+ 6FC4589B212AD5DC007CAA97 /* SRGDiagnosticInformation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SRGDiagnosticInformation.h; sourceTree = ""; };
+ 6FC4589C212AD5DC007CAA97 /* SRGDiagnosticInformation.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SRGDiagnosticInformation.m; sourceTree = ""; };
+ 6FE6E5E12148DB5D00228573 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -34,6 +85,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ 6F126F492126E34300158646 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6F126F512126E34300158646 /* SRGDiagnostics.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6F126F5A2126E79E00158646 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -41,6 +107,7 @@
isa = PBXGroup;
children = (
6F0EB53C20FC7FA9009C02CF /* Framework */,
+ 6F126F4D2126E34300158646 /* Tests */,
6F0EB53220FC7F58009C02CF /* Products */,
);
sourceTree = "";
@@ -49,6 +116,8 @@
isa = PBXGroup;
children = (
6F0EB53120FC7F58009C02CF /* SRGDiagnostics.framework */,
+ 6F126F4C2126E34300158646 /* SRGDiagnostics-tests.xctest */,
+ 6F126F5D2126E79E00158646 /* SRGDiagnostics.bundle */,
);
name = Products;
sourceTree = "";
@@ -57,6 +126,7 @@
isa = PBXGroup;
children = (
6F0EB53D20FC7FA9009C02CF /* Sources */,
+ 6FE6E5E02148DB5D00228573 /* Resources */,
6F0EB53F20FC7FA9009C02CF /* Info.plist */,
);
path = Framework;
@@ -65,10 +135,19 @@
6F0EB53D20FC7FA9009C02CF /* Sources */ = {
isa = PBXGroup;
children = (
- 6F0EB54920FC8097009C02CF /* Player */,
6F0EB54420FC8049009C02CF /* Helpers */,
+ 6FC4589B212AD5DC007CAA97 /* SRGDiagnosticInformation.h */,
+ 6FC4589C212AD5DC007CAA97 /* SRGDiagnosticInformation.m */,
+ 6F126F402126C9E700158646 /* SRGDiagnosticReport.h */,
+ 6F126F412126C9E700158646 /* SRGDiagnosticReport.m */,
+ 6F126F682126FB9900158646 /* SRGDiagnosticReport+Private.h */,
6F0EB53E20FC7FA9009C02CF /* SRGDiagnostics.h */,
6F0EB54220FC7FF4009C02CF /* SRGDiagnostics.m */,
+ 6F1EA3062126C92E0095820B /* SRGDiagnosticsService.h */,
+ 6F1EA3072126C92E0095820B /* SRGDiagnosticsService.m */,
+ 6F673C8E212A86D5006C42AE /* SRGDiagnosticsService+Private.h */,
+ 6F126F442126CF2900158646 /* SRGTimeMeasurement.h */,
+ 6F126F452126CF2900158646 /* SRGTimeMeasurement.m */,
);
path = Sources;
sourceTree = "";
@@ -78,17 +157,39 @@
children = (
6F0EB54620FC8049009C02CF /* NSBundle+SRGDiagnostics.h */,
6F0EB54520FC8049009C02CF /* NSBundle+SRGDiagnostics.m */,
+ 6F21945A212BD481000449AC /* NSTimer+SRGDiagnostics.h */,
+ 6F21945B212BD481000449AC /* NSTimer+SRGDiagnostics.m */,
+ 6F21945E212BD63D000449AC /* SRGDiagnosticsTimerTarget.h */,
+ 6F21945F212BD63E000449AC /* SRGDiagnosticsTimerTarget.m */,
);
path = Helpers;
sourceTree = "";
};
- 6F0EB54920FC8097009C02CF /* Player */ = {
+ 6F126F4D2126E34300158646 /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ 6FC45894212AA3CD007CAA97 /* Sources */,
+ 6F126F502126E34300158646 /* Info.plist */,
+ );
+ path = Tests;
+ sourceTree = "";
+ };
+ 6FC45894212AA3CD007CAA97 /* Sources */ = {
isa = PBXGroup;
children = (
- 6F0EB54A20FC80E2009C02CF /* SRGDiagnosticsService.h */,
- 6F0EB54B20FC80E2009C02CF /* SRGDiagnosticsService.m */,
+ 6FC45896212AA3CD007CAA97 /* SRGDiagnosticInformationTestCase.m */,
+ 6FC45895212AA3CD007CAA97 /* SRGDiagnosticsServiceTestCase.m */,
+ 6FC45899212AB5E8007CAA97 /* SRGTimeMeasurementTestCase.m */,
);
- path = Player;
+ path = Sources;
+ sourceTree = "";
+ };
+ 6FE6E5E02148DB5D00228573 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 6FE6E5E12148DB5D00228573 /* Info.plist */,
+ );
+ path = Resources;
sourceTree = "";
};
/* End PBXGroup section */
@@ -98,9 +199,14 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
+ 6F126F422126C9E700158646 /* SRGDiagnosticReport.h in Headers */,
6F0EB54020FC7FA9009C02CF /* SRGDiagnostics.h in Headers */,
+ 6F126F462126CF2900158646 /* SRGTimeMeasurement.h in Headers */,
+ 6FC4589D212AD5DC007CAA97 /* SRGDiagnosticInformation.h in Headers */,
+ 6F21945C212BD481000449AC /* NSTimer+SRGDiagnostics.h in Headers */,
+ 6F219460212BD63E000449AC /* SRGDiagnosticsTimerTarget.h in Headers */,
6F0EB54820FC8049009C02CF /* NSBundle+SRGDiagnostics.h in Headers */,
- 6F0EB54C20FC80E2009C02CF /* SRGDiagnosticsService.h in Headers */,
+ 6F1EA3082126C92E0095820B /* SRGDiagnosticsService.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -119,12 +225,48 @@
buildRules = (
);
dependencies = (
+ 6F126F662126E7F300158646 /* PBXTargetDependency */,
);
name = SRGDiagnostics;
productName = SRGDiagnostics;
productReference = 6F0EB53120FC7F58009C02CF /* SRGDiagnostics.framework */;
productType = "com.apple.product-type.framework";
};
+ 6F126F4B2126E34300158646 /* SRGDiagnostics-tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 6F126F542126E34300158646 /* Build configuration list for PBXNativeTarget "SRGDiagnostics-tests" */;
+ buildPhases = (
+ 6F126F482126E34300158646 /* Sources */,
+ 6F126F492126E34300158646 /* Frameworks */,
+ 6F126F4A2126E34300158646 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 6F126F532126E34300158646 /* PBXTargetDependency */,
+ );
+ name = "SRGDiagnostics-tests";
+ productName = "SRGDiagnostics-tests";
+ productReference = 6F126F4C2126E34300158646 /* SRGDiagnostics-tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 6F126F5C2126E79E00158646 /* SRGDiagnostics-resources */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 6F126F602126E79E00158646 /* Build configuration list for PBXNativeTarget "SRGDiagnostics-resources" */;
+ buildPhases = (
+ 6F126F592126E79E00158646 /* Sources */,
+ 6F126F5A2126E79E00158646 /* Frameworks */,
+ 6F126F5B2126E79E00158646 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "SRGDiagnostics-resources";
+ productName = "SRGDiagnostics-resources";
+ productReference = 6F126F5D2126E79E00158646 /* SRGDiagnostics.bundle */;
+ productType = "com.apple.product-type.bundle";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -137,6 +279,12 @@
6F0EB53020FC7F58009C02CF = {
CreatedOnToolsVersion = 9.4;
};
+ 6F126F4B2126E34300158646 = {
+ CreatedOnToolsVersion = 9.4;
+ };
+ 6F126F5C2126E79E00158646 = {
+ CreatedOnToolsVersion = 9.4;
+ };
};
};
buildConfigurationList = 6F0EB52B20FC7F58009C02CF /* Build configuration list for PBXProject "SRGDiagnostics" */;
@@ -152,12 +300,29 @@
projectRoot = "";
targets = (
6F0EB53020FC7F58009C02CF /* SRGDiagnostics */,
+ 6F126F5C2126E79E00158646 /* SRGDiagnostics-resources */,
+ 6F126F4B2126E34300158646 /* SRGDiagnostics-tests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
6F0EB52F20FC7F58009C02CF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6F126F672126E7F800158646 /* SRGDiagnostics.bundle in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6F126F4A2126E34300158646 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6F126F5B2126E79E00158646 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
@@ -171,14 +336,49 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 6F21945D212BD481000449AC /* NSTimer+SRGDiagnostics.m in Sources */,
6F0EB54720FC8049009C02CF /* NSBundle+SRGDiagnostics.m in Sources */,
+ 6F219461212BD63E000449AC /* SRGDiagnosticsTimerTarget.m in Sources */,
+ 6F126F432126C9E700158646 /* SRGDiagnosticReport.m in Sources */,
+ 6F126F472126CF2900158646 /* SRGTimeMeasurement.m in Sources */,
6F0EB54320FC7FF4009C02CF /* SRGDiagnostics.m in Sources */,
- 6F0EB54D20FC80E2009C02CF /* SRGDiagnosticsService.m in Sources */,
+ 6FC4589E212AD5DC007CAA97 /* SRGDiagnosticInformation.m in Sources */,
+ 6F1EA3092126C92E0095820B /* SRGDiagnosticsService.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6F126F482126E34300158646 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6FC45897212AA3CD007CAA97 /* SRGDiagnosticsServiceTestCase.m in Sources */,
+ 6FC4589A212AB5E8007CAA97 /* SRGTimeMeasurementTestCase.m in Sources */,
+ 6FC45898212AA3CD007CAA97 /* SRGDiagnosticInformationTestCase.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6F126F592126E79E00158646 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ 6F126F532126E34300158646 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6F0EB53020FC7F58009C02CF /* SRGDiagnostics */;
+ targetProxy = 6F126F522126E34300158646 /* PBXContainerItemProxy */;
+ };
+ 6F126F662126E7F300158646 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6F126F5C2126E79E00158646 /* SRGDiagnostics-resources */;
+ targetProxy = 6F126F652126E7F300158646 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
/* Begin XCBuildConfiguration section */
6F0EB53720FC7F58009C02CF /* Debug */ = {
isa = XCBuildConfiguration;
@@ -216,6 +416,7 @@
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
+ DYLIB_CURRENT_VERSION = 1;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -237,8 +438,6 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
- VERSIONING_SYSTEM = "apple-generic";
- VERSION_INFO_PREFIX = "";
};
name = Debug;
};
@@ -278,6 +477,7 @@
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DYLIB_CURRENT_VERSION = 1;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -293,19 +493,17 @@
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
- VERSIONING_SYSTEM = "apple-generic";
- VERSION_INFO_PREFIX = "";
};
name = Release;
};
6F0EB53A20FC7F58009C02CF /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Framework/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -318,17 +516,18 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
6F0EB53B20FC7F58009C02CF /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
- DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
INFOPLIST_FILE = Framework/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -341,9 +540,302 @@
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+ 6F126F552126E34300158646 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Tests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-tests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 6F126F562126E34300158646 /* Debug-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Tests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-tests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = "Debug-static";
+ };
+ 6F126F572126E34300158646 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Tests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-tests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ 6F126F582126E34300158646 /* Release-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Tests/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-tests";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = "Release-static";
+ };
+ 6F126F612126E79E00158646 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Framework/Resources/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-resources";
+ PRODUCT_NAME = SRGDiagnostics;
+ SKIP_INSTALL = YES;
+ WRAPPER_EXTENSION = bundle;
+ };
+ name = Debug;
+ };
+ 6F126F622126E79E00158646 /* Debug-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Framework/Resources/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-resources";
+ PRODUCT_NAME = SRGDiagnostics;
+ SKIP_INSTALL = YES;
+ WRAPPER_EXTENSION = bundle;
+ };
+ name = "Debug-static";
+ };
+ 6F126F632126E79E00158646 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Framework/Resources/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-resources";
+ PRODUCT_NAME = SRGDiagnostics;
+ SKIP_INSTALL = YES;
+ WRAPPER_EXTENSION = bundle;
};
name = Release;
};
+ 6F126F642126E79E00158646 /* Release-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_IDENTITY = "-";
+ CODE_SIGN_STYLE = Automatic;
+ INFOPLIST_FILE = Framework/Resources/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
+ PRODUCT_BUNDLE_IDENTIFIER = "ch.srgssr.SRGDiagnostics-resources";
+ PRODUCT_NAME = SRGDiagnostics;
+ SKIP_INSTALL = YES;
+ WRAPPER_EXTENSION = bundle;
+ };
+ name = "Release-static";
+ };
+ 6F1EA3022125B3160095820B /* Debug-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DYLIB_CURRENT_VERSION = 1;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ };
+ name = "Debug-static";
+ };
+ 6F1EA3032125B3160095820B /* Debug-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
+ CLANG_ENABLE_CODE_COVERAGE = NO;
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Automatic;
+ DEFINES_MODULE = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ INFOPLIST_FILE = Framework/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MACH_O_TYPE = staticlib;
+ PRODUCT_BUNDLE_IDENTIFIER = ch.srgssr.SRGDiagnostics;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = "Debug-static";
+ };
+ 6F1EA3042125B31C0095820B /* Release-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DYLIB_CURRENT_VERSION = 1;
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = "Release-static";
+ };
+ 6F1EA3052125B31C0095820B /* Release-static */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ APPLICATION_EXTENSION_API_ONLY = YES;
+ CLANG_ENABLE_CODE_COVERAGE = NO;
+ CODE_SIGN_IDENTITY = "";
+ CODE_SIGN_STYLE = Automatic;
+ DEFINES_MODULE = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ INFOPLIST_FILE = Framework/Info.plist;
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ MACH_O_TYPE = staticlib;
+ PRODUCT_BUNDLE_IDENTIFIER = ch.srgssr.SRGDiagnostics;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = "Release-static";
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -351,7 +843,9 @@
isa = XCConfigurationList;
buildConfigurations = (
6F0EB53720FC7F58009C02CF /* Debug */,
+ 6F1EA3022125B3160095820B /* Debug-static */,
6F0EB53820FC7F58009C02CF /* Release */,
+ 6F1EA3042125B31C0095820B /* Release-static */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
@@ -360,7 +854,31 @@
isa = XCConfigurationList;
buildConfigurations = (
6F0EB53A20FC7F58009C02CF /* Debug */,
+ 6F1EA3032125B3160095820B /* Debug-static */,
6F0EB53B20FC7F58009C02CF /* Release */,
+ 6F1EA3052125B31C0095820B /* Release-static */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 6F126F542126E34300158646 /* Build configuration list for PBXNativeTarget "SRGDiagnostics-tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6F126F552126E34300158646 /* Debug */,
+ 6F126F562126E34300158646 /* Debug-static */,
+ 6F126F572126E34300158646 /* Release */,
+ 6F126F582126E34300158646 /* Release-static */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 6F126F602126E79E00158646 /* Build configuration list for PBXNativeTarget "SRGDiagnostics-resources" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6F126F612126E79E00158646 /* Debug */,
+ 6F126F622126E79E00158646 /* Debug-static */,
+ 6F126F632126E79E00158646 /* Release */,
+ 6F126F642126E79E00158646 /* Release-static */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
diff --git a/SRGDiagnostics.xcodeproj/xcshareddata/xcschemes/SRGDiagnostics.xcscheme b/SRGDiagnostics.xcodeproj/xcshareddata/xcschemes/SRGDiagnostics.xcscheme
index 5a7c95a..ce23d30 100644
--- a/SRGDiagnostics.xcodeproj/xcshareddata/xcschemes/SRGDiagnostics.xcscheme
+++ b/SRGDiagnostics.xcodeproj/xcshareddata/xcschemes/SRGDiagnostics.xcscheme
@@ -26,9 +26,31 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
+
+
+
+
+
+
+
+
diff --git a/Tests/Info.plist b/Tests/Info.plist
new file mode 100644
index 0000000..26b175d
--- /dev/null
+++ b/Tests/Info.plist
@@ -0,0 +1,22 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ BNDL
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+
+
diff --git a/Tests/Sources/SRGDiagnosticInformationTestCase.m b/Tests/Sources/SRGDiagnosticInformationTestCase.m
new file mode 100644
index 0000000..280ddda
--- /dev/null
+++ b/Tests/Sources/SRGDiagnosticInformationTestCase.m
@@ -0,0 +1,208 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import
+#import
+
+// Compare two dictionaries. Numbers are compared within a given accuracy
+#define SRGAssertEqualDictionariesWithAccuracy(dictionary, referenceDictionary, accuracy) XCTAssertTrue(SRGAreDictionariesEqualWithAccuracy(dictionary, referenceDictionary, accuracy))
+
+static BOOL SRGAreDictionariesEqualWithAccuracy(NSDictionary *dictionary, NSDictionary *referenceDictionary, double accuracy)
+{
+ if (dictionary.count != referenceDictionary.count) {
+ return NO;
+ }
+
+ for (NSString *key in referenceDictionary.allKeys) {
+ id value = dictionary[key];
+ id referenceValue = referenceDictionary[key];
+
+ if ([value isKindOfClass:NSDictionary.class] && [referenceValue isKindOfClass:NSDictionary.class]) {
+ if (! SRGAreDictionariesEqualWithAccuracy(value, referenceValue, accuracy)) {
+ return NO;
+ }
+ }
+ else if ([value isKindOfClass:NSNumber.class] && [referenceValue isKindOfClass:NSNumber.class]) {
+ if (fabs([value doubleValue] - [referenceValue doubleValue]) > accuracy) {
+ return NO;
+ }
+ }
+ else if (! [value isEqual:referenceValue]) {
+ return NO;
+ }
+ }
+
+ return YES;
+}
+
+@interface SRGDiagnosticInformationTestCase : XCTestCase
+
+@end
+
+@implementation SRGDiagnosticInformationTestCase
+
+#pragma mark Tests
+
+- (void)testEmptyInformation
+{
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ XCTAssertEqualObjects([information JSONDictionary], @{});
+}
+
+- (void)testFilledInformation
+{
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ [information setBool:YES forKey:@"boolean"];
+ [information setInteger:1012 forKey:@"integer"];
+ [information setFloat:36.5678f forKey:@"float"];
+ [information setDouble:107.1234 forKey:@"double"];
+ [information setString:@"Hello, World!" forKey:@"string"];
+ [information setNumber:@7654.987 forKey:@"number"];
+ [information setURL:[NSURL URLWithString:@"https://www.apple.com"] forKey:@"url"];
+ NSDictionary *expectedDictionary = @{ @"boolean" : @YES,
+ @"integer" : @1012,
+ @"float" : @36.5678f,
+ @"double" : @107.1234,
+ @"string" : @"Hello, World!",
+ @"number" : @7654.987,
+ @"url" : @"https://www.apple.com" };
+ XCTAssertEqualObjects([information JSONDictionary], expectedDictionary);
+}
+
+- (void)testTimeMeasurement
+{
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ [information startTimeMeasurementForKey:@"time"];
+ [NSThread sleepForTimeInterval:1.];
+ [information stopTimeMeasurementForKey:@"time"];
+ NSDictionary *expectedDictionary = @{ @"time" : @1000. };
+ SRGAssertEqualDictionariesWithAccuracy([information JSONDictionary], expectedDictionary, 10.);
+}
+
+- (void)testNestedInformation
+{
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ [information setString:@"parent" forKey:@"title"];
+ SRGDiagnosticInformation *nestedInformation = [information informationForKey:@"nestedInformation"];
+ [nestedInformation setString:@"child" forKey:@"subtitle"];
+ NSDictionary *expectedDictionary = @{ @"title" : @"parent",
+ @"nestedInformation" : @{ @"subtitle" : @"child" } };
+ XCTAssertEqualObjects([information JSONDictionary], expectedDictionary);
+}
+
+- (void)testComplexReport
+{
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ [information setString:@"Letterbox/iOS/1.9" forKey:@"player"];
+ [information setString:@"iPhone 6" forKey:@"device"];
+ [information setString:@"urn:rts:video:12345" forKey:@"urn"];
+ [information setString:@"2011-07-11T14:18:47+02:00" forKey:@"clientTime"];
+ [information setString:@"3g" forKey:@"networkType"];
+ [information setString:@"success" forKey:@"result"];
+
+ SRGDiagnosticInformation *ILErrorInformation = [information informationForKey:@"ilError"];
+ [ILErrorInformation setString:@"GEOBLOCK" forKey:@"blockReason"];
+ [ILErrorInformation setString:@"111 222" forKey:@"varnish"];
+ [ILErrorInformation setBool:YES forKey:@"playableAbroad"];
+
+ SRGDiagnosticInformation *networkErrorInformation = [information informationForKey:@"networkError"];
+ [networkErrorInformation setString:@"https://domain.com/resource/path" forKey:@"url"];
+ [networkErrorInformation setInteger:404 forKey:@"responseCode"];
+ [networkErrorInformation setString:@"222 333" forKey:@"varnish"];
+ [networkErrorInformation setString:@"A network error has been encountered" forKey:@"message"];
+
+ SRGDiagnosticInformation *parsingErrorInformation = [information informationForKey:@"parsingError"];
+ [parsingErrorInformation setString:@"https://domain.com/resource/path" forKey:@"url"];
+ [parsingErrorInformation setInteger:12 forKey:@"line"];
+ [parsingErrorInformation setString:@"A parsing error has been encountered" forKey:@"message"];
+
+ SRGDiagnosticInformation *playerErrorInformation = [information informationForKey:@"playerError"];
+ [playerErrorInformation setString:@"https://domain.com/resource/path" forKey:@"url"];
+ [playerErrorInformation setString:@"1000kbps" forKey:@"variant"];
+ [playerErrorInformation setString:@"https://domain.com/license" forKey:@"licenseUrl"];
+ [playerErrorInformation setString:@"A player error has been encountered" forKey:@"message"];
+
+ [information setBool:YES forKey:@"noPlayableResourceFound"];
+
+ SRGDiagnosticInformation *timeInformation = [information informationForKey:@"time"];
+
+ [timeInformation startTimeMeasurementForKey:@"clickToPlay"];
+ [NSThread sleepForTimeInterval:0.5];
+ [timeInformation stopTimeMeasurementForKey:@"clickToPlay"];
+
+ [timeInformation startTimeMeasurementForKey:@"il"];
+ [NSThread sleepForTimeInterval:0.6];
+ [timeInformation stopTimeMeasurementForKey:@"il"];
+
+ [timeInformation startTimeMeasurementForKey:@"token"];
+ [NSThread sleepForTimeInterval:0.7];
+ [timeInformation stopTimeMeasurementForKey:@"token"];
+
+ [timeInformation startTimeMeasurementForKey:@"media"];
+ [NSThread sleepForTimeInterval:0.8];
+ [timeInformation stopTimeMeasurementForKey:@"media"];
+
+ [timeInformation startTimeMeasurementForKey:@"drm"];
+ [NSThread sleepForTimeInterval:0.9];
+ [timeInformation stopTimeMeasurementForKey:@"drm"];
+
+ NSDictionary *expectedDictionary = @{ @"player" : @"Letterbox/iOS/1.9",
+ @"device" : @"iPhone 6",
+ @"urn" : @"urn:rts:video:12345",
+ @"clientTime" : @"2011-07-11T14:18:47+02:00",
+ @"networkType" : @"3g",
+ @"result" : @"success",
+ @"ilError" : @{ @"blockReason" : @"GEOBLOCK",
+ @"varnish" : @"111 222",
+ @"playableAbroad" : @YES },
+ @"networkError" : @{ @"url" : @"https://domain.com/resource/path",
+ @"responseCode" : @404,
+ @"varnish" : @"222 333",
+ @"message" : @"A network error has been encountered" },
+ @"parsingError" : @{ @"url" : @"https://domain.com/resource/path",
+ @"line" : @12,
+ @"message" : @"A parsing error has been encountered" },
+ @"playerError" : @{ @"url" : @"https://domain.com/resource/path",
+ @"variant" : @"1000kbps",
+ @"licenseUrl" : @"https://domain.com/license",
+ @"message" : @"A player error has been encountered" },
+ @"noPlayableResourceFound" : @YES,
+ @"time" : @{ @"clickToPlay" : @500.,
+ @"il" : @600.,
+ @"token" : @700.,
+ @"media" : @800.,
+ @"drm" : @900. }
+ };
+ SRGAssertEqualDictionariesWithAccuracy([information JSONDictionary], expectedDictionary, 10.);
+}
+
+- (void)testCopy
+{
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ [information setString:@"parent" forKey:@"title"];
+ [[information informationForKey:@"nestedInformation"] setString:@"child" forKey:@"subtitle"];
+
+ [information startTimeMeasurementForKey:@"time"];
+ [NSThread sleepForTimeInterval:1.];
+ [information stopTimeMeasurementForKey:@"time"];
+
+ SRGDiagnosticInformation *informationCopy = [information copy];
+ NSDictionary *expectedDictionary = @{ @"title" : @"parent",
+ @"nestedInformation" : @{ @"subtitle" : @"child" },
+ @"time" : @1000. };
+ SRGAssertEqualDictionariesWithAccuracy([informationCopy JSONDictionary], expectedDictionary, 10.);
+}
+
+- (void)testInformationRemoval
+{
+ SRGDiagnosticInformation *information = [[SRGDiagnosticInformation alloc] init];
+ [information setString:@"Title" forKey:@"title"];
+ XCTAssertEqualObjects([information JSONDictionary], @{ @"title" : @"Title" });
+ [information setString:nil forKey:@"title"];
+ XCTAssertEqualObjects([information JSONDictionary], @{});
+}
+
+@end
diff --git a/Tests/Sources/SRGDiagnosticsServiceTestCase.m b/Tests/Sources/SRGDiagnosticsServiceTestCase.m
new file mode 100644
index 0000000..ebfb86c
--- /dev/null
+++ b/Tests/Sources/SRGDiagnosticsServiceTestCase.m
@@ -0,0 +1,304 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import
+#import
+
+@interface SRGDiagnosticsServiceTestCase : XCTestCase
+
+@end
+
+@implementation SRGDiagnosticsServiceTestCase
+
+#pragma mark Helpers
+
+- (XCTestExpectation *)expectationForElapsedTimeInterval:(NSTimeInterval)timeInterval withHandler:(void (^)(void))handler
+{
+ XCTestExpectation *expectation = [self expectationWithDescription:[NSString stringWithFormat:@"Wait for %@ seconds", @(timeInterval)]];
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+ [expectation fulfill];
+ handler ? handler() : nil;
+ });
+ return expectation;
+}
+
+#pragma mark Tests
+
+- (void)testServiceRetrieval
+{
+ NSString *name = NSUUID.UUID.UUIDString;
+ SRGDiagnosticsService *service1 = [SRGDiagnosticsService serviceWithName:name];
+ XCTAssertNotNil(service1);
+ XCTAssertNil(service1.submissionBlock);
+
+ SRGDiagnosticsService *service2 = [SRGDiagnosticsService serviceWithName:name];
+ XCTAssertEqualObjects(service1, service2);
+}
+
+- (void)testReportCreation
+{
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+
+ SRGDiagnosticReport *report1 = [service reportWithName:@"report"];
+ XCTAssertNotNil(report1);
+
+ SRGDiagnosticReport *report2 = [service reportWithName:@"report"];
+ XCTAssertEqualObjects(report1, report2);
+}
+
+- (void)testReportSuccessfulSubmission
+{
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Submission block called"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+ [expectation fulfill];
+ };
+
+ SRGDiagnosticReport *report = [service reportWithName:@"report"];
+ [report setString:@"My report" forKey:@"title"];
+ [report finish];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testReportSubmissionAfterSubmissionWithoutReports
+{
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Submission block called"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+ [service submitFinishedReports];
+
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+ [expectation fulfill];
+ };
+
+ SRGDiagnosticReport *report = [service reportWithName:@"report"];
+ [report setString:@"My report" forKey:@"title"];
+ [report finish];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testSubmissionAfterSuccessfulReportubmission
+{
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Submission block called"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+ [expectation fulfill];
+ };
+
+ SRGDiagnosticReport *report = [service reportWithName:@"report"];
+ [report setString:@"My report" forKey:@"title"];
+ [report finish];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+
+ [self expectationForElapsedTimeInterval:2. withHandler:nil];
+
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ XCTFail(@"Must not be called since the report was already submitted");
+ };
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testMultipleReportSuccessfulSubmission
+{
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Submission block called"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+
+ __block BOOL report1Submitted = NO;
+ __block BOOL report2Submitted = NO;
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+
+ if ([JSONDictionary[@"uid"] isEqualToString:@"report1"]) {
+ report1Submitted = YES;
+ }
+ else if ([JSONDictionary[@"uid"] isEqualToString:@"report2"]) {
+ report2Submitted = YES;
+ }
+
+ if (report1Submitted && report2Submitted) {
+ [expectation fulfill];
+ }
+ };
+
+ SRGDiagnosticReport *report1 = [service reportWithName:@"report1"];
+ [report1 setString:@"report1" forKey:@"uid"];
+ [report1 finish];
+
+ SRGDiagnosticReport *report2 = [service reportWithName:@"report2"];
+ [report2 setString:@"report2" forKey:@"uid"];
+ [report2 finish];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testMultipleReportSuccessfulSubmissionWithNameReuse
+{
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Submission block called"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+
+ __block BOOL report1Submitted = NO;
+ __block BOOL report2Submitted = NO;
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+
+ if ([JSONDictionary[@"uid1"] isEqualToString:@"report1"]) {
+ XCTAssertNil(JSONDictionary[@"uid2"]);
+ report1Submitted = YES;
+ }
+ else if ([JSONDictionary[@"uid2"] isEqualToString:@"report2"]) {
+ XCTAssertNil(JSONDictionary[@"uid1"]);
+ report2Submitted = YES;
+ }
+
+ if (report1Submitted && report2Submitted) {
+ [expectation fulfill];
+ }
+ };
+
+ SRGDiagnosticReport *report1 = [service reportWithName:@"report"];
+ [report1 setString:@"report1" forKey:@"uid1"];
+ [report1 finish];
+
+ SRGDiagnosticReport *report2 = [service reportWithName:@"report"];
+ [report2 setString:@"report2" forKey:@"uid2"];
+ [report2 finish];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testUnfinishedReportSubmission
+{
+ [self expectationForElapsedTimeInterval:2. withHandler:nil];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ XCTFail(@"Must not be called since the report has not been marked as finished");
+ };
+
+ SRGDiagnosticReport *report = [service reportWithName:@"report"];
+ [report setString:@"My report" forKey:@"title"];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testReportFailedFirstSubmission
+{
+ XCTestExpectation *expectation1 = [self expectationWithDescription:@"First submission block called"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(NO);
+ [expectation1 fulfill];
+ };
+
+ SRGDiagnosticReport *report = [service reportWithName:@"report"];
+ [report setString:@"My report" forKey:@"title"];
+ [report finish];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+
+ XCTestExpectation *expectation2 = [self expectationWithDescription:@"Second submission block called"];
+
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+ [expectation2 fulfill];
+ };
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testReportImmutabilityAfterSubmission
+{
+ XCTestExpectation *expectation1 = [self expectationWithDescription:@"First submission block called"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(NO);
+ [expectation1 fulfill];
+ };
+
+ SRGDiagnosticReport *report = [service reportWithName:@"report"];
+ [report setString:@"My report" forKey:@"title"];
+ [report finish];
+
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+
+ XCTestExpectation *expectation2 = [self expectationWithDescription:@"Second submission block called"];
+
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ XCTAssertEqualObjects(JSONDictionary[@"title"], @"My report");
+ completionBlock(YES);
+ [expectation2 fulfill];
+ };
+
+ [report setString:@"Modified title" forKey:@"title"];
+ [service submitFinishedReports];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+- (void)testPeriodicSubmission
+{
+ XCTestExpectation *expectation1 = [self expectationWithDescription:@"First report submitted"];
+
+ SRGDiagnosticsService *service = [SRGDiagnosticsService serviceWithName:NSUUID.UUID.UUIDString];
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+ [expectation1 fulfill];
+ };
+ service.submissionInterval = 2.;
+
+ SRGDiagnosticReport *report1 = [service reportWithName:@"report"];
+ [report1 setString:@"My report" forKey:@"title"];
+ [report1 finish];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+
+ XCTestExpectation *expectation2 = [self expectationWithDescription:@"Second report submitted"];
+
+ service.submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ completionBlock(YES);
+ [expectation2 fulfill];
+ };
+
+ SRGDiagnosticReport *report2 = [service reportWithName:@"report"];
+ [report2 setString:@"My other report" forKey:@"title"];
+ [report2 finish];
+
+ [self waitForExpectationsWithTimeout:10. handler:nil];
+}
+
+@end
diff --git a/Tests/Sources/SRGTimeMeasurementTestCase.m b/Tests/Sources/SRGTimeMeasurementTestCase.m
new file mode 100644
index 0000000..7fb29ef
--- /dev/null
+++ b/Tests/Sources/SRGTimeMeasurementTestCase.m
@@ -0,0 +1,85 @@
+//
+// Copyright (c) SRG SSR. All rights reserved.
+//
+// License information is available from the LICENSE file.
+//
+
+#import "SRGTimeMeasurement.h"
+
+#import
+
+@interface SRGTimeMeasurementTestCase : XCTestCase
+
+@end
+
+@implementation SRGTimeMeasurementTestCase
+
+- (void)testTimeMeasurementCreation
+{
+ SRGTimeMeasurement *timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ XCTAssertEqual([timeMeasurement timeInterval], SRGTimeMeasurementUndefined);
+}
+
+- (void)testValidTimeMeasurement
+{
+ SRGTimeMeasurement *timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ [timeMeasurement start];
+ XCTAssertEqual([timeMeasurement timeInterval], SRGTimeMeasurementUndefined);
+ [NSThread sleepForTimeInterval:1.];
+ [timeMeasurement stop];
+ XCTAssertEqualWithAccuracy([timeMeasurement timeInterval], 1., 0.1);
+}
+
+- (void)testUnstoppedTimeMeasurement
+{
+ SRGTimeMeasurement *timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ [timeMeasurement start];
+ XCTAssertEqual([timeMeasurement timeInterval], SRGTimeMeasurementUndefined);
+}
+
+- (void)testUnstartedTimeMeasurement
+{
+ SRGTimeMeasurement *timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ [timeMeasurement stop];
+ XCTAssertEqual([timeMeasurement timeInterval], SRGTimeMeasurementUndefined);
+}
+
+- (void)testRestartedTimeMeasurement
+{
+ SRGTimeMeasurement *timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ [timeMeasurement start];
+ XCTAssertEqual([timeMeasurement timeInterval], SRGTimeMeasurementUndefined);
+ [NSThread sleepForTimeInterval:1.];
+ [timeMeasurement stop];
+ XCTAssertEqualWithAccuracy([timeMeasurement timeInterval], 1., 0.1);
+
+ [timeMeasurement start];
+ XCTAssertEqual([timeMeasurement timeInterval], SRGTimeMeasurementUndefined);
+ [NSThread sleepForTimeInterval:1.];
+ [timeMeasurement stop];
+ XCTAssertEqualWithAccuracy([timeMeasurement timeInterval], 1., 0.1);
+}
+
+- (void)testTimeMeasurementStartedTwice
+{
+ SRGTimeMeasurement *timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ [timeMeasurement start];
+ [NSThread sleepForTimeInterval:1.];
+ [timeMeasurement start];
+ [NSThread sleepForTimeInterval:1.];
+ [timeMeasurement stop];
+ XCTAssertEqualWithAccuracy([timeMeasurement timeInterval], 2., 0.1);
+}
+
+- (void)testTimeMeasurementStoppedTwice
+{
+ SRGTimeMeasurement *timeMeasurement = [[SRGTimeMeasurement alloc] init];
+ [timeMeasurement start];
+ [NSThread sleepForTimeInterval:1.];
+ [timeMeasurement stop];
+ [NSThread sleepForTimeInterval:1.];
+ [timeMeasurement stop];
+ XCTAssertEqualWithAccuracy([timeMeasurement timeInterval], 1., 0.1);
+}
+
+@end
diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..baa8507
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000..57158b9
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,41 @@
+## Introduction
+
+Thank you for very much for your interest in contributing to our project! As a public service company, we want to shape a product that better matches our user needs and desires. Ideas or direct contributions are therefore warmly welcome and will be considered with great care, provided they fulfill a few requirements listed in this document. Please read it first before you decide to contribute.
+
+### Why guidelines?
+
+Our development team is small, our ability to quickly evaluate a need or a code submission is therefore critical. Please follow the present contributing guidelines so that we can efficiently consider your proposal. You should also read or our [code of conduct](CODE_OF_CONDUCT.md), providing a few guidelines to keep interactions as respectful as possible.
+
+### Contributions we are looking for
+
+Any kind of contribution is welcome, as long as it improves the overall quality of our product, for example:
+
+* Requests for new features or ideas.
+* Bug reports or fixes.
+* Documentation improvements.
+* Translation improvements.
+
+Contributions can either take the form of simple issues where you describe the problem you face or what you would like to see in our products. If you feel up to the challenge, you can even submit code in the form of pull requests which our team will review.
+
+### Contributions we are not looking for
+
+Requests which are too vague or not related to our product will not be taken into account. We also have no editorial influence, any issue related to the content available on our platform will simply be closed.
+
+## Contributing
+
+You can use issues to report bugs, submit ideas or request features. People with a programming background can also submit changes directly via pull requests. Creating issues or pull requests requires you to own or [open](https://github.com/join) a GitHub account.
+
+If you are not sure about the likelihood of a change you propose to be accepted, please open an issue first. We can discuss it there, especially whether it is compatible with our product or not. This way you can avoid creating an entire pull request we will never be able to merge.
+
+Templates are available when you want to contribute:
+
+* [Issues](https://github.com/SRGSSR/playsrg-ios/issues/new): Please follow our issue template. You can omit information which does not make sense but, in general, the more details you can provide, the better. This ensures we can quickly reproduce the problem you are facing, increasing the likelihood we can fix it.
+* [Pull requests](https://github.com/SRGSSR/playsrg-ios/compare): Please follow our code conventions, test your code well, and write unit tests when this makes sense. We will review your work and, if successful, merge it back into the main development branch.
+
+## Code conventions
+
+We currently have no formal code conventions, but we try to keep our codebase consistent. In general, having a look at the code itself should be enough for you to discover how you should write your changes.
+
+## Code review
+
+Pull requests, once complete, can be submitted for review by our team. Depending on the complexity of the involved changes, a few iterations might be needed. Once a pull request has been approved, it will be rebased, merged back into the development trunk and delivered with the next release.
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index 0f83a76..3f8305b 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -4,9 +4,20 @@
## About
+The SRG Diagnostics library is a set of lightweight components for collecting various application data for diagnostics purposes. The library is based on two main types of components:
+
+* Reports, for collecting data.
+* Services, responsible of report submission.
+
+Reports and services are referenced by name and lazily created when accessed first. This makes it possible to create, fill and submit reports in a decentralized way. Note that diagnostics reports are not considered critical and are therefore not persisted between application sessions. You must never use such reports to convey data you cannot afford to lose.
+
## Compatibility
-The library is suitable for applications running on iOS 9 and above. The project is meant to be opened with the latest Xcode version (currently Xcode 9).
+The library is suitable for applications running on iOS 9 and above. The project is meant to be opened with the latest Xcode version (currently Xcode 10).
+
+## Contributing
+
+If you want to contribute to the project, have a look at our [contributing guide](CONTRIBUTING.md).
## Installation
@@ -16,8 +27,6 @@ The library can be added to a project using [Carthage](https://github.com/Cartha
github "SRGSSR/srgdiagnostics-ios"
```
-Until Carthage 0.30, only dynamic frameworks could be integrated. Starting with Carthage 0.30, though, frameworks can be integrated statically as well, which avoids slow application startups usually associated with the use of too many dynamic frameworks.
-
For more information about Carthage and its use, refer to the [official documentation](https://github.com/Carthage/Carthage).
### Dependencies
@@ -69,6 +78,78 @@ Import the module where needed:
import SRGDiagnostics
```
+### Creating reports
+
+To create a report, pick appropriate service and report names first. For example, to collect playback metrics associated with playback of medias, we could use _playback_ as service name, and create a report for each media being played based on its identifier:
+
+```objective-c
+SRGDiagnosticsReport *report = [[SRGDiagnosticsService serviceWithName:@"playback"] reportWithName:@"1-23456-789"];
+```
+
+Report information is filled using `-set...:forKey:` methods:
+
+```objective-c
+[report setInteger:3000 forKey:@"kbps"];
+[report setString:@"HD" forKey:@"quality"];
+```
+
+Time measurements (in milliseconds) can be easily added to a report. For example, to start a measurement saved under the `buffering` key, call:
+
+```objective-c
+[report startTimeMeasurementForKey:@"buffering"];
+```
+
+and to stop the measurement, call:
+
+```objective-c
+[report stopTimeMeasurementForKey:@"buffering"];
+```
+
+Information can be nested in a report. We could for example imagine saving network-related information together:
+
+```objective-c
+SRGDiagnosticInformation *information = [report informationForKey:@"network"];
+[information setString:@"wifi" forKey:@"connection"];
+[information setString:@"good" forKey:@"signal"];
+```
+
+Finally, when your report is finished, mark it as such:
+
+```objective-c
+[report finish];
+```
+
+Since services and reports are retrieved based on names, filling information, performing time measurements or marking a report as finished can be made from any application subsystem in a decentralized way, from any thread.
+
+### Report submission
+
+Once a report has been marked as finished, the associated service will automatically attempt to submit it after a while. Submission is made by serializing the report as JSON and calling a submission block on the service, which you can use to specify how the data is submitted.
+
+For example, you can POST the data to a webservice expecting it as body:
+
+```objective-c
+[SRGDiagnosticsService serviceWithName:@"playback_metrics"].submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ NSURL *URL = ...;
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
+ request.HTTPMethod = @"POST";
+ request.HTTPBody = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:NULL];
+ [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
+ completionBlock(error == nil);
+ }] resume];
+};
+```
+
+or simply log the report to the console:
+
+```objective-c
+[SRGDiagnosticsService serviceWithName:@"playback_metrics"].submissionBlock = ^(NSDictionary * _Nonnull JSONDictionary, void (^ _Nonnull completionBlock)(BOOL)) {
+ NSLog(@"Report: %@", JSONDictionary);
+ completionBlock(YES);
+};
+```
+
+The submission block can be updated at any time, though it should in general be setup once early in the application lifecycle. Once submission has been made, the supplied `completionBlock` must be called, with `YES` iff the submission was successful. Successfully submitted reports are discarded, otherwise the service will attempt to submit it later. The `submissionInterval` property lets you customize at which interval submissions are made.
+
## License
See the [LICENSE](../LICENSE) file for more information.
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
new file mode 100644
index 0000000..a67b75c
--- /dev/null
+++ b/fastlane/Fastfile
@@ -0,0 +1,42 @@
+# Customise this file, documentation can be found here:
+# https://github.com/fastlane/fastlane/tree/master/fastlane/docs
+# All available actions: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Actions.md
+# can also be listed using the `fastlane actions` command
+
+# Change the syntax highlighting to Ruby
+# All lines starting with a # are ignored when running `fastlane`
+
+# This is the minimum version number required.
+fastlane_version "1.95.0"
+
+default_platform :ios
+
+platform :ios do
+ before_all do |lane|
+ ensure_git_status_clean
+ end
+
+ desc "Run library tests"
+ lane :test do
+ scan(
+ scheme: "SRGDiagnostics",
+ clean: true
+ )
+ end
+
+ after_all do |lane|
+ reset_git_repo(skip_clean: true)
+ end
+
+ error do |lane, exception|
+ clean_build_artifacts
+ reset_git_repo(skip_clean: true, force: true)
+ end
+end
+
+
+# More information about multiple platforms in fastlane: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Platforms.md
+# All available actions: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Actions.md
+
+# fastlane reports which actions are used
+# No personal data is recorded. Learn more at https://github.com/fastlane/enhancer
diff --git a/fastlane/README.md b/fastlane/README.md
new file mode 100644
index 0000000..cd66005
--- /dev/null
+++ b/fastlane/README.md
@@ -0,0 +1,29 @@
+fastlane documentation
+================
+# Installation
+
+Make sure you have the latest version of the Xcode command line tools installed:
+
+```
+xcode-select --install
+```
+
+Install _fastlane_ using
+```
+[sudo] gem install fastlane -NV
+```
+or alternatively using `brew cask install fastlane`
+
+# Available Actions
+## iOS
+### ios test
+```
+fastlane ios test
+```
+Run library tests
+
+----
+
+This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
+More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
+The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).