From ddddb6363f008c2ec50bb010031cbd851676dc8a Mon Sep 17 00:00:00 2001 From: Bas Broek Date: Mon, 5 Mar 2018 03:36:25 +0100 Subject: [PATCH] Add merge status tests (#1604) * Add merge status tests * Introduce containsAll and containsNone * Rename containsAll to containsOnly --- .../Merge/IssueMergeSectionController.swift | 26 +------ Classes/Issues/Merge/MergeHelper.swift | 35 +++++++++ Classes/Utility/Sequence+Contains.swift | 57 ++++++++++++++ .../UIViewController+CancelAction.swift | 2 +- Freetime.xcodeproj/project.pbxproj | 16 ++++ FreetimeTests/MergeTests.swift | 76 +++++++++++++++++++ FreetimeTests/SequenceTests.swift | 64 ++++++++++++++++ 7 files changed, 250 insertions(+), 26 deletions(-) create mode 100644 Classes/Issues/Merge/MergeHelper.swift create mode 100644 Classes/Utility/Sequence+Contains.swift create mode 100644 FreetimeTests/MergeTests.swift create mode 100644 FreetimeTests/SequenceTests.swift diff --git a/Classes/Issues/Merge/IssueMergeSectionController.swift b/Classes/Issues/Merge/IssueMergeSectionController.swift index 54a27ed3..cc56dd5d 100644 --- a/Classes/Issues/Merge/IssueMergeSectionController.swift +++ b/Classes/Issues/Merge/IssueMergeSectionController.swift @@ -73,7 +73,7 @@ MergeButtonDelegate { if object.contexts.count > 0 { let states = object.contexts.map { $0.state } - let (state, stateDescription) = combinedMergeStatus(for: states) + let (state, stateDescription) = MergeHelper.combinedMergeStatus(for: states) viewModels.append(IssueMergeSummaryModel(title: stateDescription, state: state)) } @@ -184,28 +184,4 @@ MergeButtonDelegate { viewController?.present(alert, animated: trueUnlessReduceMotionEnabled) } - // MARK: Private - private func combinedMergeStatus(for states: [StatusState]) -> (IssueMergeSummaryModel.State, String) { - let state: IssueMergeSummaryModel.State - let stateDescription: String - let failureDescription = NSLocalizedString("Some checks failed", comment: "") - switch states { - case let states where states.contains(.failure) || states.contains(.error): - state = .failure - stateDescription = failureDescription - case let states where states.contains(.pending): - state = .pending - stateDescription = NSLocalizedString("Merge status pending", comment: "") - case let states where states.reduce(true, { $0 && $1 == .success }): - state = .success - stateDescription = NSLocalizedString("All checks passed", comment: "") - default: - assert(false, "This should only occur when any of the `states` are of type `.expected`, which we have no clue of when it is used. The documentation (https://developer.github.com/v4/enum/statusstate/) doesn't answer that question either.") - state = .failure - stateDescription = failureDescription - } - - return (state, stateDescription) - } - } diff --git a/Classes/Issues/Merge/MergeHelper.swift b/Classes/Issues/Merge/MergeHelper.swift new file mode 100644 index 00000000..e1bfd4e2 --- /dev/null +++ b/Classes/Issues/Merge/MergeHelper.swift @@ -0,0 +1,35 @@ +// +// MergeHelper.swift +// FreetimeTests +// +// Created by Bas Broek on 03/03/2018. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation + +enum MergeHelper { + static func combinedMergeStatus(for states: [StatusState]) -> (state: IssueMergeSummaryModel.State, description: String) { + assert(!states.isEmpty, "Should only check merge status when there is at least one state") + let state: IssueMergeSummaryModel.State + let stateDescription: String + let failureDescription = NSLocalizedString("Some checks failed", comment: "") + switch states { + case let states where states.contains(.failure) || states.contains(.error): + state = .failure + stateDescription = failureDescription + case let states where states.contains(.pending): + state = .pending + stateDescription = NSLocalizedString("Merge status pending", comment: "") + case let states where states.containsOnly(.success): + state = .success + stateDescription = NSLocalizedString("All checks passed", comment: "") + default: + assert(false, "This should only occur when any of the `states` are of type `.expected`, which we have no clue of when it is used. The documentation (https://developer.github.com/v4/enum/statusstate/) doesn't answer that question either.") + state = .failure + stateDescription = failureDescription + } + + return (state, stateDescription) + } +} diff --git a/Classes/Utility/Sequence+Contains.swift b/Classes/Utility/Sequence+Contains.swift new file mode 100644 index 00000000..008add79 --- /dev/null +++ b/Classes/Utility/Sequence+Contains.swift @@ -0,0 +1,57 @@ +// +// Sequence+Contains.swift +// Freetime +// +// Created by Bas Broek on 03/03/2018. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import Foundation + +extension Sequence where Element: Equatable { + + /// Returns a Boolean value indicating whether every element of the sequence + /// is equal to the given element. + func containsOnly(_ element: Element) -> Bool { + var iterator = self.makeIterator() + guard iterator.next() != nil else { return false } + return first(where: { $0 != element }) == nil + } + + /// Returns a Boolean value indicating whether every element of the sequence + /// does not equal to the given element. + func containsNone(_ element: Element) -> Bool { + return first(where: { $0 == element }) == nil + } +} + +extension Sequence { + + /// Returns a Boolean value indicating whether every element of the sequence + /// satisfies the given predicate. + func containsOnly(where predicate: (Element) throws -> Bool) rethrows -> Bool { + var iterator = self.makeIterator() + var isNotEmpty = false + + while let element = iterator.next() { + isNotEmpty = true + if try !predicate(element) { + return false + } + } + return isNotEmpty + } + + /// Returns a Boolean value indicating whether every element of the sequence + /// does not satisfies the given predicate. + func containsNone(where predicate: (Element) throws -> Bool) rethrows -> Bool { + var iterator = self.makeIterator() + + while let element = iterator.next() { + if try predicate(element) { + return false + } + } + return true + } +} diff --git a/Classes/View Controllers/UIViewController+CancelAction.swift b/Classes/View Controllers/UIViewController+CancelAction.swift index 4fe2cb46..76a10334 100644 --- a/Classes/View Controllers/UIViewController+CancelAction.swift +++ b/Classes/View Controllers/UIViewController+CancelAction.swift @@ -20,7 +20,7 @@ extension UIViewController { } // dismiss if all text entries are empty - let canDismissNow = texts.reduce(true) { $0 && ($1 == nil || $1!.isEmpty) } + let canDismissNow = texts.containsOnly { $0 == nil || $0!.isEmpty } if canDismissNow { dismissBlock() } else { diff --git a/Freetime.xcodeproj/project.pbxproj b/Freetime.xcodeproj/project.pbxproj index fff3c49e..d94dab3a 100644 --- a/Freetime.xcodeproj/project.pbxproj +++ b/Freetime.xcodeproj/project.pbxproj @@ -360,7 +360,11 @@ 3E79A2FF1F8A7DA700E1126B /* ShortcutHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E79A2FE1F8A7DA700E1126B /* ShortcutHandler.swift */; }; 45A9D03A1F9D3D9600FD5AEF /* AttributedStringViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A9D0391F9D3D9600FD5AEF /* AttributedStringViewTests.swift */; }; 4920F1A81F72E27200131E9D /* UIViewController+UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4920F1A71F72E27200131E9D /* UIViewController+UserActivity.swift */; }; + 49AF91B1204B416500DFF325 /* MergeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AF91B0204B416500DFF325 /* MergeTests.swift */; }; + 49AF91B4204B4B6A00DFF325 /* MergeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49AF91B2204B4A6000DFF325 /* MergeHelper.swift */; }; 49D029001F91D90C00E39094 /* ReactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49D028FF1F91D90C00E39094 /* ReactionTests.swift */; }; + 49FE18FD204B5D32001681E8 /* Sequence+Contains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FE18FC204B5D32001681E8 /* Sequence+Contains.swift */; }; + 49FE18FF204B6508001681E8 /* SequenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FE18FE204B6508001681E8 /* SequenceTests.swift */; }; 49FFF4341F9FC83200335568 /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FFF4331F9FC83200335568 /* HapticFeedback.swift */; }; 54AD5E8E1F24D953004A4BD6 /* FeedSelectionProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AD5E8D1F24D953004A4BD6 /* FeedSelectionProviding.swift */; }; 5DB4DD471FC5C10000DF7ABF /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DB4DD461FC5C10000DF7ABF /* Accessibility.swift */; }; @@ -801,7 +805,11 @@ 3E79A2FE1F8A7DA700E1126B /* ShortcutHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutHandler.swift; sourceTree = ""; }; 45A9D0391F9D3D9600FD5AEF /* AttributedStringViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringViewTests.swift; sourceTree = ""; }; 4920F1A71F72E27200131E9D /* UIViewController+UserActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+UserActivity.swift"; sourceTree = ""; }; + 49AF91B0204B416500DFF325 /* MergeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergeTests.swift; sourceTree = ""; }; + 49AF91B2204B4A6000DFF325 /* MergeHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergeHelper.swift; sourceTree = ""; }; 49D028FF1F91D90C00E39094 /* ReactionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionTests.swift; sourceTree = ""; }; + 49FE18FC204B5D32001681E8 /* Sequence+Contains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Contains.swift"; sourceTree = ""; }; + 49FE18FE204B6508001681E8 /* SequenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceTests.swift; sourceTree = ""; }; 49FFF4331F9FC83200335568 /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; 516CF27F9258BBD3E034F09D /* Pods_FreetimeTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FreetimeTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 54AD5E8D1F24D953004A4BD6 /* FeedSelectionProviding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedSelectionProviding.swift; sourceTree = ""; }; @@ -1340,6 +1348,8 @@ DC60C6D21F983BB900241271 /* SignatureTests.swift */, 45A9D0381F9D3D6A00FD5AEF /* Snapshot Tests */, 29EDFE7B1F65C580005BCCEB /* SplitViewTests.swift */, + 49AF91B0204B416500DFF325 /* MergeTests.swift */, + 49FE18FE204B6508001681E8 /* SequenceTests.swift */, 295F52A61EF1B9D2000B53CF /* Test.md */, ); path = FreetimeTests; @@ -1599,6 +1609,7 @@ 2999972720310F3100995FFD /* IssueMergeContextModel.swift */, 2999972520310E9700995FFD /* IssueMergeModel.swift */, 2999972920311B3800995FFD /* IssueMergeSectionController.swift */, + 49AF91B2204B4A6000DFF325 /* MergeHelper.swift */, 2999972B20311DD700995FFD /* IssueMergeSummaryCell.swift */, 299997312031242500995FFD /* IssueMergeSummaryModel.swift */, 299997372031DEB300995FFD /* IssueMergeType.swift */, @@ -1818,6 +1829,7 @@ 754488B01F7ADF8D0032D08C /* UIAlertController+Action.swift */, 98835BCD1F1965E2005BA24F /* UIDevice+Model.swift */, 98B5A0851F6D0FFE000617D6 /* UINavigationController+Replace.swift */, + 49FE18FC204B5D32001681E8 /* Sequence+Contains.swift */, ); path = Utility; sourceTree = ""; @@ -2324,6 +2336,7 @@ 291929551F3FAADF0012067B /* FeedRefresh.swift in Sources */, 2993046F1FBA9D31007B9737 /* IssueManagingActionCell.swift in Sources */, 54AD5E8E1F24D953004A4BD6 /* FeedSelectionProviding.swift in Sources */, + 49FE18FD204B5D32001681E8 /* Sequence+Contains.swift in Sources */, 291929421F3EA8CD0012067B /* File.swift in Sources */, 299E86491EFD9DBB00E5FE70 /* FlexController.m in Sources */, 29EB1EEF1F425E5100A200B4 /* ForegroundHandler.swift in Sources */, @@ -2617,6 +2630,7 @@ 29B94E6F1FCB743900715D7E /* RepositoryFileCell.swift in Sources */, 29459A711FE7153500034A04 /* LogEnvironmentInformation.swift in Sources */, 295B51441FC26C5400C3993B /* PeopleViewController.swift in Sources */, + 49AF91B4204B4B6A00DFF325 /* MergeHelper.swift in Sources */, 2930F2731F8A27750082BA26 /* WidthCache.swift in Sources */, 2971722B1F069E6B005E43AC /* SpinnerSectionController.swift in Sources */, 2999972E203120E300995FFD /* IssueMergeButtonCell.swift in Sources */, @@ -2681,6 +2695,7 @@ 293A45771F296B7E00DD1006 /* ListKitTestCase.swift in Sources */, DC5C02C51F9C6E3500E80B9F /* SearchQueryTests.swift in Sources */, 293A457E1F296BD500DD1006 /* API.swift in Sources */, + 49AF91B1204B416500DFF325 /* MergeTests.swift in Sources */, 2986B35F1FD462B300E3CFC6 /* FilePath.swift in Sources */, 293A45781F296B7E00DD1006 /* ListTestKit.swift in Sources */, 296B4E341F7C80B800C16887 /* GraphQLIDDecodeTests.swift in Sources */, @@ -2688,6 +2703,7 @@ DC60C6D31F983BB900241271 /* SignatureTests.swift in Sources */, 293A45791F296B7E00DD1006 /* MMMarkdownASTTests.swift in Sources */, DC60C6D51F983DF800241271 /* IssueLabelCellTests.swift in Sources */, + 49FE18FF204B6508001681E8 /* SequenceTests.swift in Sources */, DC5C02C31F9C6D0B00E80B9F /* SearchRecentStoreTests.swift in Sources */, 9870B9081FC74E300009719C /* SecretsTests.swift in Sources */, 29EDFE7C1F65C580005BCCEB /* SplitViewTests.swift in Sources */, diff --git a/FreetimeTests/MergeTests.swift b/FreetimeTests/MergeTests.swift new file mode 100644 index 00000000..6f154916 --- /dev/null +++ b/FreetimeTests/MergeTests.swift @@ -0,0 +1,76 @@ +// +// MergeTests.swift +// FreetimeTests +// +// Created by Bas Broek on 03/03/2018. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import XCTest +@testable import Freetime + +class MergeTests: XCTestCase { + + func test_mergeStatuses_containsOnlyError() { + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.error, .error, .error]).state, + .failure) + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.error]).state, + .failure) + } + + func test_mergeStatuses_containsError() { + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.error, .failure]).state, + .failure) + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.error, .pending]).state, + .failure) + } + + func test_mergeStatuses_containsOnlyFailure() { + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.failure, .failure, .failure]).state, + .failure) + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.failure]).state, + .failure) + } + + func test_mergeStatuses_containsFailure() { + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.failure, .success]).state, + .failure) + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.failure, .pending]).state, + .failure) + } + + func test_mergeStatuses_containsFailure_andError() { + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.error, .failure]).state, + .failure) + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.failure, .error]).state, + .failure) + } + + func test_mergeStatuses_containsPending_butNoErrorOrFailure() { + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.pending, .pending, .pending]).state, + .pending) + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.pending, .success]).state, + .pending) + } + + func test_mergeStatuses_containsOnlySuccess() { + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.success, .success]).state, + .success) + XCTAssertEqual( + MergeHelper.combinedMergeStatus(for: [.success]).state, + .success) + } +} diff --git a/FreetimeTests/SequenceTests.swift b/FreetimeTests/SequenceTests.swift new file mode 100644 index 00000000..cb7192ad --- /dev/null +++ b/FreetimeTests/SequenceTests.swift @@ -0,0 +1,64 @@ +// +// SequenceTests.swift +// FreetimeTests +// +// Created by Bas Broek on 04/03/2018. +// Copyright © 2018 Ryan Nystrom. All rights reserved. +// + +import XCTest +@testable import Freetime + +class SequenceTests: XCTestCase { + + func test_containsAll() { + XCTAssertTrue(["a", "a", "a"].containsOnly("a")) + XCTAssertFalse(["b", "a", "a"].containsOnly("a")) + XCTAssertFalse(["a", "a", "a"].containsOnly("b")) + + XCTAssertTrue(["a", "a", "a"].containsOnly { $0 == "a" }) + XCTAssertFalse(["b", "a", "a"].containsOnly { $0 == "a" }) + XCTAssertFalse(["a", "a", "a"].containsOnly { $0 == "b" }) + + XCTAssertTrue([1, 1, 1].containsOnly(1)) + XCTAssertFalse([2, 1, 1].containsOnly(1)) + XCTAssertFalse([1, 1, 1].containsOnly(2)) + + XCTAssertTrue([1, 1, 1].containsOnly { $0 == 1 }) + XCTAssertFalse([2, 1, 1].containsOnly { $0 == 1 }) + XCTAssertFalse([1, 1, 1].containsOnly { $0 == 2 }) + } + + func test_containsNone() { + XCTAssertFalse(["a", "a", "a"].containsNone("a")) + XCTAssertFalse(["b", "a", "a"].containsNone("a")) + XCTAssertTrue(["a", "a", "a"].containsNone("b")) + + XCTAssertFalse(["a", "a", "a"].containsNone { $0 == "a" }) + XCTAssertFalse(["b", "a", "a"].containsNone { $0 == "a" }) + XCTAssertTrue(["a", "a", "a"].containsNone { $0 == "b" }) + + XCTAssertFalse([1, 1, 1].containsNone(1)) + XCTAssertFalse([2, 1, 1].containsNone(1)) + XCTAssertTrue([1, 1, 1].containsNone(2)) + + XCTAssertFalse([1, 1, 1].containsNone { $0 == 1 }) + XCTAssertFalse([2, 1, 1].containsNone { $0 == 1 }) + XCTAssertTrue([1, 1, 1].containsNone { $0 == 2 }) + } + + func test_emptySequence() { + let emptyStrings: [String] = [] + let emptyInts: [Int] = [] + + XCTAssertTrue(emptyStrings.containsNone("a")) + XCTAssertTrue(emptyStrings.containsNone { $0 == "a" }) + XCTAssertFalse(emptyStrings.containsOnly("a")) + XCTAssertFalse(emptyStrings.containsOnly { $0 == "a" }) + + XCTAssertTrue(emptyInts.containsNone(1)) + XCTAssertTrue(emptyInts.containsNone { $0 == 1 }) + XCTAssertFalse(emptyInts.containsOnly(1)) + XCTAssertFalse(emptyInts.containsOnly { $0 == 1 }) + } +}