diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 49dffdb..084822f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,3 +23,5 @@ jobs: run: rake build[release] - name: Test run: rake test + - name: E2ETests + run: rake e2e_only diff --git a/.gitignore b/.gitignore index 27a6e4d..3ed07b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,13 @@ .DS_Store /.build /Packages -/*.xcodeproj +*.xcodeproj/ +*.xcworkspace/ DerivedData /.swiftpm/ releases tmp/ .idea/ +xcuserdata +*.gem +Pods/ diff --git a/Rakefile b/Rakefile index 2563fe2..e56b58a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ # encoding: utf-8 +require_relative 'tasks/e2e' ################################ # Rake configuration @@ -77,6 +78,12 @@ task :test do spm_test() end +desc 'build and run E2E tests' +task :e2e => [:build, :e2e_only] + +desc 'run E2E tests without building the XCRemoteCache binary' +task :e2e_only => ['e2e:run'] + ################################ # Helper functions ################################ diff --git a/docs/Development.md b/docs/Development.md index a458c81..352277f 100644 --- a/docs/Development.md +++ b/docs/Development.md @@ -25,7 +25,18 @@ The generated Xcode project contains schemes for each output application (like ` #### Running tests in Xcode -All unit tests are placed in `XCRemoteCacheTests`. To run them from Xcode, just pick any application scheme and run tests (⌘+U). +All unit tests are placed in `XCRemoteCacheTests`. To run them from Xcode, just pick any application scheme and run tests (⌘+U). + +#### Running E2E tests + +E2E tests build a CocoaPods plugin, locally build both `producer` and `consumer` modes and verify 100% hit rate. All `Podfile` templates are placed in [e2eTests/tests](../e2eTests/tests). As a backend server, nginx server with a sample [nginx.conf](../e2eTests/nginx/nginx.conf) configuration is used. The sample server exposes local location `/tmp/cache` under http://localhost:8080. + +To run tests locally, install `nginx` (e.g. `brew install nginx`) and call: + +```bash +rake 'build[release]' +rake e2e_only +``` ## Project organization diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.pbxproj b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8727476 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.pbxproj @@ -0,0 +1,363 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 3695D9CC27A3218C007F3792 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CB27A3218C007F3792 /* AppDelegate.swift */; }; + 3695D9CE27A3218C007F3792 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CD27A3218C007F3792 /* SceneDelegate.swift */; }; + 3695D9D027A3218C007F3792 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3695D9CF27A3218C007F3792 /* ViewController.swift */; }; + 3695D9D327A3218C007F3792 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D127A3218C007F3792 /* Main.storyboard */; }; + 3695D9D527A3218D007F3792 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D427A3218D007F3792 /* Assets.xcassets */; }; + 3695D9D827A3218D007F3792 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XCRemoteCacheSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 3695D9CB27A3218C007F3792 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 3695D9CD27A3218C007F3792 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 3695D9CF27A3218C007F3792 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 3695D9D227A3218C007F3792 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 3695D9D427A3218D007F3792 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 3695D9D727A3218D007F3792 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 3695D9D927A3218D007F3792 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 3695D9C527A3218C007F3792 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 3695D9BF27A3218C007F3792 = { + isa = PBXGroup; + children = ( + 3695D9CA27A3218C007F3792 /* XCRemoteCacheSample */, + 3695D9C927A3218C007F3792 /* Products */, + ); + sourceTree = ""; + }; + 3695D9C927A3218C007F3792 /* Products */ = { + isa = PBXGroup; + children = ( + 3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */, + ); + name = Products; + sourceTree = ""; + }; + 3695D9CA27A3218C007F3792 /* XCRemoteCacheSample */ = { + isa = PBXGroup; + children = ( + 3695D9CB27A3218C007F3792 /* AppDelegate.swift */, + 3695D9CD27A3218C007F3792 /* SceneDelegate.swift */, + 3695D9CF27A3218C007F3792 /* ViewController.swift */, + 3695D9D127A3218C007F3792 /* Main.storyboard */, + 3695D9D427A3218D007F3792 /* Assets.xcassets */, + 3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */, + 3695D9D927A3218D007F3792 /* Info.plist */, + ); + path = XCRemoteCacheSample; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 3695D9C727A3218C007F3792 /* XCRemoteCacheSample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3695D9DC27A3218D007F3792 /* Build configuration list for PBXNativeTarget "XCRemoteCacheSample" */; + buildPhases = ( + 3695D9C427A3218C007F3792 /* Sources */, + 3695D9C527A3218C007F3792 /* Frameworks */, + 3695D9C627A3218C007F3792 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = XCRemoteCacheSample; + productName = XCRemoteCacheSample; + productReference = 3695D9C827A3218C007F3792 /* XCRemoteCacheSample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 3695D9C027A3218C007F3792 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1320; + LastUpgradeCheck = 1320; + TargetAttributes = { + 3695D9C727A3218C007F3792 = { + CreatedOnToolsVersion = 13.2.1; + }; + }; + }; + buildConfigurationList = 3695D9C327A3218C007F3792 /* Build configuration list for PBXProject "XCRemoteCacheSample" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 3695D9BF27A3218C007F3792; + productRefGroup = 3695D9C927A3218C007F3792 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 3695D9C727A3218C007F3792 /* XCRemoteCacheSample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 3695D9C627A3218C007F3792 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3695D9D827A3218D007F3792 /* LaunchScreen.storyboard in Resources */, + 3695D9D527A3218D007F3792 /* Assets.xcassets in Resources */, + 3695D9D327A3218C007F3792 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3695D9C427A3218C007F3792 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3695D9D027A3218C007F3792 /* ViewController.swift in Sources */, + 3695D9CC27A3218C007F3792 /* AppDelegate.swift in Sources */, + 3695D9CE27A3218C007F3792 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 3695D9D127A3218C007F3792 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3695D9D227A3218C007F3792 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 3695D9D627A3218D007F3792 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 3695D9D727A3218D007F3792 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 3695D9DA27A3218D007F3792 /* 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++17"; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + 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 = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 3695D9DB27A3218D007F3792 /* 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++17"; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + 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 = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 3695D9DD27A3218D007F3792 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = XCRemoteCacheSample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.XCRemoteCacheSample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3695D9DE27A3218D007F3792 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = XCRemoteCacheSample/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.xcremotecache.XCRemoteCacheSample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 3695D9C327A3218C007F3792 /* Build configuration list for PBXProject "XCRemoteCacheSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3695D9DA27A3218D007F3792 /* Debug */, + 3695D9DB27A3218D007F3792 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3695D9DC27A3218D007F3792 /* Build configuration list for PBXNativeTarget "XCRemoteCacheSample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3695D9DD27A3218D007F3792 /* Debug */, + 3695D9DE27A3218D007F3792 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 3695D9C027A3218C007F3792 /* Project object */; +} diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/AppDelegate.swift b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/AppDelegate.swift new file mode 100644 index 0000000..a0ac6ec --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/AppDelegate.swift @@ -0,0 +1,41 @@ +// Copyright (c) 2021 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + } + +} + diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/AccentColor.colorset/Contents.json b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/Contents.json b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Base.lproj/LaunchScreen.storyboard b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Base.lproj/Main.storyboard b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Info.plist b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/SceneDelegate.swift b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/SceneDelegate.swift new file mode 100644 index 0000000..9275b00 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/SceneDelegate.swift @@ -0,0 +1,44 @@ +// Copyright (c) 2021 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + } + + func sceneDidDisconnect(_ scene: UIScene) { + } + + func sceneDidBecomeActive(_ scene: UIScene) { + } + + func sceneWillResignActive(_ scene: UIScene) { + } + + func sceneWillEnterForeground(_ scene: UIScene) { + } + + func sceneDidEnterBackground(_ scene: UIScene) { + } +} + diff --git a/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/ViewController.swift b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/ViewController.swift new file mode 100644 index 0000000..f85ee49 --- /dev/null +++ b/e2eTests/XCRemoteCacheSample/XCRemoteCacheSample/ViewController.swift @@ -0,0 +1,30 @@ +// Copyright (c) 2021 Spotify AB. +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + } + + +} + diff --git a/e2eTests/nginx/nginx.conf b/e2eTests/nginx/nginx.conf new file mode 100644 index 0000000..5aad388 --- /dev/null +++ b/e2eTests/nginx/nginx.conf @@ -0,0 +1,43 @@ +# Nginx configuration used for E2E tests cache server +# Listens HTTP on port 8080 + + +worker_processes 1; + +events { + worker_connections 1024; +} + + +http { + default_type application/octet-stream; + + server { + listen 8080; + server_name localhost; + + location / { + root html; + index index.html index.htm; + } + + location /cache/ { + # The path to the directory where nginx should store the cache contents. + root /tmp/cache; + # Allow PUT + dav_methods PUT; + create_full_put_path on; + # The maximum size of a single file. + client_max_body_size 1G; + allow all; + } + sendfile on; + + keepalive_timeout 65; + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + } +} diff --git a/e2eTests/tests/default.Podfile b/e2eTests/tests/default.Podfile new file mode 100644 index 0000000..4814b2d --- /dev/null +++ b/e2eTests/tests/default.Podfile @@ -0,0 +1,8 @@ +plugin 'cocoapods-xcremotecache' + +target 'XCRemoteCacheSample' do + use_frameworks! + + pod 'Firebase/Analytics' + pod 'ReactiveSwift' +end diff --git a/e2eTests/tests/multiplePods.Podfile b/e2eTests/tests/multiplePods.Podfile new file mode 100644 index 0000000..4cd18da --- /dev/null +++ b/e2eTests/tests/multiplePods.Podfile @@ -0,0 +1,11 @@ +plugin 'cocoapods-xcremotecache' + +install! 'cocoapods', :generate_multiple_pod_projects => true + + +target 'XCRemoteCacheSample' do + use_frameworks! + + pod 'Firebase/Analytics' + pod 'ReactiveSwift' +end diff --git a/tasks/e2e.rb b/tasks/e2e.rb new file mode 100644 index 0000000..83b1096 --- /dev/null +++ b/tasks/e2e.rb @@ -0,0 +1,187 @@ +require 'json' + +desc 'Support for E2E tests: building XCRemoteCache-enabled xcodeproj using xcodebuild' +namespace :e2e do + COCOAPODS_DIR = 'cocoapods-plugin' + COCOAPODS_GEMSPEC_FILENAME = "cocoapods-xcremotecache.gemspec" + E2E_COCOAPODS_SAMPLE_DIR = 'e2eTests/XCRemoteCacheSample' + GIT_REMOTE_NAME = 'self' + # Location of the remote address that points to itself + GIT_REMOTE_ADDRESS = '.' + GIT_BRANCH = 'e2e-test-branch' + LOG_NAME = "xcodebuild.log" + DERIVED_DATA_PATH = './DerivedData' + NGINX_ROOT_DIR = '/tmp/cache' + XCRC_BINARIES = 'XCRC' + SHARED_COCOAPODS_CONFIG = { + 'cache_addresses' => ['http://localhost:8080/cache/pods'], + 'primary_repo' => GIT_REMOTE_ADDRESS, + 'primary_branch' => GIT_BRANCH, + 'mode' => 'consumer', + 'final_target' => 'XCRemoteCacheSample', + 'artifact_maximum_age' => 0 + } + + Stats = Struct.new(:hits, :misses, :hit_rate) + + # run E2E tests + task :run => [:run_cocoapods] + + # run E2E tests for CocoaPods-powered projects + task :run_cocoapods do + install_cocoapods_plugin + start_nginx + configure_git + + # Run scenarios for all Podfile scenarios + for podfile_path in Dir.glob('e2eTests/**/*.Podfile') + run_cocoapods_scenario(podfile_path) + end + # Revert all side effects + clean + end + + # Build and install a plugin + def self.install_cocoapods_plugin + Dir.chdir(COCOAPODS_DIR) do + gemfile_path = "cocoapods-xcremotecache.gem" + system("gem build #{COCOAPODS_GEMSPEC_FILENAME} -o #{gemfile_path}") + system("gem install #{gemfile_path}") + end + end + + def self.start_nginx + # Start nginx server + system('nginx -c $PWD/e2eTests/nginx/nginx.conf') + puts('starting nginx') + # Call cleanup on exit + at_exit { puts('resetting ngingx'); system('nginx -s stop') } + end + + # Create a new branch out of a current commit and + # add remote that points to itself + def self.configure_git + system("git checkout -B #{GIT_BRANCH}") + system("git remote add #{GIT_REMOTE_NAME} #{GIT_REMOTE_ADDRESS} && git fetch -q #{GIT_REMOTE_NAME}") + # Revert new remote on exit + at_exit { system("git remote remove #{GIT_REMOTE_NAME}")} + end + + def self.pre_producer_setup + clean_git + clean_server + # Link prebuilt binaries to the Project + system("ln -s $(pwd)/releases #{E2E_COCOAPODS_SAMPLE_DIR}/#{XCRC_BINARIES}") + end + + def self.pre_consumer_setup + clean_git + # Link prebuilt binaries to the Project + system("ln -s $(pwd)/releases #{E2E_COCOAPODS_SAMPLE_DIR}/#{XCRC_BINARIES}") + end + + def self.clean_server + system("rm -rf #{NGINX_ROOT_DIR}") + end + + # Revert any local changes in the test project + def self.clean_git + system("git clean -xdf #{E2E_COCOAPODS_SAMPLE_DIR}") + end + + # Cleans all extra locations that a test creates + def self.clean + clean_git + clean_server + end + + # xcremotecache configuration to add to Podfile + def self.cocoapods_configuration_string(extra_configs = {}) + configuration_lines = ['xcremotecache({'] + all_properties = SHARED_COCOAPODS_CONFIG.merge(extra_configs) + config_lines = all_properties.map {|key, value| " \"#{key}\" => #{value.inspect},"} + configuration_lines.push(*config_lines) + configuration_lines << '})' + configuration_lines.join("\n") + end + + def self.dump_podfile(config, source) + # Create producer Podfile + File.open("#{E2E_COCOAPODS_SAMPLE_DIR}/Podfile", 'w') do |f| + # Copy podfile template + File.foreach(source) { |line| f.puts line } + f.write(config) + end + end + + def self.build_project(extra_args = {}) + system('pod install') + xcodebuild_args = { + 'workspace' => 'XCRemoteCacheSample.xcworkspace', + 'scheme' => 'XCRemoteCacheSample', + 'configuration' => 'Debug', + 'sdk' => 'iphonesimulator', + 'destination' => 'generic/platform=iOS Simulator', + 'derivedDataPath' => DERIVED_DATA_PATH, + }.merge(extra_args) + xcodebuild_vars = { + 'EXCLUDED_ARCHS' => 'arm64 i386' + } + args = ['xcodebuild'] + args.push(*xcodebuild_args.map {|k,v| "-#{k} '#{v}'"}) + args.push(*xcodebuild_vars.map {|k,v| "#{k}='#{v}'"}) + args.push('clean build') + args.push("> #{LOG_NAME}") + puts 'Building a project with xcodebuild...' + system(args.join(' ')) + unless $?.success? + system("tail #{LOG_NAME}") + raise "xcodebuild failed." + end + end + + def self.read_stats + stats_json_string = JSON.parse(`#{XCRC_BINARIES}/xcprepare stats --format json`) + misses = stats_json_string.fetch('miss_count', 0) + hits = stats_json_string.fetch('hit_count', 0) + all_targets = misses + hits + raise "Failure: No XCRemoteCache targets invoked" if all_targets == 0 + hit_rate = hits * 100 / all_targets + Stats.new(hits, misses, hit_rate) + end + + # validate 100% hit rate + def self.valide_hit_rate + status = read_stats() + raise "Failure: Hit rate is only #{status.hit_rate}% (#{status.hits}/#{status.all_targets})" if status.misses > 0 + all_targets = status.misses + status.hits + puts("Hit rate: #{status.hit_rate}% (#{status.hits}/#{all_targets})") + end + + def self.run_cocoapods_scenario(template_path) + producer_configuration = cocoapods_configuration_string({'mode' => 'producer'}) + consumer_configuration = cocoapods_configuration_string() + + puts("****** Scenario: #{template_path}") + + # Run producer build + pre_producer_setup + dump_podfile(producer_configuration, template_path) + puts('Building producer ...') + Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do + build_project + # reset XCRemoteCache stats + system("#{XCRC_BINARIES}/xcprepare stats --reset --format json") + end + + # Run consumer build + pre_consumer_setup + dump_podfile(consumer_configuration, template_path) + puts('Building consumer ...') + Dir.chdir(E2E_COCOAPODS_SAMPLE_DIR) do + # TODO: Change DerivedData's path to emulate multi-machines setup. Blocked by #59 + build_project({'derivedDataPath' => "#{DERIVED_DATA_PATH}"}) + valide_hit_rate + end + end +end