From 171cc53aec846e3ef2091812f22e3604cc0eb8b9 Mon Sep 17 00:00:00 2001 From: Connor Ricks <13373737+connor-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:18:47 +0200 Subject: [PATCH 1/8] Prepare for open-source. --- .github/FUNDING.yml | 13 + .github/workflows/checks.yaml | 50 ++++ .gitignore | 97 +------- .swiftlint.yml | 130 ++++++++++ CODE_OF_CONDUCT.md | 128 ++++++++++ CONTRIBUTING.md | 93 ++++++++ Makefile | 51 ++++ Package.swift | 14 +- Package@swift-6.0.swift | 28 +++ README.md | 224 ++++++++++++++++++ Sources/Workflows/AnyWorkflow.swift | 20 +- Sources/Workflows/CachedWorkflow.swift | 98 ++++++++ Sources/Workflows/Result+Catching.swift | 11 + Sources/Workflows/SequenceWorkflow.swift | 41 ++++ Sources/Workflows/TaskWorkflow.swift | 23 -- Sources/Workflows/TupleWorkflow.swift | 112 --------- Sources/Workflows/Workflow.swift | 51 ++-- Sources/Workflows/ZipWorkflow.swift | 70 ++++++ Tests/Workflows.xctestplan | 24 ++ Tests/WorkflowsTests/AnyWorkflowTests.swift | 37 --- Tests/WorkflowsTests/ChainWorkflowTests.swift | 88 ------- .../Helpers/BlockWorkflow.swift | 14 -- .../WorkflowsTests/Helpers/Clock+Yield.swift | 11 + Tests/WorkflowsTests/Helpers/TestError.swift | 12 +- .../Helpers/ThrowingWorkflow.swift | 29 --- Tests/WorkflowsTests/Helpers/Trace.swift | 44 ++++ .../Helpers/ValueWorkflow.swift | 76 +++++- .../SequenceWorkflowTests.swift | 76 ++++++ Tests/WorkflowsTests/WorkflowTests.swift | 29 --- Tests/WorkflowsTests/WorkflowsTests.swift | 6 - Tests/WorkflowsTests/ZipWorkflowTests.swift | 149 +++++------- .../contents.xcworkspacedata | 14 ++ .../xcshareddata/IDETemplateMacros.plist | 29 +++ .../xcshareddata/swiftpm/Package.resolved | 33 +++ .../xcshareddata/xcschemes/Workflows.xcscheme | 21 +- codecov.yaml | 8 + 36 files changed, 1372 insertions(+), 582 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/checks.yaml create mode 100644 .swiftlint.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 Package@swift-6.0.swift create mode 100644 README.md create mode 100644 Sources/Workflows/CachedWorkflow.swift create mode 100644 Sources/Workflows/Result+Catching.swift create mode 100644 Sources/Workflows/SequenceWorkflow.swift delete mode 100644 Sources/Workflows/TaskWorkflow.swift delete mode 100644 Sources/Workflows/TupleWorkflow.swift create mode 100644 Sources/Workflows/ZipWorkflow.swift create mode 100644 Tests/Workflows.xctestplan delete mode 100644 Tests/WorkflowsTests/AnyWorkflowTests.swift delete mode 100644 Tests/WorkflowsTests/ChainWorkflowTests.swift delete mode 100644 Tests/WorkflowsTests/Helpers/BlockWorkflow.swift create mode 100644 Tests/WorkflowsTests/Helpers/Clock+Yield.swift delete mode 100644 Tests/WorkflowsTests/Helpers/ThrowingWorkflow.swift create mode 100644 Tests/WorkflowsTests/Helpers/Trace.swift create mode 100644 Tests/WorkflowsTests/SequenceWorkflowTests.swift delete mode 100644 Tests/WorkflowsTests/WorkflowTests.swift delete mode 100644 Tests/WorkflowsTests/WorkflowsTests.swift create mode 100644 Workflows.xcworkspace/contents.xcworkspacedata create mode 100644 Workflows.xcworkspace/xcshareddata/IDETemplateMacros.plist create mode 100644 Workflows.xcworkspace/xcshareddata/swiftpm/Package.resolved rename {.swiftpm/xcode => Workflows.xcworkspace}/xcshareddata/xcschemes/Workflows.xcscheme (81%) create mode 100644 codecov.yaml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..97d2425 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: ['https://cash.app/$zanchee'] diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..b2d61d0 --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,50 @@ +name: ๐Ÿšจ Checks + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: ๐Ÿ“ Lint + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ—„๏ธ Checkout Code + uses: actions/checkout@v4 + - name: ๐Ÿ“ SwiftLint + uses: stanfordbdhg/action-swiftlint@v4 + with: + args: --strict + test: + name: ๐Ÿงช Test + runs-on: macos-latest + strategy: + matrix: + platform: [IOS, MACOS, MAC_CATALYST, TVOS, WATCHOS, VISIONOS] + steps: + - name: ๐Ÿ‘€ Install visionOS runtime + if: matrix.platform == 'visionOS' + run: | + sudo xcodebuild -runFirstLaunch + sudo xcrun simctl list + sudo xcodebuild -downloadPlatform visionOS + sudo xcodebuild -runFirstLaunch + - name: ๐Ÿ—„๏ธ Checkout Code + uses: actions/checkout@v4 + - name: ๐Ÿงฐ Select Xcode Version + run: xcodes select 16 + - name: ๐Ÿงช Run tests + run: make XCODEBUILD_ARGUMENT=test CONFIG=Debug PLATFORM='${{ matrix.platform }}' + - name: ๐Ÿ“Š Upload Coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + swift: true diff --git a/.gitignore b/.gitignore index 330d167..1273b5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,7 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -# Package.resolved -# *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - -.build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ +.DS_Store +.build +.swiftpm +/Packages +/*.swiftinterface +/*.xcodeproj +xcuserdata/ \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..1794084 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,130 @@ +# SwiftLint configuration file for the Storybook repository. +# Run `swiftlint --fix` at the root of the repository to format all code. + +# MARK: - Settings + +#included: +# - *.swift +excluded: + - "**/.build" + +# If true, SwiftLint will not fail if no lintable files are found. +allow_zero_lintable_files: false + +# If true, SwiftLint will treat all warnings as errors. +strict: false + +# If true, SwiftLint will check for updates after linting or analyzing. +check_for_updates: true + +# reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) +reporter: "xcode" + +# MARK: - Rules + +disabled_rules: + - class_delegate_protocol + - cyclomatic_complexity + - discouraged_direct_init + - duplicate_enum_cases + - function_parameter_count + - inclusive_language + - large_tuple + - line_length + - multiple_closures_with_trailing_closure + - nesting + - non_optional_string_data_conversion + - notification_center_detachment + - shorthand_operator + - todo + - type_body_length + - void_function_in_ternary + +opt_in_rules: + - closure_end_indentation + - closure_spacing + - collection_alignment + - comma_inheritance + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discarded_notification_center_observer + - discouraged_none_name + - discouraged_object_literal + - discouraged_optional_boolean + - discouraged_optional_collection + - empty_collection_literal + - empty_count + - empty_string + - explicit_init + - fatal_error_message + - file_name_no_space + - first_where + - identical_operands + - implicit_return + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - number_separator + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - prefer_self_in_static_references + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_swiftui_state + - raw_value_for_camel_cased_codable_enum + - redundant_nil_coalescing + - return_value_from_void_function + - shorthand_optional_binding + - sorted_enum_cases + - sorted_first_last + - sorted_imports + - toggle_bool + - unhandled_throwing_task + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - yoda_condition + +analyzer_rules: + - unused_declaration + - unused_import + +# MARK: - Config + +file_length: + warning: 750 + error: 1500 + ignore_comment_only_lines: true +force_cast: + severity: warning +force_try: + severity: warning +function_body_length: + warning: 100 + error: 200 +identifier_name: + min_length: 0 + max_length: 60 +opening_brace: + - ignore_multiline_statement_conditions +trailing_comma: + mandatory_comma: true +type_name: + min_length: 0 +unused_declaration: + severity: warning diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fbf7d5a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +swift-money+connor.ricks@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bb171f5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing to `swift-money` + +First off, thanks for taking the time to contribute! โค๏ธ + +All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. ๐ŸŽ‰ + +> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: +> - Star the project +> - Tweet about it +> - Refer this project in your project's README +> - Mention the project at local meetups and tell your friends/colleagues + +## Table of Contents + +- [I Have a Question](#i-have-a-question) +- [I Want To Contribute](#i-want-to-contribute) +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Enhancements](#suggesting-enhancements) +- [Your First Code Contribution](#your-first-code-contribution) +- [Improving The Documentation](#improving-the-documentation) +- [Styleguides](#styleguides) +- [Commit Messages](#commit-messages) +- [Join The Project Team](#join-the-project-team) + +## I Have a Question + +> If you want to ask a question, we assume that you have read the available documentation. + +Before you ask a question, it is best to search for existing [Issues](https://github.com/connor-ricks/swift-money/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. + +If you then still feel the need to ask a question and need clarification, we recommend the following: + +- Open an [Issue](https://github.com/connor-ricks/swift-money/issues/new). +- Provide as much context as you can about what you're running into. +- Provide project and platform versions (iOS, tvOS, etc...), depending on what seems relevant. + +We will then take care of the issue as soon as possible. + +## I Want To Contribute + +> ### Legal Notice +> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence. + +### Reporting Bugs + +#### Before Submitting a Bug Report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. + +- Make sure that you are using the latest version. +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the documentation. If you are looking for support, you might want to check [this section](#i-have-a-question)). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [issues](https://github.com/connor-ricks/swift-money/issues). +- Collect information about the bug: +- Stack trace (Traceback) +- OS, Platform and Version (iOS, macOS, tvOS, visionOS, watchOS) +- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. +- Possibly your input and the output +- Can you reliably reproduce the issue? And can you also reproduce it with older versions? + +#### How Do I Submit a Good Bug Report? + +> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to `swift-money+connor.ricks@gmail.com`. + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [Issue](https://github.com/connor-ricks/swift-money/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) +- Explain the behaviour you would expect and the actual behaviour. +- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. +- Provide the information you collected in the previous section. + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for swift-money, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. + +#### Before Submitting an Enhancement + +- Make sure that you are using the latest version. +- Read the documentation carefully and find out if the functionality is already covered, maybe by an individual configuration. +- Perform a [search](https://github.com/connor-ricks/swift-money/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. + +#### How Do I Submit a Good Enhancement Suggestion? + +Enhancement suggestions are tracked as [issues](https://github.com/connor-ricks/swift-money/issues). + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behaviour** and **explain which behaviour you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. +- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +- **Explain why this enhancement would be useful** to most swift-money users. You may also want to point out the other projects that solved it better and which could serve as inspiration. + +## Attribution +This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..54ba84f --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +CONFIG = Debug + +PLATFORM_IOS = iOS Simulator,id=$(call udid_for,iOS,iPhone \d\+ Pro [^M]) +PLATFORM_MACOS = macOS +PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst +PLATFORM_TVOS = tvOS Simulator,id=$(call udid_for,tvOS,TV) +PLATFORM_VISIONOS = visionOS Simulator,id=$(call udid_for,visionOS,Vision) +PLATFORM_WATCHOS = watchOS Simulator,id=$(call udid_for,watchOS,Watch) + +PLATFORM = IOS +DESTINATION = platform="$(PLATFORM_$(PLATFORM))" + +SCHEME = Workflows + +WORKSPACE = Workflows.xcworkspace + +XCODEBUILD_ARGUMENT = test + +XCODEBUILD_FLAGS = \ + -configuration $(CONFIG) \ + -destination $(DESTINATION) \ + -scheme "$(SCHEME)" \ + -skipMacroValidation \ + -workspace $(WORKSPACE) + +XCODEBUILD_COMMAND = xcodebuild $(XCODEBUILD_ARGUMENT) $(XCODEBUILD_FLAGS) + +ifneq ($(strip $(shell which xcbeautify)),) + XCODEBUILD = set -o pipefail && $(XCODEBUILD_COMMAND) | xcbeautify --quiet +else + XCODEBUILD = $(XCODEBUILD_COMMAND) +endif + +TEST_RUNNER_CI = $(CI) + +xcodebuild: + $(XCODEBUILD) + +development: + brew install xcbeautify + brew install swiftlint + +lint: + swiftlint lint + +lint-fix: + swiftlint lint --fix + +define udid_for +$(shell xcrun simctl list devices available '$(1)' | grep '$(2)' | sort -r | head -1 | awk -F '[()]' '{ print $$(NF-3) }') +endef diff --git a/Package.swift b/Package.swift index fdd82d1..a02b5ae 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,16 @@ -// swift-tools-version: 6.0 -// The swift-tools-version declares the minimum version of Swift required to build this package. +// swift-tools-version: 5.9 import PackageDescription let package = Package( - name: "Workflows", + name: "swift-workflows", platforms: [ - .macOS(.v14) + .iOS(.v17), + .macOS(.v14), + .macCatalyst(.v17), + .tvOS(.v17), + .watchOS(.v10), + .visionOS(.v1), ], products: [ .library(name: "Workflows", targets: ["Workflows"]), @@ -18,7 +22,7 @@ let package = Package( .target(name: "Workflows"), .testTarget(name: "WorkflowsTests", dependencies: [ .product(name: "Clocks", package: "swift-clocks"), - "Workflows" + "Workflows", ]), ] ) diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 0000000..a02b5ae --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "swift-workflows", + platforms: [ + .iOS(.v17), + .macOS(.v14), + .macCatalyst(.v17), + .tvOS(.v17), + .watchOS(.v10), + .visionOS(.v1), + ], + products: [ + .library(name: "Workflows", targets: ["Workflows"]), + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.5"), + ], + targets: [ + .target(name: "Workflows"), + .testTarget(name: "WorkflowsTests", dependencies: [ + .product(name: "Clocks", package: "swift-clocks"), + "Workflows", + ]), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd8a2e6 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# ๐Ÿšš Workflows +![Build](https://img.shields.io/github/actions/workflow/status/connor-ricks/swift-workflows/checks.yaml?logo=GitHub) +![Codecov](https://img.shields.io/codecov/c/github/connor-ricks/swift-workflows?logo=Codecov&label=codecov) +![License](https://img.shields.io/github/license/connor-ricks/swift-workflows?color=blue) + +Create reusable units of work by breaking down logic into workflows +that can be composed together to make up complex business logic. + +## Overview + +All applications have logic, some chunk of work that tackles a problem. As +applications scale, oftentimes, developers find themselves needing to abstract +some piece of logic away from its original location in order to reuse that piece +of logic elsewhere. + +Componentizing buisness logic is a powerful tool that allows developers to +compose their logical complexities through a series of components, simplifying +usage, reducing code duplication and increasing testability of smaller units of +work. + +Workflows is a tiny package that aims to provide an interface for creating, +composing and running these smaller components of buisness logic. + +## Usage + +Learn how to create and run your workflows. + +### Creating a workflow + +Let's look at a fairly common example... Authenticating our user. + + +```swift +func authenticate( + credentials: Credentials, + authenticationService: AuthenticationService, + userService: UserService, + store: TokenStore, + cache: Cache +) async throws -> User { + let response = try await authenticationService.login( + username: credentials.username, + password: credentials.password + ) + + try await store.save( + access: response.accessToken, + refresh: response.refreshToken + ) + + let user = try await userService.user(for: response.id) + try await cache.save(user: user) + + return user +} +``` + +This is a fairly simple function, but one could imagine a use-case in which +there could be a quite a bit more logic involved. Even still, there is an +oppurtunity here to breakdown this work into multiple reusable workflows. + +1. If out application was social, fetching a user given a `UUID` and caching +the user locally is probably a piece of logic we would reuse beyond our login +logic. We would likely fetch and cache users whenever we view their profile. +This is a perfect candidate for a workflow. + +```swift +struct GetUserWorkflow: Workflow { + let id: UUID + let service: UserService + let cache: Cache + + func run() async throws -> User { + if let user = try await cache.user(id: id) { + return user + } else { + let user = try await service.user(for: id) + try await cache.save(user: user) + return user + } + } +} +``` + +2. The actual credential validation and saving is another opportunity to +breakout. We may want to write tests for our authentication service and store +without having to write all the scaffolding for the user service, which could +likely be covered by its own tests. + +```swift +struct AuthenticateWorkflow: Workflow { + let credentials: Credentials + let service: AuthenticationService + let store: TokenStore + + func run() async throws -> AuthenticationResponse { + let response = try await service.login( + username: credentials.username, + password: credentials.password + ) + + try await store.save( + access: response.accessToken, + refresh: response.refreshToken + ) + + return response + } +} +``` + +Now that we have two reusable workflows, we can compose these workflows +together into a workflow that represents the initial function we wanted to +write. + +```swift +struct LoginWorkflow: Workflow { + let credentials: Credentials + let authenticationService: AuthenticationService + let userService: UserService + let store: TokenStore + let cache: Cache + + func run() async throws -> User { + try await AuthenticateWorkflow( + credentials: credentials, + service: authenticationService, + store: store + ) + .flatMap { response in + GetUserWorkflow( + id: response.id, + service: userService, + cache: cache + ) + }.run() + } +} +``` + +Now, if we ever choose to change, or update our authentication logic or user +retrieval and caching logic, we likely won't have to update our `LoginWorkflow`. + +### Workflow dependencies + +In the example above, the `LoginWorkflow` contained a child workflow called +`GetUserWorkflow`. In that use-case, the `GetUserWorkflow` had a dependency +on the output of the `AuthenticationWorkflow`. In order to chain these workflows +successfully, passing one ouptut to the other, there are two approaches. + +```swift +// 1. Simple swift syntax +let response = try await AuthenticateWorkflow(...).run() +let user = try await GetUserWorkflow(id: response.id, ...).run() +return user + +// 2. Using a `flatMap` operation. +try await AuthenticateWorkflow(...).flatMap { response in + GetUserWorkflow(id: response.id, ...) +} +.run() +``` + +### `ZipWorkflow` + +Sometimes we may have a few requests we want to fire off concurrently, waiting +on all of their responses. We can accomplish this with a `ZipWorkflow` + +```swift +let (dogs, cats, fish) = try await ZipWorkflow( + DogsWorkflow() + CatsWorkflow() + FishWorkflow() +).result() +``` + +The `result()` function will run the child workflows concurrently, returning +their output as a tuple. If any of the workflows fail, the first error will be +thrown, and remaining workflows will be cancelled. + +If you'd rather not fail after the first error, you can make use of `run()`. The +`run()` function returns a tuple of `Result` objects for the +child workflows. + +```swift +let (dogsResult, catsResult, fishResult) = try await ZipWorkflow( + DogsWorkflow() + CatsWorkflow() + FishWorkflow() +).run() +``` + +This can be useful if you want to refresh your data, but you don't mind if some +of the workflows fail. + +### `SequenceWorkflow` + +Similarly to `ZipWorkflow`, `SequenceWorkflow` takes a tuple of child workflows +to run. However, rather than running them concurrently, the workflows will be +run syncronously. This can be useful when interacting with stateful +dependencies. + +### `CachedWorkflow` + +Sometimes, you don't want to perform an expensive block of work again and again. +`CacheWorkflow` allows you to specify a block of work to run. Once the workflow +has completed, it will cache the result for subsequent runs, and return the +output. + +### `AnyWorkflow` + +When creating APIs, it can be helpful to abstract the inner workings and +complexities away from consumers, preventing breaking changes and removing +unnecessary information. + +You can use `AnyWorkflow` to erase an underlying workflow type. + +```swift +// 1. Using the initializer. +let workflow = AnyWorkflow(DogsWorkflow()) + +// 2. Using the computed property. +let workflow = DogsWorkflow.eraseToAnyWorkflow() +``` diff --git a/Sources/Workflows/AnyWorkflow.swift b/Sources/Workflows/AnyWorkflow.swift index dcd9fd0..b91fc2a 100644 --- a/Sources/Workflows/AnyWorkflow.swift +++ b/Sources/Workflows/AnyWorkflow.swift @@ -1,8 +1,10 @@ import Foundation -// MARK: - AnyWorkflow - -/// A workflow that erases the type of the provided workflow. +/// A type-erased workflow. +/// +/// Use ``AnyWorkflow`` to wrap a workflow whose type has details you don't want to expose across API +/// boundaries, such as different modules. When you use type erasure this way, you can change the +/// underlying parser over time without affecting existing clients. public struct AnyWorkflow: Workflow { // MARK: Properties @@ -11,12 +13,18 @@ public struct AnyWorkflow: Workflow { // MARK: Initializers + init(_ block: @Sendable @escaping () async throws -> Output) { + self.block = block + } + + /// Creates a type-erased workflow from the provided workflow. public init(_ workflow: W) where W.Output == Output { self.block = workflow.run } - // MARK: Run + // MARK: Workflow + /// Runs the workflow, generating an output. public func run() async throws -> Output { try await block() } @@ -24,8 +32,8 @@ public struct AnyWorkflow: Workflow { // MARK: - Workflow + AnyWorkflow -extension Workflow { - /// Wraps this workflow with a type eraser. +extension AnyWorkflow { + /// Creates a type-erased workflow from this ``Workflow``. public func eraseToAnyWorkflow() -> AnyWorkflow { AnyWorkflow(self) } diff --git a/Sources/Workflows/CachedWorkflow.swift b/Sources/Workflows/CachedWorkflow.swift new file mode 100644 index 0000000..7b65cf6 --- /dev/null +++ b/Sources/Workflows/CachedWorkflow.swift @@ -0,0 +1,98 @@ +import Foundation + +// MARK: - OutputCache + +/// A cache used to power the ``CachedWorkflow/Output`` of a ``CachedWorkflow``. +/// +/// An ``OutputCache`` stores the result of ``CachedWorkflow/run()`, +/// allowing subsequent calls to pull from the the cache, rather than re-running +/// the ``Output`` again. +/// +/// You can create your own ``OutputCache`` objects by conforming to the ``OutputCache`` +/// protocol and implementing the ``read()`` and ``save(_:)`` requirements. +/// +/// Creating your own cache can be useful for creating more complex caches. +public protocol OutputCache: Sendable { + associatedtype Output: Sendable + func read() async throws -> Result? + func save(_ result: Result) async throws +} + +// MARK: - InMemoryOutputCache + +/// An in-memory cache used to power the ``CachedWorkflow/Output`` of a ``CachedWorkflow`` +/// +/// The ``InMemoryOutputCache`` stores its output in memory, returning the result last saved. +public actor InMemoryOutputCache: OutputCache { + + // MARK: Properties + + /// The currently cached result. + private var result: Result? + + // MARK: Initailizers + + /// Creates an in-memory cache initialized with the provided result. + public init(result: Result? = nil) { + self.result = result + } + + // MARK: OutputCache + + /// Returns the saved result, if one exists, from the cache. + public func read() -> Result? { + result + } + + /// Saves the provided result to the cache. + public func save(_ result: Result) { + self.result = result + } + + /// Deletes the saved result, if one exists, from the cache. + public func delete() { + self.result = nil + } +} + +// MARK: - CachedWorkflow + +/// A workflow that performs a child workflow once, caching the child's output for subsequent runs. +public actor CachedWorkflow: Workflow { + public typealias Output = W.Output + + // MARK: Properties + + /// The cache that backs the workflow. + private var cache: any OutputCache + + /// The wrapped workflow. + private let workflow: W + + // MARK: Initializers + + /// Creates a workflow that will cache the first output for subsequent runs. + public init(workflow: W, cache: any OutputCache = InMemoryOutputCache()) { + self.workflow = workflow + self.cache = cache + } + + // MARK: Workflow + + /// Runs the workflow, generating an output. + public func run() async throws -> W.Output { + guard let result = try await cache.read() else { + let result = await Result(catching: { + let output = try await workflow.run() + try Task.checkCancellation() + return output + }) + + try await self.cache.save(result) + return try result.get() + } + + try Task.checkCancellation() + return try result.get() + } +} diff --git a/Sources/Workflows/Result+Catching.swift b/Sources/Workflows/Result+Catching.swift new file mode 100644 index 0000000..6941be4 --- /dev/null +++ b/Sources/Workflows/Result+Catching.swift @@ -0,0 +1,11 @@ +import Foundation + +extension Result { + /// Creates a new result by evaluating an async throwing closure, capturing the + /// returned value as a success, or any thrown error as a failure. + /// + /// - Parameter body: A potentially async throwing closure to evaluate. + init(catching work: @Sendable () async throws(Failure) -> Success) async { + do { self = .success(try await work()) } catch { self = .failure(error) } + } +} diff --git a/Sources/Workflows/SequenceWorkflow.swift b/Sources/Workflows/SequenceWorkflow.swift new file mode 100644 index 0000000..74833e5 --- /dev/null +++ b/Sources/Workflows/SequenceWorkflow.swift @@ -0,0 +1,41 @@ +import Foundation + +/// A workflow that performs a pack of child workflows syncronously. +public struct SequenceWorkflow: Workflow { + public typealias Output = (repeat Result<(each W).Output, Error>) + + // MARK: Properties + + /// The pack of wrapped workflows. + private let workflow: (repeat each W) + + // MARK: Initializers + + /// Creates a workflow that will run the provided workflows syncronously.. + public init(_ workflow: repeat each W) { + self.workflow = (repeat each workflow) + } + + // MARK: Workflow + + /// Runs the workflow, generating a result for each child workflow in the sequence. + public func run() async -> Output { + (repeat await Result(catching: { + try Task.checkCancellation() + return try await (each workflow).run() + })) + } + + /// Runs the workflow, generating a tuple of outputs from the sequence. + /// + /// If any of the child workflows throws an error while generating their output, the whole sequence will fail, + /// throwing the first encountered error. Remaining workflows in the sequence will not be run. + public func result() async throws -> (repeat (each W).Output) { + let cache = (repeat CachedWorkflow(workflow: each workflow)) + for cache in repeat each cache { + _ = try await cache.run() + } + + return try await (repeat (each cache).run()) + } +} diff --git a/Sources/Workflows/TaskWorkflow.swift b/Sources/Workflows/TaskWorkflow.swift deleted file mode 100644 index 6109077..0000000 --- a/Sources/Workflows/TaskWorkflow.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -/// A workflow that outputs a Task with a success value of the provided workflow. -struct TaskWorkflow: Workflow { - - // MARK: Properties - - private let block: @Sendable () async throws -> Output - - // MARK: Initializers - - init(_ workflow: W) where W.Output == Output { - self.block = workflow.run - } - - // MARK: Run - - func run() async -> Task { - Task { - try await block() - } - } -} diff --git a/Sources/Workflows/TupleWorkflow.swift b/Sources/Workflows/TupleWorkflow.swift deleted file mode 100644 index 4a9e99c..0000000 --- a/Sources/Workflows/TupleWorkflow.swift +++ /dev/null @@ -1,112 +0,0 @@ -import Foundation - -// MARK: - TupleWorkflow - -/// A workfklow that runs a tuple of child workflows. -/// -/// You can specify whether or not the workflows should execute concurrently -/// or not using the ``TupleWorkflow/shouldExecuteConcurrently`` property. -public struct TupleWorkflow: Workflow { - public typealias Output = (repeat each O) - - // MARK: Properties - - private let workflow: (repeat AnyWorkflow) - - private let shouldExecuteConcurrently: Bool - - // MARK: Initializers - - public init(_ workflow: repeat each W, shouldExecuteConcurrently: Bool) where repeat (each W).Output == each O { - self.workflow = (repeat (each workflow).eraseToAnyWorkflow()) - self.shouldExecuteConcurrently = shouldExecuteConcurrently - } - - // MARK: Workflow - - public func run() async throws -> Output { - guard shouldExecuteConcurrently else { - return try await (repeat (each workflow).run()) - } - - /// Convert the workflows into tasks. - let task = await (repeat TaskWorkflow(each workflow).run()) - - /// Create a task group to make sure that the correct error is propogated. - /// Without this task group, the workflow throws the last seen error in the collection of workflows. - /// With this task group, the parameter pack correctly throws when the first error occurs. - try await withThrowingTaskGroup(of: Void.self) { group in - for task in repeat each task { - group.addTask { _ = try await task.value } - } - - try await group.waitForAll() - } - - return try await (repeat (each task).value) - } - -// public var result: (repeat Result) { -// get async { -// guard shouldExecuteConcurrently else { -// return await (repeat (each workflow).result) -// } -// -// /// Convert the workflows into tasks. -// let task = await (repeat TaskWorkflow(each workflow).run()) -// -// /// Create a task group to make sure that the correct error is propogated. -// /// Without this task group, the workflow throws the last seen error in the collection of workflows. -// /// With this task group, the parameter pack correctly throws when the first error occurs. -// await withTaskGroup(of: Void.self) { group in -// for task in repeat each task { -// group.addTask { _ = await task.result } -// } -// -// await group.waitForAll() -// } -// -// return await (repeat (each task).result) -// } -// } -} - -// swiftlint:disable identifier_name - -// MARK: - ZipWorkflow - -/// A workflow that performs a series of child workflows concurrently. -public func ZipWorkflow( - _ workflow: repeat each W -) -> TupleWorkflow { - TupleWorkflow(repeat each workflow, shouldExecuteConcurrently: true) -} - -// MARK: - ChainWorkflow - -/// A workflow that runs a series of child workflows syncronously. -public func ChainWorkflow( - _ workflow: repeat each W -) -> TupleWorkflow { - TupleWorkflow(repeat each workflow, shouldExecuteConcurrently: false) -} - -// swiftlint:enable identifier_name - -// MARK: - TupleWorkflow + Helpers - -//extension TupleWorkflow { -// -// // /// Creates a new workflow that transforms the output of this workflow using the provided transform. -// // public func map(_ transform: @escaping (Output) async throws -> U) async rethrows -> some Workflow { -// // BlockWorkflow { -// // try await transform(run()) -// // }.eraseToAnyWorkflow() -// // } -// -// public func append(_ other: W) async throws -> TupleWorkflow { -// TupleWorkflow( -// repeat each workflow, other, shouldExecuteConcurrently: shouldExecuteConcurrently -// ) -// } -//} diff --git a/Sources/Workflows/Workflow.swift b/Sources/Workflows/Workflow.swift index 5afc0c8..0c29cff 100644 --- a/Sources/Workflows/Workflow.swift +++ b/Sources/Workflows/Workflow.swift @@ -2,31 +2,48 @@ import Foundation // MARK: - Workflow +/// A workflow represents a block of work that is run and generates an output. +/// +/// Workflows can be useful to encapsulate and create componentized pieces of reusable business logic. +/// +/// Below we define a workflow that can be used to load dogs from a cache or api. +/// ```swift +/// struct DogsWorkflow: Workflow { +/// let service: DogsService +/// let cache: DogsCache +/// +/// func run() async throws -> some Sendable { +/// if cache.isExpired { +/// let dogs = try await service.fetchDogs() +/// cache.save(dogs) +/// return dogs +/// } else { +/// return cache.get() +/// } +/// } +/// } +/// ``` +/// Applications often contain places in which specific logic needs to be repeated and reused. +/// Workflows can help you organize that logic be creating smaller more reusable chunks of work that +/// are more easily reused and easier to test. public protocol Workflow: Sendable { associatedtype Output: Sendable - func run() async throws -> Output + /// Runs the workflow, generating an output. + @Sendable func run() async throws -> Output } -// MARK: - Workflow + Helpers +// MARK: - Workflow + Map extension Workflow { - var result: Result { - get async { - await Result { - try await run() - } - } + /// Returns a workflow that transforms the output of this workflow into a new output. + public func map(_ transform: @Sendable @escaping (Output) throws -> U) rethrows -> AnyWorkflow { + AnyWorkflow { try await transform(self.run()) } } -} - -// MARK: - Result + Async Catching -extension Result { - init(catching work: () async throws(Failure) -> Success) async { - do { - self = .success(try await work()) - } catch { - self = .failure(error) + /// Returns a workflow that transforms the ouput of this workflow into a new workflow. + public func flatMap(_ transform: @Sendable @escaping (Output) throws -> W) rethrows -> AnyWorkflow { + AnyWorkflow { + try await transform(self.run()).run() } } } diff --git a/Sources/Workflows/ZipWorkflow.swift b/Sources/Workflows/ZipWorkflow.swift new file mode 100644 index 0000000..65fcf93 --- /dev/null +++ b/Sources/Workflows/ZipWorkflow.swift @@ -0,0 +1,70 @@ +import Foundation + +/// A workflow that performs a pack of child workflows concurrently. +public struct ZipWorkflow: Workflow { + public typealias Output = (repeat Result<(each W).Output, Error>) + + // MARK: Properties + + /// The pack of wrapped workflows. + private let workflow: (repeat each W) + + // MARK: Initializers + + /// Creates a workflow that will run the provided workflows concurrently. + public init(_ workflow: repeat each W) { + self.workflow = (repeat each workflow) + } + + // MARK: Workflow + + /// Runs the workflow, generating a result for each child workflow in the zip. + public func run() async -> Output { + /// Wrap all the child workflows in a cache. + let cache = (repeat CachedWorkflow(workflow: each workflow)) + + await withTaskGroup(of: Void.self) { group in + /// Run each cache workflow. + for cache in repeat each cache { + group.addTask { _ = try? await cache.run() } + } + + /// Wait for all the cache workflows to finish. + await group.waitForAll() + } + + /// Re-run each cache inside a result to convert to the cached output to a result. + return (repeat await Result(catching: { try await (each cache).run() })) + } + + /// Runs the workflow, generating a tuple of outputs from the zip. + /// + /// If any of the child workflows throws an error while generating their output, the whole zip will fail, + /// throwing the first encountered error. Any remaining workflows in the zip will also be cancelled. + public func result() async throws -> (repeat (each W).Output) { + /// Wrap all the child workflows in a cache. + let cache = (repeat CachedWorkflow(workflow: each workflow)) + + try await withThrowingTaskGroup(of: Void.self) { group in + /// Run each cache workflow. + for cache in repeat each cache { + group.addTask { + try Task.checkCancellation() + _ = try await cache.run() + } + } + + while !group.isEmpty { + do { try await group.next() } + /// If any of the cache workflows throws an error, cancel all + /// remaining workflows and throw the error. + catch { group.cancelAll(); throw error } + } + } + + try Task.checkCancellation() + + /// Re-run each cache inside a result to convert to the cached output to a result. + return try await (repeat (each cache).run()) + } +} diff --git a/Tests/Workflows.xctestplan b/Tests/Workflows.xctestplan new file mode 100644 index 0000000..02d9c7f --- /dev/null +++ b/Tests/Workflows.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "933F3B80-E953-4778-9E47-AFEFAE23B9F9", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "WorkflowsTests", + "name" : "WorkflowsTests" + } + } + ], + "version" : 1 +} diff --git a/Tests/WorkflowsTests/AnyWorkflowTests.swift b/Tests/WorkflowsTests/AnyWorkflowTests.swift deleted file mode 100644 index 7955431..0000000 --- a/Tests/WorkflowsTests/AnyWorkflowTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -//import Clocks -//@testable import StorybookWorkflow -//import Testing -// -//@Suite("AnyWorkflow Tests") struct AnyWorkflowTests { -// @Test func testAnyWorkflow_whenRun_producesExpectedOutput() async throws { -// let string = try await AnyWorkflow( -// ValueWorkflow(value: "foo") -// ).run() -// #expect(string == "foo") -// } -// -// @Test func testAnyWorkflow_whenRunThrows_throwsExpectedError() async throws { -// let expected = TestError() -// await #expect(throws: expected) { -// try await AnyWorkflow( -// ThrowingWorkflow(error: expected) -// ).run() -// } -// } -// -// @Test func testAnyWorkflowConvenience_whenRun_producesExpectedOutput() async throws { -// let string = try await ValueWorkflow(value: "foo") -// .eraseToAnyWorkflow() -// .run() -// #expect(string == "foo") -// } -// -// @Test func testAnyWorkflowConvenience_whenRunThrows_throwsExpectedError() async throws { -// let expected = TestError() -// await #expect(throws: expected) { -// try await ThrowingWorkflow(error: expected) -// .eraseToAnyWorkflow() -// .run() -// } -// } -//} diff --git a/Tests/WorkflowsTests/ChainWorkflowTests.swift b/Tests/WorkflowsTests/ChainWorkflowTests.swift deleted file mode 100644 index 8386162..0000000 --- a/Tests/WorkflowsTests/ChainWorkflowTests.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Clocks -@testable import Workflows -import Testing - -@Suite("ChainWorkflow Tests") struct ChainWorkflowTests { - @Test func testChainWorkflow_whenRun_producesExpectedOutput() async throws { - let (string, int, bool) = try await ChainWorkflow( - ValueWorkflow(value: "foo"), - ValueWorkflow(value: 1), - ValueWorkflow(value: true) - ).run() - - #expect(string == "foo") - #expect(int == 1) - #expect(bool == true) - } - - @Test func testChainWorkflow_whenRunFirstWorkflowThrows_throwsExpectedError() async throws { - let expected = TestError(value: 1) - await #expect(throws: expected) { - try await ChainWorkflow( - ThrowingWorkflow(error: expected), - ValueWorkflow(value: "foo") - ).run() - } - } - - @Test func testChainWorkflow_whenRunLastWorkflowThrows_throwsExpectedError() async throws { - let expected = TestError() - await #expect(throws: expected) { - try await ChainWorkflow( - ValueWorkflow(value: 1), - ThrowingWorkflow(error: expected) - ).run() - } - } - - @Test func testChainWorkflow_whenRunMultipleWorkflowsThrow_throwsFirstSyncronousOccurringError() async throws { - let clock = TestClock() - let expected = TestError(value: "foo") - let unexpected = TestError(value: "bar") - - do { - async let output = try await ChainWorkflow( - ValueWorkflow(value: 1, delay: .seconds(4), clock: clock), - ThrowingWorkflow(error: expected, delay: .seconds(3), clock: clock), - ValueWorkflow(value: 2, delay: .seconds(2), clock: clock), - ThrowingWorkflow(error: unexpected, delay: .seconds(1), clock: clock) - ).run() - - await clock.run() - _ = try await output - Issue.record("ChainWorkflow should have thrown an error.") - } catch { - #expect(error as? TestError == expected) - } - } - - @Test func testChainWorkflow_whenRun_executesWorkSyncronously() async throws { - let clock = TestClock() - - class Trace: @unchecked Sendable { - var array: [String] = [] - } - - let trace = Trace() - - async let output = try await ChainWorkflow( - BlockWorkflow { - try await clock.sleep(for: .seconds(5)) - trace.array.append("foo") - return "foo" - }, - BlockWorkflow { - try await clock.sleep(for: .seconds(2)) - trace.array.append("bar") - return "bar" - } - ).run() - - await clock.run() - - let (first, second) = try await output - #expect(first == "foo") - #expect(second == "bar") - #expect(trace.array == ["foo", "bar"]) - } -} diff --git a/Tests/WorkflowsTests/Helpers/BlockWorkflow.swift b/Tests/WorkflowsTests/Helpers/BlockWorkflow.swift deleted file mode 100644 index 0e35cad..0000000 --- a/Tests/WorkflowsTests/Helpers/BlockWorkflow.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation -@testable import Workflows - -struct BlockWorkflow: Workflow { - let block: @Sendable () async throws -> Value - - init(_ block: @Sendable @escaping () async throws -> Value) { - self.block = block - } - - func run() async throws -> Value { - try await block() - } -} diff --git a/Tests/WorkflowsTests/Helpers/Clock+Yield.swift b/Tests/WorkflowsTests/Helpers/Clock+Yield.swift new file mode 100644 index 0000000..bdbac18 --- /dev/null +++ b/Tests/WorkflowsTests/Helpers/Clock+Yield.swift @@ -0,0 +1,11 @@ +import Clocks + +extension TestClock { + func run(afterYield: Bool) async { + if afterYield { + await Task.yield() + } + + await run() + } +} diff --git a/Tests/WorkflowsTests/Helpers/TestError.swift b/Tests/WorkflowsTests/Helpers/TestError.swift index 485983b..9d46bef 100644 --- a/Tests/WorkflowsTests/Helpers/TestError.swift +++ b/Tests/WorkflowsTests/Helpers/TestError.swift @@ -1,13 +1,9 @@ import Foundation -struct TestError: Error, Equatable { - let value: Value +/// An error constructed from a given value. Useful in tests for validating the expected error is thrown. +struct TestError: Error, Hashable { - init() where Value == Int { - self.init(value: 1) - } + // MARK: Properties - init(value: Value) { - self.value = value - } + let value: Value } diff --git a/Tests/WorkflowsTests/Helpers/ThrowingWorkflow.swift b/Tests/WorkflowsTests/Helpers/ThrowingWorkflow.swift deleted file mode 100644 index 279f437..0000000 --- a/Tests/WorkflowsTests/Helpers/ThrowingWorkflow.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Clocks -import Foundation -@testable import Workflows - -struct ThrowingWorkflow: Workflow { - let error: TestError - let delay: Duration - let clock: any Clock - - init(error: TestError, delay: Duration = .zero, clock: any Clock = ImmediateClock()) { - self.error = error - self.delay = delay - self.clock = clock - } - - func run() async throws -> String { - defer { print("[ INFO ] ThrowingWorkflow(error: \(error)) Completed run.") } - - print("[ INFO ] ThrowingWorkflow(error: \(error)) Starting throwing workflow run...") - if delay != .zero { - print("[ INFO ] ThrowingWorkflow(error: \(error)) Sleeping for \(delay) before throwing...") - try await clock.sleep(for: delay) - print("[ INFO ] ThrowingWorkflow(error: \(error)) Finished sleeping.") - } - - print("[ INFO ] ThrowingWorkflow(error: \(error)) Throwing error.") - throw error - } -} diff --git a/Tests/WorkflowsTests/Helpers/Trace.swift b/Tests/WorkflowsTests/Helpers/Trace.swift new file mode 100644 index 0000000..bf3e196 --- /dev/null +++ b/Tests/WorkflowsTests/Helpers/Trace.swift @@ -0,0 +1,44 @@ +import ConcurrencyExtras +import Foundation + +/// An actor that traces a sequence of values. Useful for verifying the order of workflow completions in tests. +actor Trace { + + // MARK: Properties + + private(set) var store: [AnyHashableSendable] = [] + + // MARK: Helpers + + func callAsFunction(_ value: Value) { + store.append(AnyHashableSendable(value)) + } + + static func == (lhs: Trace, rhs: [AnyHashableSendable]) async -> Bool { + await lhs.store == rhs + } + + static func == (lhs: [AnyHashableSendable], rhs: Trace) async -> Bool { + await lhs == rhs.store + } + + static func == (lhs: Trace, rhs: [Value]) async -> Bool { + await lhs.store == rhs.map(AnyHashableSendable.init) + } + + static func == (lhs: [Value], rhs: Trace) async -> Bool { + await lhs.map(AnyHashableSendable.init) == rhs.store + } +} + +extension Array where Element == AnyHashableSendable { + /// Creates an array of `AnyHashableSendable` objects with the provided parameter pack. + init(_ value: repeat each Value) { + var array: [AnyHashableSendable] = [] + for value in repeat each value { + array.append(AnyHashableSendable(value)) + } + + self = array + } +} diff --git a/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift b/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift index 53996eb..6bfd170 100644 --- a/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift +++ b/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift @@ -1,29 +1,83 @@ import Clocks import Foundation +import Testing @testable import Workflows -struct ValueWorkflow: Workflow { - let value: Value +/// Creates a workflow that outputs the provided value or throws the provided value wrapped in a `TestError`. +struct ValueWorkflow: Workflow { + + // MARK: Properties + + private let result: Result> let delay: Duration let clock: any Clock + let trace: Trace? + + // MARK: Initializers + + /// Creates a workflow that outputs the provided value. + init( + value: Value, + delay: Duration = .zero, + clock: any Clock = ImmediateClock(), + trace: Trace? = nil + ) { + self.result = .success(value) + self.delay = delay + self.clock = clock + self.trace = trace + } - init(value: Value, delay: Duration = .zero, clock: any Clock = ImmediateClock()) { - self.value = value + /// Creates a workflow that throws the provided value wrapped in a `TestError`. + init( + throwing value: Value, + delay: Duration = .zero, + clock: any Clock = ImmediateClock(), + trace: Trace? = nil + ) { + self.result = .failure(TestError(value: value)) self.delay = delay self.clock = clock + self.trace = trace } + // MARK: Workflow + func run() async throws -> Value { - defer { print("[ INFO ] ValueWorkflow(value: \(value)) Completed run.") } + defer { log("completed run.") } - print("[ INFO ] ValueWorkflow(value: \(value)) Starting value workflow run...") + log("starting run") if delay != .zero { - print("[ INFO ] ValueWorkflow(value: \(value)) Sleeping for \(delay) before returning...") - try await clock.sleep(for: delay) - print("[ INFO ] ValueWorkflow(value: \(value)) Finished sleeping.") + log("sleeping for \(delay)") + try? await clock.sleep(for: delay) + log("finished sleeping.") + } + + log("checking cancellation") + try Task.checkCancellation() + + switch result { + case .success(let value): + log("tracing value.") + await trace?(value) + log("returning value") + return value + case .failure(let error): + log("tracing value.") + await trace?(error) + log("throwing error") + throw error + } + } + + private func log(_ message: String) { + let prefix = switch result { + case .success(let value): + "[ INFO ] ValueWorkflow(value: \(value))" + case .failure(let error): + "[ INFO ] ValueWorkflow(throwing: \(error))" } - print("[ INFO ] ValueWorkflow(value: \(value)) Returning value.") - return value + print("\(prefix) \(message)") } } diff --git a/Tests/WorkflowsTests/SequenceWorkflowTests.swift b/Tests/WorkflowsTests/SequenceWorkflowTests.swift new file mode 100644 index 0000000..3c084a4 --- /dev/null +++ b/Tests/WorkflowsTests/SequenceWorkflowTests.swift @@ -0,0 +1,76 @@ +import Clocks +import ConcurrencyExtras +import Testing +@testable import Workflows + +@Suite("SequenceWorkflow Tests") struct SequenceWorkflowTests { + @Test func testSequenceWorkflow_whenRun_producesExpectedResultsSyncronously() async throws { + try await withMainSerialExecutor { + let clock = TestClock() + let trace = Trace() + + async let output = await SequenceWorkflow( + ValueWorkflow(value: 1, delay: .seconds(3), clock: clock, trace: trace), + ValueWorkflow(value: "foo", delay: .seconds(1), clock: clock, trace: trace), + ValueWorkflow(throwing: 1, delay: .seconds(4), clock: clock, trace: trace), + ValueWorkflow(value: true, delay: .seconds(2), clock: clock, trace: trace) + ).run() + + await clock.run(afterYield: true) + + let (one, two, three, four) = await output + + try #expect(one.get() == 1) + try #expect(two.get() == "foo") + #expect(throws: TestError(value: 1)) { try three.get() } + try #expect(four.get() == true) + + #expect(await trace == Array(1, "foo", TestError(value: 1), true)) + } + } + + @Test func testSequenceWorkflow_whenResultAndNoThrows_producesResultSyncronously() async throws { + try await withMainSerialExecutor { + let clock = TestClock() + let trace = Trace() + + async let output = await SequenceWorkflow( + ValueWorkflow(value: 1, delay: .seconds(3), clock: clock, trace: trace), + ValueWorkflow(value: "foo", delay: .seconds(1), clock: clock, trace: trace), + ValueWorkflow(value: true, delay: .seconds(2), clock: clock, trace: trace) + ).result() + + await clock.run(afterYield: true) + + let (one, two, three) = try await output + #expect(one == 1) + #expect(two == "foo") + #expect(three == true) + #expect(await trace == Array(1, "foo", true)) + } + } + + @Test func testSequenceWorkflow_whenResultAndThrows_producesResultSyncronouslyStoppingAfterError() async throws { + await withMainSerialExecutor { + let clock = TestClock() + let trace = Trace() + + async let output = await SequenceWorkflow( + ValueWorkflow(value: 1, delay: .seconds(2), clock: clock, trace: trace), + ValueWorkflow(throwing: 1, delay: .seconds(3), clock: clock, trace: trace), + ValueWorkflow(value: "foo", delay: .seconds(1), clock: clock, trace: trace) + ).result() + + await clock.run(afterYield: true) + + do { + _ = try await output + Issue.record("Workflow should have thrown an error.") + } catch { + #expect(error as? TestError == TestError(value: 1)) + } + + #expect(await trace == Array(1, TestError(value: 1))) + } + } +} diff --git a/Tests/WorkflowsTests/WorkflowTests.swift b/Tests/WorkflowsTests/WorkflowTests.swift deleted file mode 100644 index 5c1bdc2..0000000 --- a/Tests/WorkflowsTests/WorkflowTests.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Clocks -@testable import Workflows -import Testing - -@Suite("Workflow Tests") struct WorkflowTests { - @Test func testWorkflow_whenRun_producesExpectedOutput() async throws { - let string = try await ValueWorkflow(value: "foo").run() - #expect(string == "foo") - } - - @Test func testWorkflow_whenRunThrows_throwsExpectedError() async throws { - let expected = TestError() - await #expect(throws: expected) { - try await ThrowingWorkflow(error: expected).run() - } - } - -// @Test func testWorkflow_whenResult_producesExpectedOutput() async throws { -// let result = await ValueWorkflow(value: "foo").result -// #expect(try result.get() == "foo") -// } -// -// @Test func testWorkflow_whenResultThrows_producesExpectedOutput() async throws { -// let expected = TestError() -// await #expect(throws: expected) { -// try await ThrowingWorkflow(error: expected).result.get() -// } -// } -} diff --git a/Tests/WorkflowsTests/WorkflowsTests.swift b/Tests/WorkflowsTests/WorkflowsTests.swift deleted file mode 100644 index 4c59b96..0000000 --- a/Tests/WorkflowsTests/WorkflowsTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Testing -@testable import Workflows - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -} diff --git a/Tests/WorkflowsTests/ZipWorkflowTests.swift b/Tests/WorkflowsTests/ZipWorkflowTests.swift index a7f245c..9af16ef 100644 --- a/Tests/WorkflowsTests/ZipWorkflowTests.swift +++ b/Tests/WorkflowsTests/ZipWorkflowTests.swift @@ -1,112 +1,75 @@ import Clocks -@testable import Workflows +import ConcurrencyExtras import Testing +@testable import Workflows @Suite("ZipWorkflow Tests") struct ZipWorkflowTests { - - // MARK: - Run Tests - - @Test func testZipWorkflow_whenRun_producesExpectedOutput() async throws { - let (string, int, bool) = try await ZipWorkflow( - ValueWorkflow(value: "foo"), - ValueWorkflow(value: 1), - ValueWorkflow(value: true) - ).run() - - #expect(string == "foo") - #expect(int == 1) - #expect(bool == true) - } - - @Test func testZipWorkflow_whenRunFirstWorkflowThrows_throwsExpectedError() async throws { - let expected = TestError() - await #expect(throws: expected) { - try await ZipWorkflow( - ThrowingWorkflow(error: expected), - ValueWorkflow(value: 1) + @Test func testZipWorkflow_whenRun_producesExpectedResultsConcurrently() async throws { + try await withMainSerialExecutor { + let clock = TestClock() + let trace = Trace() + + async let output = await ZipWorkflow( + ValueWorkflow(value: 1, delay: .seconds(3), clock: clock, trace: trace), + ValueWorkflow(value: "foo", delay: .seconds(1), clock: clock, trace: trace), + ValueWorkflow(throwing: 1, delay: .seconds(4), clock: clock, trace: trace), + ValueWorkflow(value: true, delay: .seconds(2), clock: clock, trace: trace) ).run() - } - } - @Test func testZipWorkflow_whenRunLastWorkflowThrows_throwsExpectedError() async throws { - let expected = TestError() - await #expect(throws: expected) { - try await ZipWorkflow( - ValueWorkflow(value: 1), - ThrowingWorkflow(error: expected) - ).run() + await clock.run(afterYield: true) + + let (one, two, three, four) = await output + try #expect(one.get() == 1) + try #expect(two.get() == "foo") + #expect(throws: TestError(value: 1)) { try three.get() } + try #expect(four.get() == true) + #expect(await trace == Array("foo", true, 1, TestError(value: 1))) } } - @Test func testZipWorkflow_whenRunMultipleWorkflowsThrow_throwsFirstConcurrentOccurringError() async throws { - let clock = TestClock() - let expected = TestError(value: "bar") - let unexpected = TestError(value: "foo") - - do { - async let output = try await ZipWorkflow( - ValueWorkflow(value: 1, delay: .seconds(4), clock: clock), - ThrowingWorkflow(error: unexpected, delay: .seconds(3), clock: clock), - ValueWorkflow(value: 2, delay: .seconds(2), clock: clock), - ThrowingWorkflow(error: expected, delay: .seconds(1), clock: clock) - ).run() - - await clock.run() - _ = try await output - Issue.record("ZipWorkflow should have thrown an error.") - } catch { - #expect(error as? TestError == expected) + @Test func testZipWorkflow_whenResultAndNoThrows_producesResultConcurrently() async throws { + try await withMainSerialExecutor { + let clock = TestClock() + let trace = Trace() + + async let output = await ZipWorkflow( + ValueWorkflow(value: 1, delay: .seconds(3), clock: clock, trace: trace), + ValueWorkflow(value: "foo", delay: .seconds(1), clock: clock, trace: trace), + ValueWorkflow(value: true, delay: .seconds(2), clock: clock, trace: trace) + ).result() + + await clock.run(afterYield: true) + + let (one, two, three) = try await output + #expect(one == 1) + #expect(two == "foo") + #expect(three == true) + print(await trace.store) + #expect(await trace == Array("foo", true, 1)) } } - @Test func testZipWorkflow_whenRun_executesWorkConcurrently() async throws { - let clock = TestClock() + @Test func testZipWorkflow_whenResultAndThrows_producesResultConcurrentlyStoppingAfterError() async throws { + await withMainSerialExecutor { + let clock = TestClock() + let trace = Trace() - class Trace: @unchecked Sendable { - var array: [String] = [] - } + async let output = await ZipWorkflow( + ValueWorkflow(throwing: 1, delay: .seconds(2), clock: clock, trace: trace), + ValueWorkflow(value: 1, delay: .seconds(3), clock: clock, trace: trace), + ValueWorkflow(value: "foo", delay: .seconds(1), clock: clock, trace: trace) + ).result() - let trace = Trace() + await clock.run(afterYield: true) - async let output = try await ZipWorkflow( - BlockWorkflow { - try await clock.sleep(for: .seconds(5)) - trace.array.append("foo") - return "foo" - }, - BlockWorkflow { - try await clock.sleep(for: .seconds(2)) - trace.array.append("bar") - return "bar" + do { + _ = try await output + Issue.record("Workflow should have thrown an error.") + } catch { + #expect(error as? TestError == TestError(value: 1)) } - ).run() - await clock.run() - - let (first, second) = try await output - #expect(first == "foo") - #expect(second == "bar") - #expect(trace.array == ["bar", "foo"]) + #expect(await trace == Array("foo", TestError(value: 1))) + } } - - // MARK: - Result Tests - -// @Test func foo() async throws { -// let error = TestError() -// let workflow = try await ZipWorkflow( -// ThrowingWorkflow(error: error), -// ValueWorkflow(value: 1) -// ) -// .eraseToAnyWorkflow() -// .run() -// -// let lol = workflow.run() -//// .run() -// //.eraseToAnyWorkflow() -// -//// try await workflow.run() -// -//// #expect(throws: error) { try throwing.get() } -//// #expect(try value.get() == 1) -// } } diff --git a/Workflows.xcworkspace/contents.xcworkspacedata b/Workflows.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..6521258 --- /dev/null +++ b/Workflows.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/Workflows.xcworkspace/xcshareddata/IDETemplateMacros.plist b/Workflows.xcworkspace/xcshareddata/IDETemplateMacros.plist new file mode 100644 index 0000000..9ee88f2 --- /dev/null +++ b/Workflows.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -0,0 +1,29 @@ + + + + + FILEHEADER + +// MIT License +// +// Copyright (c) ___YEAR___ Connor Ricks +// +// 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. + + \ No newline at end of file diff --git a/Workflows.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Workflows.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..269e2cb --- /dev/null +++ b/Workflows.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,33 @@ +{ + "originHash" : "c406474c6956a5948ee793fd10e893f25597c794a12403be084f076da0fc36f9", + "pins" : [ + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" + } + }, + { + "identity" : "swift-concurrency-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "state" : { + "revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613", + "version" : "1.2.0" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "770f990d3e4eececb57ac04a6076e22f8c97daeb", + "version" : "1.4.2" + } + } + ], + "version" : 3 +} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Workflows.xcscheme b/Workflows.xcworkspace/xcshareddata/xcschemes/Workflows.xcscheme similarity index 81% rename from .swiftpm/xcode/xcshareddata/xcschemes/Workflows.xcscheme rename to Workflows.xcworkspace/xcshareddata/xcschemes/Workflows.xcscheme index 442791a..c993f33 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Workflows.xcscheme +++ b/Workflows.xcworkspace/xcshareddata/xcschemes/Workflows.xcscheme @@ -27,20 +27,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + Date: Tue, 22 Oct 2024 15:50:57 +0200 Subject: [PATCH 2/8] Remove beautify temporarily. --- Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 54ba84f..9fac146 100644 --- a/Makefile +++ b/Makefile @@ -25,11 +25,11 @@ XCODEBUILD_FLAGS = \ XCODEBUILD_COMMAND = xcodebuild $(XCODEBUILD_ARGUMENT) $(XCODEBUILD_FLAGS) -ifneq ($(strip $(shell which xcbeautify)),) - XCODEBUILD = set -o pipefail && $(XCODEBUILD_COMMAND) | xcbeautify --quiet -else - XCODEBUILD = $(XCODEBUILD_COMMAND) -endif +#ifneq ($(strip $(shell which xcbeautify)),) +# XCODEBUILD = set -o pipefail && $(XCODEBUILD_COMMAND) | xcbeautify --quiet +#else +XCODEBUILD = $(XCODEBUILD_COMMAND) +#endif TEST_RUNNER_CI = $(CI) From 2a7f9ce74ab0d2913a7370d45f2af031e70f903b Mon Sep 17 00:00:00 2001 From: Connor Ricks <13373737+connor-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:25:28 +0200 Subject: [PATCH 3/8] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fd8a2e6..c52303b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +TEST + # ๐Ÿšš Workflows ![Build](https://img.shields.io/github/actions/workflow/status/connor-ricks/swift-workflows/checks.yaml?logo=GitHub) ![Codecov](https://img.shields.io/codecov/c/github/connor-ricks/swift-workflows?logo=Codecov&label=codecov) From 1e8a822c63ecf57d44feb86ed1c586debfdc2003 Mon Sep 17 00:00:00 2001 From: Connor Ricks <13373737+connor-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:27:35 +0200 Subject: [PATCH 4/8] wip --- .github/workflows/checks.yaml | 2 +- .../Helpers/ValueWorkflow.swift | 38 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index b2d61d0..b602fd2 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -28,7 +28,7 @@ jobs: runs-on: macos-latest strategy: matrix: - platform: [IOS, MACOS, MAC_CATALYST, TVOS, WATCHOS, VISIONOS] + platform: [IOS] #, MACOS, MAC_CATALYST, TVOS, WATCHOS, VISIONOS] steps: - name: ๐Ÿ‘€ Install visionOS runtime if: matrix.platform == 'visionOS' diff --git a/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift b/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift index 6bfd170..6d4f64f 100644 --- a/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift +++ b/Tests/WorkflowsTests/Helpers/ValueWorkflow.swift @@ -44,40 +44,40 @@ struct ValueWorkflow: Workflow { // MARK: Workflow func run() async throws -> Value { - defer { log("completed run.") } +// defer { log("completed run.") } - log("starting run") +// log("starting run") if delay != .zero { - log("sleeping for \(delay)") +// log("sleeping for \(delay)") try? await clock.sleep(for: delay) - log("finished sleeping.") +// log("finished sleeping.") } - log("checking cancellation") +// log("checking cancellation") try Task.checkCancellation() switch result { case .success(let value): - log("tracing value.") +// log("tracing value.") await trace?(value) - log("returning value") +// log("returning value") return value case .failure(let error): - log("tracing value.") +// log("tracing value.") await trace?(error) - log("throwing error") +// log("throwing error") throw error } } - private func log(_ message: String) { - let prefix = switch result { - case .success(let value): - "[ INFO ] ValueWorkflow(value: \(value))" - case .failure(let error): - "[ INFO ] ValueWorkflow(throwing: \(error))" - } - - print("\(prefix) \(message)") - } +// private func log(_ message: String) { +// let prefix = switch result { +// case .success(let value): +// "[ INFO ] ValueWorkflow(value: \(value))" +// case .failure(let error): +// "[ INFO ] ValueWorkflow(throwing: \(error))" +// } +// +// print("\(prefix) \(message)") +// } } From 652ccce8e4217b789c905f4c9888af8559e6e284 Mon Sep 17 00:00:00 2001 From: Connor Ricks <13373737+connor-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:33:10 +0200 Subject: [PATCH 5/8] Update Workflows.xctestplan --- Tests/Workflows.xctestplan | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Workflows.xctestplan b/Tests/Workflows.xctestplan index 02d9c7f..a7c913b 100644 --- a/Tests/Workflows.xctestplan +++ b/Tests/Workflows.xctestplan @@ -13,6 +13,7 @@ }, "testTargets" : [ { + "parallelizable" : false, "target" : { "containerPath" : "container:", "identifier" : "WorkflowsTests", From 7f6a87175e1bf161b9bd0c6b20234e9550b9cbdd Mon Sep 17 00:00:00 2001 From: Connor Ricks <13373737+connor-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:40:11 +0200 Subject: [PATCH 6/8] Update checks.yaml --- .github/workflows/checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index b602fd2..b2d61d0 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -28,7 +28,7 @@ jobs: runs-on: macos-latest strategy: matrix: - platform: [IOS] #, MACOS, MAC_CATALYST, TVOS, WATCHOS, VISIONOS] + platform: [IOS, MACOS, MAC_CATALYST, TVOS, WATCHOS, VISIONOS] steps: - name: ๐Ÿ‘€ Install visionOS runtime if: matrix.platform == 'visionOS' From 4b2c14491abee3ae8d55ebefebc73ac28241856d Mon Sep 17 00:00:00 2001 From: Connor Ricks <13373737+connor-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 19:43:43 +0200 Subject: [PATCH 7/8] Update checks.yaml --- .github/workflows/checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index b2d61d0..b42ca6b 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -28,7 +28,7 @@ jobs: runs-on: macos-latest strategy: matrix: - platform: [IOS, MACOS, MAC_CATALYST, TVOS, WATCHOS, VISIONOS] + platform: [IOS, MACOS, MAC_CATALYST, TVOS, WATCHOS] steps: - name: ๐Ÿ‘€ Install visionOS runtime if: matrix.platform == 'visionOS' From 36fff0f5add9f94a02c1aab9a7e7628107685087 Mon Sep 17 00:00:00 2001 From: Connor Ricks <13373737+connor-ricks@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:13:47 +0200 Subject: [PATCH 8/8] wip --- Makefile | 3 ++- Tests/WorkflowsTests/Helpers/Clock+Yield.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9fac146..d9a9ce2 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,8 @@ XCODEBUILD_FLAGS = \ -destination $(DESTINATION) \ -scheme "$(SCHEME)" \ -skipMacroValidation \ - -workspace $(WORKSPACE) + -workspace $(WORKSPACE) \ + -verbose XCODEBUILD_COMMAND = xcodebuild $(XCODEBUILD_ARGUMENT) $(XCODEBUILD_FLAGS) diff --git a/Tests/WorkflowsTests/Helpers/Clock+Yield.swift b/Tests/WorkflowsTests/Helpers/Clock+Yield.swift index bdbac18..1a539fd 100644 --- a/Tests/WorkflowsTests/Helpers/Clock+Yield.swift +++ b/Tests/WorkflowsTests/Helpers/Clock+Yield.swift @@ -3,7 +3,7 @@ import Clocks extension TestClock { func run(afterYield: Bool) async { if afterYield { - await Task.yield() + await Task.megaYield(count: 100) } await run()