diff --git a/Sources/App/Providers/BinarySizeProvider/AppManager.swift b/Sources/App/Providers/BinarySizeProvider/AppManager.swift index 3bbab12..e06190a 100644 --- a/Sources/App/Providers/BinarySizeProvider/AppManager.swift +++ b/Sources/App/Providers/BinarySizeProvider/AppManager.swift @@ -25,11 +25,11 @@ import XcodeProj // MARK: - Constants fileprivate enum Constants { - static let appName: String = "MeasurementApp" - static let clonedRepoName: String = "swift-package-info" - static let xcodeProjName: String = "\(appName).xcodeproj" - static let xcodeProjPath: String = "\(clonedRepoName)/\(xcodeProjName)" - static let archiveName: String = "archive.xcarchive" + static let appName: String = "MeasurementApp" + static let clonedRepoName: String = "swift-package-info" + static let xcodeProjName: String = "\(appName).xcodeproj" + static let xcodeProjPath: String = "\(clonedRepoName)/\(xcodeProjName)" + static let archiveName: String = "archive.xcarchive" } // MARK: - App manager @@ -37,68 +37,69 @@ fileprivate enum Constants { /// Provides API to work with the `measurement app`. /// Can `generate archive`, `calculate its binary size` and `mutate the project` with a given Swift Package dependency`. final class AppManager { - private lazy var appPath: String = fileManager.currentDirectoryPath - .appending("/") - .appending(Constants.xcodeProjPath) - - private lazy var emptyAppDirectoryPath: String = fileManager.currentDirectoryPath - .appending("/") - .appending(Constants.clonedRepoName) - - private lazy var archivedPath: String = fileManager.temporaryDirectory - .path - .appending("/") - .appending(Constants.archiveName) - - private lazy var archivedProductPath: String = fileManager.temporaryDirectory - .path - .appending("/") - .appending(Constants.archiveName) - .appending("/Products/Applications/MeasurementApp.app") - - private let fileManager: FileManager - private let console: Console - private let verbose: Bool - private let xcconfig: URL? - - init( - fileManager: FileManager = .default, - console: Console = .default, - xcconfig: URL?, - verbose: Bool - ) { - self.fileManager = fileManager - self.console = console - self.xcconfig = xcconfig - self.verbose = verbose + private lazy var appPath: String = fileManager.currentDirectoryPath + .appending("/") + .appending(Constants.xcodeProjPath) + + private lazy var emptyAppDirectoryPath: String = fileManager.currentDirectoryPath + .appending("/") + .appending(Constants.clonedRepoName) + + private lazy var archivedPath: String = fileManager.temporaryDirectory + .path + .appending("/") + .appending(Constants.archiveName) + + private lazy var archivedProductPath: String = fileManager.temporaryDirectory + .path + .appending("/") + .appending(Constants.archiveName) + .appending("/Products/Applications/MeasurementApp.app") + + private let fileManager: FileManager + private let console: Console + private let verbose: Bool + private let xcconfig: URL? + + init( + fileManager: FileManager = .default, + console: Console = .default, + xcconfig: URL?, + verbose: Bool + ) { + self.fileManager = fileManager + self.console = console + self.xcconfig = xcconfig + self.verbose = verbose + } + + func cloneEmptyApp() throws { + do { + try Shell.performShallowGitClone( + workingDirectory: fileManager.currentDirectoryPath, + repositoryURLString: "https://github.com/marinofelipe/swift-package-info", + branchOrTag: "main", + verbose: verbose + ) + } catch { + throw BinarySizeProviderError.unableToCloneEmptyApp(errorMessage: error.localizedDescription) } + } - func cloneEmptyApp() throws { - do { - try Shell.performShallowGitClone( - workingDirectory: fileManager.currentDirectoryPath, - repositoryURLString: "https://github.com/marinofelipe/swift-package-info", - branchOrTag: "main", - verbose: verbose - ) - } catch { - throw BinarySizeProviderError.unableToCloneEmptyApp(errorMessage: error.localizedDescription) - } + func cleanUp() throws { + if fileManager.fileExists(atPath: emptyAppDirectoryPath) { + try fileManager.removeItem(atPath: emptyAppDirectoryPath) } + } - func cleanUp() throws { - if fileManager.fileExists(atPath: emptyAppDirectoryPath) { - try fileManager.removeItem(atPath: emptyAppDirectoryPath) - } + func generateArchive() throws { + let workingDirectory = fileManager.currentDirectoryPath + var cmdXCConfig: String = "" + if let xcconfig, let customXCConfigURL = URL(string: workingDirectory)?.appendingPathComponent(xcconfig.path) { + cmdXCConfig = "-xcconfig \(customXCConfigURL.path)" } - - func generateArchive() throws { - let workingDirectory = fileManager.currentDirectoryPath - var cmdXCConfig: String = "" - if let xcconfig, let customXCConfigURL = URL(string: workingDirectory)?.appendingPathComponent(xcconfig.path) { - cmdXCConfig = "-xcconfig \(customXCConfigURL.path)" - } - let command: ConsoleMessage = """ + + let command: ConsoleMessage = """ xcodebuild \ archive \ -project \(Constants.xcodeProjPath) \ @@ -112,114 +113,121 @@ final class AppManager { ENABLE_BITCODE=NO """ - if verbose { - console.lineBreakAndWrite(command) - } + if verbose { + console.lineBreakAndWrite(command) + } - let output = try Shell.run( - command.text, - workingDirectory: workingDirectory, - verbose: verbose, - timeout: nil + let output = try Shell.run( + command.text, + workingDirectory: workingDirectory, + verbose: verbose, + timeout: nil + ) + + if output.succeeded == false { + if verbose { + console.lineBreakAndWrite( + .init( + text: "Command failed...", + color: .red + ) ) + } + + let errorMessage = String(data: output.errorData, encoding: .utf8) ?? "" + throw BinarySizeProviderError.unableToGenerateArchive(errorMessage: errorMessage) + } + } + + func calculateBinarySize() throws -> SizeOnDisk { + do { + let url = URL(fileURLWithPath: archivedProductPath) + let appSize = try url.sizeOnDisk() + + if verbose { console.lineBreakAndWrite(appSize.message) } - if output.succeeded == false { - if verbose { - console.lineBreakAndWrite( - .init( - text: "Command failed...", - color: .red - ) - ) - } - - let errorMessage = String(data: output.errorData, encoding: .utf8) ?? "" - throw BinarySizeProviderError.unableToGenerateArchive(errorMessage: errorMessage) - } + return appSize + } catch { + throw BinarySizeProviderError.unableToGetBinarySizeOnDisk( + underlyingError: error as NSError + ) } + } - func calculateBinarySize() throws -> SizeOnDisk { - do { - let url = URL(fileURLWithPath: archivedProductPath) - let appSize = try url.sizeOnDisk() + func add( + asDependency swiftPackage: SwiftPackage, + isDynamic: Bool + ) throws { + let xcodeProj = try XcodeProj(path: .init(appPath)) - if verbose { console.lineBreakAndWrite(appSize.message) } + guard let appProject = xcodeProj.pbxproj.projects.first else { + throw BinarySizeProviderError.unableToRetrieveAppProject(atPath: appPath) + } - return appSize - } catch { - throw BinarySizeProviderError.unableToGetBinarySizeOnDisk( - underlyingError: error as NSError - ) - } + if swiftPackage.isLocal { + let packageReference = try appProject.addLocal(swiftPackage: swiftPackage) + xcodeProj.pbxproj.add(object: packageReference) + } else { + let packageReference = try appProject.addRemote(swiftPackage: swiftPackage) + xcodeProj.pbxproj.add(object: packageReference) } - func add( - asDependency swiftPackage: SwiftPackage, - isDynamic: Bool - ) throws { - let xcodeProj = try XcodeProj(path: .init(appPath)) - - guard let appProject = xcodeProj.pbxproj.projects.first else { - throw BinarySizeProviderError.unableToRetrieveAppProject(atPath: appPath) - } - - if swiftPackage.isLocal { - let packageReference = try appProject.addLocal(swiftPackage: swiftPackage) - xcodeProj.pbxproj.add(object: packageReference) - } else { - let packageReference = try appProject.addRemote(swiftPackage: swiftPackage) - xcodeProj.pbxproj.add(object: packageReference) - } - - try xcodeProj.write(path: .init(appPath)) - - if isDynamic { - let packageDependency = appProject.targets - .first? - .packageProductDependencies - .first - let packageBuildFile = PBXBuildFile(product: packageDependency) - - let embedFrameworksBuildPhase = appProject.targets - .first? - .embedFrameworksBuildPhases() - .first - embedFrameworksBuildPhase?.files?.append(packageBuildFile) - - try xcodeProj.write(path: .init(appPath)) - } + try xcodeProj.write(path: .init(appPath)) + + if isDynamic { + let packageDependency = appProject.targets + .first? + .packageProductDependencies + .first + let packageBuildFile = PBXBuildFile(product: packageDependency) + + let embedFrameworksBuildPhase = appProject.targets + .first? + .embedFrameworksBuildPhases() + .first + embedFrameworksBuildPhase?.files?.append(packageBuildFile) + + try xcodeProj.write(path: .init(appPath)) } + } } // MARK: - PBXProject: add(swiftPackage:targetName:) private extension PBXProject { - func addRemote( - swiftPackage: SwiftPackage, - targetName: String = Constants.appName - ) throws -> XCRemoteSwiftPackageReference { - try addSwiftPackage( - repositoryURL: swiftPackage.url.absoluteString, - productName: swiftPackage.product, - versionRequirement: .upToNextMinorVersion( - swiftPackage.version - .trimmingCharacters( - in: CharacterSet.decimalDigits.inverted - ) - ), - targetName: targetName + func addRemote( + swiftPackage: SwiftPackage, + targetName: String = Constants.appName + ) throws -> XCRemoteSwiftPackageReference { + let requirement: XCRemoteSwiftPackageReference.VersionRequirement + switch swiftPackage.resolution { + case let .revision(revision): + requirement = .revision(revision) + case let .version(tag): + requirement = .exact( + tag.trimmingCharacters( + in: CharacterSet.decimalDigits.inverted ) + ) } - func addLocal( - swiftPackage: SwiftPackage, - targetName: String = Constants.appName - ) throws -> XCSwiftPackageProductDependency { - try addLocalSwiftPackage( - // Relative path is adjusted for the location of the cloned MeasurementApp - path: .init("../\(swiftPackage.url.path)"), - productName: swiftPackage.product, - targetName: targetName - ) - } + return try addSwiftPackage( + repositoryURL: swiftPackage.url.absoluteString, + productName: swiftPackage.product, + versionRequirement: requirement, + targetName: targetName + ) + } + + func addLocal( + swiftPackage: SwiftPackage, + targetName: String = Constants.appName + ) throws -> XCSwiftPackageProductDependency { + try addLocalSwiftPackage( + // Relative path is adjusted for the location of the cloned MeasurementApp + path: .init("../\(swiftPackage.url.path)"), + productName: swiftPackage.product, + targetName: targetName + ) + } } diff --git a/Sources/App/Services/SwiftPackageService/SwiftPackageService.swift b/Sources/App/Services/SwiftPackageService/SwiftPackageService.swift index f10d468..cc597fc 100644 --- a/Sources/App/Services/SwiftPackageService/SwiftPackageService.swift +++ b/Sources/App/Services/SwiftPackageService/SwiftPackageService.swift @@ -30,23 +30,6 @@ import SourceControl import TSCBasic import TSCUtility -public enum ResourceState: Equatable, CustomStringConvertible { - case undefined - case valid - case invalid - - public var description: String { - switch self { - case .undefined: - return "undefined" - case .valid: - return "valid" - case .invalid: - return "invalid" - } - } -} - public struct SwiftPackageValidationResult { public enum SourceInformation: Equatable { case local @@ -162,23 +145,29 @@ public final class SwiftPackageService { ) let repositoryTags = try workingCopy.getSemVerOrderedTags() - - let resolvedTag: String + let tagState: ResourceState - if swiftPackage.version == ResourceState.undefined.description { + switch swiftPackage.resolution { + case let .version(version): + let resolvedTag: String + if version == ResourceState.undefined.description { + tagState = .undefined + resolvedTag = repositoryTags.last ?? "" + } else if repositoryTags.contains(version) { + tagState = .valid + resolvedTag = version + } else { + tagState = .invalid + resolvedTag = repositoryTags.last ?? "" + } + + try workingCopy.checkout(tag: resolvedTag) + case let .revision(revision): tagState = .undefined - resolvedTag = repositoryTags.last ?? "" - } else if repositoryTags.contains(swiftPackage.version) { - tagState = .valid - resolvedTag = swiftPackage.version - } else { - tagState = .invalid - resolvedTag = repositoryTags.last ?? "" + try workingCopy.checkout(revision: Revision(identifier: revision)) } - try workingCopy.checkout(tag: resolvedTag) - let package = try await packageLoader.load(cloneDirPath) return .init( diff --git a/Sources/Core/Models/ResurceState.swift b/Sources/Core/Models/ResurceState.swift new file mode 100644 index 0000000..d7895eb --- /dev/null +++ b/Sources/Core/Models/ResurceState.swift @@ -0,0 +1,36 @@ +// Copyright (c) 2022 Felipe Marino +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +public enum ResourceState: Equatable, CustomStringConvertible { + case undefined + case valid + case invalid + + public var description: String { + switch self { + case .undefined: + return "undefined" + case .valid: + return "valid" + case .invalid: + return "invalid" + } + } +} diff --git a/Sources/Core/Models/SwiftPackage.swift b/Sources/Core/Models/SwiftPackage.swift index b25e0a6..ed6aa15 100644 --- a/Sources/Core/Models/SwiftPackage.swift +++ b/Sources/Core/Models/SwiftPackage.swift @@ -21,27 +21,47 @@ import struct Foundation.URL public struct SwiftPackage: Equatable, CustomStringConvertible { + public enum Resolution: Equatable, CustomStringConvertible { + case version(String) + case revision(String) + + public var description: String { + switch self { + case let .revision(revision): + return "Revision: \(revision)" + case let .version(tag): + return "Version: \(tag)" + } + } + } + public let url: URL public let isLocal: Bool - public var version: String + public var resolution: Resolution public var product: String public init( url: URL, isLocal: Bool, version: String, + revision: String?, product: String ) { self.url = url self.isLocal = isLocal - self.version = version self.product = product + + if let revision = revision, version == ResourceState.undefined.description { + self.resolution = .revision(revision) + } else { + self.resolution = .version(version) + } } public var description: String { """ \(isLocal ? "Local path" : "Repository URL"): \(url) - Version: \(version) + \(resolution.description) Product: \(product) """ } diff --git a/Sources/CoreTestSupport/Doubles/Fixtures/Fixture+SwiftPackage.swift b/Sources/CoreTestSupport/Doubles/Fixtures/Fixture+SwiftPackage.swift index b14519c..e49b2dc 100644 --- a/Sources/CoreTestSupport/Doubles/Fixtures/Fixture+SwiftPackage.swift +++ b/Sources/CoreTestSupport/Doubles/Fixtures/Fixture+SwiftPackage.swift @@ -26,12 +26,15 @@ public extension Fixture { static func makeSwiftPackage( url: URL = URL(string: "https://www.apple.com")!, isLocal: Bool = false, + version: String = "1.0.0", + revision: String? = nil, product: String = "Some" ) -> SwiftPackage { .init( url: url, isLocal: isLocal, - version: "1.0.0", + version: version, + revision: revision, product: product ) } diff --git a/Sources/Reports/Report.swift b/Sources/Reports/Report.swift index 879a997..e1560c2 100644 --- a/Sources/Reports/Report.swift +++ b/Sources/Reports/Report.swift @@ -22,48 +22,57 @@ import TSCBasic import Core public final class Report: Reporting { - let swiftPackage: SwiftPackage - private let console: Console + let swiftPackage: SwiftPackage + private let console: Console - public init( - swiftPackage: SwiftPackage, - console: Console = .default - ) { - self.swiftPackage = swiftPackage - self.console = console - } + public init( + swiftPackage: SwiftPackage, + console: Console = .default + ) { + self.swiftPackage = swiftPackage + self.console = console + } - public func generate( - for providedInfo: ProvidedInfo, - format: ReportFormat - ) throws { - try generate( - for: [providedInfo], - format: format - ) - } + public func generate( + for providedInfo: ProvidedInfo, + format: ReportFormat + ) throws { + try generate( + for: [providedInfo], + format: format + ) + } - public func generate( - for providedInfos: [ProvidedInfo], - format: ReportFormat - ) throws { - let reportGenerator = format.makeReportGenerator() - try reportGenerator( - swiftPackage, - providedInfos - ) - } + public func generate( + for providedInfos: [ProvidedInfo], + format: ReportFormat + ) throws { + let reportGenerator = format.makeReportGenerator() + try reportGenerator( + swiftPackage, + providedInfos + ) + } } // MARK: - SwiftPackage: CustomConsoleMessageConvertible extension SwiftPackage: CustomConsoleMessageConvertible { - public var message: ConsoleMessage { - .init( - text: "\(product), \(version)", - color: .cyan, - isBold: false, - hasLineBreakAfter: false - ) + public var message: ConsoleMessage { + .init( + text: "\(product), \(versionOrRevision)", + color: .cyan, + isBold: false, + hasLineBreakAfter: false + ) + } + + private var versionOrRevision: String { + switch resolution { + case let .revision(revision): + return revision + case let .version(tag): + return tag } + } } diff --git a/Sources/Run/SwiftPackageInfo.swift b/Sources/Run/SwiftPackageInfo.swift index 4e2bd2a..2aa6ff0 100644 --- a/Sources/Run/SwiftPackageInfo.swift +++ b/Sources/Run/SwiftPackageInfo.swift @@ -80,10 +80,19 @@ struct AllArguments: ParsableArguments { .long, .customShort("v") ], - help: "Semantic version of the Swift Package. If not passed in the latest release is used" + help: "Semantic version of the Swift Package. If not passed and `revision` is not set, the latest semver tag is used" ) var packageVersion: String? + @Option( + name: [ + .long, + .customShort("r") + ], + help: "A single git commit, SHA-1 hash, or branch name. Applied when `packageVersion` is not set" + ) + var revision: String? + @Option( name: [ .long, @@ -174,6 +183,7 @@ extension ParsableCommand { url: arguments.url, isLocal: arguments.url.isValidRemote ? false : true, version: arguments.packageVersion ?? ResourceState.undefined.description, + revision: arguments.revision, product: arguments.product ?? ResourceState.undefined.description ) } @@ -200,17 +210,22 @@ extension ParsableCommand { ) } - switch tagState { - case .undefined, .invalid: - Console.default.lineBreakAndWrite("Package version was \(tagState.description)") - - if let latestTag { - Console.default.lineBreakAndWrite("Defaulting to latest found semver tag: \(latestTag)") - swiftPackage.version = latestTag + switch swiftPackage.resolution { + case let .revision(revision): + Console.default.lineBreakAndWrite("Resolved revision: \(revision)") + case .version: + switch tagState { + case .undefined, .invalid: + Console.default.lineBreakAndWrite("Package version was \(tagState.description)") + + if let latestTag { + Console.default.lineBreakAndWrite("Defaulting to latest found semver tag: \(latestTag)") + swiftPackage.resolution = .version(latestTag) + } + case .valid: + break + } } - case .valid: - break - } case .local: break } diff --git a/Tests/CoreTests/Models/SwiftPackageTests.swift b/Tests/CoreTests/Models/SwiftPackageTests.swift index fbe4174..1ad9c1e 100644 --- a/Tests/CoreTests/Models/SwiftPackageTests.swift +++ b/Tests/CoreTests/Models/SwiftPackageTests.swift @@ -24,77 +24,147 @@ import CoreTestSupport @testable import Core final class SwiftPackageTests: XCTestCase { - func testDescriptionWhenLocal() { - let sut = Fixture.makeSwiftPackage( - isLocal: true - ) - XCTAssertEqual( - sut.description, - """ - Local path: https://www.apple.com - Version: 1.0.0 - Product: Some - """ - ) - } + // MARK: - Init - func testDescriptionWhenRemote() { - let sut = Fixture.makeSwiftPackage(isLocal: false) - XCTAssertEqual( - sut.description, - """ - Repository URL: https://www.apple.com - Version: 1.0.0 - Product: Some - """ - ) + func testResolutionPermutations() { + struct Permutation: Equatable { + let version: String + let revision: String? + let expectedResolution: SwiftPackage.Resolution } - func testAccountAndRepositoryNamesWhenLocal() { - let sut = Fixture.makeSwiftPackage( - url: URL(string: "../directory")!, - isLocal: true - ) - XCTAssertTrue(sut.accountName.isEmpty) - XCTAssertTrue(sut.repositoryName.isEmpty) - } + let permutations = [ + Permutation( + version: "1.2.3", + revision: "3fag5v0", + expectedResolution: .version("1.2.3") + ), + Permutation( + version: ResourceState.undefined.description, + revision: nil, + expectedResolution: .version(ResourceState.undefined.description) + ), + Permutation( + version: ResourceState.undefined.description, + revision: "3fag5v0", + expectedResolution: .revision("3fag5v0") + ), + Permutation( + version: ResourceState.invalid.description, + revision: "3fag5v0", + expectedResolution: .version(ResourceState.invalid.description) + ), + Permutation( + version: ResourceState.undefined.description, + revision: nil, + expectedResolution: .version(ResourceState.undefined.description) + ) + ] - func testAccountAndRepositoryNamesWhenNotValidGitURL() { - let sut = Fixture.makeSwiftPackage( - url: URL(string: "https://www.where.com")!, - isLocal: false - ) - XCTAssertTrue(sut.accountName.isEmpty) - XCTAssertTrue(sut.repositoryName.isEmpty) - } + permutations.forEach { permutation in + let sut = Fixture.makeSwiftPackage( + version: permutation.version, + revision: permutation.revision + ) - func testAccountAndRepositoryNamesWhenRemoteValidGitURL() { - let sut = Fixture.makeSwiftPackage( - url: URL(string: "https://www.github.com/erica/now")!, - isLocal: false - ) - XCTAssertEqual( - sut.accountName, - "erica" - ) - XCTAssertEqual( - sut.repositoryName, - "now" - ) + XCTAssertEqual( + sut.resolution, + permutation.expectedResolution + ) } + } - func testAccountAndRepositoryNamesWhenURLHasDotGitAtTheEnd() { - let sut = Fixture.makeSwiftPackage( - url: URL(string: "https://www.github.com/erica/now.git")!, - isLocal: false - ) - XCTAssertEqual( - sut.accountName, - "erica" - ) - XCTAssertEqual( - sut.repositoryName, - "now" - ) - } + // MARK: - Description + + func testDescriptionWhenLocal() { + let sut = Fixture.makeSwiftPackage( + isLocal: true + ) + XCTAssertEqual( + sut.description, + """ + Local path: https://www.apple.com + Version: 1.0.0 + Product: Some + """ + ) + } + + func testDescriptionWhenRemote() { + let sut = Fixture.makeSwiftPackage(isLocal: false) + XCTAssertEqual( + sut.description, + """ + Repository URL: https://www.apple.com + Version: 1.0.0 + Product: Some + """ + ) + } + + func testDescriptionWhenRevision() { + let revision = "f46ab7s" + let sut = Fixture.makeSwiftPackage( + version: ResourceState.undefined.description, + revision: revision + ) + XCTAssertEqual( + sut.description, + """ + Repository URL: https://www.apple.com + Revision: \(revision) + Product: Some + """ + ) + } + + // MARK: - Account and repository + + func testAccountAndRepositoryNamesWhenLocal() { + let sut = Fixture.makeSwiftPackage( + url: URL(string: "../directory")!, + isLocal: true + ) + XCTAssertTrue(sut.accountName.isEmpty) + XCTAssertTrue(sut.repositoryName.isEmpty) + } + + func testAccountAndRepositoryNamesWhenNotValidGitURL() { + let sut = Fixture.makeSwiftPackage( + url: URL(string: "https://www.where.com")!, + isLocal: false + ) + XCTAssertTrue(sut.accountName.isEmpty) + XCTAssertTrue(sut.repositoryName.isEmpty) + } + + func testAccountAndRepositoryNamesWhenRemoteValidGitURL() { + let sut = Fixture.makeSwiftPackage( + url: URL(string: "https://www.github.com/erica/now")!, + isLocal: false + ) + XCTAssertEqual( + sut.accountName, + "erica" + ) + XCTAssertEqual( + sut.repositoryName, + "now" + ) + } + + func testAccountAndRepositoryNamesWhenURLHasDotGitAtTheEnd() { + let sut = Fixture.makeSwiftPackage( + url: URL(string: "https://www.github.com/erica/now.git")!, + isLocal: false + ) + XCTAssertEqual( + sut.accountName, + "erica" + ) + XCTAssertEqual( + sut.repositoryName, + "now" + ) + } }