diff --git a/.DS_Store b/.DS_Store index 64fd04b..aaf8bb2 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000..35d3be8 Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.github/workflows/.DS_Store b/.github/workflows/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/.github/workflows/.DS_Store differ diff --git a/.github/workflows/.github/workflows/CI.yml b/.github/workflows/.github/workflows/CI.yml new file mode 100644 index 0000000..561df0f --- /dev/null +++ b/.github/workflows/.github/workflows/CI.yml @@ -0,0 +1,32 @@ +name: CI-macOS + +# Controls when the action will run. +# Triggers the workflow on pull request events but only for the master branch. +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build-and-test" + build-and-test: + # The type of runner that the job will run on + runs-on: macos-latest + + timeout-minutes: 8 + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + - name: Select Xcode + run: sudo xcode-select -switch /Applications/Xcode_16.4.app + + - name: Xcode version + run: /usr/bin/xcodebuild -version + + - name: Build and Test + run: xcodebuild clean build test -project EssentialFeed/EssentialFeed.xcodeproj -scheme "CI_macOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk macosx -destination "platform=macOS" ONLY_ACTIVE_ARCH=YES \ No newline at end of file diff --git a/EssentialFeed/.DS_Store b/EssentialFeed/.DS_Store index cedd84c..dfda247 100644 Binary files a/EssentialFeed/.DS_Store and b/EssentialFeed/.DS_Store differ diff --git a/EssentialFeed/CI.xctestplan b/EssentialFeed/CI.xctestplan new file mode 100644 index 0000000..5e6be2c --- /dev/null +++ b/EssentialFeed/CI.xctestplan @@ -0,0 +1,39 @@ +{ + "configurations" : [ + { + "id" : "AC0E7322-42B7-4363-8F3B-6F336365CFB3", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : { + "targets" : [ + { + "containerPath" : "container:EssentialFeed.xcodeproj", + "identifier" : "080EDEF021B6DA7E00813479", + "name" : "EssentialFeed" + } + ] + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:EssentialFeed.xcodeproj", + "identifier" : "AFCCD9642D87A9BC003221BE", + "name" : "EssentialFeedAPIEndToEndTests" + } + }, + { + "target" : { + "containerPath" : "container:EssentialFeed.xcodeproj", + "identifier" : "080EDEF921B6DA7E00813479", + "name" : "EssentialFeedTests" + } + } + ], + "version" : 1 +} diff --git a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj index 200b79a..715fc07 100644 --- a/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj +++ b/EssentialFeed/EssentialFeed.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 55; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -15,6 +15,8 @@ AF9074A52D1D7AA70083835F /* RemoteFeedLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF9074A42D1D7AA70083835F /* RemoteFeedLoaderTests.swift */; }; AFAE6E0E2D862C2100AE2724 /* XCTestCase+MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFAE6E0D2D862C2100AE2724 /* XCTestCase+MemoryLeakTracking.swift */; }; AFCCD9602D87A3EC003221BE /* URLSessionHTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCCD95F2D87A3EC003221BE /* URLSessionHTTPClient.swift */; }; + AFCCD9692D87A9BC003221BE /* EssentialFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 080EDEF121B6DA7E00813479 /* EssentialFeed.framework */; }; + AFCCD96F2D87B019003221BE /* XCTestCase+MemoryLeakTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFAE6E0D2D862C2100AE2724 /* XCTestCase+MemoryLeakTracking.swift */; }; AFFE11122D2358ED00B44DEB /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFFE11112D2358ED00B44DEB /* HTTPClient.swift */; }; AFFE11142D2359EE00B44DEB /* FeedItemsMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFFE11132D2359EE00B44DEB /* FeedItemsMapper.swift */; }; /* End PBXBuildFile section */ @@ -27,6 +29,13 @@ remoteGlobalIDString = 080EDEF021B6DA7E00813479; remoteInfo = EssentialFeed; }; + AFCCD96A2D87A9BC003221BE /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 080EDEE821B6DA7E00813479 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 080EDEF021B6DA7E00813479; + remoteInfo = EssentialFeed; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -41,11 +50,17 @@ AF9074A32D1D7AA70083835F /* EssentialFeedTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "EssentialFeedTests-Bridging-Header.h"; sourceTree = ""; }; AF9074A42D1D7AA70083835F /* RemoteFeedLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFeedLoaderTests.swift; sourceTree = ""; }; AFAE6E0D2D862C2100AE2724 /* XCTestCase+MemoryLeakTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+MemoryLeakTracking.swift"; sourceTree = ""; }; + AFB964AD2E395E750098D10F /* CI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = CI.xctestplan; sourceTree = ""; }; AFCCD95F2D87A3EC003221BE /* URLSessionHTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionHTTPClient.swift; sourceTree = ""; }; + AFCCD9652D87A9BC003221BE /* EssentialFeedAPIEndToEndTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EssentialFeedAPIEndToEndTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; AFFE11112D2358ED00B44DEB /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; AFFE11132D2359EE00B44DEB /* FeedItemsMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedItemsMapper.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + AFCCD9662D87A9BC003221BE /* EssentialFeedAPIEndToEndTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = EssentialFeedAPIEndToEndTests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 080EDEEE21B6DA7E00813479 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -62,14 +77,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AFCCD9622D87A9BC003221BE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AFCCD9692D87A9BC003221BE /* EssentialFeed.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 080EDEE721B6DA7E00813479 = { isa = PBXGroup; children = ( + AFB964AD2E395E750098D10F /* CI.xctestplan */, 080EDEF321B6DA7E00813479 /* EssentialFeed */, 080EDEFE21B6DA7E00813479 /* EssentialFeedTests */, + AFCCD9662D87A9BC003221BE /* EssentialFeedAPIEndToEndTests */, 080EDEF221B6DA7E00813479 /* Products */, ); sourceTree = ""; @@ -79,6 +104,7 @@ children = ( 080EDEF121B6DA7E00813479 /* EssentialFeed.framework */, 080EDEFA21B6DA7E00813479 /* EssentialFeedTests.xctest */, + AFCCD9652D87A9BC003221BE /* EssentialFeedAPIEndToEndTests.xctest */, ); name = Products; sourceTree = ""; @@ -190,6 +216,29 @@ productReference = 080EDEFA21B6DA7E00813479 /* EssentialFeedTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + AFCCD9642D87A9BC003221BE /* EssentialFeedAPIEndToEndTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AFCCD96E2D87A9BC003221BE /* Build configuration list for PBXNativeTarget "EssentialFeedAPIEndToEndTests" */; + buildPhases = ( + AFCCD9612D87A9BC003221BE /* Sources */, + AFCCD9622D87A9BC003221BE /* Frameworks */, + AFCCD9632D87A9BC003221BE /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AFCCD96B2D87A9BC003221BE /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + AFCCD9662D87A9BC003221BE /* EssentialFeedAPIEndToEndTests */, + ); + name = EssentialFeedAPIEndToEndTests; + packageProductDependencies = ( + ); + productName = EssentialFeedAPIEndToEndTests; + productReference = AFCCD9652D87A9BC003221BE /* EssentialFeedAPIEndToEndTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -197,7 +246,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1010; + LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1600; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -209,6 +258,9 @@ CreatedOnToolsVersion = 10.1; LastSwiftMigration = 1620; }; + AFCCD9642D87A9BC003221BE = { + CreatedOnToolsVersion = 16.2; + }; }; }; buildConfigurationList = 080EDEEB21B6DA7E00813479 /* Build configuration list for PBXProject "EssentialFeed" */; @@ -226,6 +278,7 @@ targets = ( 080EDEF021B6DA7E00813479 /* EssentialFeed */, 080EDEF921B6DA7E00813479 /* EssentialFeedTests */, + AFCCD9642D87A9BC003221BE /* EssentialFeedAPIEndToEndTests */, ); }; /* End PBXProject section */ @@ -245,6 +298,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AFCCD9632D87A9BC003221BE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -271,6 +331,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + AFCCD9612D87A9BC003221BE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AFCCD96F2D87B019003221BE /* XCTestCase+MemoryLeakTracking.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -279,6 +347,11 @@ target = 080EDEF021B6DA7E00813479 /* EssentialFeed */; targetProxy = 080EDEFC21B6DA7E00813479 /* PBXContainerItemProxy */; }; + AFCCD96B2D87A9BC003221BE /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 080EDEF021B6DA7E00813479 /* EssentialFeed */; + targetProxy = AFCCD96A2D87A9BC003221BE /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -519,6 +592,45 @@ }; name = Release; }; + AFCCD96C2D87A9BC003221BE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = KM569NM77P; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.Obada.EssentialFeedAPIEndToEndTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + AFCCD96D2D87A9BC003221BE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = KM569NM77P; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.Obada.EssentialFeedAPIEndToEndTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -549,6 +661,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + AFCCD96E2D87A9BC003221BE /* Build configuration list for PBXNativeTarget "EssentialFeedAPIEndToEndTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AFCCD96C2D87A9BC003221BE /* Debug */, + AFCCD96D2D87A9BC003221BE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 080EDEE821B6DA7E00813479 /* Project object */; diff --git a/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI.xcscheme b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI.xcscheme new file mode 100644 index 0000000..3498fab --- /dev/null +++ b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/CI.xcscheme @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeedAPIEndToEndTests.xcscheme b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeedAPIEndToEndTests.xcscheme new file mode 100644 index 0000000..a6f4ed0 --- /dev/null +++ b/EssentialFeed/EssentialFeed.xcodeproj/xcshareddata/xcschemes/EssentialFeedAPIEndToEndTests.xcscheme @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/EssentialFeed/EssentialFeed/Feed Feature/FeedItem.swift b/EssentialFeed/EssentialFeed/Feed Feature/FeedItem.swift index daa0b5e..c7ba368 100644 --- a/EssentialFeed/EssentialFeed/Feed Feature/FeedItem.swift +++ b/EssentialFeed/EssentialFeed/Feed Feature/FeedItem.swift @@ -9,4 +9,16 @@ public struct FeedItem: Equatable { let description: String? let location: String? let imageURL: URL + + public init( + id: UUID, + description: String?, + location: String?, + imageURL: URL + ) { + self.id = id + self.description = description + self.location = location + self.imageURL = imageURL + } } diff --git a/EssentialFeed/EssentialFeedAPIEndToEndTests/EssentialFeedAPIEndToEndTests.swift b/EssentialFeed/EssentialFeedAPIEndToEndTests/EssentialFeedAPIEndToEndTests.swift new file mode 100644 index 0000000..a50b5fa --- /dev/null +++ b/EssentialFeed/EssentialFeedAPIEndToEndTests/EssentialFeedAPIEndToEndTests.swift @@ -0,0 +1,108 @@ +// +// EssentialFeedAPIEndToEndTests.swift +// EssentialFeedAPIEndToEndTests +// +// Created by Abdelrahman Mohamed on 17.03.2025. +// + +import XCTest +import EssentialFeed + +class EssentialFeedAPIEndToEndTests: XCTestCase { + + func test_endToEndTestServerGETFeedResult_matchesFixedTestAccountData() { + switch getFeedResult() { + case .success(let items): + XCTAssertEqual(items.count, 8, "Expected 8 items in the test account feed") + XCTAssertEqual(items[0], expectedItem(at: 0)) + XCTAssertEqual(items[1], expectedItem(at: 1)) + XCTAssertEqual(items[2], expectedItem(at: 2)) + XCTAssertEqual(items[3], expectedItem(at: 3)) + XCTAssertEqual(items[4], expectedItem(at: 4)) + XCTAssertEqual(items[5], expectedItem(at: 5)) + XCTAssertEqual(items[6], expectedItem(at: 6)) + XCTAssertEqual(items[7], expectedItem(at: 7)) + case .failure(let error): + XCTFail("Expected successful feed result, got \(error) instead") + case nil: + XCTFail("Expected successful feed result, got no result instead") + } + } + + // MARK: - Helpers + + private func getFeedResult( + file: StaticString = #filePath, + line: UInt = #line + ) -> LoadFeedResult? { + let testServerURL = URL(string: "https://essentialdeveloper.com/feed-case-study/test-api/feed")! + let client = URLSessionHTTPClient() + let loader = RemoteFeedLoader(url: testServerURL, client: client) + trackForMemoryLeaks(client, file: file, line: line) + trackForMemoryLeaks(loader, file: file, line: line) + + let exp = expectation(description: "Waiting for load completion") + + var receivedResult: LoadFeedResult? + loader.load { result in + receivedResult = result + exp.fulfill() + } + + wait(for: [exp], timeout: 5.0) + + return receivedResult + } + + private func expectedItem(at index: Int) -> FeedItem { + FeedItem( + id: id(at: index), + description: description(at: index), + location: location(at: index), + imageURL: imageURL(at: index) + ) + } + + private func id(at index: Int) -> UUID { + return UUID(uuidString: [ + "73A7F70C-75DA-4C2E-B5A3-EED40DC53AA6", + "BA298A85-6275-48D3-8315-9C8F7C1CD109", + "5A0D45B3-8E26-4385-8C5D-213E160A5E3C", + "FF0ECFE2-2879-403F-8DBE-A83B4010B340", + "DC97EF5E-2CC9-4905-A8AD-3C351C311001", + "557D87F1-25D3-4D77-82E9-364B2ED9CB30", + "A83284EF-C2DF-415D-AB73-2A9B8B04950B", + "F79BD7F8-063F-46E2-8147-A67635C3BB01" + ][index])! + } + + private func description(at index: Int) -> String? { + return [ + "Description 1", + nil, + "Description 3", + nil, + "Description 5", + "Description 6", + "Description 7", + "Description 8" + ][index] + } + + private func location(at index: Int) -> String? { + return [ + "Location 1", + "Location 2", + nil, + nil, + "Location 5", + "Location 6", + "Location 7", + "Location 8" + ][index] + } + + private func imageURL(at index: Int) -> URL { + return URL(string: "https://url-\(index+1).com")! + } +}