From 4e5c1e0ccd545079568f703a281904d705643996 Mon Sep 17 00:00:00 2001 From: Dave Gallagher Date: Wed, 27 Mar 2019 15:33:39 -0700 Subject: [PATCH] Migrated QuizTrain Example project to separate repository. --- .gitignore | 5 + .gitmodules | 3 + Cartfile.private | 1 + Cartfile.resolved | 1 + Carthage/Checkouts/QuizTrain | 1 + Example.xcodeproj/project.pbxproj | 792 ++++++++++++++++++ .../xcshareddata/xcschemes/Example.xcscheme | 111 +++ Example/AppDelegate.swift | 41 + .../AppIcon.appiconset/Contents.json | 98 +++ Example/Assets.xcassets/Contents.json | 6 + Example/Base.lproj/LaunchScreen.storyboard | 41 + Example/Base.lproj/Main.storyboard | 41 + Example/Info.plist | 45 + Example/ViewController.swift | 9 + ExampleTests/ExampleTests.swift | 40 + ExampleTests/Info.plist | 24 + ExampleUITests/ExampleUITests.swift | 51 ++ ExampleUITests/Info.plist | 24 + LICENSE | 7 + README.md | 36 +- Shared/QuizTrainManager.swift | 401 +++++++++ Shared/QuizTrainProject.swift | 266 ++++++ Shared/TestManager.swift | 100 +++ Shared/XCTContextExtensions.swift | 28 + 24 files changed, 2170 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cartfile.private create mode 100644 Cartfile.resolved create mode 160000 Carthage/Checkouts/QuizTrain create mode 100644 Example.xcodeproj/project.pbxproj create mode 100644 Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme create mode 100644 Example/AppDelegate.swift create mode 100644 Example/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/Assets.xcassets/Contents.json create mode 100644 Example/Base.lproj/LaunchScreen.storyboard create mode 100644 Example/Base.lproj/Main.storyboard create mode 100644 Example/Info.plist create mode 100644 Example/ViewController.swift create mode 100644 ExampleTests/ExampleTests.swift create mode 100644 ExampleTests/Info.plist create mode 100644 ExampleUITests/ExampleUITests.swift create mode 100644 ExampleUITests/Info.plist create mode 100644 LICENSE create mode 100644 Shared/QuizTrainManager.swift create mode 100644 Shared/QuizTrainProject.swift create mode 100644 Shared/TestManager.swift create mode 100644 Shared/XCTContextExtensions.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e5ad1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +xcuserdata/ +*.pbxuser +*.xcworkspace +Carthage/Build diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..354a122 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Carthage/Checkouts/QuizTrain"] + path = Carthage/Checkouts/QuizTrain + url = ssh://git@github.com/venmo/QuizTrain.git diff --git a/Cartfile.private b/Cartfile.private new file mode 100644 index 0000000..57eca26 --- /dev/null +++ b/Cartfile.private @@ -0,0 +1 @@ +github "venmo/QuizTrain" ~> 2.0.0 diff --git a/Cartfile.resolved b/Cartfile.resolved new file mode 100644 index 0000000..13abd89 --- /dev/null +++ b/Cartfile.resolved @@ -0,0 +1 @@ +github "venmo/QuizTrain" "2.0.0" diff --git a/Carthage/Checkouts/QuizTrain b/Carthage/Checkouts/QuizTrain new file mode 160000 index 0000000..9a55052 --- /dev/null +++ b/Carthage/Checkouts/QuizTrain @@ -0,0 +1 @@ +Subproject commit 9a550528e87ccf68b4067075edf612724f4159f6 diff --git a/Example.xcodeproj/project.pbxproj b/Example.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0ee1c20 --- /dev/null +++ b/Example.xcodeproj/project.pbxproj @@ -0,0 +1,792 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + FEA1D07A20895A77009C8341 /* QuizTrain.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = FED7AF92207EA38D00169889 /* QuizTrain.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FEA1D07C20895A86009C8341 /* QuizTrain.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = FED7AF92207EA38D00169889 /* QuizTrain.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FED7AF34207EA0F400169889 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF33207EA0F400169889 /* AppDelegate.swift */; }; + FED7AF36207EA0F400169889 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF35207EA0F400169889 /* ViewController.swift */; }; + FED7AF39207EA0F400169889 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FED7AF37207EA0F400169889 /* Main.storyboard */; }; + FED7AF3B207EA0F500169889 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FED7AF3A207EA0F500169889 /* Assets.xcassets */; }; + FED7AF3E207EA0F500169889 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FED7AF3C207EA0F500169889 /* LaunchScreen.storyboard */; }; + FED7AF49207EA0F500169889 /* ExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF48207EA0F500169889 /* ExampleTests.swift */; }; + FED7AF54207EA0F500169889 /* ExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF53207EA0F500169889 /* ExampleUITests.swift */; }; + FED7AF75207EA22700169889 /* XCTContextExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF71207EA22600169889 /* XCTContextExtensions.swift */; }; + FED7AF76207EA22700169889 /* XCTContextExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF71207EA22600169889 /* XCTContextExtensions.swift */; }; + FED7AF77207EA22700169889 /* QuizTrainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF72207EA22600169889 /* QuizTrainManager.swift */; }; + FED7AF78207EA22700169889 /* QuizTrainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF72207EA22600169889 /* QuizTrainManager.swift */; }; + FED7AF79207EA22700169889 /* TestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF73207EA22600169889 /* TestManager.swift */; }; + FED7AF7A207EA22700169889 /* TestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF73207EA22600169889 /* TestManager.swift */; }; + FED7AF7B207EA22700169889 /* QuizTrainProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF74207EA22600169889 /* QuizTrainProject.swift */; }; + FED7AF7C207EA22700169889 /* QuizTrainProject.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED7AF74207EA22600169889 /* QuizTrainProject.swift */; }; + FED7AF9F207EA39800169889 /* QuizTrain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FED7AF92207EA38D00169889 /* QuizTrain.framework */; }; + FED7AFA0207EA3A000169889 /* QuizTrain.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FED7AF92207EA38D00169889 /* QuizTrain.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + FED7AF45207EA0F500169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF28207EA0F400169889 /* Project object */; + proxyType = 1; + remoteGlobalIDString = FED7AF2F207EA0F400169889; + remoteInfo = Example; + }; + FED7AF50207EA0F500169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF28207EA0F400169889 /* Project object */; + proxyType = 1; + remoteGlobalIDString = FED7AF2F207EA0F400169889; + remoteInfo = Example; + }; + FED7AF91207EA38D00169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FE23A91C1F59F3A0007E946D; + remoteInfo = "QuizTrain-iOS"; + }; + FED7AF93207EA38D00169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FEAE997B1FEC3BEB00B52CA9; + remoteInfo = "QuizTrain-macOS"; + }; + FED7AF95207EA38D00169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FEAE995F1FEC3BCD00B52CA9; + remoteInfo = "QuizTrain-tvOS"; + }; + FED7AF97207EA38D00169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FEAE99511FEC38BA00B52CA9; + remoteInfo = "QuizTrain-watchOS"; + }; + FED7AF99207EA38D00169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FE23A9251F59F3A0007E946D; + remoteInfo = "QuizTrainTests-iOS"; + }; + FED7AF9B207EA38D00169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FEAE99831FEC3BEB00B52CA9; + remoteInfo = "QuizTrainTests-macOS"; + }; + FED7AF9D207EA38D00169889 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = FEAE99671FEC3BCE00B52CA9; + remoteInfo = "QuizTrainTests-tvOS"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + FEA1D07220895A6D009C8341 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + FEA1D07A20895A77009C8341 /* QuizTrain.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FEA1D07B20895A7E009C8341 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + FEA1D07C20895A86009C8341 /* QuizTrain.framework in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + FED7AF30207EA0F400169889 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + FED7AF33207EA0F400169889 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + FED7AF35207EA0F400169889 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + FED7AF38207EA0F400169889 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + FED7AF3A207EA0F500169889 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + FED7AF3D207EA0F500169889 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + FED7AF3F207EA0F500169889 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FED7AF44207EA0F500169889 /* ExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FED7AF48207EA0F500169889 /* ExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleTests.swift; sourceTree = ""; }; + FED7AF4A207EA0F500169889 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FED7AF4F207EA0F500169889 /* ExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + FED7AF53207EA0F500169889 /* ExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleUITests.swift; sourceTree = ""; }; + FED7AF55207EA0F500169889 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + FED7AF71207EA22600169889 /* XCTContextExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTContextExtensions.swift; sourceTree = ""; }; + FED7AF72207EA22600169889 /* QuizTrainManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuizTrainManager.swift; sourceTree = ""; }; + FED7AF73207EA22600169889 /* TestManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestManager.swift; sourceTree = ""; }; + FED7AF74207EA22600169889 /* QuizTrainProject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuizTrainProject.swift; sourceTree = ""; }; + FED7AF7D207EA35B00169889 /* Cartfile.private */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile.private; sourceTree = ""; }; + FED7AF7E207EA35B00169889 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + FED7AF7F207EA35C00169889 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = QuizTrain.xcodeproj; path = Carthage/Checkouts/QuizTrain/QuizTrain.xcodeproj; sourceTree = SOURCE_ROOT; }; + FED7AFA1207EA3D600169889 /* Cartfile.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = ""; }; + FED7AFA2207EA6D000169889 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + FED7AF2D207EA0F400169889 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FED7AF41207EA0F500169889 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FED7AF9F207EA39800169889 /* QuizTrain.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FED7AF4C207EA0F500169889 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FED7AFA0207EA3A000169889 /* QuizTrain.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + FED7AF27207EA0F400169889 = { + isa = PBXGroup; + children = ( + FED7AF32207EA0F400169889 /* Example */, + FED7AF47207EA0F500169889 /* ExampleTests */, + FED7AF52207EA0F500169889 /* ExampleUITests */, + FED7AF61207EA11300169889 /* Shared */, + FED7AF86207EA37E00169889 /* Frameworks */, + FED7AF31207EA0F400169889 /* Products */, + FED7AFA2207EA6D000169889 /* .gitignore */, + FED7AF7D207EA35B00169889 /* Cartfile.private */, + FED7AFA1207EA3D600169889 /* Cartfile.resolved */, + FED7AF7E207EA35B00169889 /* LICENSE */, + FED7AF7F207EA35C00169889 /* README.md */, + ); + sourceTree = ""; + }; + FED7AF31207EA0F400169889 /* Products */ = { + isa = PBXGroup; + children = ( + FED7AF30207EA0F400169889 /* Example.app */, + FED7AF44207EA0F500169889 /* ExampleTests.xctest */, + FED7AF4F207EA0F500169889 /* ExampleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + FED7AF32207EA0F400169889 /* Example */ = { + isa = PBXGroup; + children = ( + FED7AF33207EA0F400169889 /* AppDelegate.swift */, + FED7AF35207EA0F400169889 /* ViewController.swift */, + FED7AF37207EA0F400169889 /* Main.storyboard */, + FED7AF3A207EA0F500169889 /* Assets.xcassets */, + FED7AF3C207EA0F500169889 /* LaunchScreen.storyboard */, + FED7AF3F207EA0F500169889 /* Info.plist */, + ); + path = Example; + sourceTree = ""; + }; + FED7AF47207EA0F500169889 /* ExampleTests */ = { + isa = PBXGroup; + children = ( + FED7AF48207EA0F500169889 /* ExampleTests.swift */, + FED7AF4A207EA0F500169889 /* Info.plist */, + ); + path = ExampleTests; + sourceTree = ""; + }; + FED7AF52207EA0F500169889 /* ExampleUITests */ = { + isa = PBXGroup; + children = ( + FED7AF53207EA0F500169889 /* ExampleUITests.swift */, + FED7AF55207EA0F500169889 /* Info.plist */, + ); + path = ExampleUITests; + sourceTree = ""; + }; + FED7AF61207EA11300169889 /* Shared */ = { + isa = PBXGroup; + children = ( + FED7AF73207EA22600169889 /* TestManager.swift */, + FED7AF72207EA22600169889 /* QuizTrainManager.swift */, + FED7AF74207EA22600169889 /* QuizTrainProject.swift */, + FED7AF71207EA22600169889 /* XCTContextExtensions.swift */, + ); + path = Shared; + sourceTree = ""; + }; + FED7AF86207EA37E00169889 /* Frameworks */ = { + isa = PBXGroup; + children = ( + FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */, + ); + path = Frameworks; + sourceTree = ""; + }; + FED7AF88207EA38D00169889 /* Products */ = { + isa = PBXGroup; + children = ( + FED7AF92207EA38D00169889 /* QuizTrain.framework */, + FED7AF94207EA38D00169889 /* QuizTrain.framework */, + FED7AF96207EA38D00169889 /* QuizTrain.framework */, + FED7AF98207EA38D00169889 /* QuizTrain.framework */, + FED7AF9A207EA38D00169889 /* QuizTrainTests.xctest */, + FED7AF9C207EA38D00169889 /* QuizTrainTests.xctest */, + FED7AF9E207EA38D00169889 /* QuizTrainTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + FED7AF2F207EA0F400169889 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = FED7AF58207EA0F500169889 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + FED7AF2C207EA0F400169889 /* Sources */, + FED7AF2D207EA0F400169889 /* Frameworks */, + FED7AF2E207EA0F400169889 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Example; + productName = Example; + productReference = FED7AF30207EA0F400169889 /* Example.app */; + productType = "com.apple.product-type.application"; + }; + FED7AF43207EA0F500169889 /* ExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FED7AF5B207EA0F500169889 /* Build configuration list for PBXNativeTarget "ExampleTests" */; + buildPhases = ( + FED7AF40207EA0F500169889 /* Sources */, + FED7AF41207EA0F500169889 /* Frameworks */, + FED7AF42207EA0F500169889 /* Resources */, + FEA1D07220895A6D009C8341 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + FED7AF46207EA0F500169889 /* PBXTargetDependency */, + ); + name = ExampleTests; + productName = ExampleTests; + productReference = FED7AF44207EA0F500169889 /* ExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + FED7AF4E207EA0F500169889 /* ExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = FED7AF5E207EA0F500169889 /* Build configuration list for PBXNativeTarget "ExampleUITests" */; + buildPhases = ( + FED7AF4B207EA0F500169889 /* Sources */, + FED7AF4C207EA0F500169889 /* Frameworks */, + FED7AF4D207EA0F500169889 /* Resources */, + FEA1D07B20895A7E009C8341 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + FED7AF51207EA0F500169889 /* PBXTargetDependency */, + ); + name = ExampleUITests; + productName = ExampleUITests; + productReference = FED7AF4F207EA0F500169889 /* ExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + FED7AF28207EA0F400169889 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0930; + LastUpgradeCheck = 0930; + ORGANIZATIONNAME = Venmo; + TargetAttributes = { + FED7AF2F207EA0F400169889 = { + CreatedOnToolsVersion = 9.3; + LastSwiftMigration = 1020; + }; + FED7AF43207EA0F500169889 = { + CreatedOnToolsVersion = 9.3; + LastSwiftMigration = 1020; + TestTargetID = FED7AF2F207EA0F400169889; + }; + FED7AF4E207EA0F500169889 = { + CreatedOnToolsVersion = 9.3; + LastSwiftMigration = 1020; + TestTargetID = FED7AF2F207EA0F400169889; + }; + }; + }; + buildConfigurationList = FED7AF2B207EA0F400169889 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = FED7AF27207EA0F400169889; + productRefGroup = FED7AF31207EA0F400169889 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = FED7AF88207EA38D00169889 /* Products */; + ProjectRef = FED7AF87207EA38D00169889 /* QuizTrain.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + FED7AF2F207EA0F400169889 /* Example */, + FED7AF43207EA0F500169889 /* ExampleTests */, + FED7AF4E207EA0F500169889 /* ExampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + FED7AF92207EA38D00169889 /* QuizTrain.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = QuizTrain.framework; + remoteRef = FED7AF91207EA38D00169889 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + FED7AF94207EA38D00169889 /* QuizTrain.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = QuizTrain.framework; + remoteRef = FED7AF93207EA38D00169889 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + FED7AF96207EA38D00169889 /* QuizTrain.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = QuizTrain.framework; + remoteRef = FED7AF95207EA38D00169889 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + FED7AF98207EA38D00169889 /* QuizTrain.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = QuizTrain.framework; + remoteRef = FED7AF97207EA38D00169889 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + FED7AF9A207EA38D00169889 /* QuizTrainTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = QuizTrainTests.xctest; + remoteRef = FED7AF99207EA38D00169889 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + FED7AF9C207EA38D00169889 /* QuizTrainTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = QuizTrainTests.xctest; + remoteRef = FED7AF9B207EA38D00169889 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + FED7AF9E207EA38D00169889 /* QuizTrainTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = QuizTrainTests.xctest; + remoteRef = FED7AF9D207EA38D00169889 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + FED7AF2E207EA0F400169889 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FED7AF3E207EA0F500169889 /* LaunchScreen.storyboard in Resources */, + FED7AF3B207EA0F500169889 /* Assets.xcassets in Resources */, + FED7AF39207EA0F400169889 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FED7AF42207EA0F500169889 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FED7AF4D207EA0F500169889 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + FED7AF2C207EA0F400169889 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FED7AF36207EA0F400169889 /* ViewController.swift in Sources */, + FED7AF34207EA0F400169889 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FED7AF40207EA0F500169889 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FED7AF49207EA0F500169889 /* ExampleTests.swift in Sources */, + FED7AF75207EA22700169889 /* XCTContextExtensions.swift in Sources */, + FED7AF7B207EA22700169889 /* QuizTrainProject.swift in Sources */, + FED7AF77207EA22700169889 /* QuizTrainManager.swift in Sources */, + FED7AF79207EA22700169889 /* TestManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FED7AF4B207EA0F500169889 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FED7AF54207EA0F500169889 /* ExampleUITests.swift in Sources */, + FED7AF76207EA22700169889 /* XCTContextExtensions.swift in Sources */, + FED7AF7C207EA22700169889 /* QuizTrainProject.swift in Sources */, + FED7AF78207EA22700169889 /* QuizTrainManager.swift in Sources */, + FED7AF7A207EA22700169889 /* TestManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + FED7AF46207EA0F500169889 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FED7AF2F207EA0F400169889 /* Example */; + targetProxy = FED7AF45207EA0F500169889 /* PBXContainerItemProxy */; + }; + FED7AF51207EA0F500169889 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FED7AF2F207EA0F400169889 /* Example */; + targetProxy = FED7AF50207EA0F500169889 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + FED7AF37207EA0F400169889 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + FED7AF38207EA0F400169889 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + FED7AF3C207EA0F500169889 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + FED7AF3D207EA0F500169889 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + FED7AF56207EA0F500169889 /* Debug */ = { + 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; + DEBUG_INFORMATION_FORMAT = dwarf; + 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 = 11.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + }; + name = Debug; + }; + FED7AF57207EA0F500169889 /* Release */ = { + 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; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + 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 = 11.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 4.2; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + FED7AF59207EA0F500169889 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = QuizTrain.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FED7AF5A207EA0F500169889 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = Example/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = QuizTrain.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + FED7AF5C207EA0F500169889 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = ExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = QuizTrain.ExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Debug; + }; + FED7AF5D207EA0F500169889 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = ExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = QuizTrain.ExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Release; + }; + FED7AF5F207EA0F500169889 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = ExampleUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = QuizTrain.ExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Example; + }; + name = Debug; + }; + FED7AF60207EA0F500169889 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = ExampleUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = QuizTrain.ExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Example; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + FED7AF2B207EA0F400169889 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FED7AF56207EA0F500169889 /* Debug */, + FED7AF57207EA0F500169889 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FED7AF58207EA0F500169889 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FED7AF59207EA0F500169889 /* Debug */, + FED7AF5A207EA0F500169889 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FED7AF5B207EA0F500169889 /* Build configuration list for PBXNativeTarget "ExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FED7AF5C207EA0F500169889 /* Debug */, + FED7AF5D207EA0F500169889 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + FED7AF5E207EA0F500169889 /* Build configuration list for PBXNativeTarget "ExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FED7AF5F207EA0F500169889 /* Debug */, + FED7AF60207EA0F500169889 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = FED7AF28207EA0F400169889 /* Project object */; +} diff --git a/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme new file mode 100644 index 0000000..3c1b08b --- /dev/null +++ b/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift new file mode 100644 index 0000000..82a09c5 --- /dev/null +++ b/Example/AppDelegate.swift @@ -0,0 +1,41 @@ +// +// QuizTrain iOS Example +// +// Created by David Gallagher (david.gallagher@venmo.com). +// Copyright © 2018 Venmo. All rights reserved. +// +// _____LICENSE_____ +// +// Copyright 2018 Venmo +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + +} diff --git a/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d8db8d6 --- /dev/null +++ b/Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Assets.xcassets/Contents.json b/Example/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/Example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Example/Base.lproj/LaunchScreen.storyboard b/Example/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..6f5ca2c --- /dev/null +++ b/Example/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Base.lproj/Main.storyboard b/Example/Base.lproj/Main.storyboard new file mode 100644 index 0000000..32c8e9e --- /dev/null +++ b/Example/Base.lproj/Main.storyboard @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Info.plist b/Example/Info.plist new file mode 100644 index 0000000..16be3b6 --- /dev/null +++ b/Example/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Example/ViewController.swift b/Example/ViewController.swift new file mode 100644 index 0000000..392c2a4 --- /dev/null +++ b/Example/ViewController.swift @@ -0,0 +1,9 @@ +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + } + +} diff --git a/ExampleTests/ExampleTests.swift b/ExampleTests/ExampleTests.swift new file mode 100644 index 0000000..5a700cc --- /dev/null +++ b/ExampleTests/ExampleTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import Example + +class ExampleTests: XCTestCase { + + func testWhichPasses() { + #error("Replace the placeholder caseId below with one from your project. Then comment out this macro.") + XCTContext.runActivity(testing: 10011) { activity in + XCTAssertTrue(true, "The value is not true.") + } + } + + func testWhichFails() { + #error("Replace the placeholder caseId below with one from your project. Then comment out this macro.") + XCTContext.runActivity(testing: 10022) { activity in + XCTAssertTrue(false, "The value is not true.") + } + } + + func testWithPassingAndFailingAssertions() { + + #error("Replace the placeholder caseId below with one from your project. Then comment out this macro.") + XCTContext.runActivity(testing: 10033) { activity in + + XCTAssertTrue(true, "The value is not true.") + + #error("Replace the placeholder caseIds below with ones from your project. Then comment out this macro.") + XCTContext.runActivity(testing: [10044, 10055]) { activity in + XCTAssertTrue(false, "The value is not true.") + XCTAssertFalse(false, "The value is not false.") + } + + #error("Replace the placeholder caseIds below with ones from your project. Then comment out this macro.") + XCTContext.runActivity(testing: [10066, 10077, 10088]) { activity in + XCTAssertTrue(true, "The value is not true.") + } + } + } + +} diff --git a/ExampleTests/Info.plist b/ExampleTests/Info.plist new file mode 100644 index 0000000..acc00b0 --- /dev/null +++ b/ExampleTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSPrincipalClass + ExampleTests.TestManager + + diff --git a/ExampleUITests/ExampleUITests.swift b/ExampleUITests/ExampleUITests.swift new file mode 100644 index 0000000..50255ae --- /dev/null +++ b/ExampleUITests/ExampleUITests.swift @@ -0,0 +1,51 @@ +import XCTest + +class ExampleUITests: XCTestCase { + + let app = XCUIApplication() + + override func setUp() { + super.setUp() + app.launch() + } + + func testWhichPasses() { + #error("Replace the placeholder caseId below with one from your project. Then comment out this macro.") + XCTContext.runActivity(testing: 20011) { activity in + let query = app.staticTexts["exampleLabel"] + XCTAssertTrue(query.exists, query.debugDescription) + } + } + + func testWhichFails() { + #error("Replace the placeholder caseId below with one from your project. Then comment out this macro.") + XCTContext.runActivity(testing: 20022) { activity in + let query = app.staticTexts["non-existant accessibility identifier"] + XCTAssertTrue(query.exists, query.debugDescription) + } + } + + func testWithPassingAndFailingAssertions() { + + #error("Replace the placeholder caseId below with one from your project. Then comment out this macro.") + XCTContext.runActivity(testing: 20033) { activity in + + let queryA = app.staticTexts["exampleLabel"] + XCTAssertTrue(queryA.exists, queryA.debugDescription) + + #error("Replace the placeholder caseIds below with ones from your project. Then comment out this macro.") + XCTContext.runActivity(testing: [20044, 20055]) { activity in + let queryB = app.staticTexts["non-existant accessibility identifier"] + XCTAssertTrue(queryB.exists, queryB.debugDescription) + XCTAssertFalse(queryB.exists, queryB.debugDescription) + } + + #error("Replace the placeholder caseIds below with ones from your project. Then comment out this macro.") + XCTContext.runActivity(testing: [20066, 20077, 20088]) { activity in + let queryC = app.staticTexts["exampleLabel"] + XCTAssertEqual(queryC.label, "QuizTrain Example", queryC.debugDescription) + } + } + } + +} diff --git a/ExampleUITests/Info.plist b/ExampleUITests/Info.plist new file mode 100644 index 0000000..3f6c38d --- /dev/null +++ b/ExampleUITests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSPrincipalClass + ExampleUITests.TestManager + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c24ae68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2018 Venmo + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 39510fc..b6763c8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,34 @@ -# QuizTrainExample -Example of using QuizTrain with Unit Tests and UI Tests on iOS +# Example + +This is an example iOS project showing how you can integrate [QuizTrain](https://github.com/venmo/QuizTrain) with your unit tests and user interface tests. + +## License + +The Example project is open source software released under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Prerequisites + +- Install [Xcode](https://developer.apple.com/xcode/). +- Install [Carthage](https://github.com/Carthage/Carthage#installing-carthage). + +## Setup + +Before building and running tests there are some placeholder values you must update in code marked with Swift `#error` macros. After updating them comment out the macros. + +- `TestManager.swift` + - Update `let objectAPI = QuizTrain.ObjectAPI(...)` with valid credentials and other info for your TestRail instance. + - Update `QuizTrainProject.populatedProject(...)` with a projectId from your TestRail instance. +- `ExampleTests.swift` + - Update all of the placeholder case IDs with real case IDs for your project. +- `ExampleUITests.swift` + - Update all of the placeholder case IDs with real case IDs for your project. + +If you'd like you can create a temporary project on your TestRail instance and use that to test with. Select *Use multiple test suites to manage cases* when creating the project and then add 16 test cases. + +## Building and Testing + + cd ../Example + carthage checkout --use-submodules + open Example.xcodeproj + +Select the `Example` scheme and type `Command-U` to build Example and run unit tests and user interface tests. If everything goes well you should see a URL in the console to a closed test plan (one for unit tests, another for user interface tests) containing your results on TestRail. You should also see them appear as *QuizTrain Test Results* under *Test Runs & Results* in your project. diff --git a/Shared/QuizTrainManager.swift b/Shared/QuizTrainManager.swift new file mode 100644 index 0000000..5b98662 --- /dev/null +++ b/Shared/QuizTrainManager.swift @@ -0,0 +1,401 @@ +import QuizTrain +import XCTest + +final class QuizTrainManager: NSObject { + + let objectAPI: ObjectAPI + let project: QuizTrainProject + + var submitResults = true + var closePlanAfterSubmittingResults = true + var includeAllCasesInPlan = false + + init(objectAPI: ObjectAPI, project: QuizTrainProject) { + self.objectAPI = objectAPI + self.project = project + super.init() + } + + // MARK: - Testing + + enum Result: String { + case passed + case blocked + case untested + case retest + case failed + } + + private func status(_ result: Result) -> Status { + return project.statuses.first(where: { $0.name == result.rawValue })! + } + + private var assignedto: User { + return project.users.first(where: { $0.email == objectAPI.api.username })! // All results are assigned to the API user account. + } + + private func newResult(for caseId: Case.Id) -> NewCaseResults.Result { + let untestedStatus = status(.untested) + return NewCaseResults.Result(assignedtoId: assignedto.id, caseId: caseId, statusId: untestedStatus.id) + } + + private var started = [NewCaseResults.Result]() + private var completed = [NewCaseResults.Result]() + + /* + Starts testing one or more caseIds. This adds a new NewCaseResults.Result + to the |started| queue for each caseId with its result in an untested + state. If any failures occur before a caseId is completed it will record + those failures and be marked failed. + + For every caseId each startTesting call must be balanced with a + completeTesting call. This can be done explicitly by you or implicitly by + the XCTestObservation extension. See the extension for details. + + It is programmer error if you submit an identical caseId more than once to + this queue. + */ + func startTesting(_ caseIds: [Case.Id]) { + for caseId in caseIds { + guard started.filter({ $0.caseId == caseId }).count == 0 else { + fatalError("You cannot start caseId \(caseId) because it has already been started.") + } + guard completed.filter({ $0.caseId == caseId }).count == 0 else { + fatalError("You cannot start caseId \(caseId) because it has already been completed.") + } + started.append(newResult(for: caseId)) + } + } + + func startTesting(_ caseIds: Case.Id...) { + startTesting(caseIds) + } + + fileprivate struct Failure { + let test: XCTest + let description: String + let filePath: String? + let lineNumber: Int + var comment: String { return "Failure: \(test.name):\(filePath ?? ""):\(lineNumber): \(description)" } + } + + /* + Marks all results in the started queue as .failed and appends the failure + comment to them. + */ + fileprivate func recordFailure(_ failure: Failure) { + let failedStatus = status(.failed) + for i in started.indices { + started[i].statusId = failedStatus.id + if started[i].comment != nil { + started[i].comment! += "\n\(failure.comment)" + } else { + started[i].comment = failure.comment + } + } + } + + /* + Completes testing |caseIds|. This: + + 1. Removes them from the |started| queue. + 2. Changes their status to |result| if they are still .untested. + - If they are not .untested their status is left unchanged. + 3. Appends the |comment|. + 4. Adds them to the |completed| queue. + + It is programmer error if you complete a caseId which is not currently + started. + */ + func completeTesting(_ caseIds: [Case.Id], withResultIfUntested result: Result = .passed, comment: String? = nil) { + + // Remove from started queue. + var completed = [NewCaseResults.Result]() + for caseId in caseIds { + guard let complete = started.filter({ $0.caseId == caseId }).first, + let index = started.firstIndex(of: complete) else { + fatalError("You cannot complete caseId \(caseId) because it has not been started.") + } + completed.append(complete) + started.remove(at: index) + } + + let status = self.status(result) + let untestedStatus = self.status(.untested) + + for i in completed.indices { + + // Only set the status if untested. + if completed[i].statusId == untestedStatus.id { + completed[i].statusId = status.id + } + + // Append comment. + if let comment = comment { + if completed[i].comment != nil { + completed[i].comment! += "\n\(comment)" + } else { + completed[i].comment = comment + } + } + } + + self.completed.append(contentsOf: completed) + } + + func completeTesting(_ caseIds: Case.Id..., withResultIfUntested result: Result = .passed, comment: String? = nil) { + completeTesting(caseIds, withResultIfUntested: result, comment: comment) + } + + fileprivate func completeAllTests() { + let casesIds = started.compactMap { $0.caseId } + completeTesting(casesIds) + } + +} + +// MARK: - TestRail + +extension QuizTrainManager { + + // MARK: Results Parsing + + /* + Returns a tuple splitting |results| into two arrays. Array 0 contains + NewCaseResults.Result's whose caseIds appear in the project, and array 1 + contains those whose caseIds do not appear in the project. + + This is useful to identify results which were created with invalid/stale + caseIds. + */ + private func splitResults(_ results: [NewCaseResults.Result]) -> ([NewCaseResults.Result], [NewCaseResults.Result]) { + + var validResults = [NewCaseResults.Result]() + var invalidResults = [NewCaseResults.Result]() + + for result in results { + if project.cases.filter({ $0.id == result.caseId }).first != nil { + validResults.append(result) + } else { + invalidResults.append(result) + } + } + + return (validResults, invalidResults) + } + + // MARK: Submitting + + /* + Creates a Plan with Entry's on TestRail, collects all |completed| results, + filters out any caseIds which do not appear in QuizTrainProject and logs + them, and submits the remaining valid results to the Plan. + + If |includingAllCases| is true then the created Plan will include all + cases in the project. Otherwise it will only include those which are + completed. + + If |closePlan| is true the plan will be closed after it's submitted. You + cannot unclose a closed plan. + + This method blocks while asynchronous requests to TestRail are occurring. + */ + fileprivate func submitResultsToTestRail(includingAllCases: Bool = false, closingPlanAfterSubmittingResults closePlan: Bool = true) { + + // Filter valid/invalid results. + let (validResults, invalidResults) = splitResults(completed) + if invalidResults.isEmpty == false { + print("--------------------------------------") + print("WARNING: The following results are for invalid caseIds and will not be submitted to TestRail.") + for result in invalidResults { + let status: Status? = project[result.statusId!] + print("\(result.caseId): \(status?.name ?? "") - \(result.comment ?? "")") + } + print("--------------------------------------") + } + let validCaseIds = validResults.map { $0.caseId } + + // Get Case's for all valid results. + var cases = [Case]() + for caseId in validCaseIds { + guard let `case`: Case = project[caseId] else { + fatalError("There is no Case for caseId \(caseId) in project: \(project)") + } + guard cases.contains(where: { $0.id == caseId }) == false else { + continue // skip duplicates + } + cases.append(`case`) + } + + // Create NewPlan.Entry's for every included Suite. + var newPlanEntries = [NewPlan.Entry]() + if includingAllCases { + for suite in project.suites { + newPlanEntries.append(NewPlan.Entry(includeAll: true, suiteId: suite.id)) + } + } else { + + // Only include suite's for tested cases. + var suites = [Suite]() + for `case` in cases { + guard let suiteId = `case`.suiteId else { + fatalError("Case does not have a suiteId: \(`case`)") + } + guard let suite: Suite = project[suiteId] else { + fatalError("There is no Suite for suiteId \(suiteId) in project: \(project)") + } + guard suites.contains(suite) == false else { + continue + } + suites.append(suite) + } + + // For each suite only include the cases tested in that suite. + for suite in suites { + let casesInSuite = cases.filter { $0.suiteId == suite.id } + let caseIdsInSuite = casesInSuite.map { $0.id } + let newPlanEntry = NewPlan.Entry(assignedtoId: assignedto.id, caseIds: caseIdsInSuite, includeAll: false, suiteId: suite.id) + newPlanEntries.append(newPlanEntry) + } + } + + let group = DispatchGroup() + + // Create a Plan. + print("Plan creation started.") + guard !validResults.isEmpty else { + print("Plan creation skipped. There are no results to submit.") + return + } + let newPlan = NewPlan(description: "Created with QuizTrain - https://github.com/venmo/QuizTrain", entries: newPlanEntries, name: "QuizTrain Test Results") + var plan: Plan! + group.enter() + objectAPI.addPlan(newPlan, to: project.project) { (outcome) in + switch outcome { + case .failure(let error): + print("Plan creation failed: \(error.debugDescription)") + return + case .success(let aPlan): + plan = aPlan + } + group.leave() + } + group.wait() + print("Plan creation completed. \(plan.url)") + + guard let planEntries = plan.entries, planEntries.count > 0 else { + print("Aborting: There are no entries in the plan: \(String(describing: plan))") + return + } + + // Submit results. + print("Submitting \(validResults.count) test results started.") + var errors = [ObjectAPI.AddError]() + var results = [QuizTrain.Result]() + for planEntry in planEntries { + for run in planEntry.runs { + + // Include results in this Run. + let casesInRun = cases.filter { $0.suiteId == run.suiteId } // We detect the run a case belongs in using the suiteId. + let caseIdsInRun = casesInRun.map { $0.id } + var resultsForRun = [NewCaseResults.Result]() + for caseId in caseIdsInRun { + guard let result = validResults.filter({ $0.caseId == caseId }).first else { + continue + } + resultsForRun.append(result) + } + + guard resultsForRun.isEmpty == false else { + continue // TestRail will return an error if you submit zero results for a run. + } + + let newCaseResults = NewCaseResults(results: resultsForRun) + + group.enter() + objectAPI.addResultsForCases(newCaseResults, to: run) { (outcome) in + switch outcome { + case .failure(let error): + errors.append(error) + case .success(let someResults): + results.append(contentsOf: someResults) + } + group.leave() + } + } + } + group.wait() + + guard errors.count == 0 else { + print("Submitting test results failed with \(errors.count) error(s):") + for error in errors { + print(error.debugDescription) + } + return + } + + print("Submitting \(results.count) test results completed.") + + if closePlan { + print("Closing plan started.") + group.enter() + objectAPI.closePlan(plan) { (outcome) in + switch outcome { + case .failure(let error): + print("Closing plan failed: \(error)") + case .success(_): + break + } + group.leave() + } + group.wait() + print("Closing plan completed.") + } + } + +} + +// MARK: - XCTestObservation + +extension QuizTrainManager: XCTestObservation { + + public func testBundleWillStart(_ testBundle: Bundle) { + completeAllTests() + } + + public func testSuiteWillStart(_ testSuite: XCTestSuite) { + completeAllTests() + } + + public func testCaseWillStart(_ testCase: XCTestCase) { + completeAllTests() + } + + public func testCase(_ testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) { + recordFailure(Failure(test: testCase, description: description, filePath: filePath, lineNumber: lineNumber)) + } + + public func testCaseDidFinish(_ testCase: XCTestCase) { + completeAllTests() + } + + public func testSuite(_ testSuite: XCTestSuite, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) { + recordFailure(Failure(test: testSuite, description: description, filePath: filePath, lineNumber: lineNumber)) + } + + public func testSuiteDidFinish(_ testSuite: XCTestSuite) { + completeAllTests() + } + public func testBundleDidFinish(_ testBundle: Bundle) { + completeAllTests() + + print("\n========== QuizTrainManager ==========\n") + if submitResults { + submitResultsToTestRail(includingAllCases: includeAllCasesInPlan, closingPlanAfterSubmittingResults: closePlanAfterSubmittingResults) // blocking + } else { + print("Submitting results is disabled.") + } + print("\n======================================\n") + } + +} diff --git a/Shared/QuizTrainProject.swift b/Shared/QuizTrainProject.swift new file mode 100644 index 0000000..54e74da --- /dev/null +++ b/Shared/QuizTrainProject.swift @@ -0,0 +1,266 @@ +import QuizTrain + +struct QuizTrainProject { + + // MARK: - Properties + + let project: QuizTrain.Project + let suites: [QuizTrain.Suite] + let sections: [QuizTrain.Section] + let cases: [QuizTrain.Case] + let statuses: [QuizTrain.Status] + let users: [QuizTrain.User] + + // MARK: - Relationships + + func suite(_ section: QuizTrain.Section) -> QuizTrain.Suite { + return suites.first(where: { $0.id == section.suiteId })! // Sections should always have a Suite + } + + func suite(_ `case`: QuizTrain.Case) -> QuizTrain.Suite { + return suites.first(where: { $0.id == `case`.suiteId })! // Cases should always have a Suite + } + + func sections(_ suite: QuizTrain.Suite) -> [QuizTrain.Section] { + return sections.filter { $0.suiteId == suite.id } + } + + func parentSection(_ section: QuizTrain.Section) -> QuizTrain.Section? { + return sections.first(where: { $0.id == section.parentId }) + } + + func childSections(_ section: QuizTrain.Section) -> [QuizTrain.Section]? { + return sections.filter { $0.parentId == section.id } + } + + func section(_ `case`: QuizTrain.Case) -> QuizTrain.Section { + return sections.first(where: { $0.id == `case`.sectionId })! // Cases should always have a Section + } + + func cases(_ suite: QuizTrain.Suite) -> [QuizTrain.Case] { + return cases.filter { $0.suiteId == suite.id } + } + + func cases(_ section: QuizTrain.Section) -> [QuizTrain.Case] { + return cases.filter { $0.sectionId == section.id } + } + + // MARK: - Subscripting + + subscript(suiteId: QuizTrain.Suite.Id) -> QuizTrain.Suite? { + return suites.first(where: { $0.id == suiteId }) + } + + subscript(sectionId: QuizTrain.Section.Id) -> QuizTrain.Section? { + return sections.first(where: { $0.id == sectionId }) + } + + subscript(caseId: QuizTrain.Case.Id) -> QuizTrain.Case? { + return cases.first(where: { $0.id == caseId }) + } + + subscript(statusId: QuizTrain.Status.Id) -> QuizTrain.Status? { + return statuses.first(where: { $0.id == statusId }) + } + + subscript(userId: QuizTrain.User.Id) -> QuizTrain.User? { + return users.first(where: { $0.id == userId }) + } + + // MARK: - Strings + + /* + Generates strings for a caseId like so: + + "C[CASEID]: /PROJECT.NAME/SUITE.NAME/SECTION1.NAME/SECTION2.NAME/ - CASE.TITLE + + If no caseID match is found returns: "INVALID_CASEID_12345" + */ + func caseTitle(_ caseId: QuizTrain.Case.Id, withCaseId: Bool = true, withProjectName: Bool = false, withSuiteName: Bool = true, withSectionNames: Bool = true) -> String { + + guard let `case`: QuizTrain.Case = self[caseId] else { + return "INVALID_CASEID_\(caseId)" + } + + var string = "" + + if withCaseId { + string.append("C\(caseId): ") + } + + if withProjectName { + string.append("/\(project.name)") + } + + if withSuiteName { + string.append("/\(suite(`case`).name)") + } + + if withSectionNames { + + var childSection = section(`case`) + var sectionNames = [childSection.name] + + while let parentSection = self.parentSection(childSection) { + sectionNames.append(parentSection.name) + childSection = parentSection + } + + sectionNames = sectionNames.reversed() + + for index in 0.. [String] { + return caseTitles(caseIdVarArgs, withCaseId: withCaseId, withProjectName: withProjectName, withSuiteName: withSuiteName, withSectionNames: withSectionNames) + } + + func caseTitles(_ caseIds: [QuizTrain.Case.Id], withCaseId: Bool = true, withProjectName: Bool = false, withSuiteName: Bool = true, withSectionNames: Bool = true) -> [String] { + var caseTitles = [String]() + for caseId in caseIds { + caseTitles.append(caseTitle(caseId, withCaseId: withCaseId, withProjectName: withProjectName, withSuiteName: withSuiteName, withSectionNames: withSectionNames)) + } + return caseTitles + } + + // MARK: - Creation + + /* + Asynchronously calls the ObjectAPI and populates a QuizTrainProject. + */ + static func populatedProject(forProjectId projectId: QuizTrain.Project.Id, objectAPI: QuizTrain.ObjectAPI, completionHandler: @escaping (Swift.Result) -> Void) { + DispatchQueue.global().async { + + let group = DispatchGroup() + + // Get Project, Suites, Statuses, and Users concurrently. + + group.enter() + var projectOutcome: QuizTrain.Outcome! + objectAPI.getProject(projectId) { (outcome) in + projectOutcome = outcome + group.leave() + } + + group.enter() + var suitesOutcome: QuizTrain.Outcome<[QuizTrain.Suite], QuizTrain.ObjectAPI.GetError>! + objectAPI.getSuites(inProjectWithId: projectId) { (outcome) in + suitesOutcome = outcome + group.leave() + } + + group.enter() + var statusesOutcome: QuizTrain.Outcome<[QuizTrain.Status], QuizTrain.ObjectAPI.GetError>! + objectAPI.getStatuses { (outcome) in + statusesOutcome = outcome + group.leave() + } + + group.enter() + var usersOutcome: QuizTrain.Outcome<[QuizTrain.User], QuizTrain.ObjectAPI.GetError>! + objectAPI.getUsers { (outcome) in + usersOutcome = outcome + group.leave() + } + + group.wait() + + let project: QuizTrain.Project + switch projectOutcome! { + case .failure(let error): + completionHandler(.failure(error)) + return + case .success(let aProject): + project = aProject + } + + let suites: [QuizTrain.Suite] + switch suitesOutcome! { + case .failure(let error): + completionHandler(.failure(error)) + return + case .success(let someSuites): + suites = someSuites + } + + let statuses: [QuizTrain.Status] + switch statusesOutcome! { + case .failure(let error): + completionHandler(.failure(error)) + return + case .success(let someStatuses): + statuses = someStatuses + } + + let users: [QuizTrain.User] + switch usersOutcome! { + case .failure(let error): + completionHandler(.failure(error)) + return + case .success(let someUsers): + users = someUsers + } + + // Get Cases and Sections concurrently. + + var casesOutcomes = [QuizTrain.Outcome<[QuizTrain.Case], QuizTrain.ObjectAPI.GetError>]() + var sectionsOutcomes = [QuizTrain.Outcome<[QuizTrain.Section], QuizTrain.ObjectAPI.GetError>]() + + for suite in suites { + + group.enter() + objectAPI.getCases(in: project, in: suite) { (outcome) in + casesOutcomes.append(outcome) + group.leave() + } + + group.enter() + objectAPI.getSections(in: project, in: suite) { (outcome) in + sectionsOutcomes.append(outcome) + group.leave() + } + } + + group.wait() + + var cases = [QuizTrain.Case]() + for casesOutcome in casesOutcomes { + switch casesOutcome { + case .failure(let error): + completionHandler(.failure(error)) + return + case .success(let someCases): + cases.append(contentsOf: someCases) + } + } + + var sections = [QuizTrain.Section]() + for sectionsOutcome in sectionsOutcomes { + switch sectionsOutcome { + case .failure(let error): + completionHandler(.failure(error)) + return + case .success(let someSections): + sections.append(contentsOf: someSections) + } + } + + // Assemble the QuizTrainProject with all data. + + let quizTrainProject = QuizTrainProject(project: project, suites: suites, sections: sections, cases: cases, statuses: statuses, users: users) + completionHandler(.success(quizTrainProject)) + } + } + +} diff --git a/Shared/TestManager.swift b/Shared/TestManager.swift new file mode 100644 index 0000000..9e9f4cd --- /dev/null +++ b/Shared/TestManager.swift @@ -0,0 +1,100 @@ +import Foundation +import QuizTrain +import XCTest + +/* + Principal Class for test targets owned by their Bundle. This should be accessed + using its singleton property: TestManager.sharedInstance + + Performs logic required before any tests run and after all tests complete. + */ +final class TestManager: NSObject { + + let quizTrainManager: QuizTrainManager + + override init() { + + print("\n========== TestManager ==========\n") + defer { print("\n====================================\n") } + + print("QuizTrainManager setup started.") + #error("Update these ObjectAPI arguments below for your TestRail instance. Then comment out this macro.") + let objectAPI = QuizTrain.ObjectAPI(username: "YOUR@TESTRAIL.EMAIL", secret: "YOUR_TESTRAIL_PASSWORD_OR_API_KEY", hostname: "YOURINSTANCE.testrail.net", port: 443, scheme: "https") + var quizTrainManager: QuizTrainManager! + let group = DispatchGroup() + group.enter() + DispatchQueue.global().async { + #error("Replace the projectId below with one from your TestRail instance. Then comment out this macro.") + QuizTrainProject.populatedProject(forProjectId: 99999, objectAPI: objectAPI) { (outcome) in + switch outcome { + case .failure(let error): + print("QuizTrainManager setup failed: \(error)") + fatalError(error.localizedDescription) + case .success(let project): + quizTrainManager = QuizTrainManager(objectAPI: objectAPI, project: project) + } + group.leave() + } + } + group.wait() + self.quizTrainManager = quizTrainManager + XCTestObservationCenter.shared.addTestObserver(self.quizTrainManager) + print("QuizTrainManager setup completed.") + + super.init() + + TestManager._sharedInstance = self + } + + deinit { + XCTestObservationCenter.shared.removeTestObserver(self.quizTrainManager) + } + + // MARK: - Singleton + + private static var _sharedInstance: TestManager! + + static var sharedInstance: TestManager { + return _sharedInstance + } + +} + +// MARK: - Global + +func logTest(_ caseIds: [Case.Id], withCaseId: Bool = true, withProjectName: Bool = false, withSuiteName: Bool = true, withSectionNames: Bool = true) { + let caseTitles = TestManager.sharedInstance.quizTrainManager.project.caseTitles(caseIds, withCaseId: withCaseId, withProjectName: withProjectName, withSuiteName: withSuiteName, withSectionNames: withSectionNames) + for caseTitle in caseTitles { + print(caseTitle) + } +} + +func logTest(_ caseIds: Case.Id..., withCaseId: Bool = true, withProjectName: Bool = false, withSuiteName: Bool = true, withSectionNames: Bool = true) { + logTest(caseIds, withCaseId: withCaseId, withProjectName: withProjectName, withSuiteName: withSuiteName, withSectionNames: withSectionNames) +} + +func logAndStartTesting(_ caseIds: [Case.Id]) { + logTest(caseIds) + startTesting(caseIds) +} + +func logAndStartTesting(_ caseIds: Case.Id...) { + logTest(caseIds) + startTesting(caseIds) +} + +func startTesting(_ caseIds: [Case.Id]) { + TestManager.sharedInstance.quizTrainManager.startTesting(caseIds) +} + +func startTesting(_ caseIds: Case.Id...) { + TestManager.sharedInstance.quizTrainManager.startTesting(caseIds) +} + +func completeTesting(_ caseIds: [Case.Id], withResultIfUntested result: QuizTrainManager.Result = .passed, comment: String? = nil) { + TestManager.sharedInstance.quizTrainManager.completeTesting(caseIds, withResultIfUntested: result, comment: comment) +} + +func completeTesting(_ caseIds: Case.Id..., withResultIfUntested result: QuizTrainManager.Result = .passed, comment: String? = nil) { + TestManager.sharedInstance.quizTrainManager.completeTesting(caseIds, withResultIfUntested: result, comment: comment) +} diff --git a/Shared/XCTContextExtensions.swift b/Shared/XCTContextExtensions.swift new file mode 100644 index 0000000..288de1e --- /dev/null +++ b/Shared/XCTContextExtensions.swift @@ -0,0 +1,28 @@ +import XCTest +import QuizTrain + +extension XCTContext { + + @discardableResult public class func runActivity(named name: String? = nil, testing caseId: Case.Id, block: (XCTActivity) throws -> Result) rethrows -> Result { + return try runActivity(named: name, testing: [caseId], block: block) + } + + @discardableResult public class func runActivity(named name: String? = nil, testing caseIds: [Case.Id], block: (XCTActivity) throws -> Result) rethrows -> Result { + + let caseTitles = TestManager.sharedInstance.quizTrainManager.project.caseTitles(caseIds, withCaseId: true, withProjectName: false, withSuiteName: true, withSectionNames: true).joined(separator: " | ") + + let named: String + if let name = name { + named = name + ": " + caseTitles + } else { + named = caseTitles + } + + startTesting(caseIds) + let result = try XCTContext.runActivity(named: named, block: block) + completeTesting(caseIds) + + return result + } + +}