Browse Source

Linux and Windows Support (#3446)

* Initial Linux build.

* Add CI support.

* Fix YML.

* Use correct image.

* Initial Windows Support (#3462)

* Correct example (#3453)

* Exclude NetworkReachabilityManager from Windows

Fixes #3459. Windows does not have SystemConfiguration, so we have to exclude it

* Initial windows support

* Fix lock issues

* Add Windows tests

* Fix Windows mutex error

* Remove enable test discovery

* Combine Windows and Linux mutex lock with one common NSLock based class

* Remove locked variable check

* Simplify locking extension

* Attempt to fix Windows build.

* Reenable all platform tests.

* Formatting.

* Update README for new platforms.

* Fix shields.

* Another color.

* Fix table.

* Update issues.

* Remove fixed issue.

* Add SPM badge.

* Spell it out.

* Update to 5.4.1 on Linux, add Nightly.

* Naming update.

* Use concurrency group to cancel previous runs.

* Use correct nightly image.

* Use Firebreak for most builds.

* Build with correct archs.

* Fix Catalyst, tvOS builds.

* Run Catalyst on GitHub.

* 12.4 for Catalyst.

* Try hardened runtime on Firebreak.

* Get tests building on Linux.

* Update Linux CI to build tests as well.

* Add unsupported status for Linux and Windows.

* Fix actions.

* Run Catalyst on GitHub.

* Build tests on Windows too.

* Remove hardened runtime.

* Run jobs when workflows change too.

* Add timeouts to all jobs.

* Enable CI on changes inside workflows.

* Check changes in Package too.

Co-authored-by: Alex Taffe <alex.taffe@gmail.com>
Jon Shier 4 years ago
parent
commit
a41c07a558

+ 2 - 0
.dockerignore

@@ -0,0 +1,2 @@
+.build/
+.git/

+ 91 - 31
.github/workflows/ci.yml

@@ -5,16 +5,28 @@ on:
     branches: 
       - master
       - hotfix
+    paths:
+      - 'Source/**'
+      - 'Tests/**'
+      - '.github/workflows/**'
+      - 'Package.swift'
   pull_request:
-    branches: 
-      - '*'
+    paths:
+      - 'Source/**'
+      - 'Tests/**'
+      - '.github/workflows/**'
+      - 'Package.swift'
 
+concurrency: 
+  group: ci
+  cancel-in-progress: true
 jobs:
   macOS_5_1:
     name: Build macOS (5.1)
     runs-on: macOS-10.15
     env:
       DEVELOPER_DIR: /Applications/Xcode_11.3.1.app/Contents/Developer
+    timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
       - name: macOS (5.1)
@@ -24,6 +36,7 @@ jobs:
     runs-on: macOS-10.15
     env:
       DEVELOPER_DIR: /Applications/Xcode_11.7.app/Contents/Developer
+    timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
@@ -35,6 +48,7 @@ jobs:
     runs-on: macOS-10.15
     env:
       DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
+    timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
@@ -46,10 +60,11 @@ jobs:
     runs-on: firebreak
     env:
       DEVELOPER_DIR: /Applications/Xcode_12.5.app/Contents/Developer
+    timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
-        run: arch -arch arm64e brew install alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
+        run: arch -arch arm64e brew install alamofire/alamofire/firewalk || arch -arch arm64e brew upgrade alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
       - name: macOS (5.4)
         run: set -o pipefail && arch -arch arm64e env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire macOS" -destination "platform=macOS" clean test | xcpretty
   Catalyst:
@@ -57,6 +72,7 @@ jobs:
     runs-on: macOS-10.15
     env:
       DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
+    timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
@@ -65,66 +81,110 @@ jobs:
         run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "platform=macOS" clean test | xcpretty
   iOS:
     name: Test iOS
-    runs-on: macOS-10.15
+    runs-on: firebreak
     env:
-      DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
+      DEVELOPER_DIR: /Applications/Xcode_12.5.app/Contents/Developer
+    timeout-minutes: 10
     strategy:
       matrix:
-        destination: ["OS=14.4,name=iPhone 12 Pro"]
+        destination: ["OS=14.5,name=iPhone 12 Pro"]
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
-        run: brew install alamofire/alamofire/firewalk && firewalk &
+        run: arch -arch arm64e brew install alamofire/alamofire/firewalk || arch -arch arm64e brew upgrade alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
       - name: iOS - ${{ matrix.destination }}
-        run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" clean test | xcpretty
+        run: set -o pipefail && arch -arch arm64e env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" clean test | xcpretty
   tvOS:
     name: Test tvOS
-    runs-on: macOS-10.15
+    runs-on: firebreak
     env:
-      DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
+      DEVELOPER_DIR: /Applications/Xcode_12.5.app/Contents/Developer
+    timeout-minutes: 10
     strategy:
       matrix:
-        destination: ["OS=14.3,name=Apple TV 4K"]
+        destination: ["OS=14.5,name=Apple TV"]
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
-        run: brew install alamofire/alamofire/firewalk && firewalk &
+        run: arch -arch arm64e brew install alamofire/alamofire/firewalk || arch -arch arm64e brew upgrade alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
       - name: tvOS - ${{ matrix.destination }}
-        run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire tvOS" -destination "${{ matrix.destination }}" clean test | xcpretty
+        run: set -o pipefail && arch -arch arm64e env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire tvOS" -destination "${{ matrix.destination }}" clean test | xcpretty
   watchOS:
-    name: Build watchOS
-    runs-on: macOS-10.15
-    env:
-      DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
-    strategy:
-      matrix:
-        destination: ["OS=7.2,name=Apple Watch Series 6 - 44mm"]
-    steps:
-      - uses: actions/checkout@v2
-      - name: watchOS - ${{ matrix.destination }}
-        run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire watchOS" -destination "${{ matrix.destination }}" clean build | xcpretty
-  watchOS_Test:
     name: Test watchOS
     runs-on: firebreak
     env:
       DEVELOPER_DIR: /Applications/Xcode_12.5.app/Contents/Developer
+    timeout-minutes: 10
     strategy:
       matrix:
         destination: ["OS=7.4,name=Apple Watch Series 6 - 44mm"]
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
-        run: arch -arch arm64e brew install alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
+        run: arch -arch arm64e brew install alamofire/alamofire/firewalk || arch -arch arm64e brew upgrade alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
       - name: watchOS - ${{ matrix.destination }}
         run: set -o pipefail && arch -arch arm64e env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire watchOS" -destination "${{ matrix.destination }}" clean test | xcpretty
-  spm:
+  SPM:
     name: Test with SPM
-    runs-on: macOS-10.15
+    runs-on: firebreak
     env:
-      DEVELOPER_DIR: /Applications/Xcode_12.4.app/Contents/Developer
+      DEVELOPER_DIR: /Applications/Xcode_12.5.app/Contents/Developer
+    timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
       - name: Install Firewalk
-        run: brew install alamofire/alamofire/firewalk && firewalk &
+        run: arch -arch arm64e brew install alamofire/alamofire/firewalk || arch -arch arm64e brew upgrade alamofire/alamofire/firewalk && arch -arch x86_64 firewalk &
       - name: SPM Test
-        run: swift test -c debug
+        run: arch -arch arm64e swift test -c debug
+  Linux:
+    name: Linux
+    runs-on: ubuntu-20.04
+    container:
+      image: swift:5.4.1-focal
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v2
+      - name: SPM Linux build
+        run: swift build --build-tests -c debug
+  Linux_Nightly:
+    name: Linux Nightly
+    runs-on: ubuntu-20.04
+    container:
+      image: swiftlang/swift:nightly-focal
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v2
+      - name: SPM Linux build
+        run: swift build --build-tests -c debug
+  Windows:
+    name: Windows
+    runs-on: windows-2019
+    timeout-minutes: 10
+    steps:
+    - name: "Clone Project"
+      uses: actions/checkout@v2
+    - uses: seanmiddleditch/gha-setup-vsdevenv@master
+    - name: Install Swift
+      run: |
+        Install-Binary -Url "https://swift.org/builds/swift-5.4.1-release/windows10/swift-5.4.1-RELEASE/swift-5.4.1-RELEASE-windows10.exe" -Name "installer.exe" -ArgumentList ("-q")
+    - name: Set Environment Variables
+      run: |
+        echo "SDKROOT=C:\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+        echo "DEVELOPER_DIR=C:\Library\Developer" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
+    - name: Adjust Paths
+      run: |
+        echo "C:\Library\Swift-development\bin;C:\Library\icu-67\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+        echo "C:\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+    - name: Install Supporting Files
+      run: |
+        Copy-Item "$env:SDKROOT\usr\share\ucrt.modulemap" -destination "$env:UniversalCRTSdkDir\Include\$env:UCRTVersion\ucrt\module.modulemap"
+        Copy-Item "$env:SDKROOT\usr\share\visualc.modulemap" -destination "$env:VCToolsInstallDir\include\module.modulemap"
+        Copy-Item "$env:SDKROOT\usr\share\visualc.apinotes" -destination "$env:VCToolsInstallDir\include\visualc.apinotes"
+        Copy-Item "$env:SDKROOT\usr\share\winsdk.modulemap" -destination "$env:UniversalCRTSdkDir\Include\$env:UCRTVersion\um\module.modulemap"
+    - name: SPM Windows build
+      shell: cmd
+      run: |
+        cd ${{ github.workspace}}
+        set SDKROOT=%SystemDrive%\Library\Developer\Platforms\Windows.platform\Developer\SDKs\Windows.sdk
+        %SystemDrive%\Library\Developer\Toolchains\unknown-Asserts-development.xctoolchain\usr\bin\swift-build.exe --build-tests -c debug -Xlinker /INCREMENTAL:NO -v
+        if not exist .build\x86_64-unknown-windows-msvc\debug\Alamofire.swiftmodule exit 1

+ 0 - 10
Alamofire.xcodeproj/project.pbxproj

@@ -1842,7 +1842,6 @@
 				CLANG_ENABLE_OBJC_WEAK = YES;
 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
-				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "";
 				GCC_C_LANGUAGE_STANDARD = gnu11;
@@ -1856,7 +1855,6 @@
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.Alamofire-watchOSTests";
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				PROVISIONING_PROFILE_SPECIFIER = "";
 				SDKROOT = watchos;
 				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
 				SWIFT_VERSION = 5.0;
@@ -1875,7 +1873,6 @@
 				CLANG_ENABLE_OBJC_WEAK = YES;
 				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
 				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
-				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
 				COPY_PHASE_STRIP = NO;
 				DEVELOPMENT_TEAM = "";
@@ -1890,7 +1887,6 @@
 				MTL_FAST_MATH = YES;
 				PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.Alamofire-watchOSTests";
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				PROVISIONING_PROFILE_SPECIFIER = "";
 				SDKROOT = watchos;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 4;
@@ -2214,7 +2210,6 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				APPLICATION_EXTENSION_API_ONLY = NO;
-				CODE_SIGN_IDENTITY = "Apple Development";
 				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "";
@@ -2226,8 +2221,6 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.$(PRODUCT_NAME:rfc1034identifier)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				PROVISIONING_PROFILE_SPECIFIER = "";
-				"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
 				SDKROOT = iphoneos;
 			};
 			name = Debug;
@@ -2236,7 +2229,6 @@
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				APPLICATION_EXTENSION_API_ONLY = NO;
-				CODE_SIGN_IDENTITY = "Apple Development";
 				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
 				CODE_SIGN_STYLE = Automatic;
 				DEVELOPMENT_TEAM = "";
@@ -2248,8 +2240,6 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = "org.alamofire.$(PRODUCT_NAME:rfc1034identifier)";
 				PRODUCT_NAME = "$(TARGET_NAME)";
-				PROVISIONING_PROFILE_SPECIFIER = "";
-				"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "";
 				SDKROOT = iphoneos;
 			};
 			name = Release;

+ 27 - 18
README.md

@@ -1,12 +1,12 @@
 ![Alamofire: Elegant Networking in Swift](https://raw.githubusercontent.com/Alamofire/Alamofire/master/Resources/AlamofireLogo.png)
 
-[![Build Status](https://github.com/Alamofire/Alamofire/workflows/Alamofire%20CI/badge.svg?branch=master)](https://github.com/Alamofire/Alamofire/actions)
-[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Alamofire.svg)](https://img.shields.io/cocoapods/v/Alamofire.svg)
-[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
-[![Platform](https://img.shields.io/cocoapods/p/Alamofire.svg?style=flat)](https://alamofire.github.io/Alamofire)
-[![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat)](https://twitter.com/AlamofireSF)
-[![Gitter](https://badges.gitter.im/Alamofire/Alamofire.svg)](https://gitter.im/Alamofire/Alamofire?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
-[![Open Source Helpers](https://www.codetriage.com/alamofire/alamofire/badges/users.svg)](https://www.codetriage.com/alamofire/alamofire)
+[![Swift](https://img.shields.io/badge/Swift-5.1_5.2_5.3_5.4-orange?style=flat-square)](https://img.shields.io/badge/Swift-5.1_5.2_5.3_5.4-Orange?style=flat-square)
+[![Platforms](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_Linux_Windows-yellowgreen?style=flat-square)](https://img.shields.io/badge/Platforms-macOS_iOS_tvOS_watchOS_Linux_Windows-Green?style=flat-square)
+[![CocoaPods Compatible](https://img.shields.io/cocoapods/v/Alamofire.svg?style=flat-square)](https://img.shields.io/cocoapods/v/Alamofire.svg)
+[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat-square)](https://github.com/Carthage/Carthage)
+[![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)
+[![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat-square)](https://twitter.com/AlamofireSF)
+[![Swift Forums](https://img.shields.io/badge/Swift_Forums-Alamofire-orange?style=flat-square)](https://forums.swift.org/c/related-projects/alamofire/37)
 
 Alamofire is an HTTP networking library written in Swift.
 
@@ -58,9 +58,21 @@ In order to keep Alamofire focused specifically on core networking implementatio
 
 ## Requirements
 
-- iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
-- Xcode 11+
-- Swift 5.1+
+| Platform | Minimum Swift Version | Installation | Status |
+| --- | --- | --- | --- |
+| iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ | 5.1 | [CocoaPods](#cocoapods), [Carthage](#carthage), [Swift Package Manager](#swift-package-manager), [Manual](#manually) | Fully Tested |
+| Linux | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported |
+| Windows | Latest Only | [Swift Package Manager](#swift-package-manager) | Building But Unsupported |
+
+#### Known Issues on Linux and Windows
+
+Alamofire builds on Linux and Windows but there are missing features and many issues in the underlying `swift-corelibs-foundation` that prevent full functionality and may cause crashes. These include:
+- `ServerTrustManager` and associated certificate functionality is unavailable, so there is no certificate pinning and no client certificate support.
+- Various methods of HTTP authentication may crash, including HTTP Basic and HTTP Digest. Crashes may occur if responses contain server challenges.
+- Cache control through `CachedResponseHandler` and associated APIs is unavailable, as the underlying delegate methods aren't called.
+- `URLSessionTaskMetrics` are never gathered.
+
+Due to these issues, Alamofire is unsupported on Linux and Windows. Please report any crashes to the [Swift bug reporter](https://bugs.swift.org).
 
 ## Migration Guides
 
@@ -85,7 +97,7 @@ In order to keep Alamofire focused specifically on core networking implementatio
 [CocoaPods](https://cocoapods.org) is a dependency manager for Cocoa projects. For usage and installation instructions, visit their website. To integrate Alamofire into your Xcode project using CocoaPods, specify it in your `Podfile`:
 
 ```ruby
-pod 'Alamofire', '~> 5.2'
+pod 'Alamofire', '~> 5.4'
 ```
 
 ### Carthage
@@ -93,7 +105,7 @@ pod 'Alamofire', '~> 5.2'
 [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate Alamofire into your Xcode project using Carthage, specify it in your `Cartfile`:
 
 ```ogdl
-github "Alamofire/Alamofire" ~> 5.2
+github "Alamofire/Alamofire" ~> 5.4
 ```
 
 ### Swift Package Manager
@@ -104,7 +116,7 @@ Once you have your Swift package set up, adding Alamofire as a dependency is as
 
 ```swift
 dependencies: [
-    .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.2.0"))
+    .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.4.0"))
 ]
 ```
 
@@ -153,7 +165,6 @@ The following radars have some effect on the current implementation of Alamofire
 - [`rdar://21349340`](http://www.openradar.me/radar?id=5517037090635776) - Compiler throwing warning due to toll-free bridging issue in the test case
 - `rdar://26870455` - Background URL Session Configurations do not work in the simulator
 - `rdar://26849668` - Some URLProtocol APIs do not properly handle `URLRequest`
-- `FB7624529` - `urlSession(_:task:didFinishCollecting:)` never called on watchOS
 
 ## Resolved Radars
 
@@ -163,10 +174,8 @@ The following radars have been resolved over time after being filed against the
   - (Resolved): 9/1/17 in Xcode 9 beta 6.
 - [`rdar://36082113`](http://openradar.appspot.com/radar?id=4942308441063424) - `URLSessionTaskMetrics` failing to link on watchOS 3.0+
   - (Resolved): Just add `CFNetwork` to your linked frameworks.
-
-## Workarounds
-
-- Collection of `URLSessionTaskMetrics` is currently disabled on watchOS due to `FB7624529`.
+- `FB7624529` - `urlSession(_:task:didFinishCollecting:)` never called on watchOS
+  - (Resolved): Metrics now collected on watchOS 7+.
 
 ## FAQ
 

+ 16 - 0
Source/AFError.swift

@@ -129,6 +129,7 @@ public enum AFError: Error {
         case invalidEmptyResponse(type: String)
     }
 
+    #if !(os(Linux) || os(Windows))
     /// Underlying reason a server trust evaluation error occurred.
     public enum ServerTrustFailureReason {
         /// The output of a server trust evaluation.
@@ -178,6 +179,7 @@ public enum AFError: Error {
         /// Custom server trust evaluation failed due to the associated `Error`.
         case customEvaluationFailed(error: Error)
     }
+    #endif
 
     /// The underlying reason the `.urlRequestValidationFailed`
     public enum URLRequestValidationFailureReason {
@@ -209,8 +211,10 @@ public enum AFError: Error {
     case responseValidationFailed(reason: ResponseValidationFailureReason)
     /// Response serialization failed.
     case responseSerializationFailed(reason: ResponseSerializationFailureReason)
+    #if !(os(Linux) || os(Windows))
     /// `ServerTrustEvaluating` instance threw an error during trust evaluation.
     case serverTrustEvaluationFailed(reason: ServerTrustFailureReason)
+    #endif
     /// `Session` which issued the `Request` was deinitialized, most likely because its reference went out of scope.
     case sessionDeinitialized
     /// `Session` was explicitly invalidated, possibly with the `Error` produced by the underlying `URLSession`.
@@ -310,12 +314,14 @@ extension AFError {
         return false
     }
 
+    #if !(os(Linux) || os(Windows))
     /// Returns whether the instance is `.serverTrustEvaluationFailed`. When `true`, the `underlyingError` property will
     /// contain the associated value.
     public var isServerTrustEvaluationError: Bool {
         if case .serverTrustEvaluationFailed = self { return true }
         return false
     }
+    #endif
 
     /// Returns whether the instance is `requestRetryFailed`. When `true`, the `underlyingError` property will
     /// contain the associated value.
@@ -387,8 +393,10 @@ extension AFError {
             return reason.underlyingError
         case let .responseSerializationFailed(reason):
             return reason.underlyingError
+        #if !(os(Linux) || os(Windows))
         case let .serverTrustEvaluationFailed(reason):
             return reason.underlyingError
+        #endif
         case let .sessionInvalidated(error):
             return error
         case let .createUploadableFailed(error):
@@ -443,10 +451,12 @@ extension AFError {
         return destination
     }
 
+    #if !(os(Linux) || os(Windows))
     /// The download resume data of any underlying network error. Only produced by `DownloadRequest`s.
     public var downloadResumeData: Data? {
         (underlyingError as? URLError)?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
     }
+    #endif
 }
 
 extension AFError.ParameterEncodingFailureReason {
@@ -600,6 +610,7 @@ extension AFError.ResponseSerializationFailureReason {
     }
 }
 
+#if !(os(Linux) || os(Windows))
 extension AFError.ServerTrustFailureReason {
     var output: AFError.ServerTrustFailureReason.Output? {
         switch self {
@@ -642,6 +653,7 @@ extension AFError.ServerTrustFailureReason {
         }
     }
 }
+#endif
 
 // MARK: - Error Descriptions
 
@@ -676,8 +688,10 @@ extension AFError: LocalizedError {
             """
         case let .sessionInvalidated(error):
             return "Session was invalidated with error: \(error?.localizedDescription ?? "No description.")"
+        #if !(os(Linux) || os(Windows))
         case let .serverTrustEvaluationFailed(reason):
             return "Server trust evaluation failed due to reason: \(reason.localizedDescription)"
+        #endif
         case let .urlRequestValidationFailed(reason):
             return "URLRequest validation failed due to reason: \(reason.localizedDescription)"
         case let .createUploadableFailed(error):
@@ -808,6 +822,7 @@ extension AFError.ResponseValidationFailureReason {
     }
 }
 
+#if !(os(Linux) || os(Windows))
 extension AFError.ServerTrustFailureReason {
     var localizedDescription: String {
         switch self {
@@ -840,6 +855,7 @@ extension AFError.ServerTrustFailureReason {
         }
     }
 }
+#endif
 
 extension AFError.URLRequestValidationFailureReason {
     var localizedDescription: String {

+ 6 - 0
Source/Alamofire.swift

@@ -22,6 +22,12 @@
 //  THE SOFTWARE.
 //
 
+import Dispatch
+import Foundation
+#if canImport(FoundationNetworking)
+@_exported import FoundationNetworking
+#endif
+
 /// Reference to `Session.default` for quick bootstrapping and examples.
 public let AF = Session.default
 

+ 3 - 3
Source/HTTPHeaders.swift

@@ -371,12 +371,12 @@ extension HTTPHeader {
     /// Example: `iOS Example/1.0 (org.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0`
     public static let defaultUserAgent: HTTPHeader = {
         let info = Bundle.main.infoDictionary
-        let executable = (info?[kCFBundleExecutableKey as String] as? String) ??
+        let executable = (info?["CFBundleExecutable"] as? String) ??
             (ProcessInfo.processInfo.arguments.first?.split(separator: "/").last.map(String.init)) ??
             "Unknown"
-        let bundle = info?[kCFBundleIdentifierKey as String] as? String ?? "Unknown"
+        let bundle = info?["CFBundleIdentifier"] as? String ?? "Unknown"
         let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown"
-        let appBuild = info?[kCFBundleVersionKey as String] as? String ?? "Unknown"
+        let appBuild = info?["CFBundleVersion"] as? String ?? "Unknown"
 
         let osNameVersion: String = {
             let version = ProcessInfo.processInfo.operatingSystemVersion

+ 4 - 0
Source/MultipartFormData.swift

@@ -213,6 +213,7 @@ open class MultipartFormData {
         //              Check 2 - is file URL reachable?
         //============================================================
 
+        #if !(os(Linux) || os(Windows))
         do {
             let isReachable = try fileURL.checkPromisedItemIsReachable()
             guard isReachable else {
@@ -223,6 +224,7 @@ open class MultipartFormData {
             setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
             return
         }
+        #endif
 
         //============================================================
         //            Check 3 - is file URL a directory?
@@ -509,11 +511,13 @@ open class MultipartFormData {
     // MARK: - Private - Mime Type
 
     private func mimeType(forPathExtension pathExtension: String) -> String {
+        #if !(os(Linux) || os(Windows))
         if
             let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(),
             let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() {
             return contentType as String
         }
+        #endif
 
         return "application/octet-stream"
     }

+ 1 - 1
Source/NetworkReachabilityManager.swift

@@ -22,7 +22,7 @@
 //  THE SOFTWARE.
 //
 
-#if !(os(watchOS) || os(Linux))
+#if !(os(watchOS) || os(Linux) || os(Windows))
 
 import Foundation
 import SystemConfiguration

+ 4 - 31
Source/Protected.swift

@@ -49,37 +49,10 @@ extension Lock {
     }
 }
 
-#if os(Linux)
-/// A `pthread_mutex_t` wrapper.
-final class MutexLock: Lock {
-    private var mutex: UnsafeMutablePointer<pthread_mutex_t>
+#if os(Linux) || os(Windows)
 
-    init() {
-        mutex = .allocate(capacity: 1)
-
-        var attr = pthread_mutexattr_t()
-        pthread_mutexattr_init(&attr)
-        pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
-
-        let error = pthread_mutex_init(mutex, &attr)
-        precondition(error == 0, "Failed to create pthread_mutex")
-    }
-
-    deinit {
-        let error = pthread_mutex_destroy(mutex)
-        precondition(error == 0, "Failed to destroy pthread_mutex")
-    }
+extension NSLock: Lock {}
 
-    fileprivate func lock() {
-        let error = pthread_mutex_lock(mutex)
-        precondition(error == 0, "Failed to lock pthread_mutex")
-    }
-
-    fileprivate func unlock() {
-        let error = pthread_mutex_unlock(mutex)
-        precondition(error == 0, "Failed to unlock pthread_mutex")
-    }
-}
 #endif
 
 #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
@@ -113,8 +86,8 @@ final class UnfairLock: Lock {
 final class Protected<T> {
     #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
     private let lock = UnfairLock()
-    #elseif os(Linux)
-    private let lock = MutexLock()
+    #elseif os(Linux) || os(Windows)
+    private let lock = NSLock()
     #endif
     private var value: T
 

+ 12 - 1
Source/Request.swift

@@ -1298,12 +1298,14 @@ public final class DataStreamRequest: Request {
 
     func didReceive(data: Data) {
         $streamMutableState.write { state in
+            #if !(os(Linux) || os(Windows))
             if let stream = state.outputStream {
                 underlyingQueue.async {
                     var bytes = Array(data)
                     stream.write(&bytes, maxLength: bytes.count)
                 }
             }
+            #endif
             state.numberOfExecutingStreams += state.streams.count
             let localState = state
             underlyingQueue.async { localState.streams.forEach { $0(data) } }
@@ -1337,6 +1339,7 @@ public final class DataStreamRequest: Request {
         return self
     }
 
+    #if !(os(Linux) || os(Windows))
     /// Produces an `InputStream` that receives the `Data` received by the instance.
     ///
     /// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`.
@@ -1359,6 +1362,7 @@ public final class DataStreamRequest: Request {
 
         return inputStream
     }
+    #endif
 
     func capturingError(from closure: () throws -> Void) {
         do {
@@ -1532,7 +1536,14 @@ public class DownloadRequest: Request {
     /// using the `download(resumingWith data:)` API.
     ///
     /// - Note: For more information about `resumeData`, see [Apple's documentation](https://developer.apple.com/documentation/foundation/urlsessiondownloadtask/1411634-cancel).
-    public var resumeData: Data? { mutableDownloadState.resumeData ?? error?.downloadResumeData }
+    public var resumeData: Data? {
+        #if !(os(Linux) || os(Windows))
+        return mutableDownloadState.resumeData ?? error?.downloadResumeData
+        #else
+        return mutableDownloadState.resumeData
+        #endif
+    }
+
     /// If the download is successful, the `URL` where the file was downloaded.
     public var fileURL: URL? { mutableDownloadState.fileURL }
 

+ 8 - 4
Source/ServerTrustEvaluation.swift

@@ -48,6 +48,7 @@ open class ServerTrustManager {
         self.evaluators = evaluators
     }
 
+    #if !(os(Linux) || os(Windows))
     /// Returns the `ServerTrustEvaluating` value for the given host, if one is set.
     ///
     /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override
@@ -69,12 +70,13 @@ open class ServerTrustManager {
 
         return evaluator
     }
+    #endif
 }
 
 /// A protocol describing the API used to evaluate server trusts.
 public protocol ServerTrustEvaluating {
-    #if os(Linux)
-    // Implement this once Linux has API for evaluating server trusts.
+    #if os(Linux) || os(Windows)
+    // Implement this once Linux/Windows has API for evaluating server trusts.
     #else
     /// Evaluates the given `SecTrust` value for the given `host`.
     ///
@@ -89,6 +91,7 @@ public protocol ServerTrustEvaluating {
 
 // MARK: - Server Trust Evaluators
 
+#if !(os(Linux) || os(Windows))
 /// An evaluator which uses the default server trust evaluation while allowing you to control whether to validate the
 /// host provided by the challenge. Applications are encouraged to always validate the host in production environments
 /// to guarantee the validity of the server's certificate chain.
@@ -358,8 +361,8 @@ public final class DisabledTrustEvaluator: ServerTrustEvaluating {
 // MARK: - Extensions
 
 extension Array where Element == ServerTrustEvaluating {
-    #if os(Linux)
-    // Add this same convenience method for Linux.
+    #if os(Linux) || os(Windows)
+    // Add this same convenience method for Linux/Windows.
     #else
     /// Evaluates the given `SecTrust` value for the given `host`.
     ///
@@ -617,3 +620,4 @@ extension AlamofireExtension where ExtendedType == SecTrustResultType {
         type == .unspecified || type == .proceed
     }
 }
+#endif

+ 8 - 2
Source/SessionDelegate.swift

@@ -91,11 +91,15 @@ extension SessionDelegate: URLSessionTaskDelegate {
 
         let evaluation: ChallengeEvaluation
         switch challenge.protectionSpace.authenticationMethod {
+        case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM,
+             NSURLAuthenticationMethodNegotiate:
+            evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task)
+        #if !(os(Linux) || os(Windows))
         case NSURLAuthenticationMethodServerTrust:
             evaluation = attemptServerTrustAuthentication(with: challenge)
-        case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM,
-             NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate:
+        case NSURLAuthenticationMethodClientCertificate:
             evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task)
+        #endif
         default:
             evaluation = (.performDefaultHandling, nil, nil)
         }
@@ -107,6 +111,7 @@ extension SessionDelegate: URLSessionTaskDelegate {
         completionHandler(evaluation.disposition, evaluation.credential)
     }
 
+    #if !(os(Linux) || os(Windows))
     /// Evaluates the server trust `URLAuthenticationChallenge` received.
     ///
     /// - Parameter challenge: The `URLAuthenticationChallenge`.
@@ -133,6 +138,7 @@ extension SessionDelegate: URLSessionTaskDelegate {
             return (.cancelAuthenticationChallenge, nil, error.asAFError(or: .serverTrustEvaluationFailed(reason: .customEvaluationFailed(error: error))))
         }
     }
+    #endif
 
     /// Evaluates the credential-based authentication `URLAuthenticationChallenge` received for `task`.
     ///

+ 2 - 0
Tests/AFError+AlamofireTests.swift

@@ -335,6 +335,7 @@ extension AFError.ResponseValidationFailureReason {
 
 // MARK: -
 
+#if !(os(Linux) || os(Windows))
 extension AFError.ServerTrustFailureReason {
     var isNoRequiredEvaluator: Bool {
         if case .noRequiredEvaluator = self { return true }
@@ -391,6 +392,7 @@ extension AFError.ServerTrustFailureReason {
         return false
     }
 }
+#endif
 
 extension AFError.URLRequestValidationFailureReason {
     var isBodyDataInGETRequest: Bool {

+ 2 - 0
Tests/AuthenticationInterceptorTests.swift

@@ -291,6 +291,7 @@ final class AuthenticationInterceptorTestCase: BaseTestCase {
 
     // MARK: - Tests - Retry
 
+    #if !(os(Linux) || os(Windows)) // URLRequest to /invalid/path is a fatal error.
     func testThatInterceptorDoesNotRetryWithoutResponse() {
         // Given
         let credential = TestCredential()
@@ -324,6 +325,7 @@ final class AuthenticationInterceptorTestCase: BaseTestCase {
 
         XCTAssertEqual(request.retryCount, 0)
     }
+    #endif
 
     func testThatInterceptorDoesNotRetryWhenRequestDoesNotFailDueToAuthError() {
         // Given

+ 7 - 2
Tests/BaseTestCase.swift

@@ -29,8 +29,13 @@ import XCTest
 class BaseTestCase: XCTestCase {
     let timeout: TimeInterval = 10
 
-    static var testDirectoryURL: URL { FileManager.temporaryDirectoryURL.appendingPathComponent("org.alamofire.tests") }
-    var testDirectoryURL: URL { BaseTestCase.testDirectoryURL }
+    var testDirectoryURL: URL {
+        FileManager.temporaryDirectoryURL.appendingPathComponent("org.alamofire.tests")
+    }
+
+    var temporaryFileURL: URL {
+        testDirectoryURL.appendingPathComponent(UUID().uuidString)
+    }
 
     override func setUp() {
         super.setUp()

+ 2 - 0
Tests/DataStreamTests.swift

@@ -310,6 +310,7 @@ final class DataStreamTests: BaseTestCase {
         XCTAssertNil(decodingError)
     }
 
+    #if !(os(Linux) || os(Windows))
     func testThatDataStreamRequestProducesWorkingInputStream() {
         // Given
         let expect = expectation(description: "stream complete")
@@ -332,6 +333,7 @@ final class DataStreamTests: BaseTestCase {
         XCTAssertTrue(parsed)
         XCTAssertNil(parser.parserError)
     }
+    #endif
 
     func testThatDataStreamCanBeManuallyResumed() {
         // Given

+ 3 - 1
Tests/DownloadTests.swift

@@ -555,10 +555,12 @@ final class DownloadResumeDataTestCase: BaseTestCase {
         XCTAssertNil(response?.fileURL)
         XCTAssertNotNil(response?.error)
 
-        XCTAssertNotNil(download.error?.downloadResumeData)
         XCTAssertNotNil(response?.resumeData)
         XCTAssertNotNil(download.resumeData)
+        #if !(os(Linux) || os(Windows))
+        XCTAssertNotNil(download.error?.downloadResumeData)
         XCTAssertEqual(download.error?.downloadResumeData, response?.resumeData)
+        #endif
         XCTAssertEqual(response?.resumeData, download.resumeData)
     }
 

+ 9 - 11
Tests/MultipartFormDataTests.swift

@@ -56,8 +56,6 @@ enum BoundaryGenerator {
     }
 }
 
-private func temporaryFileURL() -> URL { BaseTestCase.testDirectoryURL.appendingPathComponent(UUID().uuidString) }
-
 // MARK: -
 
 class MultipartFormDataPropertiesTestCase: BaseTestCase {
@@ -426,12 +424,12 @@ class MultipartFormDataEncodingTestCase: BaseTestCase {
 
 // MARK: -
 
-class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
+final class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
     let crlf = EncodingCharacters.crlf
 
     func testWritingEncodedDataBodyPartToDisk() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
         let multipartFormData = MultipartFormData()
 
         let data = Data("Lorem ipsum dolor sit amet.".utf8)
@@ -466,7 +464,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
 
     func testWritingMultipleEncodedDataBodyPartsToDisk() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
         let multipartFormData = MultipartFormData()
 
         let frenchData = Data("français".utf8)
@@ -515,7 +513,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
     #if !SWIFT_PACKAGE
     func testWritingEncodedFileBodyPartToDisk() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
         let multipartFormData = MultipartFormData()
 
         let unicornImageURL = url(forResource: "unicorn", withExtension: "png")
@@ -554,7 +552,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
 
     func testWritingMultipleEncodedFileBodyPartsToDisk() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
         let multipartFormData = MultipartFormData()
 
         let unicornImageURL = url(forResource: "unicorn", withExtension: "png")
@@ -603,7 +601,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
 
     func testWritingEncodedStreamBodyPartToDisk() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
         let multipartFormData = MultipartFormData()
 
         let unicornImageURL = url(forResource: "unicorn", withExtension: "png")
@@ -649,7 +647,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
 
     func testWritingMultipleEncodedStreamBodyPartsToDisk() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
         let multipartFormData = MultipartFormData()
 
         let unicornImageURL = url(forResource: "unicorn", withExtension: "png")
@@ -711,7 +709,7 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase {
 
     func testWritingMultipleEncodedBodyPartsWithVaryingTypesToDisk() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
         let multipartFormData = MultipartFormData()
 
         let loremData = Data("Lorem ipsum.".utf8)
@@ -868,7 +866,7 @@ class MultipartFormDataFailureTestCase: BaseTestCase {
 
     func testThatWritingEncodedDataToExistingFileURLFails() {
         // Given
-        let fileURL = temporaryFileURL()
+        let fileURL = temporaryFileURL
 
         var writerError: Error?
 

+ 193 - 196
Tests/RedirectHandlerTests.swift

@@ -26,199 +26,196 @@ import Alamofire
 import Foundation
 import XCTest
 
-// Disabled due to HTTPBin issue: https://github.com/postmanlabs/httpbin/issues/617
-// final class RedirectHandlerTestCase: BaseTestCase {
-//    // MARK: - Properties
-//
-//    private var redirectURLString: String { URL.makeHTTPBinURL().absoluteString }
-//    private var urlString: String { "\(String.httpBinURLString)/redirect-to?url=\(redirectURLString)" }
-//
-//    // MARK: - Tests - Per Request
-//
-//    func testThatRequestRedirectHandlerCanFollowRedirects() {
-//        // Given
-//        let session = Session()
-//
-//        var response: DataResponse<Data?, AFError>?
-//        let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)")
-//
-//        // When
-//        session.request(urlString).redirect(using: Redirector.follow).response { resp in
-//            response = resp
-//            expectation.fulfill()
-//        }
-//
-//        waitForExpectations(timeout: timeout)
-//
-//        // Then
-//        XCTAssertNotNil(response?.request)
-//        XCTAssertNotNil(response?.response)
-//        XCTAssertNotNil(response?.data)
-//        XCTAssertNil(response?.error)
-//
-//        XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString)
-//        XCTAssertEqual(response?.response?.statusCode, 200)
-//    }
-//
-//    func testThatRequestRedirectHandlerCanNotFollowRedirects() {
-//        // Given
-//        let session = Session()
-//
-//        var response: DataResponse<Data?, AFError>?
-//        let expectation = self.expectation(description: "Request should NOT redirect to \(redirectURLString)")
-//
-//        // When
-//        session.request(urlString).redirect(using: Redirector.doNotFollow).response { resp in
-//            response = resp
-//            expectation.fulfill()
-//        }
-//
-//        waitForExpectations(timeout: timeout)
-//
-//        // Then
-//        XCTAssertNotNil(response?.request)
-//        XCTAssertNotNil(response?.response)
-//        XCTAssertNil(response?.data)
-//        XCTAssertNil(response?.error)
-//
-//        XCTAssertEqual(response?.response?.url?.absoluteString, urlString)
-//        XCTAssertEqual(response?.response?.statusCode, 302)
-//    }
-//
-//    func testThatRequestRedirectHandlerCanModifyRedirects() {
-//        // Given
-//        let session = Session()
-//        let redirectURLString = URL.makeHTTPBinURL().absoluteString
-//        let redirectURLRequest = URLRequest(url: URL(string: redirectURLString)!)
-//
-//        var response: DataResponse<Data?, AFError>?
-//        let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)")
-//
-//        // When
-//        let redirector = Redirector(behavior: .modify { _, _, _ in redirectURLRequest })
-//
-//        session.request(urlString).redirect(using: redirector).response { resp in
-//            response = resp
-//            expectation.fulfill()
-//        }
-//
-//        waitForExpectations(timeout: timeout)
-//
-//        // Then
-//        XCTAssertNotNil(response?.request)
-//        XCTAssertNotNil(response?.response)
-//        XCTAssertNotNil(response?.data)
-//        XCTAssertNil(response?.error)
-//
-//        XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString)
-//        XCTAssertEqual(response?.response?.statusCode, 200)
-//    }
-//
-//    // MARK: - Tests - Per Session
-//
-//    func testThatSessionRedirectHandlerCanFollowRedirects() {
-//        // Given
-//        let session = Session(redirectHandler: Redirector.follow)
-//
-//        var response: DataResponse<Data?, AFError>?
-//        let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)")
-//
-//        // When
-//        session.request(urlString).response { resp in
-//            response = resp
-//            expectation.fulfill()
-//        }
-//
-//        waitForExpectations(timeout: timeout)
-//
-//        // Then
-//        XCTAssertNotNil(response?.request)
-//        XCTAssertNotNil(response?.response)
-//        XCTAssertNotNil(response?.data)
-//        XCTAssertNil(response?.error)
-//
-//        XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString)
-//        XCTAssertEqual(response?.response?.statusCode, 200)
-//    }
-//
-//    func testThatSessionRedirectHandlerCanNotFollowRedirects() {
-//        // Given
-//        let session = Session(redirectHandler: Redirector.doNotFollow)
-//
-//        var response: DataResponse<Data?, AFError>?
-//        let expectation = self.expectation(description: "Request should NOT redirect to \(redirectURLString)")
-//
-//        // When
-//        session.request(urlString).response { resp in
-//            response = resp
-//            expectation.fulfill()
-//        }
-//
-//        waitForExpectations(timeout: timeout)
-//
-//        // Then
-//        XCTAssertNotNil(response?.request)
-//        XCTAssertNotNil(response?.response)
-//        XCTAssertNil(response?.data)
-//        XCTAssertNil(response?.error)
-//
-//        XCTAssertEqual(response?.response?.url?.absoluteString, urlString)
-//        XCTAssertEqual(response?.response?.statusCode, 302)
-//    }
-//
-//    func testThatSessionRedirectHandlerCanModifyRedirects() {
-//        // Given
-//        let redirectURLString = URL.makeHTTPBinURL().absoluteString
-//        let redirectURLRequest = URLRequest(url: URL(string: redirectURLString)!)
-//
-//        let redirector = Redirector(behavior: .modify { _, _, _ in redirectURLRequest })
-//        let session = Session(redirectHandler: redirector)
-//
-//        var response: DataResponse<Data?, AFError>?
-//        let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)")
-//
-//        // When
-//        session.request(urlString).response { resp in
-//            response = resp
-//            expectation.fulfill()
-//        }
-//
-//        waitForExpectations(timeout: timeout)
-//
-//        // Then
-//        XCTAssertNotNil(response?.request)
-//        XCTAssertNotNil(response?.response)
-//        XCTAssertNotNil(response?.data)
-//        XCTAssertNil(response?.error)
-//
-//        XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString)
-//        XCTAssertEqual(response?.response?.statusCode, 200)
-//    }
-//
-//    // MARK: - Tests - Per Request Prioritization
-//
-//    func testThatRequestRedirectHandlerIsPrioritizedOverSessionRedirectHandler() {
-//        // Given
-//        let session = Session(redirectHandler: Redirector.doNotFollow)
-//
-//        var response: DataResponse<Data?, AFError>?
-//        let expectation = self.expectation(description: "Request should redirect to \(redirectURLString)")
-//
-//        // When
-//        session.request(urlString).redirect(using: Redirector.follow).response { resp in
-//            response = resp
-//            expectation.fulfill()
-//        }
-//
-//        waitForExpectations(timeout: timeout)
-//
-//        // Then
-//        XCTAssertNotNil(response?.request)
-//        XCTAssertNotNil(response?.response)
-//        XCTAssertNotNil(response?.data)
-//        XCTAssertNil(response?.error)
-//
-//        XCTAssertEqual(response?.response?.url?.absoluteString, redirectURLString)
-//        XCTAssertEqual(response?.response?.statusCode, 200)
-//    }
-// }
+final class RedirectHandlerTestCase: BaseTestCase {
+    // MARK: - Properties
+
+    private var redirectEndpoint: Endpoint { .get }
+    private var endpoint: Endpoint { .redirectTo(redirectEndpoint) }
+
+    // MARK: - Tests - Per Request
+
+    func testThatRequestRedirectHandlerCanFollowRedirects() {
+        // Given
+        let session = Session()
+
+        var response: DataResponse<Data?, AFError>?
+        let expectation = self.expectation(description: "Request should redirect to /get")
+
+        // When
+        session.request(endpoint).redirect(using: Redirector.follow).response { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNotNil(response?.data)
+        XCTAssertNil(response?.error)
+
+        XCTAssertEqual(response?.response?.url, redirectEndpoint.url)
+        XCTAssertEqual(response?.response?.statusCode, 200)
+    }
+
+    func testThatRequestRedirectHandlerCanNotFollowRedirects() {
+        // Given
+        let session = Session()
+
+        var response: DataResponse<Data?, AFError>?
+        let expectation = self.expectation(description: "Request should NOT redirect to /get")
+
+        // When
+        session.request(endpoint).redirect(using: Redirector.doNotFollow).response { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNil(response?.data)
+        XCTAssertNil(response?.error)
+
+        XCTAssertEqual(response?.response?.url, endpoint.url)
+        XCTAssertEqual(response?.response?.statusCode, 302)
+    }
+
+    func testThatRequestRedirectHandlerCanModifyRedirects() {
+        // Given
+        let session = Session()
+        let customRedirectEndpoint = Endpoint.method(.patch)
+
+        var response: DataResponse<Data?, AFError>?
+        let expectation = self.expectation(description: "Request should redirect to /patch")
+
+        // When
+        let redirector = Redirector(behavior: .modify { _, _, _ in customRedirectEndpoint.urlRequest })
+
+        session.request(endpoint).redirect(using: redirector).response { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNotNil(response?.data)
+        XCTAssertNil(response?.error)
+
+        XCTAssertEqual(response?.response?.url, customRedirectEndpoint.url)
+        XCTAssertEqual(response?.response?.statusCode, 200)
+    }
+
+    // MARK: - Tests - Per Session
+
+    func testThatSessionRedirectHandlerCanFollowRedirects() {
+        // Given
+        let session = Session(redirectHandler: Redirector.follow)
+
+        var response: DataResponse<Data?, AFError>?
+        let expectation = self.expectation(description: "Request should redirect to /get")
+
+        // When
+        session.request(endpoint).response { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNotNil(response?.data)
+        XCTAssertNil(response?.error)
+
+        XCTAssertEqual(response?.response?.url, redirectEndpoint.url)
+        XCTAssertEqual(response?.response?.statusCode, 200)
+    }
+
+    func testThatSessionRedirectHandlerCanNotFollowRedirects() {
+        // Given
+        let session = Session(redirectHandler: Redirector.doNotFollow)
+
+        var response: DataResponse<Data?, AFError>?
+        let expectation = self.expectation(description: "Request should NOT redirect to /get")
+
+        // When
+        session.request(endpoint).response { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNil(response?.data)
+        XCTAssertNil(response?.error)
+
+        XCTAssertEqual(response?.response?.url, endpoint.url)
+        XCTAssertEqual(response?.response?.statusCode, 302)
+    }
+
+    func testThatSessionRedirectHandlerCanModifyRedirects() {
+        // Given
+        let customRedirectEndpoint = Endpoint.method(.patch)
+
+        let redirector = Redirector(behavior: .modify { _, _, _ in customRedirectEndpoint.urlRequest })
+        let session = Session(redirectHandler: redirector)
+
+        var response: DataResponse<Data?, AFError>?
+        let expectation = self.expectation(description: "Request should redirect to /patch")
+
+        // When
+        session.request(endpoint).response { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNotNil(response?.data)
+        XCTAssertNil(response?.error)
+
+        XCTAssertEqual(response?.response?.url, customRedirectEndpoint.url)
+        XCTAssertEqual(response?.response?.statusCode, 200)
+    }
+
+    // MARK: - Tests - Per Request Prioritization
+
+    func testThatRequestRedirectHandlerIsPrioritizedOverSessionRedirectHandler() {
+        // Given
+        let session = Session(redirectHandler: Redirector.doNotFollow)
+
+        var response: DataResponse<Data?, AFError>?
+        let expectation = self.expectation(description: "Request should redirect to /get")
+
+        // When
+        session.request(endpoint).redirect(using: Redirector.follow).response { resp in
+            response = resp
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertNotNil(response?.request)
+        XCTAssertNotNil(response?.response)
+        XCTAssertNotNil(response?.data)
+        XCTAssertNil(response?.error)
+
+        XCTAssertEqual(response?.response?.url, redirectEndpoint.url)
+        XCTAssertEqual(response?.response?.statusCode, 200)
+    }
+}

+ 1 - 11
Tests/SessionTests.swift

@@ -1701,7 +1701,7 @@ final class SessionMassActionTestCase: BaseTestCase {
 
 final class SessionConfigurationHeadersTestCase: BaseTestCase {
     enum ConfigurationType {
-        case `default`, ephemeral, background
+        case `default`, ephemeral
     }
 
     func testThatDefaultConfigurationHeadersAreSentWithRequest() {
@@ -1714,13 +1714,6 @@ final class SessionConfigurationHeadersTestCase: BaseTestCase {
         executeAuthorizationHeaderTest(for: .ephemeral)
     }
 
-    #if os(macOS)
-    func disabled_testThatBackgroundConfigurationHeadersAreSentWithRequest() {
-        // Given, When, Then
-        executeAuthorizationHeaderTest(for: .background)
-    }
-    #endif
-
     private func executeAuthorizationHeaderTest(for type: ConfigurationType) {
         // Given
         let session: Session = {
@@ -1732,9 +1725,6 @@ final class SessionConfigurationHeadersTestCase: BaseTestCase {
                     configuration = .default
                 case .ephemeral:
                     configuration = .ephemeral
-                case .background:
-                    let identifier = "org.alamofire.test.manager-configuration-tests"
-                    configuration = .background(withIdentifier: identifier)
                 }
 
                 var headers = HTTPHeaders.default

+ 7 - 0
Tests/TestHelpers.swift

@@ -192,6 +192,13 @@ struct Endpoint {
         return Endpoint(path: .redirectTo, queryItems: items)
     }
 
+    static func redirectTo(_ endpoint: Endpoint, code: Int? = nil) -> Endpoint {
+        var items = [URLQueryItem(name: "url", value: endpoint.url.absoluteString)]
+        items = code.map { items + [.init(name: "statusCode", value: "\($0)")] } ?? items
+
+        return Endpoint(path: .redirectTo, queryItems: items)
+    }
+
     static var responseHeaders: Endpoint {
         Endpoint(path: .responseHeaders)
     }

+ 19 - 6
watchOS Example/watchOS Example.xcodeproj/xcshareddata/xcschemes/watchOS Example WatchKit App.xcscheme

@@ -56,8 +56,10 @@
       debugServiceExtension = "internal"
       enableGPUValidationMode = "1"
       allowLocationSimulation = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
+      <RemoteRunnable
+         runnableDebuggingMode = "2"
+         BundleIdentifier = "com.apple.Carousel"
+         RemotePath = "/watchOS Example WatchKit App">
          <BuildableReference
             BuildableIdentifier = "primary"
             BlueprintIdentifier = "318E330D2419AD1C00BDE48F"
@@ -65,7 +67,7 @@
             BlueprintName = "watchOS Example WatchKit App"
             ReferencedContainer = "container:watchOS Example.xcodeproj">
          </BuildableReference>
-      </BuildableProductRunnable>
+      </RemoteRunnable>
       <AdditionalOptions>
          <AdditionalOption
             key = "NSZombieEnabled"
@@ -80,8 +82,10 @@
       savedToolIdentifier = ""
       useCustomWorkingDirectory = "NO"
       debugDocumentVersioning = "YES">
-      <BuildableProductRunnable
-         runnableDebuggingMode = "0">
+      <RemoteRunnable
+         runnableDebuggingMode = "2"
+         BundleIdentifier = "com.apple.Carousel"
+         RemotePath = "/watchOS Example WatchKit App">
          <BuildableReference
             BuildableIdentifier = "primary"
             BlueprintIdentifier = "318E330D2419AD1C00BDE48F"
@@ -89,7 +93,16 @@
             BlueprintName = "watchOS Example WatchKit App"
             ReferencedContainer = "container:watchOS Example.xcodeproj">
          </BuildableReference>
-      </BuildableProductRunnable>
+      </RemoteRunnable>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "318E330D2419AD1C00BDE48F"
+            BuildableName = "watchOS Example WatchKit App.app"
+            BlueprintName = "watchOS Example WatchKit App"
+            ReferencedContainer = "container:watchOS Example.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
    </ProfileAction>
    <AnalyzeAction
       buildConfiguration = "Debug">