Categories
Swift Swift Package

Running tests in Swift package with GitHub actions

Some time ago I published a tiny Swift package IndexedDataStore which tackles a problem of storing data blobs on disk. It could the image data or anything else. When working with Swift packages then it is extremely easy to build and run tests on macOS but when we want to build and run tests on iOS simulator then we need to drop using swift build and swift test commands. Fortunately xcodebuild can help here and we can build and run tests without generating a project file ourselves with swift package generate-xcodeproj. Something I would like to mention about the generate-xcodeproj command is that it generates a project which does not contain resource files. The aim of this blog post is to configure a GitHub action which runs unit-tests both on macOS and iOS.

GitHub actions need to be in the repostory’s .github/workflows folder. GitHub workflows are defined in YAML files which need to be in that folder. Let’s jump into it and start creating a worflow which runs tests on macOS and iOS. GitHub workflows must define a name, in our case, name: CI. Secondly we’ll need to define when the workflow should be triggered. Running the workflow whenever pull request is created of when something was pushed to main branch will suffice.

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches:
      - '*'

The next step is creating a job and defining a number of steps. Jobs require a name, then which resource it needs to use. The first step is the repository checkout followed by running swiftlint which is already part of the enviornment. The linting step uses the default configuration file .swiftlint.yml at the repository root. At this point we are ready to build and run tests.

jobs:
  unit_tests:
    runs-on: macos-latest
    steps:
    - name: Repository checkout
      uses: actions/checkout@v2
    - name: Lint
      run: swiftlint

The simplest way to build and test Swift packages is to use swift build and swift test commands which builds and runs tests on macOS. Unfortunately there is not a way to use those commands and setting the deplyoment to iOS. Therefore we’ll need to use xcodebuild command instead. It will know how to deal with Swift packages and therefore we only need to define scheme and destination arguments. It also makes sense to separate building (build-for-testing) from running tests (test-without-building) as it makes it easier to see where a failure happened. We can make the xcodebuild output a little bit cleaner by using xcpretty. In addition it is also reasonable to use set -o pipefail which makes the pipeline to use an occured error code as the final code of the whole pipeline. Finally, we’ll disable any buffering by setting NSUnbufferedIO to YES.

jobs:
  unit_tests:
    runs-on: macos-latest
    steps:
    - name: Repository checkout
      uses: actions/checkout@v2
    - name: Lint
      run: swiftlint
    - name: Build for macOS
      run: swift build -v
    - name: Run macOS tests
      run: swift test -v
    - name: Build for iOS
      run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild build-for-testing -scheme IndexedDataStore -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty
    - name: Run iOS tests
      run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -scheme IndexedDataStore -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty

This completes our workflow and we can check in the YAML file which gets then picked up by GitHub. All the actions can be see under the Actions section on GitHub.

Summary

We looked into how to add a GitHub workflow for running tests both on macOS and iOS. Although swift build does not support building the package for iOS we could still do that with xcodebuild.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS macOS Swift Xcode

Adding SwiftLint to a project in Xcode

SwiftLint is a tool for validating the style and conventions in Swift files. It is easy to integrate to a project. Minimum set up consists of installing SwiftLint and adding a build phase to your app target in Xcode. SwiftLint comes with a default list of rules, but if you would like to change the rules then it is required to create a configuration file in yaml. Let’s take a look on how to install, add build step, and use a separate configuration file with a custom set of rules.

SwiftLint is available in homebrew so the easies way is to install it with brew install swiftlint command. Next step is to add the build phase to a target. I am gonna use one of my example app which is available on GitHub: SwiftPackageAppWorkspace. This project uses a workspace and includes an app project and Swift package.

git clone https://github.com/laevandus/SwiftPackageAppWorkspace.git
open ButtonGallery.xcworkspace
view raw homebrew.sh hosted with ❤ by GitHub
Cloning example project and opening it in Xcode.

Click on the ButtonGallery project in the file navigator, then on the iOS target and build phases. Will use the + button for adding a new run script phase. Note that we already use the config argument for letting SwiftLint know where the config file exists (by default SwiftLint looks for .swiftlint.yml file in the same folder the project file is).

if which swiftlint >/dev/null; then
# Relative path from the .xcodeproj which contains this script
swiftlint lint –config ../swiftlint.yml
else
echo "warning: SwiftLint not installed"
fi
view raw LintSources.sh hosted with ❤ by GitHub
Build phase calling swiftlint with custom configuration file in one folder up from the .xcodeproj.
Build phase which triggers swiftlint with custom configuration.

Last step is to add a custom configuration file to the repository checkout. We’ll add it to the checkout’s root folder which is the parent folder of the ButtonGallery.xcodeproj. I have went through the full list of rules available for SwiftLint and picked the ones which match with my coding style. SwiftLint has a list of default rules. The list of evaluated rules can be expanded with opt_in_rules and rules from default list can be disabled with disabled_rules list. Also I prefer to have else on a newline so I added statement_position configuration with statement_mode: uncuddled_else. Included defines a list of folder paths relative to the .xcodeproj calling swiftlint.

included:
– ../ButtonKit
– ../ButtonGallery
disabled_rules:
– compiler_protocol_init
– cyclomatic_complexity
– file_length
– force_cast
– function_body_length
– function_parameter_count
– identifier_name
– multiple_closures_with_trailing_closure
– notification_center_detachment
– line_length
– trailing_whitespace
– type_body_length
– type_name
– todo
opt_in_rules:
– anyobject_protocol
– array_init
– closure_end_indentation
– closure_spacing
– collection_alignment
– contains_over_filter_count
– contains_over_filter_is_empty
– contains_over_first_not_nil
– contains_over_range_nil_comparison
– convenience_type
– discouraged_object_literal
– discouraged_optional_boolean
– empty_collection_literal
– empty_count
– empty_string
– empty_xctest_method
– enum_case_associated_values_count
– expiring_todo
– explicit_init
– fallthrough
– fatal_error_message
– file_name_no_space
– first_where
– flatmap_over_map_reduce
– identical_operands
– joined_default_parameter
– last_where
– legacy_multiple
– legacy_random
– literal_expression_end_indentation
– lower_acl_than_parent
# useful for frameworks with public interface – missing_docs
– multiline_function_chains
– multiline_parameters
– multiline_parameters_brackets
– nslocalizedstring_key
– operator_usage_whitespace
– optional_enum_case_matching
– overridden_super_call
– override_in_extension
– pattern_matching_keywords
– prefer_self_type_over_type_of_self
– prefer_zero_over_explicit_init
– private_outlet
– prohibited_super_call
– reduce_into
– redundant_nil_coalescing
– redundant_type_annotation
– single_test_class
– sorted_first_last
– sorted_imports
– static_operator
– toggle_bool
– unneeded_parentheses_in_closure_argument
– unused_declaration
– unused_import
– vertical_parameter_alignment_on_call
– vertical_whitespace_closing_braces
# if {
# }
# else {
# }
statement_position:
statement_mode: uncuddled_else
view raw swiftlint.yml hosted with ❤ by GitHub
Custom configuration file for SwiftLint.

The next time the target is built it will run SwiftLint with custom configuration and show warnings and/or errors in Xcode.

Warnings generated by SwiftLint in the example project.

Summary

SwiftLint is easy to set up and helps to keep the code style consistent in projects. In additional to style SwiftLint is capable of suggesting different coding conventions like use enum instead of struct with only static functions. For making it easy to add custom configuration to new project I have set up a command alias in ~/.zshrc which looks like this: alias xcode_lint_add='cp ~/Dev/swiftlint.yml swiftlint.yml && mate swiftlint.yml' Run xcode_lint_add in the root of the cloned project.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.