From 88c7bb96d513cb4fdfa40011c62143f5dc640a77 Mon Sep 17 00:00:00 2001 From: Henrik Panhans Date: Tue, 26 Mar 2024 14:31:58 +0100 Subject: [PATCH] GH-34 Allow more flexible slicing (#35) Signed-off-by: Henrik Panhans --- .github/workflows/swift.yml | 17 ++- Example/example.config | 2 + Example/example.yaml | 2 + Package.resolved | 4 +- Package.swift | 4 +- Sources/SwiftFrame/Render.swift | 10 +- Sources/SwiftFrame/Scaffold.swift | 6 +- Sources/SwiftFrame/SwiftFrame.swift | 2 +- .../SwiftFrameCore/Config/ConfigData.swift | 42 ++----- .../SwiftFrameCore/Config/DeviceData.swift | 107 ++++++++---------- .../Config/ScreenshotData.swift | 6 +- Sources/SwiftFrameCore/Config/TextData.swift | 7 +- Sources/SwiftFrameCore/Config/TextGroup.swift | 4 +- .../Extensions/CGSize+Extensions.swift | 9 ++ .../Extensions/Collection+Extensions.swift | 17 +-- .../Extensions/FileManager+Extensions.swift | 9 -- .../NSAttributedString+Extensions.swift | 2 +- .../NSBitmapImageRep+Extensions.swift | 2 +- .../Extensions/NSError+Extensions.swift | 10 +- .../Extensions/String+Extensions.swift | 4 - .../Helper Types/DecodableDefault.swift | 37 +----- .../Helper Types/DecodableSize.swift | 13 --- .../Helper Types/Pluralizer.swift | 17 +++ .../SwiftFrameCore/Helper Types/Point.swift | 6 +- .../Helper Types/VerbosePrintable.swift | 7 -- .../Workers/CommandLineFormatter.swift | 14 +-- .../Workers/ConfigProcessor.swift | 39 ++----- .../SwiftFrameCore/Workers/ImageLoader.swift | 7 -- .../SwiftFrameCore/Workers/ImageWriter.swift | 2 +- .../Workers/SliceSizeCalculator.swift | 26 +++++ .../SwiftFrameCore/Workers/TextRenderer.swift | 4 - .../Config Tests/ConfigDataTests.swift | 2 +- .../Config Tests/DeviceDataTests.swift | 29 ++--- .../Config Tests/TextDataTests.swift | 12 +- .../SwiftFrameTests/ImageComposerTests.swift | 4 +- Tests/SwiftFrameTests/ImageLoaderTests.swift | 4 +- Tests/SwiftFrameTests/PluralizerTests.swift | 23 ++++ Tests/SwiftFrameTests/PointTests.swift | 33 ++++++ Tests/SwiftFrameTests/RegexMatchTests.swift | 50 ++++---- .../SliceSizeCalculatorTests.swift | 40 +++++++ Tests/SwiftFrameTests/Utility/BaseTest.swift | 26 ----- .../Utility/BaseTestCase.swift | 16 +++ .../Utility/ConfigDataFixtures.swift | 16 +-- .../Utility/DeviceDataFixtures.swift | 46 ++++---- .../Utility/StringFilesContainer.swift | 1 - .../SwiftFrameTests/Utility/TestHelpers.swift | 9 -- .../Utility/TestingUtility.swift | 2 +- .../Utility/TextDataFixtures.swift | 25 ++-- .../Utility/TextGroupFixtures.swift | 2 +- benchmark.py | 8 +- install.sh | 2 +- 51 files changed, 373 insertions(+), 415 deletions(-) create mode 100644 Sources/SwiftFrameCore/Extensions/CGSize+Extensions.swift delete mode 100644 Sources/SwiftFrameCore/Helper Types/DecodableSize.swift create mode 100644 Sources/SwiftFrameCore/Helper Types/Pluralizer.swift create mode 100644 Sources/SwiftFrameCore/Workers/SliceSizeCalculator.swift create mode 100644 Tests/SwiftFrameTests/PluralizerTests.swift create mode 100644 Tests/SwiftFrameTests/PointTests.swift create mode 100644 Tests/SwiftFrameTests/SliceSizeCalculatorTests.swift delete mode 100644 Tests/SwiftFrameTests/Utility/BaseTest.swift create mode 100644 Tests/SwiftFrameTests/Utility/BaseTestCase.swift diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 1e08389..7f37e3f 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -4,11 +4,16 @@ on: [push] jobs: build: - runs-on: macos-latest + runs-on: macos-14 steps: - - uses: actions/checkout@v2 - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v \ No newline at end of file + - name: Checkout repository + uses: actions/checkout@v4 + - name: Configure Xcode version + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "15.2" + - name: Build + run: swift build -v + - name: Run tests + run: swift test -v diff --git a/Example/example.config b/Example/example.config index b18d02d..47af0e1 100644 --- a/Example/example.config +++ b/Example/example.config @@ -12,6 +12,7 @@ "outputSuffixes": ["iPhone X 5.8 inches"], "screenshots": "Example/Screenshots/iPhone X/", "templateFile": "Example/Template Files/iPhone X TemplateFile.png", + "numberOfSlices": 5, "screenshotData": [ { "screenshotName": "launchscreen.png", @@ -91,6 +92,7 @@ "outputSuffixes": ["ipadPro", "ipadPro129"], "screenshots": "Example/Screenshots/iPad Pro/", "templateFile": "Example/Template Files/iPad Pro TemplateFile.png", + "numberOfSlices": 4, "screenshotData": [ { "screenshotName": "launchscreen.png", diff --git a/Example/example.yaml b/Example/example.yaml index 33ac3b6..f96fe0e 100644 --- a/Example/example.yaml +++ b/Example/example.yaml @@ -11,6 +11,7 @@ deviceData: - iPhone X 5.8 inches screenshots: Example/Screenshots/iPhone X/ templateFile: Example/Template Files/iPhone X TemplateFile.png + numberOfSlices: 4 screenshotData: - screenshotName: launchscreen.png zIndex: 12 @@ -66,6 +67,7 @@ deviceData: - iPad Pro 12.9 inch screenshots: Example/Screenshots/iPad Pro/ templateFile: Example/Template Files/iPad Pro TemplateFile.png + numberOfSlices: 5 screenshotData: - screenshotName: launchscreen.png zIndex: 12 diff --git a/Package.resolved b/Package.resolved index ea15f5b..43ea6c6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/apple/swift-argument-parser", "state": { "branch": null, - "revision": "8f4d2753f0e4778c76d5f05ad16c74f707390531", - "version": "1.2.3" + "revision": "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version": "1.3.0" } }, { diff --git a/Package.swift b/Package.swift index 3bb2cc5..26f7c75 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SwiftFrame", platforms: [ - .macOS(.v10_15) + .macOS(.v13) ], products: [ .executable(name: "swiftframe", targets: ["SwiftFrame"]) diff --git a/Sources/SwiftFrame/Render.swift b/Sources/SwiftFrame/Render.swift index 027a2b1..b458410 100644 --- a/Sources/SwiftFrame/Render.swift +++ b/Sources/SwiftFrame/Render.swift @@ -34,15 +34,17 @@ struct Render: ParsableCommand { @Flag( name: .long, + inversion: .prefixedNo, help: "Disables any colored output. Useful when running in CI" ) - var noColorOutput = false + var colorOutput = true @Flag( name: .long, + inversion: .prefixedNo, help: "Disables clearing the output directories before writing images to them" ) - var noClearDirectories = false + var clearDirectories = true // MARK: - Run @@ -55,8 +57,8 @@ struct Render: ParsableCommand { verbose: verbose, shouldValidateManually: manualValidation, shouldOutputWholeImage: outputWholeImage, - shouldClearDirectories: !noClearDirectories, - shouldColorOutput: !noColorOutput + shouldClearDirectories: clearDirectories, + shouldColorOutput: colorOutput ) try processor.validate() try processor.run() diff --git a/Sources/SwiftFrame/Scaffold.swift b/Sources/SwiftFrame/Scaffold.swift index 9a216db..c16f2e3 100644 --- a/Sources/SwiftFrame/Scaffold.swift +++ b/Sources/SwiftFrame/Scaffold.swift @@ -86,12 +86,8 @@ struct Scaffold: ParsableCommand, VerbosePrintable { print("Created \(numberOfCreatedFiles) files".formattedGreen()) } - private func lowercasedDirectoryIfNeeded(_ string: String) -> String { - lowercasedDirectories ? string.lowercased() : string - } - private func makeScaffoldRootURL() -> URL { - if let path = path { + if let path { return URL(fileURLWithPath: path) } else { return URL(fileURLWithPath: FileManager.default.currentDirectoryPath) diff --git a/Sources/SwiftFrame/SwiftFrame.swift b/Sources/SwiftFrame/SwiftFrame.swift index 07d56b5..e6de05c 100644 --- a/Sources/SwiftFrame/SwiftFrame.swift +++ b/Sources/SwiftFrame/SwiftFrame.swift @@ -8,7 +8,7 @@ struct SwiftFrame: ParsableCommand { static let configuration = CommandConfiguration( commandName: "swiftframe", abstract: "CLI application for speedy screenshot framing", - version: "5.0.2", + version: "6.0.0", subcommands: [Render.self, Scaffold.self], defaultSubcommand: Render.self, helpNames: .shortAndLong diff --git a/Sources/SwiftFrameCore/Config/ConfigData.swift b/Sources/SwiftFrameCore/Config/ConfigData.swift index f9a4c38..abb602f 100644 --- a/Sources/SwiftFrameCore/Config/ConfigData.swift +++ b/Sources/SwiftFrameCore/Config/ConfigData.swift @@ -20,8 +20,6 @@ struct ConfigData: Decodable, ConfigValidateable { let textColorSource: ColorSource let outputFormat: FileFormat let localesRegex: String? - let clearDirectories: Bool? - let outputWholeImage: Bool? @DecodableDefault.EmptyList var textGroups: [TextGroup] @@ -31,8 +29,6 @@ struct ConfigData: Decodable, ConfigValidateable { // MARK: - Coding Keys enum CodingKeys: String, CodingKey { - case clearDirectories - case outputWholeImage case deviceData case textGroups case stringsPath @@ -46,7 +42,7 @@ struct ConfigData: Decodable, ConfigValidateable { // MARK: - Init - public init( + init( textGroups: [TextGroup] = [], stringsPath: FileURL, maxFontSize: CGFloat, @@ -54,8 +50,6 @@ struct ConfigData: Decodable, ConfigValidateable { fontSource: FontSource, textColorSource: ColorSource, outputFormat: FileFormat, - clearDirectories: Bool?, - outputWholeImage: Bool?, deviceData: [DeviceData], localesRegex: String? = nil) { @@ -66,20 +60,17 @@ struct ConfigData: Decodable, ConfigValidateable { self.fontSource = fontSource self.textColorSource = textColorSource self.outputFormat = outputFormat - self.clearDirectories = clearDirectories - self.outputWholeImage = outputWholeImage self.deviceData = deviceData self.localesRegex = localesRegex } // MARK: - Processing - mutating public func process() throws { - let regex: NSRegularExpression? - if let pattern = localesRegex { - regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive) + mutating func process() throws { + let regex: Regex? = if let localesRegex, !localesRegex.isEmpty { + try Regex(localesRegex) } else { - regex = nil + nil } deviceData = try deviceData.map { try $0.makeProcessedData(localesRegex: regex) } @@ -91,23 +82,7 @@ struct ConfigData: Decodable, ConfigValidateable { // MARK: - ConfigValidateable - public func validate() throws { - if clearDirectories != nil { - let warningMessage = """ - Specifying clearDirectories in the config file is deprecated and will be ignored in a future version. - Please use the CLI argument --clear-directories instead. - """ - print(CommandLineFormatter.formatWarning(text: warningMessage)) - } - - if outputWholeImage != nil { - let warningMessage = """ - Specifying outputWholeImage in the config file is deprecated and will be ignored in a future version. - Please use the CLI argument --output-whole-image instead. - """ - print(CommandLineFormatter.formatWarning(text: warningMessage)) - } - + func validate() throws { guard !deviceData.isEmpty else { throw NSError( description: "No screenshot data was supplied", @@ -123,10 +98,9 @@ struct ConfigData: Decodable, ConfigValidateable { try deviceData.forEach { try $0.validate() } } - public func printSummary(insetByTabs tabs: Int) { + func printSummary(insetByTabs tabs: Int) { ky_print("### Config Summary Start", insetByTabs: tabs) - CommandLineFormatter.printKeyValue("Outputs whole image as well in addition to slices", value: outputWholeImage) - CommandLineFormatter.printKeyValue("Title Color", value: textColorSource.hexString, insetBy: tabs) + CommandLineFormatter.printKeyValue("Title Color", value: textColorSource.hexString.uppercased(), insetBy: tabs) CommandLineFormatter.printKeyValue("Title Font", value: try? fontSource.font().fontName, insetBy: tabs) CommandLineFormatter.printKeyValue("Title Max Font Size", value: maxFontSize, insetBy: tabs) CommandLineFormatter.printKeyValue( diff --git a/Sources/SwiftFrameCore/Config/DeviceData.swift b/Sources/SwiftFrameCore/Config/DeviceData.swift index 9733f6d..6b6f609 100644 --- a/Sources/SwiftFrameCore/Config/DeviceData.swift +++ b/Sources/SwiftFrameCore/Config/DeviceData.swift @@ -1,7 +1,7 @@ import AppKit import Foundation -public struct DeviceData: Decodable, ConfigValidateable { +struct DeviceData: Decodable, ConfigValidateable { // MARK: - Properties @@ -9,8 +9,8 @@ public struct DeviceData: Decodable, ConfigValidateable { let outputSuffixes: [String] let templateImagePath: FileURL - private let screenshotsPath: FileURL - let sliceSizeOverride: DecodableSize? + let screenshotsPath: FileURL + let numberOfSlices: Int @DecodableDefault.IntZero var gapWidth: Int @@ -19,29 +19,25 @@ public struct DeviceData: Decodable, ConfigValidateable { private(set) var screenshotData = [ScreenshotData]() private(set) var textData = [TextData]() - private var suffixesStringRepresentation: String { - outputSuffixes.map { "\"\($0)\"" }.joined(separator: ", ") - } - // MARK: - Coding Keys enum CodingKeys: String, CodingKey { case outputSuffixes case screenshotsPath = "screenshots" case templateImagePath = "templateFile" - case sliceSizeOverride case screenshotData case textData case gapWidth + case numberOfSlices } // MARK: - Init - internal init( + init( outputSuffixes: [String], templateImagePath: FileURL, screenshotsPath: FileURL, - sliceSizeOverride: DecodableSize? = nil, + numberOfSlices: Int, screenshotsGroupedByLocale: [String: [String: URL]]? = nil, templateImage: NSBitmapImageRep? = nil, screenshotData: [ScreenshotData] = [ScreenshotData](), @@ -51,7 +47,7 @@ public struct DeviceData: Decodable, ConfigValidateable { self.outputSuffixes = outputSuffixes self.templateImagePath = templateImagePath self.screenshotsPath = screenshotsPath - self.sliceSizeOverride = sliceSizeOverride + self.numberOfSlices = numberOfSlices self.screenshotsGroupedByLocale = screenshotsGroupedByLocale self.templateImage = templateImage self.screenshotData = screenshotData @@ -61,8 +57,8 @@ public struct DeviceData: Decodable, ConfigValidateable { // MARK: - Methods - func makeProcessedData(localesRegex: NSRegularExpression?) throws -> DeviceData { - guard let rep = ImageLoader.loadRepresentation(at: templateImagePath.absoluteURL) else { + func makeProcessedData(localesRegex: Regex?) throws -> DeviceData { + guard let templateImage = ImageLoader.loadRepresentation(at: templateImagePath.absoluteURL) else { throw NSError(description: "Error while loading template image at path \(templateImagePath.absoluteString)") } @@ -77,21 +73,22 @@ public struct DeviceData: Decodable, ConfigValidateable { parsedScreenshots[folder.lastPathComponent] = dictionary } - let processedTextData = try textData.map { try $0.makeProcessedData(size: rep.size) } + let processedTextData = try textData.map { try $0.makeProcessedData(size: templateImage.size) } let processedScreenshotData = screenshotData - .map { $0.makeProcessedData(size: rep.size) } + .map { $0.makeProcessedData(size: templateImage.size) } .sorted { $0.zIndex < $1.zIndex } return DeviceData( outputSuffixes: outputSuffixes, templateImagePath: templateImagePath, screenshotsPath: screenshotsPath, - sliceSizeOverride: sliceSizeOverride, + numberOfSlices: numberOfSlices, screenshotsGroupedByLocale: parsedScreenshots, - templateImage: rep, + templateImage: templateImage, screenshotData: processedScreenshotData, textData: processedTextData, - gapWidth: gapWidth) + gapWidth: gapWidth + ) } // MARK: - ConfigValidateable @@ -101,27 +98,28 @@ public struct DeviceData: Decodable, ConfigValidateable { throw NSError(description: "No screenshots were loaded, most likely caused by a faulty regular expression") } - try screenshotsGroupedByLocale.forEach { localeDict in - guard let first = localeDict.value.first?.value else { - return - } - try localeDict.value.forEach { - if let size = NSBitmapImageRep.ky_loadFromURL($0.value)?.ky_nativeSize, size != NSBitmapImageRep.ky_loadFromURL(first)?.ky_nativeSize { - throw NSError( - description: "Image file with mismatching resolution found in folder \"\(localeDict.key)\"", - expectation: "All screenshots should have the same resolution", - actualValue: "Screenshot with dimensions \(size)") - } - } + guard numberOfSlices > 0 else { + throw NSError( + description: "Invalid numberOfSlices value", + expectation: "numberOfSlices value should be >= 1", + actualValue: "numberOfSlices value is \(numberOfSlices)" + ) } - // Now that we know all screenshots have the same resolution, we can validate that template image is multiple in width - // plus specified gap width in between - if let screenshotSize = sliceSizeOverride?.cgSize ?? NSBitmapImageRep.ky_loadFromURL(screenshotsGroupedByLocale.first?.value.first?.value)?.ky_nativeSize { - guard let templateImageSize = templateImage?.ky_nativeSize else { - throw NSError(description: "Template image for output suffixes \(suffixesStringRepresentation) could not be loaded for validation") - } - try validateSize(templateImageSize, screenshotSize: screenshotSize) + guard gapWidth >= 0 else { + throw NSError( + description: "Invalid gapWidth value", + expectation: "gapWidth value should be >= 0 or ommitted from config", + actualValue: "gapWdith value is \(gapWidth)" + ) + } + + if let templateImage { + _ = try SliceSizeCalculator.calculateSliceSize( + templateImageSize: templateImage.ky_nativeSize, + numberOfSlices: numberOfSlices, + gapWidth: gapWidth + ) } try screenshotData.forEach { try $0.validate() } @@ -137,34 +135,27 @@ public struct DeviceData: Decodable, ConfigValidateable { } } - private func validateSize(_ templateSize: CGSize, screenshotSize: CGSize) throws { - let remainingPixels = templateSize.width.truncatingRemainder(dividingBy: screenshotSize.width) - if gapWidth == 0 { - guard remainingPixels == 0 else { - throw NSError( - description: "Template image for output suffixes \(suffixesStringRepresentation) is not a multiple in width as associated screenshot width", - expectation: "Width should be multiple of \(Int(screenshotSize.width))px", - actualValue: "\(Int(screenshotSize.width))px") - } - } else { - // Make sure there's at least one gap - guard remainingPixels.truncatingRemainder(dividingBy: CGFloat(gapWidth)) == 0 && remainingPixels != 0 else { - throw NSError( - description: "Template image for output suffixes \(suffixesStringRepresentation) is not a multiple in width as associated screenshot width", - expectation: "Template image width should be = (x * screenshot width) + (x - 1) * gap width", - actualValue: "Template image width: \(templateSize.width)px, screenshot width: \(screenshotSize.width), gap width: \(gapWidth)") - } - } - } - func printSummary(insetByTabs tabs: Int) { CommandLineFormatter.printKeyValue("Ouput suffixes", value: outputSuffixes.joined(separator: ", "), insetBy: tabs) CommandLineFormatter.printKeyValue("Template file path", value: templateImagePath.path, insetBy: tabs) + CommandLineFormatter.printKeyValue("Number of slices", value: numberOfSlices, insetBy: tabs) CommandLineFormatter.printKeyValue("Gap Width", value: gapWidth, insetBy: tabs) + + if let templateImage { + let sliceSize = try? SliceSizeCalculator.calculateSliceSize( + templateImageSize: templateImage.ky_nativeSize, + numberOfSlices: numberOfSlices, + gapWidth: gapWidth + ) + CommandLineFormatter.printKeyValue("Output slice size", value: sliceSize?.configValidationRepresentation, insetBy: tabs) + } + CommandLineFormatter.printKeyValue( "Screenshot folders", value: screenshotsGroupedByLocale.isEmpty ? "none" : screenshotsGroupedByLocale.keys.joined(separator: ", "), - insetBy: tabs) + insetBy: tabs + ) + screenshotData.forEach { $0.printSummary(insetByTabs: tabs) } textData.forEach { $0.printSummary(insetByTabs: tabs) } } diff --git a/Sources/SwiftFrameCore/Config/ScreenshotData.swift b/Sources/SwiftFrameCore/Config/ScreenshotData.swift index 01e8b55..6ef4a34 100644 --- a/Sources/SwiftFrameCore/Config/ScreenshotData.swift +++ b/Sources/SwiftFrameCore/Config/ScreenshotData.swift @@ -1,6 +1,6 @@ import Foundation -public struct ScreenshotData: Decodable, ConfigValidateable, Equatable { +struct ScreenshotData: Decodable, ConfigValidateable, Equatable { // MARK: - Properties @@ -14,7 +14,7 @@ public struct ScreenshotData: Decodable, ConfigValidateable, Equatable { // MARK: - Init - internal init(screenshotName: String, bottomLeft: Point, bottomRight: Point, topLeft: Point, topRight: Point, zIndex: Int?) { + init(screenshotName: String, bottomLeft: Point, bottomRight: Point, topLeft: Point, topRight: Point, zIndex: Int?) { self.screenshotName = screenshotName self.bottomLeft = bottomLeft self.bottomRight = bottomRight @@ -45,6 +45,6 @@ public struct ScreenshotData: Decodable, ConfigValidateable, Equatable { CommandLineFormatter.printKeyValue("Bottom Right", value: bottomRight, insetBy: tabs + 1) CommandLineFormatter.printKeyValue("Top Left", value: topLeft, insetBy: tabs + 1) CommandLineFormatter.printKeyValue("Top Right", value: topRight, insetBy: tabs + 1) - CommandLineFormatter.printKeyValue("Z Index", value: zIndex, insetBy: tabs + 1) + CommandLineFormatter.printKeyValue("Z-Index", value: zIndex, insetBy: tabs + 1) } } diff --git a/Sources/SwiftFrameCore/Config/TextData.swift b/Sources/SwiftFrameCore/Config/TextData.swift index e7b4192..153259a 100644 --- a/Sources/SwiftFrameCore/Config/TextData.swift +++ b/Sources/SwiftFrameCore/Config/TextData.swift @@ -87,7 +87,8 @@ struct TextData: Decodable, ConfigValidateable { throw NSError( description: "Bad text bounds for identifier \"\(titleIdentifier)\"", expectation: "Top Left coordinates should have smaller x coordinates and smaller y coordinates than bottom right", - actualValue: "Top Left: \(topLeft), Bottom Right: \(bottomRight)") + actualValue: "Top Left: \(topLeft), Bottom Right: \(bottomRight)" + ) } } @@ -104,8 +105,8 @@ struct TextData: Decodable, ConfigValidateable { CommandLineFormatter.printKeyValue("Max Point Size", value: ptSize, insetBy: tabs + 1) } - if let textColorOverride = textColorOverride { - CommandLineFormatter.printKeyValue("Custom color", value: textColorOverride.ky_hexString, insetBy: tabs + 1) + if let textColorOverride { + CommandLineFormatter.printKeyValue("Custom color", value: textColorOverride.ky_hexString.uppercased(), insetBy: tabs + 1) } } } diff --git a/Sources/SwiftFrameCore/Config/TextGroup.swift b/Sources/SwiftFrameCore/Config/TextGroup.swift index d8d2b86..c5c7741 100644 --- a/Sources/SwiftFrameCore/Config/TextGroup.swift +++ b/Sources/SwiftFrameCore/Config/TextGroup.swift @@ -1,9 +1,7 @@ import AppKit import Foundation -private let kNumTitleLines = 3 - -public struct TextGroup: Decodable, ConfigValidateable, Hashable { +struct TextGroup: Decodable, ConfigValidateable, Hashable { // MARK: - Properties diff --git a/Sources/SwiftFrameCore/Extensions/CGSize+Extensions.swift b/Sources/SwiftFrameCore/Extensions/CGSize+Extensions.swift new file mode 100644 index 0000000..f5ddd7b --- /dev/null +++ b/Sources/SwiftFrameCore/Extensions/CGSize+Extensions.swift @@ -0,0 +1,9 @@ +import Foundation + +extension CGSize { + + var configValidationRepresentation: String { + "Size(width: \(width), height: \(height))" + } + +} diff --git a/Sources/SwiftFrameCore/Extensions/Collection+Extensions.swift b/Sources/SwiftFrameCore/Extensions/Collection+Extensions.swift index 9cf8ca9..ad27e50 100644 --- a/Sources/SwiftFrameCore/Extensions/Collection+Extensions.swift +++ b/Sources/SwiftFrameCore/Extensions/Collection+Extensions.swift @@ -1,24 +1,15 @@ import Foundation -extension Array where Element == URL { +extension [URL] { - func filterByFileOrFoldername(regex: NSRegularExpression?) throws -> Self { - guard let regex = regex else { + func filterByFileOrFoldername(regex: Regex?) throws -> Self { + guard let regex else { return self } return self.filter { url in let lastComponent = url.deletingPathExtension().lastPathComponent - return regex.matches(lastComponent) + return !lastComponent.matches(of: regex).isEmpty } } } - -extension NSRegularExpression { - - func matches(_ string: String) -> Bool { - let range = NSRange(location: 0, length: (string as NSString).length) - return firstMatch(in: string, options: [], range: range) != nil - } - -} diff --git a/Sources/SwiftFrameCore/Extensions/FileManager+Extensions.swift b/Sources/SwiftFrameCore/Extensions/FileManager+Extensions.swift index aa58665..d621644 100644 --- a/Sources/SwiftFrameCore/Extensions/FileManager+Extensions.swift +++ b/Sources/SwiftFrameCore/Extensions/FileManager+Extensions.swift @@ -7,15 +7,6 @@ extension FileManager { .filter { pathExtension == nil ? true : $0.pathExtension == pathExtension } } - // Doesn't throw if the directory doesn't exist or another error occurred - func ky_unsafeFilesAtPath(_ url: URL) -> [URL] { - do { - return try ky_filesAtPath(url) - } catch { - return [] - } - } - func ky_subDirectoriesAtPath(_ url: URL) -> [URL] { guard let urls = try? contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) else { return [] diff --git a/Sources/SwiftFrameCore/Extensions/NSAttributedString+Extensions.swift b/Sources/SwiftFrameCore/Extensions/NSAttributedString+Extensions.swift index 6818861..83a2a5b 100644 --- a/Sources/SwiftFrameCore/Extensions/NSAttributedString+Extensions.swift +++ b/Sources/SwiftFrameCore/Extensions/NSAttributedString+Extensions.swift @@ -13,7 +13,7 @@ extension NSMutableAttributedString { options: [.characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil ) - if let string = string { + if let string { return string } else { throw NSError(description: "Could not parse HTML string") diff --git a/Sources/SwiftFrameCore/Extensions/NSBitmapImageRep+Extensions.swift b/Sources/SwiftFrameCore/Extensions/NSBitmapImageRep+Extensions.swift index fc70701..ce980f8 100644 --- a/Sources/SwiftFrameCore/Extensions/NSBitmapImageRep+Extensions.swift +++ b/Sources/SwiftFrameCore/Extensions/NSBitmapImageRep+Extensions.swift @@ -13,7 +13,7 @@ extension NSBitmapImageRep { } static func ky_loadFromURL(_ url: URL?) -> NSBitmapImageRep? { - guard let url = url else { + guard let url else { return nil } return ImageLoader.loadRepresentation(at: url) diff --git a/Sources/SwiftFrameCore/Extensions/NSError+Extensions.swift b/Sources/SwiftFrameCore/Extensions/NSError+Extensions.swift index 966117f..6e71001 100644 --- a/Sources/SwiftFrameCore/Extensions/NSError+Extensions.swift +++ b/Sources/SwiftFrameCore/Extensions/NSError+Extensions.swift @@ -13,7 +13,8 @@ public extension NSError { NSLocalizedDescriptionKey: description, NSError.kExpectationKey: expectation as Any, NSError.kActualValueKey: actualValue as Any - ]) + ] + ) } var expectation: String? { @@ -26,7 +27,7 @@ public extension NSError { } -public func ky_executeOrExit(verbose: Bool = false, _ work: () throws -> T) -> T { +func ky_executeOrExit(verbose: Bool = false, _ work: () throws -> T) -> T { do { return try work() } catch let error as NSError { @@ -36,10 +37,7 @@ public func ky_executeOrExit(verbose: Bool = false, _ work: () throws -> T) - public func ky_exitWithError(_ error: Error, verbose: Bool = false) -> Never { let error = error as NSError - let errorMessage = verbose - ? CommandLineFormatter.formatError(error.description) - : CommandLineFormatter.formatError(error.localizedDescription) - print(errorMessage) + print(CommandLineFormatter.formatError(verbose ? error.description : error.localizedDescription)) error.expectation.flatMap { print(CommandLineFormatter.formatWarning(title: "EXPECTATION", text: $0)) } error.actualValue.flatMap { print(CommandLineFormatter.formatWarning(title: "ACTUAL", text: $0)) } diff --git a/Sources/SwiftFrameCore/Extensions/String+Extensions.swift b/Sources/SwiftFrameCore/Extensions/String+Extensions.swift index a22d4ed..9b40e41 100644 --- a/Sources/SwiftFrameCore/Extensions/String+Extensions.swift +++ b/Sources/SwiftFrameCore/Extensions/String+Extensions.swift @@ -6,10 +6,6 @@ extension String { public func formattedGreen() -> String { CommandLineFormatter.formatWithColorIfNeeded(self, color: .green) } - - func formattedRed() -> String { - CommandLineFormatter.formatWithColorIfNeeded(self, color: .red) - } func ky_containsHTMLTags() -> Bool { let regex = try! NSRegularExpression(pattern: "<(.*)>.*?|<(.*)/>") diff --git a/Sources/SwiftFrameCore/Helper Types/DecodableDefault.swift b/Sources/SwiftFrameCore/Helper Types/DecodableDefault.swift index 48e674b..9e6805b 100644 --- a/Sources/SwiftFrameCore/Helper Types/DecodableDefault.swift +++ b/Sources/SwiftFrameCore/Helper Types/DecodableDefault.swift @@ -10,7 +10,7 @@ protocol DecodableDefaultSource { } -public enum DecodableDefault {} +enum DecodableDefault {} extension DecodableDefault { @@ -45,37 +45,16 @@ extension DecodableDefault { typealias Source = DecodableDefaultSource typealias List = Decodable & ExpressibleByArrayLiteral - typealias Map = Decodable & ExpressibleByDictionaryLiteral enum Sources { - enum True: Source { - static var defaultValue: Bool { true } - } - - enum False: Source { - static var defaultValue: Bool { false } - } - - enum EmptyString: Source { - static var defaultValue: String { "" } - } - enum EmptyList: Source { static var defaultValue: T { [] } } - enum EmptyMap: Source { - static var defaultValue: T { [:] } - } - enum IntZero: Source { static var defaultValue: Swift.Int { 0 } } - enum DoubleZero: Source { - static var defaultValue: Swift.Double { 0.00 } - } - enum CGFloatZero: Source { static var defaultValue: CGFloat { 0.00 } } @@ -85,25 +64,11 @@ extension DecodableDefault { extension DecodableDefault { - typealias True = Wrapper - typealias False = Wrapper - typealias EmptyString = Wrapper typealias EmptyList = Wrapper> - typealias EmptyMap = Wrapper> typealias IntZero = Wrapper - typealias DoubleZero = Wrapper typealias CGFloatZero = Wrapper } extension DecodableDefault.Wrapper: Equatable where Value: Equatable {} extension DecodableDefault.Wrapper: Hashable where Value: Hashable {} - -extension DecodableDefault.Wrapper: Encodable where Value: Encodable { - - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(wrappedValue) - } - -} diff --git a/Sources/SwiftFrameCore/Helper Types/DecodableSize.swift b/Sources/SwiftFrameCore/Helper Types/DecodableSize.swift deleted file mode 100644 index 518ecd9..0000000 --- a/Sources/SwiftFrameCore/Helper Types/DecodableSize.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation - -/// Wrapper struct used to work around weird decoding behavior of `CGSize` -struct DecodableSize: Codable { - - let width: CGFloat - let height: CGFloat - - var cgSize: CGSize { - CGSize(width: width, height: height) - } - -} diff --git a/Sources/SwiftFrameCore/Helper Types/Pluralizer.swift b/Sources/SwiftFrameCore/Helper Types/Pluralizer.swift new file mode 100644 index 0000000..e702b0c --- /dev/null +++ b/Sources/SwiftFrameCore/Helper Types/Pluralizer.swift @@ -0,0 +1,17 @@ +import Foundation + +enum Pluralizer { + + static func pluralize(_ value: Int, singular: String, plural: String, zero: String? = nil) -> String { + let absoluteValue = abs(value) + return switch absoluteValue { + case 0: + "\(value) \(zero ?? plural)" + case 1: + "\(value) \(singular)" + default: + "\(value) \(plural)" + } + } + +} diff --git a/Sources/SwiftFrameCore/Helper Types/Point.swift b/Sources/SwiftFrameCore/Helper Types/Point.swift index 4e28ee5..2355dfe 100644 --- a/Sources/SwiftFrameCore/Helper Types/Point.swift +++ b/Sources/SwiftFrameCore/Helper Types/Point.swift @@ -12,13 +12,9 @@ struct Point: Codable, Equatable { CIVector(x: CGFloat(x), y: CGFloat(y)) } - var cgPoint: CGPoint { - CGPoint(x: x, y: y) - } - // MARK: - Coordinate space conversion - public func convertingToBottomLeftOrigin(withSize size: CGSize) -> Point { + func convertingToBottomLeftOrigin(withSize size: CGSize) -> Point { let newY = Int(size.height) - y return Point(x: x, y: newY) } diff --git a/Sources/SwiftFrameCore/Helper Types/VerbosePrintable.swift b/Sources/SwiftFrameCore/Helper Types/VerbosePrintable.swift index 8bfbb29..2409e41 100644 --- a/Sources/SwiftFrameCore/Helper Types/VerbosePrintable.swift +++ b/Sources/SwiftFrameCore/Helper Types/VerbosePrintable.swift @@ -25,11 +25,4 @@ public extension VerbosePrintable { printVerbose(CommandLineFormatter.formatTimeMeasurement(messageString)) } - @inlinable func performAndPrintElapsedTime(_ name: @autoclosure () -> String, work: () throws -> T) rethrows -> T { - let startTime = CFAbsoluteTimeGetCurrent() - let result = try work() - printElapsedTime(name(), startTime: startTime) - return result - } - } diff --git a/Sources/SwiftFrameCore/Workers/CommandLineFormatter.swift b/Sources/SwiftFrameCore/Workers/CommandLineFormatter.swift index c96ab7a..c563694 100644 --- a/Sources/SwiftFrameCore/Workers/CommandLineFormatter.swift +++ b/Sources/SwiftFrameCore/Workers/CommandLineFormatter.swift @@ -1,6 +1,6 @@ import Foundation -public final class CommandLineFormatter { +final class CommandLineFormatter { // MARK: - Nested Types @@ -19,17 +19,17 @@ public final class CommandLineFormatter { // MARK: - Message Formatting - public class func formatWarning(title: String = "WARNING", text: String) -> String { + class func formatWarning(title: String = "WARNING", text: String) -> String { let message = "[\(title)] \(text)" return formatWithColorIfNeeded(message, color: .yellow) } - public class func formatError(_ text: String) -> String { + class func formatError(_ text: String) -> String { let message = "[ERROR] \(text)" return formatWithColorIfNeeded(message, color: .red) } - public class func formatTimeMeasurement(_ text: String) -> String { + class func formatTimeMeasurement(_ text: String) -> String { let message = "[TIME] \(text)" return formatWithColorIfNeeded(message, color: .green) } @@ -44,8 +44,8 @@ public final class CommandLineFormatter { // MARK: - Key-Value Formatting - public class func printKeyValue(_ key: String, value: Any?, insetBy tabs: Int = 0) { - guard let value = value else { + class func printKeyValue(_ key: String, value: Any?, insetBy tabs: Int = 0) { + guard let value else { return } print(CommandLineFormatter.formatKeyValue(key, value: value, insetBy: tabs)) @@ -59,7 +59,7 @@ public final class CommandLineFormatter { } -public func ky_print(_ objects: Any..., insetByTabs tabs: Int) { +func ky_print(_ objects: Any..., insetByTabs tabs: Int) { let tabsString = String(repeating: CommandLineFormatter.tabsString, count: tabs) let arguments = objects.count == 1 ? String(describing: objects[0]) diff --git a/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift b/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift index ec27b04..766e1ae 100644 --- a/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift +++ b/Sources/SwiftFrameCore/Workers/ConfigProcessor.swift @@ -23,8 +23,8 @@ public class ConfigProcessor: VerbosePrintable { shouldValidateManually: Bool, shouldOutputWholeImage: Bool, shouldClearDirectories: Bool, - shouldColorOutput: Bool) throws - { + shouldColorOutput: Bool + ) throws { data = try DecodableParser.parseData(fromURL: configURL) self.verbose = verbose self.shouldValidateManually = shouldValidateManually @@ -36,25 +36,14 @@ public class ConfigProcessor: VerbosePrintable { // MARK: - Methods public func validate() throws { - try process() - try data.validate() - - if data.outputWholeImage != nil { - printDeprecationWarning(for: "ouputWholeImage") - } - if data.clearDirectories != nil { - printDeprecationWarning(for: "clearDirectories") - } - } - - private func process() throws { try data.process() + try data.validate() } public func run() throws { if shouldValidateManually { data.printSummary(insetByTabs: 0) - print("Press return key to continue") + print("Press any key to continue") _ = readLine() } @@ -94,7 +83,7 @@ public class ConfigProcessor: VerbosePrintable { DispatchQueue.concurrentPerform(iterations: data.deviceData.count) { index in ky_executeOrExit(verbose: verbose) { [weak self] in - guard let `self` = self else { + guard let `self` else { throw NSError(description: "Could not reference weak self") } try self.process(deviceData: self.data.deviceData[index]) @@ -110,14 +99,17 @@ public class ConfigProcessor: VerbosePrintable { try deviceData.screenshotsGroupedByLocale.forEach { locale, imageDict in group.enter() + defer { group.leave() } guard let templateImage = deviceData.templateImage else { throw NSError(description: "No template image found") } - guard let sliceSize = deviceData.sliceSizeOverride?.cgSize ?? NSBitmapImageRep.ky_loadFromURL(imageDict.first?.value)?.ky_nativeSize else { - throw NSError(description: "No screenshots supplied, so it's impossible to slice into the correct size") - } + let sliceSize = try SliceSizeCalculator.calculateSliceSize( + templateImageSize: templateImage.ky_nativeSize, + numberOfSlices: deviceData.numberOfSlices, + gapWidth: deviceData.gapWidth + ) let composer = try ImageComposer(canvasSize: templateImage.ky_nativeSize) try composer.add(screenshots: imageDict, with: deviceData.screenshotData, for: locale) @@ -139,18 +131,9 @@ public class ConfigProcessor: VerbosePrintable { deviceData.outputSuffixes.forEach { suffix in print("Finished \(locale)-\(suffix)") } - - group.leave() } group.wait() } - // MARK: - Helpers - - private func printDeprecationWarning(for configProperty: String) { - let warningMessage = "\(configProperty) was specified in the config file, which is deprecated. The value will be ignored" - print(CommandLineFormatter.formatWarning(text: warningMessage)) - } - } diff --git a/Sources/SwiftFrameCore/Workers/ImageLoader.swift b/Sources/SwiftFrameCore/Workers/ImageLoader.swift index c2c7a01..69c97a1 100644 --- a/Sources/SwiftFrameCore/Workers/ImageLoader.swift +++ b/Sources/SwiftFrameCore/Workers/ImageLoader.swift @@ -5,13 +5,6 @@ final class ImageLoader { // MARK: - Image Loading - func loadImage(atURL url: URL) throws -> NSImage { - guard let image = NSImage(contentsOf: url) else { - throw NSError(description: "Could not load image at \(url.absoluteString)") - } - return image - } - func loadImage(atPath path: String) throws -> NSImage { guard let image = NSImage(contentsOfFile: path) else { throw NSError(description: "Could not load image at \(path)") diff --git a/Sources/SwiftFrameCore/Workers/ImageWriter.swift b/Sources/SwiftFrameCore/Workers/ImageWriter.swift index e13631d..c51b0fb 100644 --- a/Sources/SwiftFrameCore/Workers/ImageWriter.swift +++ b/Sources/SwiftFrameCore/Workers/ImageWriter.swift @@ -1,7 +1,7 @@ import AppKit import Foundation -public final class ImageWriter { +final class ImageWriter { // MARK: - Exporting diff --git a/Sources/SwiftFrameCore/Workers/SliceSizeCalculator.swift b/Sources/SwiftFrameCore/Workers/SliceSizeCalculator.swift new file mode 100644 index 0000000..aa28d72 --- /dev/null +++ b/Sources/SwiftFrameCore/Workers/SliceSizeCalculator.swift @@ -0,0 +1,26 @@ +import Foundation + +struct SliceSizeCalculator { + + static func calculateSliceSize(templateImageSize: CGSize, numberOfSlices: Int, gapWidth: Int?) throws -> CGSize { + guard numberOfSlices > 0 else { + throw NSError(description: "Number of slices must be larger than 0") + } + // number of slices minus 1 because gaps are only in between, multiplied by gapWidth + let totalGapWidthIfAny = gapWidth.flatMap { (numberOfSlices - 1) * $0 } + let templateWidthSubstractingGaps = templateImageSize.width - CGFloat(totalGapWidthIfAny ?? 0) + + guard Int(templateWidthSubstractingGaps) >= numberOfSlices else { + let minimumTemplateWidth = numberOfSlices + (totalGapWidthIfAny ?? 0) + throw NSError( + description: "Template image is not wide enough to accommodate \(Pluralizer.pluralize(numberOfSlices, singular: "slice", plural: "slices"))", + expectation: "Template image should be at least \(minimumTemplateWidth) pixels wide", + actualValue: "Template image is \(templateImageSize.width) pixels wide" + ) + } + + // Resulting slice is remaining width divided by expected number of slices, height can just be forwarded + return CGSize(width: templateWidthSubstractingGaps / CGFloat(numberOfSlices), height: templateImageSize.height) + } + +} diff --git a/Sources/SwiftFrameCore/Workers/TextRenderer.swift b/Sources/SwiftFrameCore/Workers/TextRenderer.swift index 118b320..64bd903 100644 --- a/Sources/SwiftFrameCore/Workers/TextRenderer.swift +++ b/Sources/SwiftFrameCore/Workers/TextRenderer.swift @@ -158,10 +158,6 @@ final class TextRenderer { } -private func >=(lhs: CGSize, rhs: CGSize) -> Bool { - lhs.width >= rhs.width && lhs.height >= rhs.height -} - private func <=(lhs: CGSize, rhs: CGSize) -> Bool { lhs.width <= rhs.width && lhs.height <= rhs.height } diff --git a/Tests/SwiftFrameTests/Config Tests/ConfigDataTests.swift b/Tests/SwiftFrameTests/Config Tests/ConfigDataTests.swift index 03399ab..1289848 100644 --- a/Tests/SwiftFrameTests/Config Tests/ConfigDataTests.swift +++ b/Tests/SwiftFrameTests/Config Tests/ConfigDataTests.swift @@ -2,7 +2,7 @@ import Foundation import XCTest @testable import SwiftFrameCore -class ConfigDataTests: BaseTest { +class ConfigDataTests: BaseTestCase { func testValidData() throws { var data = ConfigData.goodData diff --git a/Tests/SwiftFrameTests/Config Tests/DeviceDataTests.swift b/Tests/SwiftFrameTests/Config Tests/DeviceDataTests.swift index fa82f15..4eac2d0 100644 --- a/Tests/SwiftFrameTests/Config Tests/DeviceDataTests.swift +++ b/Tests/SwiftFrameTests/Config Tests/DeviceDataTests.swift @@ -2,37 +2,30 @@ import Foundation import XCTest @testable import SwiftFrameCore -class DeviceDataTests: BaseTest { +class DeviceDataTests: BaseTestCase { - func testValidData() throws { - let data = try DeviceData.goodData.makeProcessedData(localesRegex: nil) + func testDeviceData_IsValid_WhenAllDataIsValid() throws { + let data = try DeviceData.validData().makeProcessedData(localesRegex: nil) XCTAssertNoThrow(try data.validate()) } - func testGapDataValid() throws { - try TestingUtility.setupMockDirectoryWithScreenshots(gapWidth: 16) - - let data = try DeviceData.gapData.makeProcessedData(localesRegex: nil) + func testDeviceData_IsValid_WhenGapWidthIsPositive() throws { + let data = try DeviceData.validData(gapWidth: 16).makeProcessedData(localesRegex: nil) XCTAssertNoThrow(try data.validate()) } - func testGapDataInvalid() throws { - let data = try DeviceData.gapData.makeProcessedData(localesRegex: nil) + func testDeviceData_IsInvalid_WhenTextDataIsInvalid() throws { + let data = try DeviceData.invalidTextData.makeProcessedData(localesRegex: nil) XCTAssertThrowsError(try data.validate()) } - func testInvalidData() throws { - let data = try DeviceData.invalidData.makeProcessedData(localesRegex: nil) + func testDeviceData_IsInvalid_WhenNumberOfSlicesIsZero() throws { + let data = try DeviceData.invalidNumberOfSlices.makeProcessedData(localesRegex: nil) XCTAssertThrowsError(try data.validate()) } - func testMismatchingDeviceSizeData() throws { - let data = try DeviceData.mismatchingDeviceSizeData.makeProcessedData(localesRegex: nil) - XCTAssertNoThrow(try data.validate()) - } - - func testFaultyMismatchingDeviceSizeData() throws { - let data = try DeviceData.faultyMismatchingDeviceSizeData.makeProcessedData(localesRegex: nil) + func testDeviceData_IsInvalid_WhenGapWidthNegative() throws { + let data = try DeviceData.invalidGapWidth.makeProcessedData(localesRegex: nil) XCTAssertThrowsError(try data.validate()) } diff --git a/Tests/SwiftFrameTests/Config Tests/TextDataTests.swift b/Tests/SwiftFrameTests/Config Tests/TextDataTests.swift index 2d91672..26df444 100644 --- a/Tests/SwiftFrameTests/Config Tests/TextDataTests.swift +++ b/Tests/SwiftFrameTests/Config Tests/TextDataTests.swift @@ -2,17 +2,17 @@ import Foundation import XCTest @testable import SwiftFrameCore -class TextDataTests: BaseTest { +class TextDataTests: BaseTestCase { - func testValidData() throws { + func testTextData_IsValid() throws { let size = CGSize(width: 200, height: 200) - let data = try TextData.goodData.makeProcessedData(size: size) + let processedData = try TextData.validData.makeProcessedData(size: size) - XCTAssertNoThrow(try data.validate()) + XCTAssertNoThrow(try processedData.validate()) } - func testInvalidData() throws { - XCTAssertThrowsError(try TextData.invalidData.validate()) + func testTextData_IsInvalid_WhenTextBoundsAreInvalid() throws { + XCTAssertThrowsError(try TextData.invalidTextBounds.validate()) } } diff --git a/Tests/SwiftFrameTests/ImageComposerTests.swift b/Tests/SwiftFrameTests/ImageComposerTests.swift index 4a81bd6..be3d7ad 100644 --- a/Tests/SwiftFrameTests/ImageComposerTests.swift +++ b/Tests/SwiftFrameTests/ImageComposerTests.swift @@ -10,7 +10,7 @@ class ImageComposerTests: XCTestCase { let composer = try ImageComposer(canvasSize: size) try composer.addTemplateImage(templateFile) - let image = try ky_unwrap(composer.context.cg.makeImage()) + let image = try XCTUnwrap(composer.context.cg.makeImage()) XCTAssertEqual(image.width, Int(size.width)) XCTAssertEqual(image.height, Int(size.height)) } @@ -21,7 +21,7 @@ class ImageComposerTests: XCTestCase { let composer = try ImageComposer(canvasSize: size) try composer.addTemplateImage(templateFile) - let image = try ky_unwrap(composer.context.cg.makeImage()) + let image = try XCTUnwrap(composer.context.cg.makeImage()) let slices = try ImageWriter.sliceImage(image, with: NSSize(width: 20, height: 50), gapWidth: 0) XCTAssertEqual(slices.count, 5) for slice in slices { diff --git a/Tests/SwiftFrameTests/ImageLoaderTests.swift b/Tests/SwiftFrameTests/ImageLoaderTests.swift index b015de4..1f77c5a 100644 --- a/Tests/SwiftFrameTests/ImageLoaderTests.swift +++ b/Tests/SwiftFrameTests/ImageLoaderTests.swift @@ -2,12 +2,12 @@ import Foundation import XCTest @testable import SwiftFrameCore -class ImageLoaderTests: BaseTest { +class ImageLoaderTests: BaseTestCase { func testLoadImage() throws { let context = try GraphicsContext(size: .square100Pixels) let rep = context.cg.makePlainWhiteImageRep() - let cgImage = try ky_unwrap(rep.cgImage) + let cgImage = try XCTUnwrap(rep.cgImage) let url = URL(fileURLWithPath: "testing/en/en-testing_device.png") diff --git a/Tests/SwiftFrameTests/PluralizerTests.swift b/Tests/SwiftFrameTests/PluralizerTests.swift new file mode 100644 index 0000000..b03fb52 --- /dev/null +++ b/Tests/SwiftFrameTests/PluralizerTests.swift @@ -0,0 +1,23 @@ +import Foundation +import XCTest +@testable import SwiftFrameCore + +class PluralizerTests: XCTestCase { + + func testPluralizer_ProducesSingularString_WhenSpecifyingOne() { + XCTAssertEqual(Pluralizer.pluralize(1, singular: "slice", plural: "slices"), "1 slice") + } + + func testPluralizer_ProducesPluralString_WhenSpecifyingBigNumber() { + XCTAssertEqual(Pluralizer.pluralize(32, singular: "slice", plural: "slices"), "32 slices") + } + + func testPluralizer_ProducesPluralString_WhenSpecifyingZero() { + XCTAssertEqual(Pluralizer.pluralize(0, singular: "slice", plural: "slices"), "0 slices") + } + + func testPluralizer_ProducesZeroString_WhenSpecifyingZero() { + XCTAssertEqual(Pluralizer.pluralize(0, singular: "slice", plural: "slices", zero: "bogus"), "0 bogus") + } + +} diff --git a/Tests/SwiftFrameTests/PointTests.swift b/Tests/SwiftFrameTests/PointTests.swift new file mode 100644 index 0000000..d0c7edb --- /dev/null +++ b/Tests/SwiftFrameTests/PointTests.swift @@ -0,0 +1,33 @@ +import Foundation +import XCTest +@testable import SwiftFrameCore + +class PointTests: XCTestCase { + + func testPoint_DecodesCorrectly_WhenJSONIsWellFormed() throws { + let expectedX = 10 + let expectedY = 5 + + let jsonString = "{ \"x\": \(expectedX), \"y\": \(expectedY) }" + let jsonData = try XCTUnwrap(jsonString.data(using: .utf8)) + + let decodedPoint = try JSONDecoder().decode(Point.self, from: jsonData) + XCTAssertEqual(decodedPoint, Point(x: expectedX, y: expectedY)) + } + + func testPoint_FailsToDecode_WhenJSONIsMalformed() throws { + let jsonString = #"{ "x": 10, "yCoord": 5 }"# + let jsonData = try XCTUnwrap(jsonString.data(using: .utf8)) + + XCTAssertThrowsError(try JSONDecoder().decode(Point.self, from: jsonData)) + } + + func testPoint_ConvertsToBottomLeftOrigin() throws { + let point = Point(x: 10, y: 5) + let size = CGSize(width: 30, height: 30) + + let convertedPoint = point.convertingToBottomLeftOrigin(withSize: size) + XCTAssertEqual(convertedPoint, Point(x: 10, y: 25)) + } + +} diff --git a/Tests/SwiftFrameTests/RegexMatchTests.swift b/Tests/SwiftFrameTests/RegexMatchTests.swift index 1d55447..11fab52 100644 --- a/Tests/SwiftFrameTests/RegexMatchTests.swift +++ b/Tests/SwiftFrameTests/RegexMatchTests.swift @@ -4,52 +4,50 @@ import XCTest class RegexMatchTests: XCTestCase { - static let urls: [URL] = { - [ - URL(fileURLWithPath: "strings/en.strings"), - URL(fileURLWithPath: "strings/de.strings"), - URL(fileURLWithPath: "strings/fr.strings"), - URL(fileURLWithPath: "strings/ru.strings") - ] - }() + private let urls: [URL] = [ + URL(fileURLWithPath: "strings/en.strings"), + URL(fileURLWithPath: "strings/de.strings"), + URL(fileURLWithPath: "strings/fr.strings"), + URL(fileURLWithPath: "strings/ru.strings") + ] func testAllURLs() throws { - let urls = try RegexMatchTests.urls.filterByFileOrFoldername(regex: nil) - XCTAssertEqual(urls, RegexMatchTests.urls) + let filteredURLs = try urls.filterByFileOrFoldername(regex: nil) + XCTAssertEqual(filteredURLs, urls) } func testFranceFilteredOut() throws { - let regex = try NSRegularExpression(pattern: "^(?!fr$)\\w*$", options: .caseInsensitive) - let urls = try RegexMatchTests.urls.filterByFileOrFoldername(regex: regex) + let regex = try Regex("^(?!fr$)\\w*$") + let filteredURLs = try urls.filterByFileOrFoldername(regex: regex) - guard ky_assertEqual(urls.count, 3) else { + guard ky_assertEqual(filteredURLs.count, 3) else { return } - XCTAssertTrue(urls[0].absoluteString.hasSuffix("en.strings")) - XCTAssertTrue(urls[1].absoluteString.hasSuffix("de.strings")) - XCTAssertTrue(urls[2].absoluteString.hasSuffix("ru.strings")) + XCTAssertTrue(filteredURLs[0].absoluteString.hasSuffix("en.strings")) + XCTAssertTrue(filteredURLs[1].absoluteString.hasSuffix("de.strings")) + XCTAssertTrue(filteredURLs[2].absoluteString.hasSuffix("ru.strings")) } func testFranceAndRussiaFilteredOut() throws { - let regex = try NSRegularExpression(pattern: "^(?!fr|ru$)\\w*$", options: .caseInsensitive) - let urls = try RegexMatchTests.urls.filterByFileOrFoldername(regex: regex) + let regex = try Regex("^(?!fr|ru$)\\w*$") + let filteredURLs = try urls.filterByFileOrFoldername(regex: regex) - guard ky_assertEqual(urls.count, 2) else { + guard ky_assertEqual(filteredURLs.count, 2) else { return } - XCTAssertTrue(urls[0].absoluteString.hasSuffix("en.strings")) - XCTAssertTrue(urls[1].absoluteString.hasSuffix("de.strings")) + XCTAssertTrue(filteredURLs[0].absoluteString.hasSuffix("en.strings")) + XCTAssertTrue(filteredURLs[1].absoluteString.hasSuffix("de.strings")) } func testOnlyRussiaAndFrance() throws { - let regex = try NSRegularExpression(pattern: "ru|fr", options: .caseInsensitive) - let urls = try RegexMatchTests.urls.filterByFileOrFoldername(regex: regex) + let regex = try Regex("ru|fr") + let filteredURLs = try urls.filterByFileOrFoldername(regex: regex) - guard ky_assertEqual(urls.count, 2) else { + guard ky_assertEqual(filteredURLs.count, 2) else { return } - XCTAssertEqual(urls[0].lastPathComponent, "fr.strings") - XCTAssertEqual(urls[1].lastPathComponent, "ru.strings") + XCTAssertEqual(filteredURLs[0].lastPathComponent, "fr.strings") + XCTAssertEqual(filteredURLs[1].lastPathComponent, "ru.strings") } } diff --git a/Tests/SwiftFrameTests/SliceSizeCalculatorTests.swift b/Tests/SwiftFrameTests/SliceSizeCalculatorTests.swift new file mode 100644 index 0000000..1150c6c --- /dev/null +++ b/Tests/SwiftFrameTests/SliceSizeCalculatorTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import SwiftFrameCore + +final class SliceSizeCalculatorTests: BaseTestCase { + + func testSliceSizeCalculator_ProducesFiveSlices_WhenNotUsingGapWidth() throws { + let templateSize = CGSize(width: 100, height: 50) + + let calculatedSliceSize = try SliceSizeCalculator.calculateSliceSize( + templateImageSize: templateSize, + numberOfSlices: 5, + gapWidth: nil + ) + XCTAssertEqual(calculatedSliceSize, CGSize(width: 20, height: 50)) + } + + func testSliceSizeCalculator_ProducesFiveSlices_WhenUsingGapWidth() throws { + let templateSize = CGSize(width: 100, height: 50) + + let calculatedSliceSize = try SliceSizeCalculator.calculateSliceSize( + templateImageSize: templateSize, + numberOfSlices: 5, + gapWidth: 5 + ) + XCTAssertEqual(calculatedSliceSize, CGSize(width: 16, height: 50)) + } + + func testSliceSizeCalculator_ThrowsError_WhenTotalWidthIsNotEnough() { + let templateSize = CGSize(width: 24, height: 50) + + XCTAssertThrowsError( + try SliceSizeCalculator.calculateSliceSize( + templateImageSize: templateSize, + numberOfSlices: 7, + gapWidth: 6 + ) + ) + } + +} diff --git a/Tests/SwiftFrameTests/Utility/BaseTest.swift b/Tests/SwiftFrameTests/Utility/BaseTest.swift deleted file mode 100644 index 2c975c9..0000000 --- a/Tests/SwiftFrameTests/Utility/BaseTest.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import XCTest - -class BaseTest: XCTestCase { - - override func setUp() { - super.setUp() - - do { - try TestingUtility.setupMockDirectoryWithScreenshots() - } catch let error { - XCTFail(error.localizedDescription) - } - } - - override func tearDown() { - super.tearDown() - - do { - try TestingUtility.clearTestingDirectory() - } catch let error { - XCTFail(error.localizedDescription) - } - } - -} diff --git a/Tests/SwiftFrameTests/Utility/BaseTestCase.swift b/Tests/SwiftFrameTests/Utility/BaseTestCase.swift new file mode 100644 index 0000000..27917f5 --- /dev/null +++ b/Tests/SwiftFrameTests/Utility/BaseTestCase.swift @@ -0,0 +1,16 @@ +import Foundation +import XCTest + +class BaseTestCase: XCTestCase { + + override func setUpWithError() throws { + try TestingUtility.setupMockDirectoryWithScreenshots() + try super.setUpWithError() + } + + override func tearDownWithError() throws { + try TestingUtility.clearTestingDirectory() + try super.tearDownWithError() + } + +} diff --git a/Tests/SwiftFrameTests/Utility/ConfigDataFixtures.swift b/Tests/SwiftFrameTests/Utility/ConfigDataFixtures.swift index ec9ff32..0a20439 100644 --- a/Tests/SwiftFrameTests/Utility/ConfigDataFixtures.swift +++ b/Tests/SwiftFrameTests/Utility/ConfigDataFixtures.swift @@ -12,9 +12,7 @@ extension ConfigData { fontSource: .nsFont(.systemFont(ofSize: 20)), textColorSource: try! ColorSource(hexString: "#ff00ff"), outputFormat: .png, - clearDirectories: true, - outputWholeImage: true, - deviceData: [.goodData] + deviceData: [.validData()] ) static let skippedLocaleData = ConfigData( @@ -25,9 +23,7 @@ extension ConfigData { fontSource: .nsFont(.systemFont(ofSize: 20)), textColorSource: try! ColorSource(hexString: "#ff00ff"), outputFormat: .png, - clearDirectories: true, - outputWholeImage: true, - deviceData: [.goodData], + deviceData: [.validData()], localesRegex: "^(?!en|fr$)\\w*$" ) @@ -39,9 +35,7 @@ extension ConfigData { fontSource: .nsFont(.systemFont(ofSize: 20)), textColorSource: try! ColorSource(hexString: "#ff00ff"), outputFormat: .png, - clearDirectories: true, - outputWholeImage: true, - deviceData: [.goodData], + deviceData: [.validData()], localesRegex: "en" ) @@ -53,9 +47,7 @@ extension ConfigData { fontSource: .nsFont(.systemFont(ofSize: 20)), textColorSource: try! ColorSource(hexString: "#ff00ff"), outputFormat: .png, - clearDirectories: true, - outputWholeImage: true, - deviceData: [.invalidData] + deviceData: [.invalidTextData] ) } diff --git a/Tests/SwiftFrameTests/Utility/DeviceDataFixtures.swift b/Tests/SwiftFrameTests/Utility/DeviceDataFixtures.swift index 981df7b..8be829e 100644 --- a/Tests/SwiftFrameTests/Utility/DeviceDataFixtures.swift +++ b/Tests/SwiftFrameTests/Utility/DeviceDataFixtures.swift @@ -3,48 +3,44 @@ import Foundation extension DeviceData { - static let goodData = DeviceData( - outputSuffixes: ["iPhone X"], - templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"), - screenshotsPath: FileURL(path: "testing/screenshots/"), - screenshotData: [.goodData], - textData: [.goodData] - ) - - static let gapData = DeviceData( - outputSuffixes: ["iPhone X"], - templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"), - screenshotsPath: FileURL(path: "testing/screenshots/"), - screenshotData: [.goodData], - textData: [.goodData], - gapWidth: 16 - ) + static func validData(gapWidth: Int = 0) -> DeviceData { + DeviceData( + outputSuffixes: ["iPhone X"], + templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"), + screenshotsPath: FileURL(path: "testing/screenshots/"), + numberOfSlices: 4, + screenshotData: [.goodData], + textData: [.validData], + gapWidth: gapWidth + ) + } - static let invalidData = DeviceData( + static let invalidTextData = DeviceData( outputSuffixes: ["iPhone X"], templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"), screenshotsPath: FileURL(path: "testing/screenshots/"), + numberOfSlices: 4, screenshotData: [.goodData], - textData: [.invalidData] + textData: [.invalidTextBounds] ) - static let mismatchingDeviceSizeData = DeviceData( + static let invalidNumberOfSlices = DeviceData( outputSuffixes: ["iPhone X"], templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"), screenshotsPath: FileURL(path: "testing/screenshots/"), - sliceSizeOverride: DecodableSize(width: 50, height: 100), + numberOfSlices: 0, screenshotData: [.goodData], - textData: [.goodData] + textData: [.validData] ) - static let faultyMismatchingDeviceSizeData = DeviceData( + static let invalidGapWidth = DeviceData( outputSuffixes: ["iPhone X"], templateImagePath: FileURL(path: "testing/templatefile-debug_device1.png"), screenshotsPath: FileURL(path: "testing/screenshots/"), - sliceSizeOverride: DecodableSize(width: 50, height: 100), + numberOfSlices: 5, screenshotData: [.goodData], - textData: [.goodData], - gapWidth: 2 + textData: [.validData], + gapWidth: -10 ) } diff --git a/Tests/SwiftFrameTests/Utility/StringFilesContainer.swift b/Tests/SwiftFrameTests/Utility/StringFilesContainer.swift index 0c5d9bf..909d9de 100644 --- a/Tests/SwiftFrameTests/Utility/StringFilesContainer.swift +++ b/Tests/SwiftFrameTests/Utility/StringFilesContainer.swift @@ -3,6 +3,5 @@ import Foundation struct StringFilesContainer { static let goodData = ["\"someID\"": "\"Some interesting title\""] - static let wrongKeyData = ["\"someIdentifier\"": "\"Some interesting title\""] } diff --git a/Tests/SwiftFrameTests/Utility/TestHelpers.swift b/Tests/SwiftFrameTests/Utility/TestHelpers.swift index 2c0eb29..a68adf6 100644 --- a/Tests/SwiftFrameTests/Utility/TestHelpers.swift +++ b/Tests/SwiftFrameTests/Utility/TestHelpers.swift @@ -40,12 +40,3 @@ extension CGContext { } } - -/// Since `XCTUnwrap` is currently unavailable when calling `swift test` from the command line, we use a custom wrapper -/// See https://bugs.swift.org/browse/SR-11501 -func ky_unwrap(_ value: T?) throws -> T { - guard let value = value else { - throw NSError(description: "Value of type \(T.self) was nil") - } - return value -} diff --git a/Tests/SwiftFrameTests/Utility/TestingUtility.swift b/Tests/SwiftFrameTests/Utility/TestingUtility.swift index 38c6b01..feee375 100644 --- a/Tests/SwiftFrameTests/Utility/TestingUtility.swift +++ b/Tests/SwiftFrameTests/Utility/TestingUtility.swift @@ -1,7 +1,7 @@ import Foundation @testable import SwiftFrameCore -public typealias JSONDictionary = [String: Encodable] +typealias JSONDictionary = [String: Encodable] struct TestingUtility { diff --git a/Tests/SwiftFrameTests/Utility/TextDataFixtures.swift b/Tests/SwiftFrameTests/Utility/TextDataFixtures.swift index b38070c..abb94aa 100644 --- a/Tests/SwiftFrameTests/Utility/TextDataFixtures.swift +++ b/Tests/SwiftFrameTests/Utility/TextDataFixtures.swift @@ -4,8 +4,8 @@ import Foundation extension TextData { - static let goodData = TextData( - titleIdentifier: "someId", + static let validData = TextData( + titleIdentifier: "someID", textAlignment: TextAlignment(horizontal: .center, vertical: .top), maxFontSizeOverride: nil, fontOverride: nil, @@ -13,10 +13,11 @@ extension TextData { groupIdentifier: nil, topLeft: Point(x: 10, y: 5), bottomRight: Point(x: 15, y: 20), - textColorOverride: nil) + textColorOverride: nil + ) - static let invalidData = TextData( - titleIdentifier: "someId", + static let invalidTextBounds = TextData( + titleIdentifier: "someID", textAlignment: TextAlignment(horizontal: .center, vertical: .top), maxFontSizeOverride: nil, fontOverride: nil, @@ -24,17 +25,7 @@ extension TextData { groupIdentifier: nil, topLeft: Point(x: 10, y: 5), bottomRight: Point(x: 8, y: 20), - textColorOverride: nil) - - static let invertedData = TextData( - titleIdentifier: "someId", - textAlignment: TextAlignment(horizontal: .center, vertical: .top), - maxFontSizeOverride: nil, - fontOverride: nil, - textColorOverrideString: nil, - groupIdentifier: nil, - topLeft: Point(x: 10, y: 20), - bottomRight: Point(x: 15, y: 5), - textColorOverride: nil) + textColorOverride: nil + ) } diff --git a/Tests/SwiftFrameTests/Utility/TextGroupFixtures.swift b/Tests/SwiftFrameTests/Utility/TextGroupFixtures.swift index b93989e..0c04a67 100644 --- a/Tests/SwiftFrameTests/Utility/TextGroupFixtures.swift +++ b/Tests/SwiftFrameTests/Utility/TextGroupFixtures.swift @@ -4,6 +4,6 @@ import Foundation extension TextGroup { - static let goodData = TextGroup(identifier: "SomeID", maxFontSize: 200.00) + static let goodData = TextGroup(identifier: "someID", maxFontSize: 200.00) } diff --git a/benchmark.py b/benchmark.py index 5d8b560..873ee47 100755 --- a/benchmark.py +++ b/benchmark.py @@ -4,7 +4,7 @@ import subprocess import sys import time -from distutils.dir_util import copy_tree +from shutil import copytree from os import path from pathlib import Path from shutil import copy2, rmtree @@ -34,7 +34,7 @@ def exit_handler(): # Copy template files TEMPLATE_FILES_FOLDER = "Example/Template Files/" -copy_tree(TEMPLATE_FILES_FOLDER, "Benchmark/Template Files/") +copytree(TEMPLATE_FILES_FOLDER, "Benchmark/Template Files/") # Copying over screenshots and titles for locale in LOCALES: @@ -42,7 +42,7 @@ def exit_handler(): for device in ["iPhone X", "iPad Pro"]: screenshot_source_folder = f"Example/Screenshots/{device}/en" screenshot_target_folder = f"Benchmark/Screenshots/{device}/{locale}" - copy_tree(screenshot_source_folder, screenshot_target_folder) + copytree(screenshot_source_folder, screenshot_target_folder) STRING_SOURCE_FILE = "Example/Strings/en.strings" STRING_DESTINATION_FILE = f"Benchmark/Strings/{locale}.strings" @@ -56,7 +56,7 @@ def exit_handler(): compile_process = subprocess.run("swift build -c release", shell=True, check=True) if compile_process.returncode != 0: - exit(compile_process.returncode) + sys.exit(compile_process.returncode) # Running benchmark benchmark_start = time.time() diff --git a/install.sh b/install.sh index 522e2bb..505cef3 100755 --- a/install.sh +++ b/install.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -e