├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── .ruby-version ├── .swiftformat ├── CLI ├── Package.resolved └── Package.swift ├── CacheAdvance.podspec ├── Contributing.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── Scripts ├── build.swift └── prepare-coverage-reports.sh ├── Sources ├── CADCacheAdvance │ └── CADCacheAdvance.swift ├── CacheAdvance │ ├── BigEndianHostSwappable.swift │ ├── BoolExtensions.swift │ ├── Bytes.swift │ ├── CacheAdvance.swift │ ├── CacheAdvanceError.swift │ ├── CacheHeaderHandle.swift │ ├── CacheReader.swift │ ├── EncodableMessage.swift │ ├── FileHandleExtensions.swift │ ├── FileHeader.swift │ ├── MessageDecoder.swift │ ├── MessageEncoder.swift │ ├── MessageSpan.swift │ ├── UInt32+BigEndianHostSwappable.swift │ ├── UInt64+BigEndianHostSwappable.swift │ └── UInt8+BigEndianHostSwappable.swift └── LorumIpsum │ └── LorumIpsumMessages.swift ├── Tests ├── CADCacheAdvanceTests │ └── CADCacheAdvanceTests.m └── CacheAdvanceTests │ ├── BigEndianHostSwappableTests.swift │ ├── BoolExtensionsTests.swift │ ├── CacheAdvanceTests.swift │ ├── CacheHeaderHandleTests.swift │ ├── EncodableMessageTests.swift │ ├── FileHeaderTests.swift │ ├── FlushableCachePerformanceComparisonTests.swift │ ├── SQLitePerformanceComparisonTests.swift │ └── TestableMessage.swift ├── codecov.yml └── lint.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | insert_final_newline = true 3 | indent_size = 4 4 | 5 | # swift 6 | [*.swift] 7 | indent_style = tab 8 | 9 | # sh 10 | [*.sh] 11 | indent_style = tab 12 | 13 | # documentation, utils 14 | [*.{md,mdx,diff}] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Switched from spaces to tabs for indentation. 2 | 02ad13edec20389228ce164b47d183d64aaee234 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: dfed 2 | buy_me_a_coffee: dfed 3 | custom: https://cash.app/$dan 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Goals** 11 | What do you want this feature to accomplish? What are the effects of your change? 12 | 13 | **Non-Goals** 14 | What aren’t you trying to accomplish? What are the boundaries of the proposed work? 15 | 16 | **Investigations** 17 | What other solutions (if any) did you investigate? Why didn’t you choose them? 18 | 19 | **Design** 20 | What are you proposing? What are the details of your chosen design? Include an API overview, technical details, and (potentially) some example headers, along with anything else you think will be useful. This is where you sell the design to yourself and project maintainers. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | pod-lint: 11 | name: Pod Lint 12 | runs-on: macOS-15 13 | steps: 14 | - name: Checkout Repo 15 | uses: actions/checkout@v4 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: '3.3.5' 19 | bundler-cache: true 20 | - name: Select Xcode Version 21 | run: sudo xcode-select --switch /Applications/Xcode_16.app/Contents/Developer 22 | - name: Download visionOS 23 | run: | 24 | sudo xcodebuild -runFirstLaunch 25 | sudo xcrun simctl list 26 | sudo xcodebuild -downloadPlatform visionOS 27 | sudo xcodebuild -runFirstLaunch 28 | - name: Lint Podspec 29 | run: bundle exec pod lib lint --verbose --fail-fast --swift-version=6.0 --allow-warnings # Cocoapods v1.6 now warns about potential naming colisions. We can fix this in the next breaking change. 30 | spm-16: 31 | name: Build Xcode 16 32 | runs-on: macOS-15 33 | strategy: 34 | matrix: 35 | platforms: [ 36 | 'iOS_18,watchOS_11', 37 | 'macOS_15,tvOS_18', 38 | 'macCatalyst_15', 39 | 'visionOS_2' 40 | ] 41 | fail-fast: false 42 | steps: 43 | - name: Checkout Repo 44 | uses: actions/checkout@v4 45 | - uses: ruby/setup-ruby@v1 46 | with: 47 | ruby-version: '3.3.5' 48 | bundler-cache: true 49 | - name: Select Xcode Version 50 | run: sudo xcode-select --switch /Applications/Xcode_16.app/Contents/Developer 51 | - name: Download visionOS 52 | if: matrix.platforms == 'visionOS_2' 53 | run: | 54 | sudo xcodebuild -runFirstLaunch 55 | sudo xcrun simctl list 56 | sudo xcodebuild -downloadPlatform visionOS 57 | sudo xcodebuild -runFirstLaunch 58 | - name: Build and Test Framework 59 | run: Scripts/build.swift ${{ matrix.platforms }} 60 | - name: Prepare Coverage Reports 61 | run: ./Scripts/prepare-coverage-reports.sh 62 | - name: Upload Coverage Reports 63 | if: success() 64 | uses: codecov/codecov-action@v4 65 | spm-16-swift: 66 | name: Swift Build Xcode 16 67 | runs-on: macOS-15 68 | steps: 69 | - name: Checkout Repo 70 | uses: actions/checkout@v4 71 | - uses: ruby/setup-ruby@v1 72 | with: 73 | ruby-version: '3.3.5' 74 | bundler-cache: true 75 | - name: Select Xcode Version 76 | run: sudo xcode-select --switch /Applications/Xcode_16.app/Contents/Developer 77 | - name: Build and Test Framework 78 | run: xcrun swift test -c release -Xswiftc -enable-testing 79 | linux: 80 | name: Build and Test on Linux 81 | runs-on: ubuntu-latest 82 | container: swift:6.0.3 83 | steps: 84 | - name: Checkout Repo 85 | uses: actions/checkout@v4 86 | - name: Build and Test Framework 87 | run: swift test -c release --enable-code-coverage -Xswiftc -enable-testing 88 | - name: Prepare Coverage Reports 89 | run: | 90 | llvm-cov export -format="lcov" .build/x86_64-unknown-linux-gnu/release/CacheAdvancePackageTests.xctest -instr-profile .build/x86_64-unknown-linux-gnu/release/codecov/default.profdata > coverage.lcov 91 | - name: Upload Coverage Reports 92 | if: success() 93 | uses: codecov/codecov-action@v4 94 | with: 95 | fail_ci_if_error: true 96 | verbose: true 97 | os: linux 98 | readme-validation: 99 | name: Check Markdown links 100 | runs-on: ubuntu-latest 101 | steps: 102 | - name: Checkout Repo 103 | uses: actions/checkout@v4 104 | - name: Link Checker 105 | uses: AlexanderDokuchaev/md-dead-link-check@v1.0.1 106 | lint-swift: 107 | name: Lint Swift 108 | runs-on: ubuntu-latest 109 | container: swift:6.0 110 | steps: 111 | - name: Checkout Repo 112 | uses: actions/checkout@v4 113 | - name: Lint Swift 114 | run: swift run --package-path CLI swiftformat . --lint 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/ 4 | .build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | *.xcworkspace 14 | !default.xcworkspace 15 | *xcuserdata 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | .idea/ 20 | .swiftpm/ 21 | generated/ 22 | *coverage.txt 23 | 24 | # We can generate Xcode projects from the Swift packages. 25 | *.xcodeproj/ 26 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # format options 2 | --indent tab 3 | --modifierorder nonisolated,open,public,internal,fileprivate,private,private(set),final,override,required,convenience 4 | --ranges no-space 5 | --extensionacl on-declarations 6 | --funcattributes prev-line 7 | --typeattributes prev-line 8 | --storedvarattrs same-line 9 | --hexgrouping none 10 | --decimalgrouping 3 11 | 12 | # rules 13 | --enable isEmpty 14 | --enable wrapEnumCases 15 | --enable wrapMultilineStatementBraces 16 | --disable consistentSwitchCaseSpacing 17 | --disable blankLineAfterSwitchCase 18 | 19 | # global 20 | --swiftversion 6.0 21 | -------------------------------------------------------------------------------- /CLI/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "b2996d8b20521bfede6aa79472b75c70d7ac765721c6e529be7f5e3dc3d95b1d", 3 | "pins" : [ 4 | { 5 | "identity" : "swiftformat", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/nicklockwood/SwiftFormat", 8 | "state" : { 9 | "revision" : "1d02d0f54a5123c3ef67084b318f4421427b7a51", 10 | "version" : "0.56.2" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /CLI/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CLI", 8 | platforms: [ 9 | .macOS(.v14), 10 | ], 11 | products: [], 12 | dependencies: [ 13 | .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.56.1"), 14 | ], 15 | targets: [] 16 | ) 17 | -------------------------------------------------------------------------------- /CacheAdvance.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'CacheAdvance' 3 | s.version = '3.1.0' 4 | s.license = 'Apache License, Version 2.0' 5 | s.summary = 'A performant cache for logging systems. CacheAdvance persists log events 30x faster than SQLite.' 6 | s.homepage = 'https://github.com/dfed/CacheAdvance' 7 | s.authors = 'Dan Federman' 8 | s.source = { :git => 'https://github.com/dfed/CacheAdvance.git', :tag => s.version } 9 | s.source_files = 'Sources/**/*.{swift}', 'Sources/**/*.{h,m}' 10 | s.ios.deployment_target = '13.0' 11 | s.tvos.deployment_target = '13.0' 12 | s.watchos.deployment_target = '6.0' 13 | s.macos.deployment_target = '10.15' 14 | s.visionos.deployment_target = '1.0' 15 | end 16 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | ### One issue or bug per Pull Request 2 | 3 | Keep your Pull Requests small. Small PRs are easier to reason about which makes them significantly more likely to get merged. 4 | 5 | ### Issues before features 6 | 7 | If you want to add a feature, please file an [Issue](https://github.com/dfed/cacheadvance/issues) first. An Issue gives us the opportunity to discuss the requirements and implications of a feature with you before you start writing code. 8 | 9 | ### Backwards compatibility 10 | 11 | Respect the minimum deployment target. If you are adding code that uses new APIs, make sure to prevent older clients from crashing or misbehaving. Our CI runs against our minimum deployment targets, so you will not get a green build unless your code is backwards compatible. 12 | 13 | ### Forwards compatibility 14 | 15 | Please do not write new code using deprecated APIs. 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | ruby '3.3.5' 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'cocoapods', '~> 1.16.0' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.2.1.2) 9 | base64 10 | bigdecimal 11 | concurrent-ruby (~> 1.0, >= 1.3.1) 12 | connection_pool (>= 2.2.5) 13 | drb 14 | i18n (>= 1.6, < 2) 15 | logger (>= 1.4.2) 16 | minitest (>= 5.1) 17 | securerandom (>= 0.3) 18 | tzinfo (~> 2.0, >= 2.0.5) 19 | addressable (2.8.7) 20 | public_suffix (>= 2.0.2, < 7.0) 21 | algoliasearch (1.27.5) 22 | httpclient (~> 2.8, >= 2.8.3) 23 | json (>= 1.5.1) 24 | atomos (0.1.3) 25 | base64 (0.2.0) 26 | bigdecimal (3.1.8) 27 | claide (1.1.0) 28 | cocoapods (1.16.1) 29 | addressable (~> 2.8) 30 | claide (>= 1.0.2, < 2.0) 31 | cocoapods-core (= 1.16.1) 32 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 33 | cocoapods-downloader (>= 2.1, < 3.0) 34 | cocoapods-plugins (>= 1.0.0, < 2.0) 35 | cocoapods-search (>= 1.0.0, < 2.0) 36 | cocoapods-trunk (>= 1.6.0, < 2.0) 37 | cocoapods-try (>= 1.1.0, < 2.0) 38 | colored2 (~> 3.1) 39 | escape (~> 0.0.4) 40 | fourflusher (>= 2.3.0, < 3.0) 41 | gh_inspector (~> 1.0) 42 | molinillo (~> 0.8.0) 43 | nap (~> 1.0) 44 | ruby-macho (>= 2.3.0, < 3.0) 45 | xcodeproj (>= 1.26.0, < 2.0) 46 | cocoapods-core (1.16.1) 47 | activesupport (>= 5.0, < 8) 48 | addressable (~> 2.8) 49 | algoliasearch (~> 1.0) 50 | concurrent-ruby (~> 1.1) 51 | fuzzy_match (~> 2.0.4) 52 | nap (~> 1.0) 53 | netrc (~> 0.11) 54 | public_suffix (~> 4.0) 55 | typhoeus (~> 1.0) 56 | cocoapods-deintegrate (1.0.5) 57 | cocoapods-downloader (2.1) 58 | cocoapods-plugins (1.0.0) 59 | nap 60 | cocoapods-search (1.0.1) 61 | cocoapods-trunk (1.6.0) 62 | nap (>= 0.8, < 2.0) 63 | netrc (~> 0.11) 64 | cocoapods-try (1.2.0) 65 | colored2 (3.1.2) 66 | concurrent-ruby (1.3.4) 67 | connection_pool (2.4.1) 68 | drb (2.2.1) 69 | escape (0.0.4) 70 | ethon (0.16.0) 71 | ffi (>= 1.15.0) 72 | ffi (1.17.0) 73 | fourflusher (2.3.1) 74 | fuzzy_match (2.0.4) 75 | gh_inspector (1.1.3) 76 | httpclient (2.8.3) 77 | i18n (1.14.6) 78 | concurrent-ruby (~> 1.0) 79 | json (2.7.5) 80 | logger (1.6.1) 81 | minitest (5.25.1) 82 | molinillo (0.8.0) 83 | nanaimo (0.4.0) 84 | nap (1.1.0) 85 | netrc (0.11.0) 86 | nkf (0.2.0) 87 | public_suffix (4.0.7) 88 | rexml (3.3.9) 89 | ruby-macho (2.5.1) 90 | securerandom (0.3.1) 91 | typhoeus (1.4.1) 92 | ethon (>= 0.9.0) 93 | tzinfo (2.0.6) 94 | concurrent-ruby (~> 1.0) 95 | xcodeproj (1.26.0) 96 | CFPropertyList (>= 2.3.3, < 4.0) 97 | atomos (~> 0.1.3) 98 | claide (>= 1.0.2, < 2.0) 99 | colored2 (~> 3.1) 100 | nanaimo (~> 0.4.0) 101 | rexml (>= 3.3.6, < 4.0) 102 | 103 | PLATFORMS 104 | ruby 105 | 106 | DEPENDENCIES 107 | cocoapods (~> 1.16.0) 108 | 109 | RUBY VERSION 110 | ruby 3.3.5p94 111 | 112 | BUNDLED WITH 113 | 2.5.16 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CacheAdvance", 8 | platforms: [ 9 | .iOS(.v13), 10 | .tvOS(.v13), 11 | .watchOS(.v6), 12 | .macOS(.v10_15), 13 | .macCatalyst(.v13), 14 | ], 15 | products: [ 16 | .library( 17 | name: "CacheAdvance", 18 | targets: ["CacheAdvance"] 19 | ), 20 | .library( 21 | name: "CADCacheAdvance", 22 | targets: ["CADCacheAdvance"] 23 | ), 24 | ].filter { 25 | #if os(Linux) 26 | $0.name != "CADCacheAdvance" 27 | #else 28 | true 29 | #endif 30 | }, 31 | targets: [ 32 | .target( 33 | name: "CacheAdvance", 34 | swiftSettings: [ 35 | .swiftLanguageMode(.v6), 36 | .define("SWIFT_PACKAGE_MANAGER"), 37 | ] 38 | ), 39 | .testTarget( 40 | name: "CacheAdvanceTests", 41 | dependencies: ["CacheAdvance", "LorumIpsum"], 42 | swiftSettings: [ 43 | .swiftLanguageMode(.v6), 44 | ] 45 | ), 46 | .target( 47 | name: "CADCacheAdvance", 48 | dependencies: ["CacheAdvance"], 49 | swiftSettings: [ 50 | .swiftLanguageMode(.v6), 51 | .define("SWIFT_PACKAGE_MANAGER"), 52 | ] 53 | ), 54 | .target( 55 | name: "LorumIpsum", 56 | dependencies: [], 57 | swiftSettings: [ 58 | .swiftLanguageMode(.v6), 59 | ] 60 | ), 61 | .testTarget( 62 | name: "CADCacheAdvanceTests", 63 | dependencies: ["CADCacheAdvance", "LorumIpsum"], 64 | swiftSettings: [ 65 | .swiftLanguageMode(.v6), 66 | ] 67 | ), 68 | ].filter { 69 | #if os(Linux) 70 | $0.name != "CADCacheAdvance" 71 | && $0.name != "CADCacheAdvanceTests" 72 | #else 73 | true 74 | #endif 75 | } 76 | ) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CacheAdvance 2 | 3 | [![CI Status](https://img.shields.io/github/actions/workflow/status/dfed/cacheadvance/ci.yml?branch=main)](https://github.com/dfed/cacheadvance/actions?query=workflow%3ACI+branch%3Amain) 4 | [![codecov](https://codecov.io/gh/dfed/CacheAdvance/branch/main/graph/badge.svg?token=nZBHcZZ63F)](https://codecov.io/gh/dfed/CacheAdvance) 5 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](https://spdx.org/licenses/Apache-2.0.html) 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdfed%2FCacheAdvance%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/dfed/CacheAdvance) 7 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdfed%2FCacheAdvance%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/dfed/CacheAdvance) 8 | 9 | A performant cache for logging systems. CacheAdvance persists log events 30x faster than SQLite. 10 | 11 | ## Usage 12 | 13 | ### Basic Initialization 14 | 15 | ```swift 16 | let myCache = try CacheAdvance( 17 | file: FileManager.default.temporaryDirectory.appendingPathComponent("MyCache"), 18 | maximumBytes: 5000, 19 | shouldOverwriteOldMessages: false) 20 | ``` 21 | 22 | ```objc 23 | CADCacheAdvance *const cache = [[CADCacheAdvance alloc] 24 | initWithFileURL:[NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:@"MyCache"] 25 | maximumBytes:5000 26 | shouldOverwriteOldMessages:YES 27 | error:nil]; 28 | ``` 29 | To begin caching messages, you need to create a CacheAdvance instance with: 30 | 31 | * A file URL – this URL must represent a file that has already been created. You can create a file by using `FileManager`'s [createFile(atPath:contents:attributes:)](https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile) API. 32 | * A maximum number of bytes on disk the cache can consume. 33 | * Whether the cache should overwrite old messages. If you need to preserve every message, set this value to `false`. If you care only about preserving recent messages, set this value to `true`. 34 | 35 | ### Appending messages to disk 36 | 37 | ```swift 38 | try myCache.append(message: someMessage) 39 | ``` 40 | 41 | ```objc 42 | [cache appendMessage:someData error:nil]; 43 | ``` 44 | 45 | By the time the above method exits, the message will have been persisted to disk. A CacheAdvance keeps no in-memory buffer. Appending a new message is cheap, as a CacheAdvance needs to encode and persist only the new message and associated metadata. 46 | 47 | A CacheAdvance instance that does not overwrite old messages will throw a `CacheAdvanceError.messageDataTooLarge` if appending a message would exceed the cache's `maximumBytes`. A CacheAdvance instance that does overwrite old messages will throw a `CacheAdvanceError.messageDataTooLarge` if the message would require more than `maximumBytes` to store even after evicting all older messages from the cache. 48 | 49 | To ensure that caches can be read from 32bit devices, messages should not be larger than 2GB in size. 50 | 51 | ### Retrieving messages from disk 52 | 53 | ```swift 54 | let cachedMessages = try myCache.messages() 55 | ``` 56 | 57 | ```objc 58 | NSArray *const cachedMessages = [cache messagesAndReturnError:nil]; 59 | ``` 60 | 61 | This method reads all cached messages from disk into memory. 62 | 63 | ### Thread safety 64 | 65 | CacheAdvances are not thread safe: a single CacheAdvance instance should always be interacted with from a single, serial queue. Since CacheAdvance reads from and writes to the disk synchronously, it is best to interact with a CacheAdvance on a background queue to prevent blocking the main queue. 66 | 67 | ### Error handling 68 | 69 | A CacheAdvance will never fatal error: only recoverable errors will be thrown. A CacheAdvance may throw a `CacheAdvanceError`, or errors related to reading or writing with `FileHandle`s. 70 | 71 | If a `CacheAdvanceError.fileCorrupted` error is thrown, the cache file is corrupt and should be deleted. 72 | 73 | ## How it works 74 | 75 | CacheAdvance immediately persists each appended messages to disk using `FileHandle`s. Messages are encoded into `Data` using a `JSONEncoder` by default, though the encoding/decoding mechanism can be customized. Messages are written to disk as an encoded data blob that is prefixed with the length of the message. The length of a message is stored using a `UInt32` to ensure that the size of the data on disk that stores a message's length is consistent between devices. 76 | 77 | The first 64bytes of a CacheAdvance is reserved for storing metadata about the file. Any configuration data that must be static between cache opens should be stored in this header. It is also reasonable to store mutable information in the header, if doing so speeds up reads or writes to the file. The header format is managed by [FileHeader.swift](Sources/CacheAdvance/FileHeader.swift). 78 | 79 | ## Requirements 80 | 81 | * Xcode 16.0 or later. 82 | * iOS 13 or later. 83 | * tvOS 13 or later. 84 | * watchOS 6 or later. 85 | * macOS 10.15 or later. 86 | * Swift 5.10 or later. 87 | 88 | ## Installation 89 | 90 | ### Swift Package Manager 91 | 92 | To install CacheAdvance in your project with [Swift Package Manager](https://github.com/apple/swift-package-manager), the following lines can be added to your `Package.swift` file: 93 | 94 | ```swift 95 | dependencies: [ 96 | .package(url: "https://github.com/dfed/CacheAdvance", from: "3.0.0"), 97 | ] 98 | ``` 99 | 100 | ### CocoaPods 101 | 102 | To install CacheAdvance in your project with [CocoaPods](https://blog.cocoapods.org/CocoaPods-Specs-Repo), add the following to your `Podfile`: 103 | 104 | ``` 105 | pod 'CacheAdvance', '~> 3.0' 106 | ``` 107 | 108 | ## Contributing 109 | 110 | I’m glad you’re interested in CacheAdvance, and I’d love to see where you take it. Please read the [contributing guidelines](Contributing.md) prior to submitting a Pull Request. 111 | 112 | Thanks, and happy caching! 113 | 114 | ## Developing 115 | 116 | Double-click on `Package.swift` in the root of the repository to open the project in Xcode. 117 | 118 | ## Default branch 119 | 120 | The default branch of this repository is `main`. Between the initial commit and [`151ab3f`](https://github.com/dfed/CacheAdvance/commit/151ab3f1d57531b9ae7a9219d0d8a33300704595), the default branch of this repository was `master`. See #46 for more details on why this change was made. 121 | 122 | ## Appreciations 123 | 124 | Shout out to [Peter Westen](https://twitter.com/pwesten) who inspired the creation of this library. 125 | 126 | Big thanks to [Michael Bachand](https://github.com/bachand) who has reviewed nearly every change to this library over the years. 127 | -------------------------------------------------------------------------------- /Scripts/build.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env swift 2 | 3 | import Foundation 4 | 5 | // Usage: build.swift platforms 6 | 7 | func execute(commandPath: String, arguments: [String]) throws { 8 | let task = Process() 9 | task.executableURL = .init(filePath: commandPath) 10 | task.arguments = arguments 11 | print("Launching command: \(commandPath) \(arguments.joined(separator: " "))") 12 | try task.run() 13 | task.waitUntilExit() 14 | guard task.terminationStatus == 0 else { 15 | throw TaskError.code(task.terminationStatus) 16 | } 17 | } 18 | 19 | enum TaskError: Error { 20 | case code(Int32) 21 | } 22 | 23 | enum Platform: String, CaseIterable, CustomStringConvertible { 24 | case iOS_18 25 | case tvOS_18 26 | case macOS_15 27 | case macCatalyst_15 28 | case watchOS_11 29 | case visionOS_2 30 | 31 | var destination: String { 32 | switch self { 33 | case .iOS_18: 34 | "platform=iOS Simulator,OS=18.0,name=iPad (10th generation)" 35 | case .tvOS_18: 36 | "platform=tvOS Simulator,OS=18.0,name=Apple TV" 37 | case .tvOS_18: 38 | "platform=tvOS Simulator,OS=18,name=Apple TV" 39 | 40 | case .macOS_15, 41 | .macCatalyst_15: 42 | "platform=OS X" 43 | 44 | case .watchOS_11: 45 | "OS=11.0,name=Apple Watch Series 10 (46mm)" 46 | case .visionOS_2: 47 | "OS=2.0,name=Apple Vision Pro" 48 | } 49 | } 50 | 51 | var sdk: String { 52 | switch self { 53 | case .iOS_18: 54 | "iphonesimulator" 55 | 56 | case .tvOS_18: 57 | "appletvsimulator" 58 | 59 | case .macOS_15, 60 | .macCatalyst_15: 61 | "macosx15.0" 62 | 63 | case .watchOS_11: 64 | "watchsimulator" 65 | 66 | case .visionOS_2: 67 | "xrsimulator" 68 | } 69 | } 70 | 71 | var derivedDataPath: String { 72 | ".build/derivedData/" + description 73 | } 74 | 75 | var description: String { 76 | rawValue 77 | } 78 | } 79 | 80 | guard CommandLine.arguments.count > 1 else { 81 | print("Usage: build.swift platforms") 82 | throw TaskError.code(1) 83 | } 84 | 85 | let rawPlatforms = CommandLine.arguments[1].components(separatedBy: ",") 86 | 87 | var isFirstRun = true 88 | for rawPlatform in rawPlatforms { 89 | guard let platform = Platform(rawValue: rawPlatform) else { 90 | print("Received unknown platform type \(rawPlatform)") 91 | print("Possible platform types are: \(Platform.allCases)") 92 | throw TaskError.code(1) 93 | } 94 | 95 | var xcodeBuildArguments = [ 96 | "-scheme", "CacheAdvance-Package", 97 | "-sdk", platform.sdk, 98 | "-derivedDataPath", platform.derivedDataPath, 99 | "-PBXBuildsContinueAfterErrors=0", 100 | "OTHER_SWIFT_FLAGS=-warnings-as-errors", 101 | ] 102 | if !platform.destination.isEmpty { 103 | xcodeBuildArguments.append("-destination") 104 | xcodeBuildArguments.append(platform.destination) 105 | } 106 | xcodeBuildArguments.append("-enableCodeCoverage") 107 | xcodeBuildArguments.append("YES") 108 | xcodeBuildArguments.append("build") 109 | xcodeBuildArguments.append("test") 110 | 111 | try execute(commandPath: "/usr/bin/xcodebuild", arguments: xcodeBuildArguments) 112 | isFirstRun = false 113 | } 114 | -------------------------------------------------------------------------------- /Scripts/prepare-coverage-reports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh -l 2 | set -e 3 | 4 | function exportlcov() { 5 | build_type=$1 6 | executable_name=$2 7 | 8 | executable=$(find "${directory}" -type f -name $executable_name) 9 | profile=$(find "${directory}" -type f -name 'Coverage.profdata') 10 | output_file_name="$executable_name.lcov" 11 | 12 | can_proceed=true 13 | if [[ $build_type == watchOS* ]]; then 14 | echo "\tAborting creation of $output_file_name – watchOS not supported." 15 | elif [[ -z $profile ]]; then 16 | echo "\tAborting creation of $output_file_name – no profile found." 17 | elif [[ -z $executable ]]; then 18 | echo "\tAborting creation of $output_file_name – no executable found." 19 | else 20 | output_dir=".build/artifacts/$build_type" 21 | mkdir -p $output_dir 22 | 23 | output_file="$output_dir/$output_file_name" 24 | echo "\tExporting $output_file" 25 | xcrun llvm-cov export -format="lcov" $executable -instr-profile $profile >$output_file 26 | fi 27 | } 28 | 29 | for directory in $(git rev-parse --show-toplevel)/.build/derivedData/*/; do 30 | build_type=$(basename $directory) 31 | echo "Finding coverage information for $build_type" 32 | 33 | exportlcov $build_type 'CacheAdvanceTests' 34 | exportlcov $build_type 'CADCacheAdvanceTests' 35 | done 36 | -------------------------------------------------------------------------------- /Sources/CADCacheAdvance/CADCacheAdvance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 4/24/20. 3 | // Copyright © 2020 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | #if SWIFT_PACKAGE_MANAGER 19 | // Swift Package Manager defines multiple modules, while other distribution mechanisms do not. 20 | // We only need to import CacheAdvance if this project is being built with Swift Package Manager. 21 | import CacheAdvance 22 | #endif 23 | import Foundation 24 | 25 | /// A cache that enables the performant persistence of individual messages to disk. 26 | /// This cache is intended to be written to and read from using the same serial queue. 27 | /// - Attention: This type is meant to be used by Objective-C code, and is not exposed to Swift. Swift code should use CacheAdvance. 28 | @objc(CADCacheAdvance) 29 | @available(swift, obsoleted: 1.0) 30 | public final class __ObjectiveCCompatibleCacheAdvanceWithGenericData: NSObject { 31 | // MARK: Initialization 32 | 33 | /// Creates a new instance of the receiver. 34 | /// 35 | /// - Parameters: 36 | /// - fileURL: The file URL indicating the desired location of the on-disk store. This file should already exist. 37 | /// - maximumBytes: The maximum size of the cache, in bytes. Logs larger than this size will fail to append to the store. 38 | /// - shouldOverwriteOldMessages: When `true`, once the on-disk store exceeds maximumBytes, new entries will replace the oldest entry. 39 | /// 40 | /// - Warning: `maximumBytes` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache. 41 | /// - Warning: `shouldOverwriteOldMessages` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache. 42 | @objc 43 | public init( 44 | fileURL: URL, 45 | maximumBytes: Bytes, 46 | shouldOverwriteOldMessages: Bool 47 | ) 48 | throws 49 | { 50 | cache = try CacheAdvance( 51 | fileURL: fileURL, 52 | maximumBytes: maximumBytes, 53 | shouldOverwriteOldMessages: shouldOverwriteOldMessages, 54 | decoder: PassthroughDataDecoder(), 55 | encoder: PassthroughDataEncoder() 56 | ) 57 | } 58 | 59 | // MARK: Public 60 | 61 | @objc 62 | public var fileURL: URL { 63 | cache.fileURL 64 | } 65 | 66 | /// Checks if the all the header metadata provided at initialization matches the persisted header. If not, the cache is not writable. 67 | /// - Returns: `true` if the cache is writable. 68 | @objc 69 | public var isWritable: Bool { 70 | (try? cache.isWritable()) ?? false 71 | } 72 | 73 | /// Appends a message to the cache. 74 | /// - Parameter message: A message to write to disk. Must be smaller than both `maximumBytes - FileHeader.expectedEndOfHeaderInFile` and `MessageSpan.max`. 75 | @objc 76 | public func appendMessage(_ message: Data) throws { 77 | try cache.append(message: message) 78 | } 79 | 80 | /// - Returns: `true` when there are no messages written to the file, or when the file can not be read. 81 | @objc 82 | public var isEmpty: Bool { 83 | (try? cache.isEmpty()) ?? true 84 | } 85 | 86 | /// Fetches all messages from the cache. 87 | @objc 88 | public func messages() throws -> [Data] { 89 | try cache.messages() 90 | } 91 | 92 | // MARK: Private 93 | 94 | private let cache: CacheAdvance 95 | } 96 | 97 | // MARK: - PassthroughDataDecoder 98 | 99 | /// A decoder that treats all messages as if they are `Data`. 100 | final class PassthroughDataDecoder: MessageDecoder { 101 | func decode(_: T.Type, from data: Data) throws -> T where T: Decodable { 102 | // Force cast because this type is only used with a CacheAdvance type. 103 | data as! T 104 | } 105 | } 106 | 107 | // MARK: - PassthroughDataEncoder 108 | 109 | /// A encoder that treats all messages as if they are `Data`. 110 | final class PassthroughDataEncoder: MessageEncoder { 111 | func encode(_ value: some Encodable) throws -> Data { 112 | // Force cast because this type is only used with a CacheAdvance type. 113 | value as! Data 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/BigEndianHostSwappable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/3/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | /// A protocol representing an integer that can be encoded as data. 21 | protocol BigEndianHostSwappable where Self: FixedWidthInteger { 22 | /// Converts the big-endian value in x to the current endian format and returns the resulting value. 23 | init(bigEndian value: Self) 24 | 25 | /// The maximum representable integer in this type. 26 | static var max: Self { get } 27 | } 28 | 29 | extension BigEndianHostSwappable { 30 | /// Initializes encodable data from a data blob. 31 | /// 32 | /// - Parameter data: A data blob representing encodable data. Must be of length `Self.storageLength`. 33 | init(_ data: Data) { 34 | var decodedSize: Self = 0 35 | _ = withUnsafeMutableBytes(of: &decodedSize) { 36 | data.copyBytes(to: $0, count: data.count) 37 | } 38 | self = Self(bigEndian: decodedSize) 39 | } 40 | 41 | /// The length of a contiguous data blob required to store this type. 42 | static var storageLength: Int { MemoryLayout.size } 43 | } 44 | 45 | extension Data { 46 | /// Initializes Data from a numeric value. The data will always be of length `Number.storageLength`. 47 | /// - Parameter value: the value to encode as data. 48 | init(_ value: Number) { 49 | var valueToEncode = value.bigEndian 50 | self.init(bytes: &valueToEncode, count: Number.storageLength) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/BoolExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/25/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | extension Bool { 21 | init(_ data: Data) { 22 | self = UInt8(data) == 1 ? true : false 23 | } 24 | 25 | static var storageLength: Int { UInt8.storageLength } 26 | } 27 | 28 | extension Data { 29 | /// Initializes Data from a numeric value. The data will always be of length 1. 30 | /// - Parameter value: the value to encode as data. 31 | init(_ value: Bool) { 32 | self.init(UInt8(value ? 1 : 0)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/Bytes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/10/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | /// A storage unit that counts bytes. Used for describing a maximum cache size. 21 | /// 22 | /// This type should only be used when interacting with public API. This type enables changing how 23 | /// the maximum file size is stored in a future breaking change without changing much code internally. 24 | /// 25 | /// - Warning: If this value is changed, previously persisted message encodings will not be readable. 26 | public typealias Bytes = UInt64 27 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/CacheAdvance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/9/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | /// A cache that enables the performant persistence of individual messages to disk. 21 | /// This cache is intended to be written to and read from using the same serial queue. 22 | public final class CacheAdvance { 23 | // MARK: Initialization 24 | 25 | /// Creates a new instance of the receiver. 26 | /// 27 | /// - Parameters: 28 | /// - fileURL: The file URL indicating the desired location of the on-disk store. This file should already exist. 29 | /// - maximumBytes: The maximum size of the cache, in bytes. Logs larger than this size will fail to append to the store. 30 | /// - shouldOverwriteOldMessages: When `true`, once the on-disk store exceeds maximumBytes, new entries will replace the oldest entry. 31 | /// - decoder: The decoder that will be used to decode already-persisted messages. 32 | /// - encoder: The encoder that will be used to encode messages prior to persistence. 33 | /// 34 | /// - Warning: `maximumBytes` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache. 35 | /// - Warning: `shouldOverwriteOldMessages` must be consistent for the life of a cache. Changing this value after logs have been persisted to a cache will prevent appending new messages to this cache. 36 | /// - Warning: `decoder` must have a consistent implementation for the life of a cache. Changing this value after logs have been persisted to a cache may prevent reading messages from this cache. 37 | /// - Warning: `encoder` must have a consistent implementation for the life of a cache. Changing this value after logs have been persisted to a cache may prevent reading messages from this cache. 38 | public convenience init( 39 | fileURL: URL, 40 | maximumBytes: Bytes, 41 | shouldOverwriteOldMessages: Bool, 42 | decoder: MessageDecoder = JSONDecoder(), 43 | encoder: MessageEncoder = JSONEncoder() 44 | ) 45 | throws 46 | { 47 | try self.init( 48 | fileURL: fileURL, 49 | writer: FileHandle(forWritingTo: fileURL), 50 | reader: CacheReader(forReadingFrom: fileURL), 51 | header: CacheHeaderHandle( 52 | forReadingFrom: fileURL, 53 | maximumBytes: maximumBytes, 54 | overwritesOldMessages: shouldOverwriteOldMessages 55 | ), 56 | decoder: decoder, 57 | encoder: encoder 58 | ) 59 | } 60 | 61 | /// An internal initializer with no logic. Can be used to create pathological test cases. 62 | required init( 63 | fileURL: URL, 64 | writer: FileHandle, 65 | reader: CacheReader, 66 | header: CacheHeaderHandle, 67 | decoder: MessageDecoder, 68 | encoder: MessageEncoder 69 | ) { 70 | self.fileURL = fileURL 71 | self.writer = writer 72 | self.reader = reader 73 | self.header = header 74 | self.decoder = decoder 75 | self.encoder = encoder 76 | } 77 | 78 | deinit { 79 | try? writer.closeHandle() 80 | } 81 | 82 | // MARK: Public 83 | 84 | public let fileURL: URL 85 | 86 | /// Checks if the all the header metadata provided at initialization matches the persisted header. If not, the cache is not writable. 87 | /// - Returns: `true` if the cache is writable. 88 | public func isWritable() throws -> Bool { 89 | try setUpFileHandlesIfNecessary() 90 | return try header.canWriteToFile() 91 | } 92 | 93 | /// Appends a message to the cache. 94 | /// - Parameter message: A message to write to disk. Must be smaller than both `maximumBytes - FileHeader.expectedEndOfHeaderInFile` and `MessageSpan.max`. 95 | public func append(message: T) throws { 96 | try setUpFileHandlesIfNecessary() 97 | try header.checkFile() 98 | guard try header.canWriteToFile() else { 99 | throw CacheAdvanceError.fileNotWritable 100 | } 101 | 102 | let encodableMessage = EncodableMessage(message: message, encoder: encoder) 103 | let messageData = try encodableMessage.encodedData() 104 | let bytesNeededToStoreMessage = Bytes(messageData.count) 105 | 106 | guard 107 | bytesNeededToStoreMessage <= header.maximumBytes - FileHeader.expectedEndOfHeaderInFile, // Make sure we have room in this file for this message. 108 | bytesNeededToStoreMessage < Int32.max // Make sure we can read this message back out with Int on a 32-bit device. 109 | else { 110 | // The message is too long to be written to a cache of this size. 111 | throw CacheAdvanceError.messageLargerThanCacheCapacity 112 | } 113 | 114 | let cacheHasSpaceForNewMessageBeforeEndOfFile = writer.offsetInFile + bytesNeededToStoreMessage <= header.maximumBytes 115 | if header.overwritesOldMessages { 116 | if !cacheHasSpaceForNewMessageBeforeEndOfFile { 117 | // This message can't be written without exceeding our maximum file length. 118 | // We'll need to start writing the file from the beginning of the file. 119 | 120 | // Trim the file to the current writer position to remove soon-to-be-abandoned data from the file. 121 | try writer.truncate(at: writer.offsetInFile) 122 | 123 | // Set the offset back to the beginning of the file. 124 | try writer.seek(to: FileHeader.expectedEndOfHeaderInFile) 125 | 126 | // We know the oldest message is at the beginning of the file, since we are about to toss out the rest of the file. 127 | reader.offsetInFileOfOldestMessage = FileHeader.expectedEndOfHeaderInFile 128 | try reader.seekToBeginningOfOldestMessage() 129 | 130 | // We know we're about to overwrite the oldest message, so advance the reader to the second oldest message. 131 | try reader.seekToNextMessage() 132 | } 133 | 134 | // Prepare the reader before writing the message. 135 | try prepareReaderForWriting(dataOfLength: bytesNeededToStoreMessage) 136 | 137 | // Create the marker for the offset representing the beginning of the message that will be the oldest once our write is done. 138 | let offsetInFileOfOldestMessage = UInt64(reader.offsetInFile) 139 | 140 | // Update the offsetInFileOfOldestMessage in our header before we write the message. 141 | // If the application crashes between writing the header and writing the message data, we'll have lost the messages between the previous offsetInFileOfOldestMessage and the new offsetInFileOfOldestMessage. 142 | try header.updateOffsetInFileOfOldestMessage(to: offsetInFileOfOldestMessage) 143 | 144 | // Let the reader know where the oldest message begins. 145 | reader.offsetInFileOfOldestMessage = offsetInFileOfOldestMessage 146 | 147 | // Write the message. 148 | try write(messageData: messageData) 149 | 150 | } else if cacheHasSpaceForNewMessageBeforeEndOfFile { 151 | // Write the message. 152 | try write(messageData: messageData) 153 | 154 | } else { 155 | // We're out of room. 156 | throw CacheAdvanceError.messageLargerThanRemainingCacheSize 157 | } 158 | } 159 | 160 | /// - Returns: `true` when there are no messages written to the file. 161 | public func isEmpty() throws -> Bool { 162 | try setUpFileHandlesIfNecessary() 163 | try header.checkFile() 164 | 165 | return header.offsetInFileAtEndOfNewestMessage == FileHeader.expectedEndOfHeaderInFile 166 | } 167 | 168 | /// Fetches all messages from the cache. 169 | public func messages() throws -> [T] { 170 | try setUpFileHandlesIfNecessary() 171 | try header.checkFile() 172 | 173 | var messages = [T]() 174 | if reader.offsetInFileOfOldestMessage < reader.offsetInFileAtEndOfNewestMessage { 175 | // There is only one range: | `offsetInFileOfOldestMessage` -> `offsetInFileAtEndOfNewestMessage` | 176 | let encodedMessages = try reader.encodedMessagesFromOffset( 177 | reader.offsetInFileOfOldestMessage, 178 | endOffset: reader.offsetInFileAtEndOfNewestMessage 179 | ) 180 | for encodedMessage in encodedMessages { 181 | try messages.append(decoder.decode(T.self, from: encodedMessage)) 182 | } 183 | } else if reader.offsetInFileOfOldestMessage == reader.offsetInFileAtEndOfNewestMessage { 184 | // This is an empty cache. 185 | return [] 186 | } else { 187 | // In this case, the messages could be split to two ranges 188 | // | First Range | (GAP: ignore) | Second Range | 189 | 190 | // This is second range: | `offsetInFileOfOldestMessage` -> EOF | 191 | let olderMessages = try reader.encodedMessagesFromOffset(reader.offsetInFileOfOldestMessage) 192 | for encodedMessage in olderMessages { 193 | try messages.append(decoder.decode(T.self, from: encodedMessage)) 194 | } 195 | 196 | // This is first range: | `expectedEndOfHeaderInFile` -> `offsetInFileAtEndOfNewestMessage` | 197 | let newerMessages = try reader.encodedMessagesFromOffset( 198 | FileHeader.expectedEndOfHeaderInFile, 199 | endOffset: reader.offsetInFileAtEndOfNewestMessage 200 | ) 201 | for encodedMessage in newerMessages { 202 | try messages.append(decoder.decode(T.self, from: encodedMessage)) 203 | } 204 | } 205 | // Now that we've read all messages, seek back to the oldest message. 206 | try reader.seekToBeginningOfOldestMessage() 207 | 208 | return messages 209 | } 210 | 211 | // MARK: Private 212 | 213 | /// Seeks the reader and writer file handles to the correct place. 214 | /// Only necessary the first time this method is called. 215 | private func setUpFileHandlesIfNecessary() throws { 216 | guard !hasSetUpFileHandles else { 217 | return 218 | } 219 | 220 | // Read our header data. 221 | try header.synchronizeHeaderData() 222 | 223 | // Update the reader with data from the header 224 | reader.offsetInFileOfOldestMessage = header.offsetInFileOfOldestMessage 225 | reader.offsetInFileAtEndOfNewestMessage = header.offsetInFileAtEndOfNewestMessage 226 | 227 | // This is our first cache action. 228 | // Seek our writer to where we should write our next message. 229 | try writer.seek(to: header.offsetInFileAtEndOfNewestMessage) 230 | 231 | // Seek our reader to the oldest message. 232 | try reader.seekToBeginningOfOldestMessage() 233 | 234 | hasSetUpFileHandles = true 235 | } 236 | 237 | /// Advances the reader until there is room to store a new message without writing past the reader. 238 | /// This method should only be called on a cache that overwrites old messages. 239 | /// - Parameter messageLength: the length of the next message that will be written. 240 | private func prepareReaderForWriting(dataOfLength messageLength: Bytes) throws { 241 | // If our writer is behind our reader, 242 | while writer.offsetInFile < reader.offsetInFile, 243 | // And our writer doesn't have enough room to write a message such that it stays behind the current reader position. 244 | writer.offsetInFile + messageLength >= reader.offsetInFile 245 | { 246 | // Then writing this message would write into the oldest-known message. 247 | // We must advance our reader to the next-oldest message to help make room for the next message we want to write. 248 | try reader.seekToNextMessage() 249 | } 250 | } 251 | 252 | /// Writes message data to the cache. 253 | /// 254 | /// - Parameters: 255 | /// - messageData: an `EncodableMessage`'s encoded data. 256 | private func write(messageData: Data) throws { 257 | // Write the message data. 258 | try writer.write(data: messageData) 259 | 260 | // Update the offsetInFileAtEndOfNewestMessage in our header and reader now that we've written the message. 261 | // If the application crashes between writing the message data and writing the header, we'll have lost the most recent message. 262 | try header.updateOffsetInFileAtEndOfNewestMessage(to: writer.offsetInFile) 263 | reader.offsetInFileAtEndOfNewestMessage = writer.offsetInFile 264 | } 265 | 266 | private let writer: FileHandle 267 | private let reader: CacheReader 268 | private let header: CacheHeaderHandle 269 | 270 | private var hasSetUpFileHandles = false 271 | 272 | private let decoder: MessageDecoder 273 | private let encoder: MessageEncoder 274 | } 275 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/CacheAdvanceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/10/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | public enum CacheAdvanceError: Error, Equatable { 19 | /// Thrown when the message being appended is too large to be stored in a cache of this size. 20 | case messageLargerThanCacheCapacity 21 | /// Thrown when a message being appended to a cache that does not overwrite old messages is too large to store in the remaining space. 22 | case messageLargerThanRemainingCacheSize 23 | /// Thrown when a cache's persisted header is incompatible with the current implementation. 24 | /// - Parameter persistedVersion: The header version of the file being read. 25 | case incompatibleHeader(persistedVersion: UInt8) 26 | /// Thrown when the cache file's persisted static header data is inconsistent with the metadata with which the cache was initialized. 27 | case fileNotWritable 28 | /// Thrown when the cache file is of an unexpected format. 29 | /// A corrupted file should be deleted. Corruption can occur if an application crashes while writing to the file. 30 | case fileCorrupted 31 | } 32 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/CacheHeaderHandle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/26/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | final class CacheHeaderHandle { 21 | // MARK: Initialization 22 | 23 | /// Creates a new instance of the receiver. 24 | /// 25 | /// - Parameters: 26 | /// - file: The file URL indicating the desired location of the on-disk store. This file should already exist. 27 | /// - maximumBytes: The maximum size of the cache, in bytes. Logs larger than this size will fail to append to the store. 28 | /// - overwritesOldMessages: When `true`, the cache encodes a pointer to the oldest message after the newest message marker. 29 | /// - version: The file's expected header version. 30 | init( 31 | forReadingFrom file: URL, 32 | maximumBytes: Bytes, 33 | overwritesOldMessages: Bool, 34 | version: UInt8 = FileHeader.version 35 | ) 36 | throws 37 | { 38 | handle = try FileHandle(forUpdating: file) 39 | self.maximumBytes = maximumBytes 40 | self.overwritesOldMessages = overwritesOldMessages 41 | self.version = version 42 | offsetInFileOfOldestMessage = FileHeader.expectedEndOfHeaderInFile 43 | offsetInFileAtEndOfNewestMessage = FileHeader.expectedEndOfHeaderInFile 44 | } 45 | 46 | deinit { 47 | try? handle.closeHandle() 48 | } 49 | 50 | // MARK: Internal 51 | 52 | let maximumBytes: Bytes 53 | let overwritesOldMessages: Bool 54 | private(set) var offsetInFileOfOldestMessage: UInt64 55 | private(set) var offsetInFileAtEndOfNewestMessage: UInt64 56 | 57 | func updateOffsetInFileOfOldestMessage(to offset: UInt64) throws { 58 | offsetInFileOfOldestMessage = offset 59 | 60 | // Seek to the beginning of this field in the header. 61 | try handle.seek(to: CacheHeaderHandle.beginningOfHeaderField_offsetInFileOfOldestMessage) 62 | 63 | // Write this updated value to disk. 64 | try handle.write(data: expectedHeader.data(for: .offsetInFileOfOldestMessage)) 65 | } 66 | 67 | func updateOffsetInFileAtEndOfNewestMessage(to offset: UInt64) throws { 68 | offsetInFileAtEndOfNewestMessage = offset 69 | 70 | // Seek to the beginning of this field in the header. 71 | try handle.seek(to: CacheHeaderHandle.beginningOfHeaderField_offsetInFileAtEndOfNewestMessage) 72 | 73 | // Write this updated value to disk. 74 | try handle.write(data: expectedHeader.data(for: .offsetInFileAtEndOfNewestMessage)) 75 | } 76 | 77 | /// Checks that the file is formatted as expected. 78 | /// 79 | /// - Throws: `CacheAdvanceError.incompatibleHeader` if this object's header version does not match that of `fileHeader`. May also throw a file reading error if the file can not be read. 80 | func checkFile() throws { 81 | try checkFile(with: memoizedMetadata()) 82 | } 83 | 84 | /// Checks if the all the header metadata provided at initialization matches the persisted header. 85 | /// 86 | /// - Returns: `true` if this object's static metadata matches that of the persisted `fileHeader`; otherwise `false`. 87 | func canWriteToFile() throws -> Bool { 88 | try canWriteToFile(with: memoizedMetadata()) 89 | } 90 | 91 | /// Reads the header data from the file. Writes header information to disk if no header exists. 92 | /// If the header data persisted to the file is not consistent with expectations, the file will be deleted. 93 | func synchronizeHeaderData() throws { 94 | let headerData = try readHeaderData() 95 | 96 | guard !headerData.isEmpty else { 97 | // The file is empty. Write a header to disk. 98 | let writtenHeader = try writeHeaderData() 99 | persistedMetadata = Metadata(fileHeader: writtenHeader) 100 | return 101 | } 102 | 103 | guard let fileHeader = FileHeader(from: headerData) else { 104 | // We can't read the header data. 105 | throw CacheAdvanceError.fileCorrupted 106 | } 107 | 108 | let persistedMetadata = Metadata(fileHeader: fileHeader) 109 | do { 110 | try checkFile(with: persistedMetadata) 111 | } catch { 112 | return 113 | } 114 | 115 | self.persistedMetadata = persistedMetadata 116 | offsetInFileOfOldestMessage = fileHeader.offsetInFileOfOldestMessage 117 | offsetInFileAtEndOfNewestMessage = fileHeader.offsetInFileAtEndOfNewestMessage 118 | } 119 | 120 | // MARK: Private 121 | 122 | private let handle: FileHandle 123 | private let version: UInt8 124 | 125 | private var persistedMetadata: Metadata? 126 | 127 | private var expectedHeader: FileHeader { 128 | FileHeader( 129 | version: version, 130 | maximumBytes: maximumBytes, 131 | overwritesOldMessages: overwritesOldMessages, 132 | offsetInFileOfOldestMessage: offsetInFileOfOldestMessage, 133 | offsetInFileAtEndOfNewestMessage: offsetInFileAtEndOfNewestMessage 134 | ) 135 | } 136 | 137 | /// Attempts to read the header data of the file. 138 | /// 139 | /// - Returns: As much of the header data as exists. The returned data may not form a complete header. 140 | private func readHeaderData() throws -> Data { 141 | // Start at the beginning of the file. 142 | try handle.seek(to: 0) 143 | 144 | // Read the entire header. 145 | return try handle.readDataUp(toLength: Int(FileHeader.expectedEndOfHeaderInFile)) 146 | } 147 | 148 | /// Checks that the file's persisted metadata has the expected values. 149 | /// 150 | /// - Parameter persistedMetadata: The persisted header metadata. 151 | /// - Throws: `CacheAdvanceError.incompatibleHeader` if this object's header version does not match that of `fileHeader`. 152 | private func checkFile(with persistedMetadata: Metadata) throws { 153 | // Our current file header version is 1. 154 | // That means there is only one header version we can understand. 155 | // Our header version must be our expected version for us to open the file successfully. 156 | if persistedMetadata.version != version { 157 | throw CacheAdvanceError.incompatibleHeader(persistedVersion: persistedMetadata.version) 158 | } 159 | } 160 | 161 | /// Checks if the all the header metadata provided at initialization matches the persisted header. 162 | /// 163 | /// - Parameter persistedMetadata: The persisted header metadata. 164 | /// - Returns: `true` if this object's static metadata matches that of the persisted `fileHeader`; otherwise `false`. 165 | private func canWriteToFile(with persistedMetadata: Metadata) -> Bool { 166 | do { 167 | try checkFile(with: persistedMetadata) 168 | } catch { 169 | // If we can't open the file, we can't write to it. 170 | return false 171 | } 172 | 173 | return persistedMetadata.maximumBytes == maximumBytes 174 | && persistedMetadata.overwritesOldMessages == overwritesOldMessages 175 | } 176 | 177 | /// Writes header data to the file. 178 | private func writeHeaderData() throws -> FileHeader { 179 | let header = expectedHeader 180 | 181 | // Seek to the beginning of the file before writing the header. 182 | try handle.seek(to: 0) 183 | 184 | // Write the header to disk. 185 | try handle.write(data: header.asData) 186 | 187 | return header 188 | } 189 | 190 | private func memoizedMetadata() throws -> Metadata { 191 | if let persistedMetadata { 192 | persistedMetadata 193 | } else { 194 | try Metadata(headerData: readHeaderData()) 195 | } 196 | } 197 | 198 | private static let beginningOfHeaderField_offsetInFileOfOldestMessage = FileHeader.Field.offsetInFileOfOldestMessage.expectedBeginningOfFieldInFile 199 | private static let beginningOfHeaderField_offsetInFileAtEndOfNewestMessage = FileHeader.Field.offsetInFileAtEndOfNewestMessage.expectedBeginningOfFieldInFile 200 | 201 | // MARK: Metadata 202 | 203 | /// A snapshot of data read from a persisted file header. 204 | private struct Metadata { 205 | // MARK: Initialization 206 | 207 | init(fileHeader: FileHeader) { 208 | version = fileHeader.version 209 | maximumBytes = fileHeader.maximumBytes 210 | overwritesOldMessages = fileHeader.overwritesOldMessages 211 | } 212 | 213 | init(headerData: Data) throws { 214 | guard let fileHeader = FileHeader(from: headerData) else { 215 | // We can't read the header data. 216 | throw CacheAdvanceError.fileCorrupted 217 | } 218 | self = .init(fileHeader: fileHeader) 219 | } 220 | 221 | // MARK: Properties 222 | 223 | let version: UInt8 224 | let maximumBytes: UInt64 225 | let overwritesOldMessages: Bool 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/CacheReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/26/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | final class CacheReader { 21 | // MARK: Initialization 22 | 23 | /// Creates a new instance of the receiver. 24 | /// 25 | /// - Parameter file: The file URL indicating the desired location of the on-disk store. This file should already exist. 26 | init(forReadingFrom file: URL) throws { 27 | reader = try FileHandle(forReadingFrom: file) 28 | } 29 | 30 | deinit { 31 | try? reader.closeHandle() 32 | } 33 | 34 | // MARK: Internal 35 | 36 | var offsetInFileOfOldestMessage: UInt64 = 0 37 | var offsetInFileAtEndOfNewestMessage: UInt64 = 0 38 | 39 | var offsetInFile: UInt64 { 40 | reader.offsetInFile 41 | } 42 | 43 | /// Returns the encodable messages in a range 44 | /// 45 | /// - Parameter startOffset: the offset from which to start reading 46 | /// - Parameter endOffset: the offset at which to stop reading. If `nil`, the end offset will be the EOF 47 | func encodedMessagesFromOffset(_ startOffset: UInt64, endOffset: UInt64? = nil) throws -> [Data] { 48 | var encodedMessages = [Data]() 49 | try reader.seek(to: startOffset) 50 | while let data = try nextEncodedMessage() { 51 | encodedMessages.append(data) 52 | if let endOffset { 53 | if offsetInFile == endOffset { 54 | break 55 | } else if offsetInFile > endOffset { 56 | // The messages on disk are out of sync with our header data. 57 | throw CacheAdvanceError.fileCorrupted 58 | } 59 | } 60 | } 61 | if let endOffset, offsetInFile != endOffset { 62 | // If we finished reading messages but our offset in the file is less (or greater) than our expected ending offset, our header data is incorrect. 63 | throw CacheAdvanceError.fileCorrupted 64 | } 65 | return encodedMessages 66 | } 67 | 68 | /// Seeks to the beginning of the oldest message in the file. 69 | func seekToBeginningOfOldestMessage() throws { 70 | try reader.seek(to: offsetInFileOfOldestMessage) 71 | } 72 | 73 | /// Seeks to the next message. Returns `true` when the span skipped represented a message. 74 | func seekToNextMessage() throws { 75 | switch try nextEncodedMessageSpan() { 76 | case let .span(messageLength): 77 | // There's a valid message here. Seek ahead of it. 78 | try reader.seek(to: offsetInFile + UInt64(messageLength)) 79 | 80 | case .emptyRead: 81 | // We hit an empty read. Seek to the next message. 82 | try reader.seek(to: FileHeader.expectedEndOfHeaderInFile) 83 | 84 | case .invalidFormat: 85 | throw CacheAdvanceError.fileCorrupted 86 | } 87 | } 88 | 89 | // MARK: Private 90 | 91 | /// Returns the next encodable message, seeking to the beginning of the next message. 92 | private func nextEncodedMessage() throws -> Data? { 93 | switch try nextEncodedMessageSpan() { 94 | case let .span(messageLength): 95 | let message = try reader.readDataUp(toLength: Int(messageLength)) 96 | guard !message.isEmpty else { 97 | throw CacheAdvanceError.fileCorrupted 98 | } 99 | 100 | return message 101 | 102 | case .emptyRead: 103 | // An empty read means we hit the EOF. It is the responsibility of the calling code to validate this assumption. 104 | return nil 105 | 106 | case .invalidFormat: 107 | throw CacheAdvanceError.fileCorrupted 108 | } 109 | } 110 | 111 | /// Returns the next encoded message span, seeking to the end the span. 112 | private func nextEncodedMessageSpan() throws -> NextMessageSpan { 113 | let messageSizeData = try reader.readDataUp(toLength: MessageSpan.storageLength) 114 | 115 | guard !messageSizeData.isEmpty else { 116 | // We haven't written anything to this file yet, or we've reached the end of the file. 117 | return .emptyRead 118 | } 119 | 120 | guard messageSizeData.count == MessageSpan.storageLength else { 121 | // The file is improperly formatted. 122 | return .invalidFormat 123 | } 124 | 125 | return .span(MessageSpan(messageSizeData)) 126 | } 127 | 128 | private let reader: FileHandle 129 | } 130 | 131 | private enum NextMessageSpan { 132 | case span(MessageSpan) 133 | case emptyRead 134 | case invalidFormat 135 | } 136 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/EncodableMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/9/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | /// A struct that encodes a message of type T into data. 21 | /// A message is encoded with the following format: 22 | /// `[messageSize][data]` 23 | /// - `messageSize` is a big-endian encoded `MessageSpan` of length `messageSpanStorageLength`. 24 | /// - `data` is length `messageSize`. 25 | struct EncodableMessage { 26 | // MARK: Initialization 27 | 28 | /// Initializes an encoded message from the raw, codable source. 29 | /// - Parameters: 30 | /// - message: The messages to encode. 31 | /// - encoder: The encoder to use. 32 | init(message: T, encoder: MessageEncoder) { 33 | self.message = message 34 | self.encoder = encoder 35 | } 36 | 37 | // MARK: Internal 38 | 39 | /// The encoded message, prefixed with the size of the message blob. 40 | func encodedData() throws -> Data { 41 | let messageData = try encoder.encode(message) 42 | guard messageData.count < Size.max else { 43 | // We can't encode the length this message in a MessageSpan. 44 | throw CacheAdvanceError.messageLargerThanCacheCapacity 45 | } 46 | let encodedSize = Data(MessageSpan(messageData.count)) 47 | return encodedSize + messageData 48 | } 49 | 50 | // MARK: Private 51 | 52 | private let message: T 53 | private let encoder: MessageEncoder 54 | } 55 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/FileHandleExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/10/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | extension FileHandle { 21 | // MARK: Internal 22 | 23 | /// A method to read data from a file handle that is safe to call in Swift from any operation system version. 24 | func readDataUp(toLength length: Int) throws -> Data { 25 | #if os(Linux) 26 | if let data = try read(upToCount: length) { 27 | data 28 | } else { 29 | Data() 30 | } 31 | #else 32 | if #available(iOS 13.4, tvOS 13.4, watchOS 6.2, macOS 10.15.4, *) { 33 | if let data = try read(upToCount: length) { 34 | data 35 | } else { 36 | Data() 37 | } 38 | } else { 39 | try __readDataUp(toLength: length) 40 | } 41 | #endif 42 | } 43 | 44 | /// A method to write data to a file handle that is safe to call in Swift from any operation system version. 45 | func write(data: Data) throws { 46 | #if os(Linux) 47 | try write(contentsOf: data) 48 | #else 49 | if #available(iOS 13.4, tvOS 13.4, watchOS 6.2, macOS 10.15.4, *) { 50 | try write(contentsOf: data) 51 | } else { 52 | try __write(data, error: ()) 53 | } 54 | #endif 55 | } 56 | 57 | /// A method to seek on a file handle that is safe to call in Swift from any operation system version. 58 | func seek(to offset: UInt64) throws { 59 | try seek(toOffset: offset) 60 | } 61 | 62 | /// A method to close a file handle that is safe to call in Swift from any operation system version. 63 | func closeHandle() throws { 64 | try close() 65 | } 66 | 67 | /// A method to truncate a file handle that is safe to call in Swift from any operation system version. 68 | func truncate(at offset: UInt64) throws { 69 | try truncate(atOffset: offset) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/FileHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/26/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | struct FileHeader { 21 | // MARK: Lifecycle 22 | 23 | init( 24 | version: UInt8 = FileHeader.version, 25 | maximumBytes: Bytes, 26 | overwritesOldMessages: Bool, 27 | offsetInFileOfOldestMessage: UInt64, 28 | offsetInFileAtEndOfNewestMessage: UInt64 29 | ) { 30 | self.version = version 31 | self.maximumBytes = maximumBytes 32 | self.overwritesOldMessages = overwritesOldMessages 33 | self.offsetInFileOfOldestMessage = offsetInFileOfOldestMessage 34 | self.offsetInFileAtEndOfNewestMessage = offsetInFileAtEndOfNewestMessage 35 | } 36 | 37 | init?(from data: Data) { 38 | guard data.count == FileHeader.expectedEndOfHeaderInFile else { 39 | return nil 40 | } 41 | 42 | let versionData = data.subdata(in: FileHeader.Field.version.rangeOfFieldInFile) 43 | let maximumBytesData = data.subdata(in: FileHeader.Field.maximumBytes.rangeOfFieldInFile) 44 | let overwritesOldMessagesData = data.subdata(in: FileHeader.Field.overwriteOldMessages.rangeOfFieldInFile) 45 | let offsetInFileOfOldestMessageData = data.subdata(in: FileHeader.Field.offsetInFileOfOldestMessage.rangeOfFieldInFile) 46 | let offsetInFileAtEndOfNewestMessageData = data.subdata(in: FileHeader.Field.offsetInFileAtEndOfNewestMessage.rangeOfFieldInFile) 47 | 48 | let version = UInt8(versionData) 49 | let maximumBytes = Bytes(maximumBytesData) 50 | let overwritesOldMessages = Bool(overwritesOldMessagesData) 51 | let offsetInFileOfOldestMessage = UInt64(offsetInFileOfOldestMessageData) 52 | let offsetInFileAtEndOfNewestMessage = UInt64(offsetInFileAtEndOfNewestMessageData) 53 | 54 | self = FileHeader( 55 | version: version, 56 | maximumBytes: maximumBytes, 57 | overwritesOldMessages: overwritesOldMessages, 58 | offsetInFileOfOldestMessage: offsetInFileOfOldestMessage, 59 | offsetInFileAtEndOfNewestMessage: offsetInFileAtEndOfNewestMessage 60 | ) 61 | } 62 | 63 | // MARK: Internal 64 | 65 | let version: UInt8 66 | let maximumBytes: Bytes 67 | let overwritesOldMessages: Bool 68 | let offsetInFileOfOldestMessage: UInt64 69 | let offsetInFileAtEndOfNewestMessage: UInt64 70 | 71 | var asData: Data { 72 | Field.allCases.reduce(Data()) { header, field in 73 | header + data(for: field) 74 | } 75 | } 76 | 77 | /// The expected version of the header. 78 | /// Whenever there is a breaking change to the header format, update this value. 79 | static let version: UInt8 = 1 80 | 81 | /// Calculates the offset in the file where the header should end. 82 | static let expectedEndOfHeaderInFile = Field(rawValue: Field.allCases.endIndex)!.expectedEndOfFieldInFile 83 | 84 | func data(for field: Field) -> Data { 85 | switch field { 86 | case .version: 87 | Data(version) 88 | case .maximumBytes: 89 | Data(maximumBytes) 90 | case .overwriteOldMessages: 91 | Data(overwritesOldMessages) 92 | case .offsetInFileOfOldestMessage: 93 | Data(offsetInFileOfOldestMessage) 94 | case .offsetInFileAtEndOfNewestMessage: 95 | Data(offsetInFileAtEndOfNewestMessage) 96 | case .reservedSpace: 97 | Data(repeating: 0, count: field.storageLength) 98 | } 99 | } 100 | 101 | enum Field: Int, CaseIterable { 102 | // Header format: 103 | // [headerVersion:UInt8][maximumBytes:UInt64][overwritesOldMessages:Bool][offsetInFileOfOldestMessage:UInt64][offsetInFileAtEndOfNewestMessage:UInt64][reservedSpace] 104 | 105 | case version = 1 106 | case maximumBytes 107 | case overwriteOldMessages 108 | case offsetInFileOfOldestMessage 109 | case offsetInFileAtEndOfNewestMessage 110 | case reservedSpace 111 | 112 | var storageLength: Int { 113 | switch self { 114 | case .version: 115 | UInt8.storageLength 116 | case .maximumBytes: 117 | Bytes.storageLength 118 | case .overwriteOldMessages: 119 | Bool.storageLength 120 | case .offsetInFileOfOldestMessage: 121 | UInt64.storageLength 122 | case .offsetInFileAtEndOfNewestMessage: 123 | UInt64.storageLength 124 | case .reservedSpace: 125 | // Subtract from this value every time another additive field is added. 126 | // We currently have a total of 64 bytes reserved for the header. 127 | // We're currently using only 26 of them. 128 | 38 129 | } 130 | } 131 | 132 | var rangeOfFieldInFile: Range { 133 | Int(expectedBeginningOfFieldInFile)..(_ type: T.Type, from data: Data) throws -> T where T: Decodable 23 | } 24 | 25 | extension JSONDecoder: MessageDecoder {} 26 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/MessageEncoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 4/26/20. 3 | // Copyright © 2020 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | /// An object capable of encoding a message of type `T` to `Data`. 21 | public protocol MessageEncoder { 22 | func encode(_ value: some Encodable) throws -> Data 23 | } 24 | 25 | extension JSONEncoder: MessageEncoder {} 26 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/MessageSpan.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/10/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | /// A storage unit that measures message length. 21 | /// 22 | /// - Warning: If this value is changed, previously persisted message encodings will not be readable. 23 | typealias MessageSpan = UInt32 24 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/UInt32+BigEndianHostSwappable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/3/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | extension UInt32: BigEndianHostSwappable {} 21 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/UInt64+BigEndianHostSwappable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/3/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | extension UInt64: BigEndianHostSwappable {} 21 | -------------------------------------------------------------------------------- /Sources/CacheAdvance/UInt8+BigEndianHostSwappable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/25/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | extension UInt8: BigEndianHostSwappable {} 21 | -------------------------------------------------------------------------------- /Sources/LorumIpsum/LorumIpsumMessages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 4/23/20. 3 | // Copyright © 2020 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS"BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | 20 | public final class LorumIpsum: NSObject { 21 | public static let messages: [String] = [ 22 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 23 | "Arcu cursus euismod quis viverra nibh cras pulvinar.", 24 | "Et malesuada fames ac turpis.", 25 | "Maecenas accumsan lacus vel facilisis volutpat est velit egestas.", 26 | "Amet massa vitae tortor condimentum lacinia quis.", 27 | "At volutpat diam ut venenatis tellus in.", 28 | "Cras ornare arcu dui vivamus arcu felis bibendum ut.", 29 | "In arcu cursus euismod quis viverra nibh.", 30 | "Sodales neque sodales ut etiam sit.", 31 | "Risus commodo viverra maecenas accumsan lacus vel facilisis volutpat.", 32 | "Scelerisque varius morbi enim nunc faucibus a pellentesque sit amet.", 33 | "Condimentum vitae sapien pellentesque habitant morbi.", 34 | "Adipiscing enim eu turpis egestas pretium aenean pharetra magna.", 35 | "Eu turpis egestas pretium aenean pharetra magna ac.", 36 | "Eros in cursus turpis massa tincidunt dui ut ornare.", 37 | "Mi ipsum faucibus vitae aliquet nec ullamcorper sit amet risus.", 38 | "Dui id ornare arcu odio ut.", 39 | "Aliquet bibendum enim facilisis gravida neque convallis a cras.", 40 | "Blandit libero volutpat sed cras.", 41 | "Purus faucibus ornare suspendisse sed nisi lacus sed viverra tellus.", 42 | "Cursus turpis massa tincidunt dui.", 43 | "At tellus at urna condimentum mattis pellentesque id nibh tortor.", 44 | "Urna id volutpat lacus laoreet.", 45 | "Nunc scelerisque viverra mauris in aliquam sem.", 46 | "In cursus turpis massa tincidunt dui ut ornare lectus sit.", 47 | "Amet facilisis magna etiam tempor orci eu.", 48 | "Urna nec tincidunt praesent semper feugiat nibh sed pulvinar proin.", 49 | "Phasellus egestas tellus rutrum tellus pellentesque eu tincidunt tortor.", 50 | "Nulla pharetra diam sit amet nisl.", 51 | "Rhoncus mattis rhoncus urna neque viverra justo.", 52 | "Duis convallis convallis tellus id interdum velit laoreet id donec.", 53 | "Sagittis vitae et leo duis ut diam quam nulla.", 54 | "Sem viverra aliquet eget sit amet.", 55 | "At in tellus integer feugiat scelerisque varius morbi enim nunc.", 56 | "Mollis nunc sed id semper risus in.", 57 | "Elementum nibh tellus molestie nunc non.", 58 | "Auctor neque vitae tempus quam.", 59 | "Magna fermentum iaculis eu non diam phasellus vestibulum lorem sed.", 60 | "Justo eget magna fermentum iaculis eu.", 61 | "Sit amet nulla facilisi morbi tempus iaculis urna id.", 62 | "Convallis posuere morbi leo urna molestie at elementum eu facilisis.", 63 | "Ac tortor dignissim convallis aenean et tortor at.", 64 | "Arcu non sodales neque sodales ut etiam.", 65 | "Cras semper auctor neque vitae tempus.", 66 | "Est velit egestas dui id ornare arcu odio ut.", 67 | "Libero enim sed faucibus turpis in eu mi bibendum neque.", 68 | "Mauris sit amet massa vitae tortor condimentum lacinia quis vel.", 69 | "Justo donec enim diam vulputate ut.", 70 | "Convallis convallis tellus id interdum velit.", 71 | "Donec enim diam vulputate ut pharetra sit.", 72 | "Sed velit dignissim sodales ut eu sem.", 73 | "Libero enim sed faucibus turpis in eu.", 74 | "Senectus et netus et malesuada fames ac turpis egestas sed.", 75 | "Aliquet porttitor lacus luctus accumsan tortor.", 76 | "Aenean et tortor at risus.", 77 | "Convallis a cras semper auctor neque vitae.", 78 | "Ac orci phasellus egestas tellus.", 79 | "Iaculis urna id volutpat lacus laoreet non curabitur gravida.", 80 | "Nascetur ridiculus mus mauris vitae ultricies leo.", 81 | "Sed faucibus turpis in eu mi bibendum.", 82 | "Sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque.", 83 | "In dictum non consectetur a.", 84 | "Id diam maecenas ultricies mi.", 85 | "Venenatis lectus magna fringilla urna porttitor rhoncus dolor.", 86 | "Id ornare arcu odio ut sem.", 87 | "Arcu cursus euismod quis viverra.", 88 | "Pulvinar pellentesque habitant morbi tristique senectus.", 89 | "Blandit massa enim nec dui nunc mattis.", 90 | "Iaculis at erat pellentesque adipiscing commodo elit at imperdiet dui.", 91 | "Sit amet dictum sit amet justo donec enim diam vulputate.", 92 | "Id diam maecenas ultricies mi eget mauris.", 93 | "Faucibus pulvinar elementum integer enim neque volutpat.", 94 | "Rhoncus dolor purus non enim praesent elementum.", 95 | "Sollicitudin aliquam ultrices sagittis orci a scelerisque purus semper.", 96 | "Ipsum dolor sit amet consectetur.", 97 | "Diam quis enim lobortis scelerisque fermentum dui faucibus in.", 98 | "Fermentum iaculis eu non diam phasellus.", 99 | "Elit sed vulputate mi sit amet mauris commodo.", 100 | "Commodo elit at imperdiet dui accumsan sit amet nulla.", 101 | "Etiam dignissim diam quis enim lobortis.", 102 | "In iaculis nunc sed augue lacus viverra.", 103 | "Tempus imperdiet nulla malesuada pellentesque elit eget.", 104 | "Quis lectus nulla at volutpat.", 105 | "Tellus rutrum tellus pellentesque eu tincidunt tortor.", 106 | "Tristique senectus et netus et.", 107 | "Quis lectus nulla at volutpat diam ut venenatis tellus in.", 108 | "Nisi lacus sed viverra tellus in hac habitasse platea dictumst.", 109 | "Justo eget magna fermentum iaculis eu non diam phasellus.", 110 | "Vehicula ipsum a arcu cursus vitae congue mauris.", 111 | "Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac.", 112 | "Purus faucibus ornare suspendisse sed nisi lacus sed viverra.", 113 | "At risus viverra adipiscing at in tellus integer feugiat scelerisque.", 114 | "Suspendisse interdum consectetur libero id faucibus nisl.", 115 | "Sit amet venenatis urna cursus eget nunc scelerisque viverra mauris.", 116 | "Dui nunc mattis enim ut tellus elementum sagittis vitae et.", 117 | "Sed id semper risus in hendrerit.", 118 | "Dignissim cras tincidunt lobortis feugiat vivamus at augue eget arcu.", 119 | "Amet nisl suscipit adipiscing bibendum est ultricies integer.", 120 | "Mi in nulla posuere sollicitudin aliquam.", 121 | "Tempus imperdiet nulla malesuada pellentesque elit eget gravida cum.", 122 | "Quisque non tellus orci ac auctor augue mauris augue neque.", 123 | "Sapien nec sagittis aliquam malesuada.", 124 | "Tristique risus nec feugiat in fermentum posuere.", 125 | "Bibendum est ultricies integer quis.", 126 | "Non odio euismod lacinia at quis risus.", 127 | "Tortor dignissim convallis aenean et tortor at risus viverra.", 128 | "Vitae et leo duis ut diam quam nulla.", 129 | "Sit amet nisl suscipit adipiscing.", 130 | "Interdum varius sit amet mattis vulputate enim nulla aliquet.", 131 | "Placerat vestibulum lectus mauris ultrices eros in.", 132 | "Mattis pellentesque id nibh tortor id.", 133 | "At lectus urna duis convallis convallis tellus.", 134 | "Malesuada fames ac turpis egestas sed.", 135 | "Amet mauris commodo quis imperdiet massa tincidunt nunc pulvinar sapien.", 136 | "At augue eget arcu dictum varius duis at.", 137 | "Non enim praesent elementum facilisis.", 138 | "Et sollicitudin ac orci phasellus egestas tellus.", 139 | "Tortor vitae purus faucibus ornare.", 140 | "A cras semper auctor neque vitae.", 141 | "Egestas purus viverra accumsan in nisl nisi.", 142 | "Etiam tempor orci eu lobortis elementum nibh tellus molestie.", 143 | "Ac turpis egestas sed tempus urna et pharetra pharetra massa.", 144 | "Vitae semper quis lectus nulla at.", 145 | "Varius quam quisque id diam vel.", 146 | "Mattis enim ut tellus elementum sagittis.", 147 | "Vel orci porta non pulvinar.", 148 | "Eget aliquet nibh praesent tristique magna sit amet purus gravida.", 149 | "Suscipit tellus mauris a diam maecenas.", 150 | "Elementum curabitur vitae nunc sed.", 151 | "Lacinia at quis risus sed vulputate odio ut enim.", 152 | "Faucibus nisl tincidunt eget nullam.", 153 | "Sed felis eget velit aliquet.", 154 | "Mauris nunc congue nisi vitae suscipit tellus mauris a diam.", 155 | "Sit amet nisl suscipit adipiscing bibendum.", 156 | "Ut faucibus pulvinar elementum integer enim.", 157 | "Id diam vel quam elementum pulvinar etiam non quam.", 158 | "Massa vitae tortor condimentum lacinia.", 159 | "Vulputate sapien nec sagittis aliquam malesuada.", 160 | "Urna porttitor rhoncus dolor purus non.", 161 | "Nec nam aliquam sem et tortor consequat.", 162 | "Senectus et netus et malesuada fames ac turpis egestas integer.", 163 | "Eu volutpat odio facilisis mauris sit amet massa vitae tortor.", 164 | "Magna ac placerat vestibulum lectus mauris ultrices eros in cursus.", 165 | "Nec feugiat in fermentum posuere urna.", 166 | "Risus commodo viverra maecenas accumsan lacus vel facilisis volutpat.", 167 | "Justo donec enim diam vulputate ut pharetra.", 168 | "Amet justo donec enim diam vulputate ut pharetra.", 169 | "Ipsum a arcu cursus vitae congue.", 170 | "Convallis convallis tellus id interdum velit laoreet.", 171 | "Vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet.", 172 | "Non consectetur a erat nam.", 173 | "Proin sed libero enim sed faucibus turpis in.", 174 | "Sed nisi lacus sed viverra tellus.", 175 | "Suscipit adipiscing bibendum est ultricies integer quis.", 176 | "Tincidunt praesent semper feugiat nibh sed pulvinar proin.", 177 | "Tristique nulla aliquet enim tortor at auctor urna nunc id.", 178 | "Lacus vel facilisis volutpat est velit egestas dui.", 179 | "Sit amet cursus sit amet.", 180 | "Vestibulum lorem sed risus ultricies tristique nulla aliquet.", 181 | "Nec ullamcorper sit amet risus nullam eget felis eget nunc.", 182 | "Justo eget magna fermentum iaculis eu non diam phasellus vestibulum.", 183 | "Placerat duis ultricies lacus sed turpis tincidunt.", 184 | "Duis tristique sollicitudin nibh sit amet commodo nulla facilisi nullam.", 185 | "Id eu nisl nunc mi ipsum faucibus vitae aliquet nec.", 186 | "At quis risus sed vulputate odio ut enim.", 187 | "Consectetur lorem donec massa sapien faucibus.", 188 | "Vitae purus faucibus ornare suspendisse sed nisi lacus.", 189 | "Pellentesque sit amet porttitor eget dolor morbi non arcu risus.", 190 | "Neque gravida in fermentum et sollicitudin ac orci.", 191 | "Lobortis feugiat vivamus at augue eget arcu dictum varius.", 192 | "Felis bibendum ut tristique et egestas quis ipsum.", 193 | "Tortor aliquam nulla facilisi cras fermentum odio eu feugiat.", 194 | "Amet mauris commodo quis imperdiet massa tincidunt nunc pulvinar.", 195 | "Egestas pretium aenean pharetra magna ac placerat.", 196 | "Massa vitae tortor condimentum lacinia quis vel eros donec.", 197 | "Sed odio morbi quis commodo odio aenean.", 198 | "Suspendisse sed nisi lacus sed viverra tellus in.", 199 | "Nam aliquam sem et tortor consequat id porta nibh venenatis.", 200 | "Congue nisi vitae suscipit tellus.", 201 | "Vitae ultricies leo integer malesuada nunc vel risus commodo.", 202 | "At tempor commodo ullamcorper a lacus vestibulum sed.", 203 | "Pellentesque habitant morbi tristique senectus et netus et malesuada fames.", 204 | "Id velit ut tortor pretium viverra suspendisse potenti nullam.", 205 | "Euismod lacinia at quis risus sed vulputate odio ut enim.", 206 | "Nibh ipsum consequat nisl vel pretium lectus.", 207 | "Pretium lectus quam id leo in.", 208 | "Nullam vehicula ipsum a arcu cursus vitae congue.", 209 | "Habitasse platea dictumst vestibulum rhoncus.", 210 | "Morbi tempus iaculis urna id volutpat lacus laoreet non.", 211 | "Elit pellentesque habitant morbi tristique senectus et netus et malesuada.", 212 | "Vitae suscipit tellus mauris a diam maecenas.", 213 | "Morbi tristique senectus et netus et.", 214 | "Pulvinar pellentesque habitant morbi tristique senectus et.", 215 | "Bibendum neque egestas congue quisque egestas diam in.", 216 | "Id aliquet risus feugiat in ante.", 217 | "Magna fringilla urna porttitor rhoncus dolor purus non.", 218 | "Volutpat lacus laoreet non curabitur gravida arcu ac tortor.", 219 | "Sit amet consectetur adipiscing elit pellentesque habitant morbi tristique.", 220 | "Laoreet suspendisse interdum consectetur libero id faucibus.", 221 | "Bibendum at varius vel pharetra vel turpis.", 222 | "Magna etiam tempor orci eu lobortis elementum.", 223 | "Leo urna molestie at elementum eu facilisis sed odio morbi.", 224 | "Nibh venenatis cras sed felis.", 225 | "Hac habitasse platea dictumst quisque sagittis purus sit amet.", 226 | "Tellus rutrum tellus pellentesque eu tincidunt.", 227 | "Sapien et ligula ullamcorper malesuada proin libero nunc consequat.", 228 | "Cursus in hac habitasse platea dictumst.", 229 | "Egestas sed sed risus pretium quam vulputate.", 230 | "Ullamcorper a lacus vestibulum sed arcu non odio.", 231 | "Ultrices in iaculis nunc sed augue lacus viverra.", 232 | "Amet nulla facilisi morbi tempus iaculis urna id.", 233 | "Nulla facilisi cras fermentum odio eu feugiat pretium.", 234 | "Elementum nisi quis eleifend quam adipiscing.", 235 | "Aliquet eget sit amet tellus cras.", 236 | "In massa tempor nec feugiat nisl pretium.", 237 | "Tristique risus nec feugiat in fermentum posuere urna nec tincidunt.", 238 | "Integer vitae justo eget magna fermentum iaculis.", 239 | "Dignissim diam quis enim lobortis scelerisque fermentum.", 240 | "Id nibh tortor id aliquet lectus proin.", 241 | "Lacus sed turpis tincidunt id aliquet.", 242 | "Vitae justo eget magna fermentum iaculis eu non diam phasellus.", 243 | "Orci porta non pulvinar neque.", 244 | "Et molestie ac feugiat sed lectus vestibulum mattis ullamcorper.", 245 | "Posuere urna nec tincidunt praesent semper feugiat nibh.", 246 | "Tristique magna sit amet purus gravida quis blandit turpis cursus.", 247 | "Mattis molestie a iaculis at erat.", 248 | "Eget nunc lobortis mattis aliquam faucibus purus.", 249 | "Nunc aliquet bibendum enim facilisis gravida.", 250 | "Nunc sed augue lacus viverra vitae congue.", 251 | "Arcu dui vivamus arcu felis bibendum ut tristique et egestas.", 252 | "Facilisi morbi tempus iaculis urna id volutpat lacus laoreet non.", 253 | "Commodo odio aenean sed adipiscing diam donec adipiscing.", 254 | "Nunc scelerisque viverra mauris in aliquam sem fringilla ut morbi.", 255 | "Non diam phasellus vestibulum lorem sed risus ultricies tristique.", 256 | "Urna et pharetra pharetra massa massa ultricies mi quis hendrerit.", 257 | "Diam ut venenatis tellus in metus vulputate eu scelerisque.", 258 | "Tempus urna et pharetra pharetra massa massa ultricies mi.", 259 | "Urna nunc id cursus metus.", 260 | "Gravida neque convallis a cras.", 261 | "Vitae turpis massa sed elementum tempus egestas sed sed.", 262 | "Facilisi cras fermentum odio eu.", 263 | "Consequat nisl vel pretium lectus quam id leo in vitae.", 264 | "Mi quis hendrerit dolor magna.", 265 | "Sed risus ultricies tristique nulla aliquet enim tortor at.", 266 | "Praesent semper feugiat nibh sed pulvinar proin.", 267 | "Enim sed faucibus turpis in.", 268 | "Bibendum est ultricies integer quis auctor elit sed vulputate.", 269 | "Orci sagittis eu volutpat odio facilisis mauris sit.", 270 | "Mi quis hendrerit dolor magna eget.", 271 | "Eu scelerisque felis imperdiet proin fermentum leo.", 272 | "Integer eget aliquet nibh praesent tristique magna sit amet.", 273 | "Arcu dictum varius duis at consectetur lorem.", 274 | "Sed felis eget velit aliquet.", 275 | "Ullamcorper velit sed ullamcorper morbi tincidunt ornare.", 276 | "Dui accumsan sit amet nulla facilisi morbi tempus iaculis.", 277 | "Senectus et netus et malesuada fames ac turpis.", 278 | "Pulvinar etiam non quam lacus.", 279 | "Sollicitudin ac orci phasellus egestas.", 280 | "Eu facilisis sed odio morbi quis commodo odio aenean sed.", 281 | "Maecenas ultricies mi eget mauris.", 282 | "Sed adipiscing diam donec adipiscing tristique risus nec feugiat.", 283 | "Orci eu lobortis elementum nibh.", 284 | "Libero id faucibus nisl tincidunt eget nullam non nisi est.", 285 | "Dictum at tempor commodo ullamcorper a lacus.", 286 | "Pretium lectus quam id leo in vitae turpis massa.", 287 | "Quisque egestas diam in arcu cursus.", 288 | "Lectus arcu bibendum at varius vel pharetra vel turpis.", 289 | "Posuere lorem ipsum dolor sit amet.", 290 | "Nunc sed augue lacus viverra vitae congue.", 291 | "Vel quam elementum pulvinar etiam non quam.", 292 | "Leo vel orci porta non.", 293 | "Porttitor leo a diam sollicitudin.", 294 | "Molestie nunc non blandit massa enim nec dui nunc.", 295 | "Aliquet nec ullamcorper sit amet risus nullam eget felis.", 296 | "Feugiat nisl pretium fusce id velit ut tortor.", 297 | "Sapien nec sagittis aliquam malesuada bibendum arcu vitae elementum curabitur.", 298 | "Parturient montes nascetur ridiculus mus mauris vitae ultricies.", 299 | "Congue mauris rhoncus aenean vel elit scelerisque mauris pellentesque pulvinar.", 300 | "Donec massa sapien faucibus et molestie.", 301 | "At auctor urna nunc id.", 302 | "Fermentum iaculis eu non diam phasellus.", 303 | "Imperdiet proin fermentum leo vel orci.", 304 | "Non consectetur a erat nam.", 305 | "Elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus at.", 306 | "Nibh praesent tristique magna sit amet purus gravida quis.", 307 | "Lacus sed turpis tincidunt id aliquet risus feugiat in ante.", 308 | "Urna id volutpat lacus laoreet.", 309 | "Amet massa vitae tortor condimentum lacinia quis vel.", 310 | "Eget nullam non nisi est sit amet facilisis magna etiam.", 311 | "Quam elementum pulvinar etiam non quam lacus suspendisse faucibus.", 312 | "Nisi quis eleifend quam adipiscing.", 313 | "Ac turpis egestas maecenas pharetra convallis posuere morbi leo.", 314 | "Convallis a cras semper auctor neque vitae tempus.", 315 | "Tincidunt augue interdum velit euismod in pellentesque massa.", 316 | "Purus gravida quis blandit turpis cursus in hac habitasse.", 317 | "Sed egestas egestas fringilla phasellus faucibus scelerisque.", 318 | "Scelerisque in dictum non consectetur a erat nam at lectus.", 319 | "Nunc sed blandit libero volutpat sed cras.", 320 | "Risus at ultrices mi tempus imperdiet nulla malesuada pellentesque.", 321 | "Egestas erat imperdiet sed euismod nisi.", 322 | "Euismod quis viverra nibh cras pulvinar mattis nunc.", 323 | "Sapien et ligula ullamcorper malesuada proin libero nunc consequat interdum.", 324 | "Quis hendrerit dolor magna eget est lorem ipsum dolor.", 325 | "Aenean vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi.", 326 | "Enim lobortis scelerisque fermentum dui.", 327 | "Vulputate sapien nec sagittis aliquam malesuada bibendum arcu.", 328 | "Mattis ullamcorper velit sed ullamcorper morbi tincidunt.", 329 | "Donec pretium vulputate sapien nec sagittis aliquam malesuada.", 330 | "Eget egestas purus viverra accumsan in nisl nisi scelerisque eu.", 331 | "Massa tempor nec feugiat nisl pretium.", 332 | "Turpis cursus in hac habitasse.", 333 | "Morbi tristique senectus et netus et malesuada.", 334 | "Consequat semper viverra nam libero justo laoreet sit amet cursus.", 335 | "Velit egestas dui id ornare.", 336 | "Mi quis hendrerit dolor magna eget est.", 337 | "Ac turpis egestas sed tempus urna et pharetra.", 338 | "Vitae nunc sed velit dignissim sodales ut eu sem integer.", 339 | "Tincidunt ornare massa eget egestas purus viverra accumsan.", 340 | "Faucibus a pellentesque sit amet porttitor eget.", 341 | "In ante metus dictum at tempor commodo.", 342 | "Elementum curabitur vitae nunc sed velit dignissim.", 343 | "Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus in.", 344 | "Nulla at volutpat diam ut venenatis tellus in.", 345 | "Amet dictum sit amet justo donec enim.", 346 | "Massa id neque aliquam vestibulum.", 347 | "Facilisi nullam vehicula ipsum a.", 348 | "Neque volutpat ac tincidunt vitae semper quis.", 349 | "Dictum sit amet justo donec enim diam vulputate ut.", 350 | "Est velit egestas dui id ornare arcu.", 351 | "Ultricies mi quis hendrerit dolor magna.", 352 | "Maecenas volutpat blandit aliquam etiam erat velit scelerisque in dictum.", 353 | "Ut lectus arcu bibendum at.", 354 | "Enim ut tellus elementum sagittis.", 355 | "Cras adipiscing enim eu turpis egestas pretium aenean pharetra magna.", 356 | "A arcu cursus vitae congue mauris rhoncus aenean vel elit.", 357 | "Bibendum neque egestas congue quisque egestas diam in arcu cursus.", 358 | "Nulla facilisi cras fermentum odio eu feugiat.", 359 | "Porttitor leo a diam sollicitudin tempor id eu nisl nunc.", 360 | "Ac placerat vestibulum lectus mauris ultrices eros in.", 361 | "Vulputate eu scelerisque felis imperdiet proin fermentum leo vel.", 362 | "Urna porttitor rhoncus dolor purus non enim praesent elementum.", 363 | "At imperdiet dui accumsan sit.", 364 | "Condimentum mattis pellentesque id nibh tortor id aliquet lectus.", 365 | "Augue interdum velit euismod in pellentesque massa placerat.", 366 | "Mauris pellentesque pulvinar pellentesque habitant morbi tristique.", 367 | "Platea dictumst quisque sagittis purus sit amet volutpat.", 368 | "Volutpat lacus laoreet non curabitur gravida.", 369 | "Fringilla ut morbi tincidunt augue interdum velit.", 370 | "Arcu felis bibendum ut tristique et egestas.", 371 | "Quis viverra nibh cras pulvinar mattis nunc.", 372 | "Vel quam elementum pulvinar etiam non.", 373 | "A scelerisque purus semper eget duis at tellus at urna.", 374 | "Lectus magna fringilla urna porttitor rhoncus dolor purus.", 375 | "Consequat id porta nibh venenatis.", 376 | "Sed faucibus turpis in eu mi bibendum neque egestas congue.", 377 | "Enim ut sem viverra aliquet eget sit amet tellus cras.", 378 | "Viverra nibh cras pulvinar mattis nunc sed blandit libero.", 379 | "Duis at tellus at urna condimentum mattis pellentesque id nibh.", 380 | "Sed vulputate mi sit amet mauris commodo.", 381 | "Turpis cursus in hac habitasse platea dictumst.", 382 | "Id interdum velit laoreet id donec ultrices tincidunt.", 383 | "Nunc consequat interdum varius sit.", 384 | "Ut aliquam purus sit amet luctus venenatis lectus.", 385 | "Ultrices eros in cursus turpis.", 386 | "Mattis vulputate enim nulla aliquet.", 387 | "Amet porttitor eget dolor morbi non arcu risus quis varius.", 388 | "Urna et pharetra pharetra massa massa.", 389 | "Posuere ac ut consequat semper viverra nam.", 390 | "Hac habitasse platea dictumst vestibulum rhoncus est pellentesque.", 391 | "Faucibus nisl tincidunt eget nullam non.", 392 | "Blandit massa enim nec dui.", 393 | "Sed adipiscing diam donec adipiscing.", 394 | "Mauris rhoncus aenean vel elit scelerisque.", 395 | "Enim ut sem viverra aliquet eget sit amet.", 396 | "Fermentum iaculis eu non diam phasellus vestibulum.", 397 | "Lacus sed turpis tincidunt id aliquet.", 398 | "Cras ornare arcu dui vivamus arcu.", 399 | "Mi bibendum neque egestas congue quisque egestas diam in.", 400 | "Elementum pulvinar etiam non quam lacus suspendisse.", 401 | "Eget mauris pharetra et ultrices neque ornare aenean.", 402 | "In fermentum posuere urna nec tincidunt praesent semper feugiat nibh.", 403 | "Phasellus egestas tellus rutrum tellus pellentesque eu.", 404 | "Ipsum dolor sit amet consectetur adipiscing elit ut.", 405 | "Tristique senectus et netus et malesuada fames ac.", 406 | "Lacus vestibulum sed arcu non odio euismod lacinia.", 407 | "Orci sagittis eu volutpat odio facilisis.", 408 | "Et tortor consequat id porta nibh venenatis cras sed felis.", 409 | "Lorem ipsum dolor sit amet consectetur adipiscing.", 410 | "Odio tempor orci dapibus ultrices in iaculis nunc sed augue.", 411 | "Interdum consectetur libero id faucibus.", 412 | "Nunc sed id semper risus.", 413 | "Varius duis at consectetur lorem donec massa sapien faucibus.", 414 | "Nisl rhoncus mattis rhoncus urna.", 415 | "Aliquam id diam maecenas ultricies mi eget mauris pharetra.", 416 | "Ut placerat orci nulla pellentesque dignissim enim sit amet.", 417 | "Aenean et tortor at risus.", 418 | "Tempor orci dapibus ultrices in iaculis nunc sed.", 419 | "Non tellus orci ac auctor augue mauris augue neque gravida.", 420 | "Porttitor eget dolor morbi non arcu risus quis varius.", 421 | "Dictum non consectetur a erat nam at lectus urna.", 422 | "Pellentesque elit eget gravida cum sociis natoque.", 423 | "Pretium nibh ipsum consequat nisl vel pretium lectus quam.", 424 | "Mi bibendum neque egestas congue quisque egestas diam in.", 425 | "A cras semper auctor neque vitae tempus.", 426 | "Sollicitudin tempor id eu nisl nunc mi ipsum.", 427 | "Elementum tempus egestas sed sed risus pretium quam vulputate.", 428 | "Pharetra massa massa ultricies mi quis.", 429 | "Diam ut venenatis tellus in metus vulputate eu scelerisque felis.", 430 | "Eget aliquet nibh praesent tristique magna sit amet purus.", 431 | "Amet volutpat consequat mauris nunc congue nisi.", 432 | "Venenatis lectus magna fringilla urna porttitor.", 433 | "Vitae proin sagittis nisl rhoncus.", 434 | "Turpis egestas integer eget aliquet nibh.", 435 | "Nisl suscipit adipiscing bibendum est ultricies integer quis auctor.", 436 | "Massa sed elementum tempus egestas sed sed risus.", 437 | "Odio pellentesque diam volutpat commodo sed egestas egestas.", 438 | "Id cursus metus aliquam eleifend mi in nulla posuere sollicitudin.", 439 | "Massa tincidunt nunc pulvinar sapien.", 440 | "Sit amet justo donec enim diam vulputate ut.", 441 | "Aliquam ultrices sagittis orci a scelerisque purus.", 442 | "Eget nunc scelerisque viverra mauris in aliquam.", 443 | "Amet est placerat in egestas erat.", 444 | "Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras.", 445 | "Integer malesuada nunc vel risus commodo viverra maecenas accumsan lacus.", 446 | "Integer quis auctor elit sed.", 447 | "Sit amet facilisis magna etiam tempor.", 448 | "Pellentesque habitant morbi tristique senectus et netus.", 449 | "Facilisis gravida neque convallis a cras semper.", 450 | "Et netus et malesuada fames ac.", 451 | "Enim lobortis scelerisque fermentum dui.", 452 | "Vivamus arcu felis bibendum ut tristique et egestas quis.", 453 | "Nunc mi ipsum faucibus vitae.", 454 | "Volutpat sed cras ornare arcu dui vivamus arcu felis.", 455 | "Morbi tristique senectus et netus et malesuada fames ac.", 456 | "Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique.", 457 | "Tincidunt id aliquet risus feugiat in ante metus dictum at.", 458 | "Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac.", 459 | "Imperdiet dui accumsan sit amet nulla facilisi morbi tempus.", 460 | "Vulputate odio ut enim blandit volutpat maecenas volutpat blandit aliquam.", 461 | "Semper risus in hendrerit gravida.", 462 | "Sit amet nulla facilisi morbi tempus iaculis.", 463 | "Gravida in fermentum et sollicitudin ac orci phasellus.", 464 | "Viverra tellus in hac habitasse platea.", 465 | "Viverra orci sagittis eu volutpat odio facilisis mauris.", 466 | "Risus feugiat in ante metus dictum at tempor commodo ullamcorper.", 467 | "Non odio euismod lacinia at quis risus.", 468 | "Vel fringilla est ullamcorper eget nulla facilisi etiam dignissim diam.", 469 | "Sed sed risus pretium quam vulputate dignissim suspendisse.", 470 | "Odio eu feugiat pretium nibh ipsum consequat nisl vel pretium.", 471 | "Et netus et malesuada fames.", 472 | "Facilisis volutpat est velit egestas dui id ornare.", 473 | "Nec ullamcorper sit amet risus nullam eget.", 474 | "Pellentesque habitant morbi tristique senectus et netus et.", 475 | "Sit amet nulla facilisi morbi tempus iaculis.", 476 | "Et egestas quis ipsum suspendisse ultrices.", 477 | "Faucibus turpis in eu mi.", 478 | "Eu consequat ac felis donec.", 479 | "Id consectetur purus ut faucibus pulvinar elementum integer enim.", 480 | "Scelerisque viverra mauris in aliquam sem fringilla ut.", 481 | "Viverra maecenas accumsan lacus vel facilisis.", 482 | "Sit amet luctus venenatis lectus magna fringilla.", 483 | "Purus ut faucibus pulvinar elementum integer.", 484 | "Risus nec feugiat in fermentum posuere urna nec.", 485 | "Vitae elementum curabitur vitae nunc.", 486 | "Dictum fusce ut placerat orci nulla pellentesque dignissim.", 487 | "Et tortor at risus viverra adipiscing.", 488 | "Ut ornare lectus sit amet est.", 489 | "Quisque id diam vel quam elementum pulvinar.", 490 | "Sed odio morbi quis commodo odio aenean sed.", 491 | "Convallis aenean et tortor at risus viverra adipiscing.", 492 | "Libero volutpat sed cras ornare arcu dui vivamus.", 493 | "Nulla malesuada pellentesque elit eget.", 494 | "Id porta nibh venenatis cras sed felis eget.", 495 | "Semper auctor neque vitae tempus quam pellentesque.", 496 | "Auctor neque vitae tempus quam pellentesque.", 497 | "Maecenas ultricies mi eget mauris pharetra et ultrices neque ornare.", 498 | "In nulla posuere sollicitudin aliquam ultrices sagittis.", 499 | "Urna nunc id cursus metus aliquam eleifend mi.", 500 | "Vitae congue eu consequat ac felis donec et.", 501 | "Eu turpis egestas pretium aenean pharetra magna.", 502 | ] 503 | } 504 | 505 | #if !os(Linux) 506 | @objc 507 | public final class CADLorumIpsum: NSObject { 508 | @objc public static let messages = LorumIpsum.messages 509 | } 510 | #endif 511 | -------------------------------------------------------------------------------- /Tests/CADCacheAdvanceTests/CADCacheAdvanceTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 4/24/20. 3 | // Copyright © 2020 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS"BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | #if GENERATED_XCODE_PROJECT 19 | // This import works when working within the generated CacheAdvance.xcodeproj used in CI. 20 | #import 21 | #import 22 | #else 23 | // This import works when working within Package.swift. 24 | @import CADCacheAdvance; 25 | @import LorumIpsum; 26 | #endif 27 | @import XCTest; 28 | 29 | /// Tests that exercise the API of CADCacheAdvance. 30 | /// - Note: Since CADCacheAdvance is a thin wrapper on CacheAdvance, these tests are intended to do nothing more than exercise the API. 31 | @interface CADCacheAdvanceTests : XCTestCase 32 | @end 33 | 34 | @implementation CADCacheAdvanceTests 35 | 36 | // MARK: Behavior Tests 37 | 38 | - (void)test_fileURL_returnsUnderlyingFileURL; 39 | { 40 | CADCacheAdvance *const cache = [self createCacheThatOverwitesOldMessages:YES]; 41 | XCTAssertEqualObjects(cache.fileURL, [self testFileLocation]); 42 | } 43 | 44 | - (void)test_isWritable_returnsTrueForAWritableCache; 45 | { 46 | CADCacheAdvance *const cache = [self createCacheThatOverwitesOldMessages:YES]; 47 | XCTAssertTrue(cache.isWritable); 48 | } 49 | 50 | - (void)test_isEmpty_returnsTrueForAnEmptyCache; 51 | { 52 | CADCacheAdvance *const cache = [self createCacheThatOverwitesOldMessages:YES]; 53 | XCTAssertTrue(cache.isEmpty); 54 | } 55 | 56 | - (void)test_isEmpty_returnsFalseForANonEmptyCache; 57 | { 58 | CADCacheAdvance *const cache = [self createCacheThatOverwitesOldMessages:YES]; 59 | NSError *error = nil; 60 | [cache appendMessage:[@"Test" dataUsingEncoding:NSUTF8StringEncoding] 61 | error:&error]; 62 | XCTAssertNil(error); 63 | XCTAssertFalse(cache.isEmpty); 64 | } 65 | 66 | - (void)test_messagesAndReturnError_returnsAppendedMessage; 67 | { 68 | CADCacheAdvance *const cache = [self createCacheThatOverwitesOldMessages:YES]; 69 | NSData *const message = [@"Test" dataUsingEncoding:NSUTF8StringEncoding]; 70 | NSError *error = nil; 71 | [cache appendMessage:message 72 | error:&error]; 73 | XCTAssertNil(error); 74 | XCTAssertEqualObjects([cache messagesAndReturnError:nil], @[message]); 75 | } 76 | 77 | // MARK: Performance Tests 78 | 79 | - (void)test_performance_append_fillableCache; 80 | { 81 | CADCacheAdvance *const cache = [self createCacheThatOverwitesOldMessages:NO]; 82 | // Fill the cache before the test starts. 83 | for (NSString *const message in CADLorumIpsum.messages) { 84 | [cache appendMessage:[message dataUsingEncoding:NSUTF8StringEncoding] 85 | error:nil]; 86 | } 87 | 88 | [self measureBlock:^{ 89 | for (NSString *const message in CADLorumIpsum.messages) { 90 | [cache appendMessage:[message dataUsingEncoding:NSUTF8StringEncoding] 91 | error:nil]; 92 | } 93 | }]; 94 | } 95 | 96 | - (void)test_performance_messages_fillableCache; 97 | { 98 | CADCacheAdvance *const cache = [self createCacheThatOverwitesOldMessages:NO]; 99 | // Fill the cache before the test starts. 100 | for (NSString *const message in CADLorumIpsum.messages) { 101 | [cache appendMessage:[message dataUsingEncoding:NSUTF8StringEncoding] 102 | error:nil]; 103 | } 104 | 105 | [self measureBlock:^{ 106 | (void)[cache messagesAndReturnError:nil]; 107 | }]; 108 | } 109 | 110 | // MARK: Private 111 | 112 | - (CADCacheAdvance *)createCacheThatOverwitesOldMessages:(BOOL)overwritesOldMessages; 113 | { 114 | [NSFileManager.defaultManager createFileAtPath:[self testFileLocation].path 115 | contents:nil 116 | attributes:nil]; 117 | NSError *error = nil; 118 | CADCacheAdvance *const cache = [[CADCacheAdvance alloc] 119 | initWithFileURL:self.testFileLocation 120 | maximumBytes:1000000 121 | shouldOverwriteOldMessages:overwritesOldMessages 122 | error:&error]; 123 | XCTAssertNil(error, "Failed to create cache due to %@", error); 124 | return cache; 125 | } 126 | 127 | - (NSURL *)testFileLocation; 128 | { 129 | return [NSFileManager.defaultManager.temporaryDirectory URLByAppendingPathComponent:@"CADCacheAdvanceTests"]; 130 | } 131 | 132 | @end 133 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/BigEndianHostSwappableTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/3/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | import XCTest 20 | 21 | @testable import CacheAdvance 22 | 23 | final class BigEndianHostSwappableTests: XCTestCase { 24 | // MARK: Behavior Tests 25 | 26 | func test_init_canBeInitializedFromEncodedData() { 27 | let expectedValue: UInt64 = 10 28 | XCTAssertEqual(UInt64(Data(expectedValue)), expectedValue) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/BoolExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/27/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | import XCTest 20 | 21 | @testable import CacheAdvance 22 | 23 | final class BoolExtensionsTests: XCTestCase { 24 | // MARK: Behavior Tests 25 | 26 | func test_init_canBeInitializedFromEncodedData() { 27 | XCTAssertEqual(Bool(Data(true)), true) 28 | XCTAssertEqual(Bool(Data(false)), false) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/CacheAdvanceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/9/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | // http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS"BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | import XCTest 20 | 21 | @testable import CacheAdvance 22 | 23 | final class CacheAdvanceTests: XCTestCase { 24 | override func setUp() { 25 | super.setUp() 26 | clearCacheFile() 27 | } 28 | 29 | // MARK: Behavior Tests 30 | 31 | func test_isEmpty_returnsTrueWhenCacheIsEmpty() throws { 32 | let cache = try createCache(overwritesOldMessages: false) 33 | 34 | XCTAssertTrue(try cache.isEmpty()) 35 | } 36 | 37 | func test_isEmpty_returnsFalseWhenCacheHasASingleMessage() throws { 38 | let message: TestableMessage = "This is a test" 39 | let cache = try createCache(sizedToFit: [message], overwritesOldMessages: false) 40 | try cache.append(message: message) 41 | 42 | XCTAssertFalse(try cache.isEmpty()) 43 | } 44 | 45 | func test_isEmpty_returnsFalseWhenOpenedOnCacheThatHasASingleMessage() throws { 46 | let message: TestableMessage = "This is a test" 47 | let cache = try createCache(overwritesOldMessages: false) 48 | try cache.append(message: message) 49 | 50 | let sameCache = try createCache(overwritesOldMessages: false) 51 | 52 | XCTAssertFalse(try sameCache.isEmpty()) 53 | } 54 | 55 | func test_isEmpty_returnsFalseWhenCacheThatDoesNotOverwriteIsFull() throws { 56 | let message: TestableMessage = "This is a test" 57 | let cache = try createCache(sizedToFit: [message], overwritesOldMessages: false) 58 | try cache.append(message: message) 59 | 60 | XCTAssertFalse(try cache.isEmpty()) 61 | } 62 | 63 | func test_isEmpty_returnsFalseWhenCacheThatOverwritesIsFull() throws { 64 | let cache = try createCache(overwritesOldMessages: true) 65 | for message in TestableMessage.lorumIpsum { 66 | try cache.append(message: message) 67 | } 68 | 69 | XCTAssertFalse(try cache.isEmpty()) 70 | } 71 | 72 | func test_isEmpty_throwsIncompatibleHeaderWhenHeaderVersionDoesNotMatch() throws { 73 | let originalHeader = try createHeaderHandle( 74 | overwritesOldMessages: false, 75 | version: 0 76 | ) 77 | try originalHeader.synchronizeHeaderData() 78 | 79 | let sut = try createCache(overwritesOldMessages: false) 80 | XCTAssertThrowsError(try sut.isEmpty()) { 81 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.incompatibleHeader(persistedVersion: 0)) 82 | } 83 | } 84 | 85 | func test_messages_canReadEmptyCacheThatDoesNotOverwriteOldestMessages() throws { 86 | let cache = try createCache(overwritesOldMessages: false) 87 | 88 | let messages = try cache.messages() 89 | XCTAssertEqual(messages, []) 90 | } 91 | 92 | func test_messages_canReadEmptyCacheThatOverwritesOldestMessages() throws { 93 | let cache = try createCache(overwritesOldMessages: true) 94 | 95 | let messages = try cache.messages() 96 | XCTAssertEqual(messages, []) 97 | } 98 | 99 | func test_messages_whenOffsetInFileAtEndOfNewestMessageIsBeyondEndOfNewestMessageButBeforeEndOfFile_throwsFileCorrupted() throws { 100 | let message: TestableMessage = "This is a test" 101 | let requiredByteCount = try requiredByteCount(for: [message]) 102 | let maximumBytes = requiredByteCount + 2 103 | let header = try CacheHeaderHandle( 104 | forReadingFrom: testFileLocation, 105 | maximumBytes: maximumBytes, 106 | overwritesOldMessages: true 107 | ) 108 | 109 | func makeCache() throws -> CacheAdvance { 110 | try CacheAdvance( 111 | fileURL: testFileLocation, 112 | writer: FileHandle(forWritingTo: testFileLocation), 113 | reader: CacheReader( 114 | forReadingFrom: testFileLocation), 115 | header: header, 116 | decoder: JSONDecoder(), 117 | encoder: JSONEncoder() 118 | ) 119 | } 120 | let writingCache = try makeCache() 121 | try writingCache.append(message: message) 122 | 123 | // Make the file corrupted by setting the offset at end of newest message to be further in the file. 124 | // This could happen if a crash occurred during a write of `header.offsetInFileAtEndOfNewestMessage` on a big-endian device. 125 | // Big-endian devices write the most significant digits first, meaning that if we were offsetInFileAtEndOfNewestMessage from 00001010 to 00010000, it would be possible to crash with the following bytes written to disk: 00011010. 126 | // The 00011010 value is a larger value what we intended to write, which would lead to file corruption. 127 | try header.updateOffsetInFileAtEndOfNewestMessage( 128 | to: requiredByteCount + 1) 129 | 130 | // Create a new cache instance that uses the corrupted data persisted to disk 131 | let corruptedReadingCache = try makeCache() 132 | 133 | XCTAssertThrowsError(try corruptedReadingCache.messages()) { 134 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted) 135 | } 136 | } 137 | 138 | func test_messages_whenOffsetInFileAtEndOfNewestMessageIsBeyondEndOfFile_throwsFileCorrupted() throws { 139 | let message: TestableMessage = "This is a test" 140 | let maximumBytes = try requiredByteCount(for: [message]) 141 | let header = try CacheHeaderHandle( 142 | forReadingFrom: testFileLocation, 143 | maximumBytes: maximumBytes, 144 | overwritesOldMessages: true 145 | ) 146 | 147 | func makeCache() throws -> CacheAdvance { 148 | try CacheAdvance( 149 | fileURL: testFileLocation, 150 | writer: FileHandle(forWritingTo: testFileLocation), 151 | reader: CacheReader( 152 | forReadingFrom: testFileLocation), 153 | header: header, 154 | decoder: JSONDecoder(), 155 | encoder: JSONEncoder() 156 | ) 157 | } 158 | let writingCache = try makeCache() 159 | try writingCache.append(message: message) 160 | 161 | // Make the file corrupted by setting the offset at end of newest message to be further in the file. 162 | // This could happen if a crash occurred during a write of `header.offsetInFileAtEndOfNewestMessage` on a big-endian device. 163 | // Big-endian devices write the most significant digits first, meaning that if we were offsetInFileAtEndOfNewestMessage from 00001010 to 00010000, it would be possible to crash with the following bytes written to disk: 00011010. 164 | // The 00011010 value is a larger value what we intended to write, which would lead to file corruption. 165 | try header.updateOffsetInFileAtEndOfNewestMessage( 166 | to: header.offsetInFileAtEndOfNewestMessage + 1) 167 | 168 | // Create a new cache instance that uses the corrupted data persisted to disk 169 | let corruptedReadingCache = try makeCache() 170 | 171 | XCTAssertThrowsError(try corruptedReadingCache.messages()) { 172 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted) 173 | } 174 | } 175 | 176 | func test_messages_whenOffsetInFileAtEndOfNewestMessageIsBeforeEndOfNewestMessage_throwsFileCorrupted() throws { 177 | let message: TestableMessage = "This is a test" 178 | let maximumBytes = try requiredByteCount(for: [message]) 179 | let header = try CacheHeaderHandle( 180 | forReadingFrom: testFileLocation, 181 | maximumBytes: maximumBytes, 182 | overwritesOldMessages: true 183 | ) 184 | 185 | func makeCache() throws -> CacheAdvance { 186 | try CacheAdvance( 187 | fileURL: testFileLocation, 188 | writer: FileHandle(forWritingTo: testFileLocation), 189 | reader: CacheReader( 190 | forReadingFrom: testFileLocation), 191 | header: header, 192 | decoder: JSONDecoder(), 193 | encoder: JSONEncoder() 194 | ) 195 | } 196 | let writingCache = try makeCache() 197 | try writingCache.append(message: message) 198 | 199 | // Make the file corrupted by setting the offset at end of newest message to be earlier in the file. 200 | // This could happen if a crash occurred during a write of `header.offsetInFileAtEndOfNewestMessage` on a little-endian device. 201 | // Little-endian devices write the lest significant digits first, meaning that if we were offsetInFileAtEndOfNewestMessage from 01010000 to 00001000, it would be possible to crash with the following bytes written to disk: 00010000. 202 | // The 00010000 value is a smaller value what we intended to write, which would lead to file corruption. 203 | try header.updateOffsetInFileAtEndOfNewestMessage( 204 | to: header.offsetInFileAtEndOfNewestMessage - 1) 205 | 206 | // Create a new cache instance that uses the corrupted data persisted to disk 207 | let corruptedReadingCache = try makeCache() 208 | 209 | XCTAssertThrowsError(try corruptedReadingCache.messages()) { 210 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted) 211 | } 212 | } 213 | 214 | func test_isWritable_returnsTrueWhenStaticHeaderMetadataMatches() throws { 215 | let originalCache = try createCache(overwritesOldMessages: false) 216 | XCTAssertTrue(try originalCache.isWritable()) 217 | 218 | let sut = try createCache(overwritesOldMessages: false) 219 | XCTAssertTrue(try sut.isWritable()) 220 | } 221 | 222 | func test_isWritable_returnsFalseWhenHeaderVersionDoesNotMatch() throws { 223 | let originalHeader = try createHeaderHandle( 224 | overwritesOldMessages: false, 225 | version: 0 226 | ) 227 | try originalHeader.synchronizeHeaderData() 228 | 229 | let sut = try createCache(overwritesOldMessages: false) 230 | XCTAssertFalse(try sut.isWritable()) 231 | } 232 | 233 | func test_isWritable_returnsFalseWhenMaximumBytesDoesNotMatch() throws { 234 | let originalCache = try createCache(overwritesOldMessages: false) 235 | XCTAssertTrue(try originalCache.isWritable()) 236 | 237 | let sut = try createCache( 238 | sizedToFit: TestableMessage.lorumIpsum.dropLast(), 239 | overwritesOldMessages: false 240 | ) 241 | XCTAssertFalse(try sut.isWritable()) 242 | } 243 | 244 | func test_isWritable_returnsFalseWhenOverwritesOldMessagesDoesNotMatch() throws { 245 | let originalCache = try createCache(overwritesOldMessages: false) 246 | XCTAssertTrue(try originalCache.isWritable()) 247 | 248 | let sut = try createCache(overwritesOldMessages: true) 249 | XCTAssertFalse(try sut.isWritable()) 250 | } 251 | 252 | func test_append_singleMessageThatFits_canBeRetrieved() throws { 253 | let message: TestableMessage = "This is a test" 254 | let cache = try createCache(sizedToFit: [message], overwritesOldMessages: false) 255 | try cache.append(message: message) 256 | 257 | let messages = try cache.messages() 258 | XCTAssertEqual(messages, [message]) 259 | } 260 | 261 | func test_append_singleMessageThatDoesNotFit_throwsError() throws { 262 | let message: TestableMessage = "This is a test" 263 | let cache = try createCache(sizedToFit: [message], overwritesOldMessages: false, maximumByteSubtractor: 1) 264 | 265 | XCTAssertThrowsError(try cache.append(message: message)) { 266 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.messageLargerThanCacheCapacity) 267 | } 268 | 269 | let messages = try cache.messages() 270 | XCTAssertEqual(messages, [], "Expected failed first write to result in an empty cache") 271 | } 272 | 273 | func test_append_singleMessageThrowsIfDoesNotFitAndCacheRolls() throws { 274 | let message: TestableMessage = "This is a test" 275 | let cache = try createCache(sizedToFit: [message], overwritesOldMessages: true, maximumByteSubtractor: 1) 276 | 277 | XCTAssertThrowsError(try cache.append(message: message)) { 278 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.messageLargerThanCacheCapacity) 279 | } 280 | 281 | let messages = try cache.messages() 282 | XCTAssertEqual(messages, [], "Expected failed first write to result in an empty cache") 283 | } 284 | 285 | func test_append_multipleMessagesCanBeRetrieved() throws { 286 | let cache = try createCache(overwritesOldMessages: false) 287 | for message in TestableMessage.lorumIpsum { 288 | try cache.append(message: message) 289 | } 290 | 291 | let messages = try cache.messages() 292 | XCTAssertEqual(messages, TestableMessage.lorumIpsum) 293 | } 294 | 295 | func test_append_multipleMessagesCanBeRetrievedTwiceFromNonOverwritingCache() throws { 296 | let cache = try createCache(overwritesOldMessages: false) 297 | for message in TestableMessage.lorumIpsum { 298 | try cache.append(message: message) 299 | } 300 | 301 | XCTAssertEqual(try cache.messages(), try cache.messages()) 302 | } 303 | 304 | func test_append_multipleMessagesCanBeRetrievedTwiceFromOverwritingCache() throws { 305 | let cache = try createCache(overwritesOldMessages: true, maximumByteDivisor: 3) 306 | for message in TestableMessage.lorumIpsum { 307 | try cache.append(message: message) 308 | } 309 | 310 | XCTAssertEqual(try cache.messages(), try cache.messages()) 311 | } 312 | 313 | func test_append_dropsLastMessageIfCacheDoesNotRollAndLastMessageDoesNotFit() throws { 314 | let cache = try createCache(overwritesOldMessages: false) 315 | for message in TestableMessage.lorumIpsum { 316 | try cache.append(message: message) 317 | } 318 | 319 | XCTAssertThrowsError(try cache.append(message: "This message won't fit")) { 320 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.messageLargerThanRemainingCacheSize) 321 | } 322 | 323 | let messages = try cache.messages() 324 | XCTAssertEqual(messages, TestableMessage.lorumIpsum) 325 | } 326 | 327 | func test_append_dropsOldestMessageIfCacheRollsAndLastMessageDoesNotFitAndIsShorterThanOldestMessage() throws { 328 | let cache = try createCache(overwritesOldMessages: true) 329 | for message in TestableMessage.lorumIpsum { 330 | try cache.append(message: message) 331 | } 332 | 333 | // Append a message that is shorter than the first message in TestableMessage.lorumIpsum. 334 | let shortMessage: TestableMessage = "Short message" 335 | try cache.append(message: shortMessage) 336 | 337 | let messages = try cache.messages() 338 | XCTAssertEqual(messages, Array(TestableMessage.lorumIpsum.dropFirst()) + [shortMessage]) 339 | } 340 | 341 | func test_append_dropsFirstTwoMessagesIfCacheRollsAndLastMessageDoesNotFitAndIsLargerThanOldestMessage() throws { 342 | let cache = try createCache(overwritesOldMessages: true) 343 | for message in TestableMessage.lorumIpsum { 344 | try cache.append(message: message) 345 | } 346 | 347 | // Append a message that is slightly longer than the first message in TestableMessage.lorumIpsum. 348 | let barelyLongerMessage = TestableMessage(stringLiteral: TestableMessage.lorumIpsum[0].value + "hi") 349 | try cache.append(message: barelyLongerMessage) 350 | 351 | let messages = try cache.messages() 352 | XCTAssertEqual(messages, Array(TestableMessage.lorumIpsum.dropFirst(2)) + [barelyLongerMessage]) 353 | } 354 | 355 | func test_append_inAnOverwritingCacheEvictsMinimumPossibleNumberOfMessages() throws { 356 | let message: TestableMessage = "test-message" 357 | let maxMessagesBeforeOverwriting = [message, message, message] 358 | let cache = try createCache( 359 | sizedToFit: maxMessagesBeforeOverwriting, 360 | overwritesOldMessages: true 361 | ) 362 | for message in maxMessagesBeforeOverwriting { 363 | try cache.append(message: message) 364 | } 365 | 366 | // All of our messages have been stored and our cache is full. 367 | 368 | // The byte layout in the cache should now be as follows: 369 | // [header][length|test-message][length|test-message][length|test-message] 370 | // ^ reading handle ^ writing handle 371 | 372 | // When we read messages, we read from the current position of the reading handle – which is at the start of the oldest persisted message – 373 | // up until the current position of the writing handle – which is at the end of the newest persisted message. This algorithm implies that if 374 | // the reading handle and the writing handle are at the same position in the file, then the file is empty. Therefore, when writing a message 375 | // and overwriting, we must ensure that we do not accidentally write a message such that the reading handle and the writing handle end up in 376 | // the same position. 377 | 378 | // Prove to ourselves we've stored all of the messages. 379 | XCTAssertEqual( 380 | try cache.messages(), 381 | maxMessagesBeforeOverwriting 382 | ) 383 | 384 | // Append one more message of the same size. 385 | try cache.append(message: message) 386 | 387 | // Because we can not have the writing handle in the same position as the writing handle, the byte layout in the cache should now be as follows: 388 | // [header][length|test-message] [length|test-message] 389 | // ^ writing handle ^ reading handle 390 | 391 | // In other words, we had to evict a single message in order to ensure that our writing and reading handles did not point at the same byte. 392 | // If more messages have been dropped, that indicates that our `prepareReaderForWriting` method has a bug. 393 | XCTAssertEqual( 394 | try cache.messages(), 395 | [message, message] 396 | ) 397 | } 398 | 399 | func test_append_dropsOldMessagesAsNecessary() throws { 400 | for maximumByteDivisor in stride(from: 1, to: 50, by: 0.1) { 401 | let cache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 402 | for message in TestableMessage.lorumIpsum { 403 | try cache.append(message: message) 404 | } 405 | 406 | let messages = try cache.messages() 407 | XCTAssertEqual(expectedMessagesInOverwritingCache(givenOriginal: TestableMessage.lorumIpsum, newMessageCount: messages.count), messages) 408 | 409 | // Prepare ourselves for the next run. 410 | clearCacheFile() 411 | } 412 | } 413 | 414 | func test_append_canWriteMessagesToCacheCreatedByADifferentCache() throws { 415 | let cache = try createCache(overwritesOldMessages: false) 416 | for message in TestableMessage.lorumIpsum.dropLast() { 417 | try cache.append(message: message) 418 | } 419 | 420 | let cachedMessages = try cache.messages() 421 | let secondCache = try createCache(overwritesOldMessages: false) 422 | try secondCache.append(message: TestableMessage.lorumIpsum.last!) 423 | XCTAssertEqual(cachedMessages + [TestableMessage.lorumIpsum.last!], try secondCache.messages()) 424 | } 425 | 426 | func test_append_canWriteMessagesToCacheCreatedByADifferentOverridingCache() throws { 427 | for maximumByteDivisor in stride(from: 1, to: 50, by: 0.5) { 428 | let cache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 429 | for message in TestableMessage.lorumIpsum.dropLast() { 430 | try cache.append(message: message) 431 | } 432 | 433 | let cachedMessages = try cache.messages() 434 | 435 | let secondCache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 436 | 437 | // Check that we can retrieve the messages from the first cache. 438 | XCTAssertFalse(try secondCache.isEmpty()) 439 | 440 | try secondCache.append(message: TestableMessage.lorumIpsum.last!) 441 | let secondCacheMessages = try secondCache.messages() 442 | 443 | XCTAssertEqual(expectedMessagesInOverwritingCache(givenOriginal: cachedMessages + [TestableMessage.lorumIpsum.last!], newMessageCount: secondCacheMessages.count), secondCacheMessages) 444 | 445 | // Prepare ourselves for the next run. 446 | clearCacheFile() 447 | } 448 | } 449 | 450 | func test_append_canWriteMessagesAfterRetrievingMessages() throws { 451 | for maximumByteDivisor in stride(from: 1, to: 50, by: 0.5) { 452 | let cache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 453 | for message in TestableMessage.lorumIpsum.dropLast() { 454 | try cache.append(message: message) 455 | } 456 | 457 | let cachedMessages = try cache.messages() 458 | try cache.append(message: TestableMessage.lorumIpsum.last!) 459 | 460 | let cachedMessagesAfterAppend = try cache.messages() 461 | XCTAssertEqual(expectedMessagesInOverwritingCache(givenOriginal: cachedMessages + [TestableMessage.lorumIpsum.last!], newMessageCount: cachedMessagesAfterAppend.count), cachedMessagesAfterAppend) 462 | 463 | // Prepare ourselves for the next run. 464 | clearCacheFile() 465 | } 466 | } 467 | 468 | func test_append_throwsIncompatibleHeaderWhenHeaderVersionDoesNotMatch() throws { 469 | let originalHeader = try createHeaderHandle( 470 | overwritesOldMessages: false, 471 | version: 0 472 | ) 473 | try originalHeader.synchronizeHeaderData() 474 | 475 | let sut = try createCache( 476 | sizedToFit: TestableMessage.lorumIpsum.dropLast(), 477 | overwritesOldMessages: false 478 | ) 479 | XCTAssertThrowsError(try sut.append(message: TestableMessage.lorumIpsum.last!)) { 480 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.incompatibleHeader(persistedVersion: 0)) 481 | } 482 | } 483 | 484 | func test_append_throwsFileNotWritableWhenMaximumBytesDoesNotMatch() throws { 485 | let originalCache = try createCache(overwritesOldMessages: false) 486 | XCTAssertTrue(try originalCache.isWritable()) 487 | 488 | let sut = try createCache( 489 | sizedToFit: TestableMessage.lorumIpsum.dropLast(), 490 | overwritesOldMessages: false 491 | ) 492 | XCTAssertThrowsError(try sut.append(message: TestableMessage.lorumIpsum.last!)) { 493 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileNotWritable) 494 | } 495 | } 496 | 497 | func test_append_throwsFileNotWritableWhenOverwritesOldMessagesDoesNotMatch() throws { 498 | let originalCache = try createCache(overwritesOldMessages: false) 499 | XCTAssertTrue(try originalCache.isWritable()) 500 | 501 | let sut = try createCache(overwritesOldMessages: true) 502 | XCTAssertThrowsError(try sut.append(message: TestableMessage.lorumIpsum.last!)) { 503 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileNotWritable) 504 | } 505 | } 506 | 507 | func test_messages_canReadMessagesWrittenByADifferentCache() throws { 508 | let cache = try createCache(overwritesOldMessages: false) 509 | for message in TestableMessage.lorumIpsum { 510 | try cache.append(message: message) 511 | } 512 | 513 | let secondCache = try createCache(overwritesOldMessages: false) 514 | XCTAssertEqual(try cache.messages(), try secondCache.messages()) 515 | } 516 | 517 | func test_messages_canReadMessagesWrittenByADifferentFullCache() throws { 518 | let cache = try createCache(overwritesOldMessages: false, maximumByteSubtractor: 1) 519 | for message in TestableMessage.lorumIpsum.dropLast() { 520 | try cache.append(message: message) 521 | } 522 | XCTAssertThrowsError(try cache.append(message: TestableMessage.lorumIpsum.last!)) 523 | 524 | let secondCache = try createCache(overwritesOldMessages: false, maximumByteSubtractor: 1) 525 | XCTAssertEqual(try cache.messages(), try secondCache.messages()) 526 | } 527 | 528 | func test_messages_canReadMessagesWrittenByADifferentOverwritingCache() throws { 529 | for maximumByteDivisor in stride(from: 1, to: 50, by: 0.5) { 530 | let cache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 531 | for message in TestableMessage.lorumIpsum { 532 | try cache.append(message: message) 533 | } 534 | 535 | let secondCache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 536 | XCTAssertEqual(try cache.messages(), try secondCache.messages()) 537 | 538 | // Prepare ourselves for the next run. 539 | clearCacheFile() 540 | } 541 | } 542 | 543 | func test_messages_cacheThatDoesNotOverwrite_canReadMessagesWrittenByAnOverwritingCache() throws { 544 | let cache = try createCache(overwritesOldMessages: false) 545 | for message in TestableMessage.lorumIpsum { 546 | try cache.append(message: message) 547 | } 548 | 549 | let secondCache = try createCache(overwritesOldMessages: true) 550 | XCTAssertEqual(try cache.messages(), try secondCache.messages()) 551 | } 552 | 553 | func test_messages_cacheThatOverwrites_canReadMessagesWrittenByAnOverwritingCacheWithDifferentMaximumBytes() throws { 554 | for maximumByteDivisor in stride(from: 1, to: 50, by: 0.5) { 555 | let cache = try createCache(overwritesOldMessages: true) 556 | for message in TestableMessage.lorumIpsum { 557 | try cache.append(message: message) 558 | } 559 | 560 | let secondCache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 561 | XCTAssertEqual(try cache.messages(), try secondCache.messages()) 562 | 563 | // Prepare ourselves for the next run. 564 | clearCacheFile() 565 | } 566 | } 567 | 568 | func test_messages_cacheThatOverwrites_canReadMessagesWrittenByANonOverwritingCache() throws { 569 | for maximumByteDivisor in stride(from: 1, to: 50, by: 0.5) { 570 | let cache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 571 | for message in TestableMessage.lorumIpsum { 572 | try cache.append(message: message) 573 | } 574 | 575 | let secondCache = try createCache(overwritesOldMessages: false) 576 | XCTAssertEqual(try cache.messages(), try secondCache.messages()) 577 | 578 | // Prepare ourselves for the next run. 579 | clearCacheFile() 580 | } 581 | } 582 | 583 | func test_messages_cacheThatDoesNotOverwrite_canReadMessagesWrittenByAnOverwritingCacheWithDifferentMaximumBytes() throws { 584 | for maximumByteDivisor in stride(from: 1, to: 50, by: 0.5) { 585 | let cache = try createCache(overwritesOldMessages: false) 586 | for message in TestableMessage.lorumIpsum { 587 | try cache.append(message: message) 588 | } 589 | 590 | let secondCache = try createCache(overwritesOldMessages: true, maximumByteDivisor: maximumByteDivisor) 591 | XCTAssertEqual(try cache.messages(), try secondCache.messages()) 592 | 593 | // Prepare ourselves for the next run. 594 | clearCacheFile() 595 | } 596 | } 597 | 598 | func test_messages_throwsIncompatibleHeaderWhenHeaderVersionDoesNotMatch() throws { 599 | let originalHeader = try createHeaderHandle( 600 | overwritesOldMessages: false, 601 | version: 0 602 | ) 603 | try originalHeader.synchronizeHeaderData() 604 | 605 | let sut = try createCache(overwritesOldMessages: false) 606 | XCTAssertThrowsError(try sut.messages()) { 607 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.incompatibleHeader(persistedVersion: 0)) 608 | } 609 | } 610 | 611 | // MARK: Performance Tests 612 | 613 | func test_performance_createCacheAndAppendSingleMessage() throws { 614 | measure { 615 | clearCacheFile() 616 | 617 | guard let sut = try? createCache(maximumByes: 100, overwritesOldMessages: true) else { 618 | XCTFail("Could not create cache") 619 | return 620 | } 621 | try? sut.append(message: "test message") 622 | } 623 | } 624 | 625 | func test_performance_append_fillableCache() throws { 626 | let maximumBytes = try Bytes(Double(requiredByteCount(for: TestableMessage.lorumIpsum))) 627 | // Create a cache that won't run out of room over multiple test runs 628 | guard let sut = try? createCache(maximumByes: maximumBytes * 10, overwritesOldMessages: false) else { 629 | XCTFail("Could not create cache") 630 | return 631 | } 632 | // Force the cache to set up before we start writing messages. 633 | _ = try sut.isWritable() 634 | measure { 635 | for message in TestableMessage.lorumIpsum { 636 | try? sut.append(message: message) 637 | } 638 | } 639 | } 640 | 641 | func test_performance_append_overwritingCache() throws { 642 | let sut = try createCache(overwritesOldMessages: true) 643 | // Fill the cache before the test starts. 644 | for message in TestableMessage.lorumIpsum { 645 | try sut.append(message: message) 646 | } 647 | measure { 648 | for message in TestableMessage.lorumIpsum { 649 | try? sut.append(message: message) 650 | } 651 | } 652 | } 653 | 654 | func test_performance_messages_fillableCache() throws { 655 | let sut = try createCache(overwritesOldMessages: false) 656 | for message in TestableMessage.lorumIpsum { 657 | try sut.append(message: message) 658 | } 659 | measure { 660 | guard (try? sut.messages()) != nil else { 661 | XCTFail("Could not read messages") 662 | return 663 | } 664 | } 665 | } 666 | 667 | func test_performance_messages_overwritingCache() throws { 668 | let sut = try createCache(overwritesOldMessages: true) 669 | for message in TestableMessage.lorumIpsum + TestableMessage.lorumIpsum { 670 | try sut.append(message: message) 671 | } 672 | measure { 673 | guard (try? sut.messages()) != nil else { 674 | XCTFail("Could not read messages") 675 | return 676 | } 677 | } 678 | } 679 | 680 | // MARK: Private 681 | 682 | private func requiredByteCount(for messages: [T]) throws -> UInt64 { 683 | let encoder = JSONEncoder() 684 | return try FileHeader.expectedEndOfHeaderInFile 685 | + messages.reduce(0) { allocatedSize, message in 686 | let encodableMessage = EncodableMessage(message: message, encoder: encoder) 687 | let data = try encodableMessage.encodedData() 688 | return allocatedSize + UInt64(data.count) 689 | } 690 | } 691 | 692 | private func createHeaderHandle( 693 | sizedToFit messages: [TestableMessage] = TestableMessage.lorumIpsum, 694 | overwritesOldMessages: Bool, 695 | maximumByteDivisor: Double = 1, 696 | maximumByteSubtractor: Bytes = 0, 697 | version: UInt8 = FileHeader.version, 698 | zeroOutExistingFile: Bool = true 699 | ) 700 | throws 701 | -> CacheHeaderHandle 702 | { 703 | if zeroOutExistingFile { clearCacheFile() } 704 | return try CacheHeaderHandle( 705 | forReadingFrom: testFileLocation, 706 | maximumBytes: Bytes(Double(requiredByteCount(for: messages)) / maximumByteDivisor) - maximumByteSubtractor, 707 | overwritesOldMessages: overwritesOldMessages, 708 | version: version 709 | ) 710 | } 711 | 712 | private func createCache( 713 | sizedToFit messages: [TestableMessage] = TestableMessage.lorumIpsum, 714 | overwritesOldMessages: Bool, 715 | maximumByteDivisor: Double = 1, 716 | maximumByteSubtractor: Bytes = 0 717 | ) 718 | throws 719 | -> CacheAdvance 720 | { 721 | try createCache( 722 | maximumByes: Bytes(Double(requiredByteCount(for: messages)) / maximumByteDivisor) - maximumByteSubtractor, 723 | overwritesOldMessages: overwritesOldMessages 724 | ) 725 | } 726 | 727 | private func createCache( 728 | maximumByes: Bytes, 729 | overwritesOldMessages: Bool 730 | ) 731 | throws 732 | -> CacheAdvance 733 | { 734 | try CacheAdvance( 735 | fileURL: testFileLocation, 736 | maximumBytes: maximumByes, 737 | shouldOverwriteOldMessages: overwritesOldMessages 738 | ) 739 | } 740 | 741 | private func expectedMessagesInOverwritingCache( 742 | givenOriginal messages: [TestableMessage], 743 | newMessageCount: Int 744 | ) 745 | -> [TestableMessage] 746 | { 747 | Array(messages.dropFirst(messages.count - newMessageCount)) 748 | } 749 | 750 | private func clearCacheFile() { 751 | XCTAssertTrue(FileManager.default.createFile( 752 | atPath: testFileLocation.path, 753 | contents: nil, 754 | attributes: nil 755 | )) 756 | } 757 | 758 | private let testFileLocation = FileManager.default.temporaryDirectory.appendingPathComponent("CacheAdvanceTests") 759 | } 760 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/CacheHeaderHandleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/27/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS"BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | import XCTest 20 | 21 | @testable import CacheAdvance 22 | 23 | final class CacheHeaderHandleTests: XCTestCase { 24 | // MARK: XCTestCase 25 | 26 | override func setUp() { 27 | super.setUp() 28 | XCTAssertTrue(FileManager.default.createFile(atPath: testFileLocation.path, contents: nil, attributes: nil)) 29 | } 30 | 31 | override func tearDown() { 32 | super.tearDown() 33 | try? FileManager.default.removeItem(at: testFileLocation) 34 | } 35 | 36 | // MARK: Behavior Tests 37 | 38 | func test_synchronizeHeaderData_returnsSameVersionAsWasLastPersistedToDisk() throws { 39 | let headerHandle1 = try createHeaderHandle(version: 2) 40 | try headerHandle1.synchronizeHeaderData() 41 | try headerHandle1.updateOffsetInFileAtEndOfNewestMessage(to: 1_000) 42 | try headerHandle1.updateOffsetInFileOfOldestMessage(to: 2_000) 43 | 44 | let headerHandle2 = try createHeaderHandle(version: 2) 45 | try headerHandle2.synchronizeHeaderData() 46 | 47 | XCTAssertEqual(headerHandle2.offsetInFileAtEndOfNewestMessage, 1_000) 48 | XCTAssertEqual(headerHandle2.offsetInFileOfOldestMessage, 2_000) 49 | } 50 | 51 | func test_synchronizeHeaderData_doesNotThrowWhenUnexpectedVersionIsOnDisk() throws { 52 | let headerHandle1 = try createHeaderHandle(version: 2) 53 | try headerHandle1.synchronizeHeaderData() 54 | let defaultOffsetInFileAtEndOfNewestMessage = headerHandle1.offsetInFileAtEndOfNewestMessage 55 | let defaultOffsetInFileOfOldestMessage = headerHandle1.offsetInFileOfOldestMessage 56 | try headerHandle1.updateOffsetInFileAtEndOfNewestMessage(to: 1_000) 57 | try headerHandle1.updateOffsetInFileOfOldestMessage(to: 2_000) 58 | let fileData = try Data(contentsOf: testFileLocation) 59 | 60 | XCTAssertNotEqual(headerHandle1.offsetInFileAtEndOfNewestMessage, defaultOffsetInFileAtEndOfNewestMessage) 61 | XCTAssertNotEqual(headerHandle1.offsetInFileOfOldestMessage, defaultOffsetInFileOfOldestMessage) 62 | 63 | let headerHandle2 = try createHeaderHandle(version: 3) 64 | XCTAssertNoThrow(try headerHandle2.synchronizeHeaderData()) 65 | XCTAssertEqual(try Data(contentsOf: testFileLocation), fileData, "Opening handle with different version caused file to be changed") 66 | } 67 | 68 | func test_synchronizeHeaderData_doesNotThrowWhenMaximumBytesIsInconsistent() throws { 69 | let headerHandle1 = try createHeaderHandle(maximumBytes: 5_000) 70 | try headerHandle1.synchronizeHeaderData() 71 | let defaultOffsetInFileAtEndOfNewestMessage = headerHandle1.offsetInFileAtEndOfNewestMessage 72 | let defaultOffsetInFileOfOldestMessage = headerHandle1.offsetInFileOfOldestMessage 73 | try headerHandle1.updateOffsetInFileAtEndOfNewestMessage(to: 1_000) 74 | try headerHandle1.updateOffsetInFileOfOldestMessage(to: 2_000) 75 | let fileData = try Data(contentsOf: testFileLocation) 76 | 77 | XCTAssertNotEqual(headerHandle1.offsetInFileAtEndOfNewestMessage, defaultOffsetInFileAtEndOfNewestMessage) 78 | XCTAssertNotEqual(headerHandle1.offsetInFileOfOldestMessage, defaultOffsetInFileOfOldestMessage) 79 | 80 | let headerHandle2 = try createHeaderHandle(maximumBytes: 10_000) 81 | XCTAssertNoThrow(try headerHandle2.synchronizeHeaderData()) 82 | 83 | XCTAssertEqual(try Data(contentsOf: testFileLocation), fileData, "Opening handle with different maximumBytes caused file to be changed") 84 | } 85 | 86 | func test_synchronizeHeaderData_writesHeaderWhenFileIsEmpty() throws { 87 | let handle = try FileHandle(forReadingFrom: testFileLocation) 88 | let fileData = try handle.readDataUp(toLength: 1) 89 | XCTAssertTrue(fileData.isEmpty) 90 | 91 | let headerHandle = try createHeaderHandle() 92 | try headerHandle.synchronizeHeaderData() 93 | 94 | // Verify that the file is now the size of the header. This means that we rewrote the file. 95 | try handle.seek(to: 0) 96 | let headerData = try handle.readDataUp(toLength: Int(FileHeader.expectedEndOfHeaderInFile)) 97 | XCTAssertEqual(headerData.count, Int(FileHeader.expectedEndOfHeaderInFile)) 98 | 99 | try handle.closeHandle() 100 | } 101 | 102 | func test_synchronizeHeaderData_throwsFileCorruptedWhenFileHeaderCannotBeCreated() throws { 103 | // Write a file that is too short for us to parse a `FileHeader` object. 104 | let handle = try FileHandle(forUpdating: testFileLocation) 105 | handle.write(Data(repeating: 0, count: Int(FileHeader.expectedEndOfHeaderInFile) - 1)) 106 | 107 | let headerHandle = try createHeaderHandle() 108 | XCTAssertThrowsError(try headerHandle.synchronizeHeaderData()) { 109 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted) 110 | } 111 | 112 | try handle.closeHandle() 113 | } 114 | 115 | func test_checkFile_versionMismatch_throwsIncompatibleHeader() throws { 116 | let originalHeader = try createHeaderHandle( 117 | maximumBytes: 1_000, 118 | overwritesOldMessages: true, 119 | version: 1 120 | ) 121 | try originalHeader.synchronizeHeaderData() 122 | 123 | let sut = try createHeaderHandle( 124 | maximumBytes: 1_000, 125 | overwritesOldMessages: true, 126 | version: 2 127 | ) 128 | 129 | XCTAssertThrowsError(try sut.checkFile()) { 130 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.incompatibleHeader(persistedVersion: 1)) 131 | } 132 | } 133 | 134 | func test_checkFile_emptyFile_throwsFileCorrupted() throws { 135 | let sut = try createHeaderHandle( 136 | maximumBytes: 1_000, 137 | overwritesOldMessages: true, 138 | version: 2 139 | ) 140 | 141 | XCTAssertThrowsError(try sut.checkFile()) { 142 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted) 143 | } 144 | } 145 | 146 | func test_canWriteToFile_maximumBytesMismatch_returnsFalse() throws { 147 | let originalHeader = try createHeaderHandle( 148 | maximumBytes: 1_000, 149 | overwritesOldMessages: true, 150 | version: 1 151 | ) 152 | try originalHeader.synchronizeHeaderData() 153 | 154 | let sut = try createHeaderHandle( 155 | maximumBytes: 2_000, 156 | overwritesOldMessages: true, 157 | version: 1 158 | ) 159 | 160 | XCTAssertFalse(try sut.canWriteToFile()) 161 | } 162 | 163 | func test_canWriteToFile_overwritesOldMessagesMismatch_returnsFalse() throws { 164 | let originalHeader = try createHeaderHandle( 165 | maximumBytes: 1_000, 166 | overwritesOldMessages: true, 167 | version: 1 168 | ) 169 | try originalHeader.synchronizeHeaderData() 170 | 171 | let sut = try createHeaderHandle( 172 | maximumBytes: 1_000, 173 | overwritesOldMessages: false, 174 | version: 1 175 | ) 176 | 177 | XCTAssertFalse(try sut.canWriteToFile()) 178 | } 179 | 180 | func test_canWriteToFile_versionMismatch_returnsFalse() throws { 181 | let originalHeader = try createHeaderHandle( 182 | maximumBytes: 1_000, 183 | overwritesOldMessages: true, 184 | version: 1 185 | ) 186 | try originalHeader.synchronizeHeaderData() 187 | 188 | let sut = try createHeaderHandle( 189 | maximumBytes: 1_000, 190 | overwritesOldMessages: true, 191 | version: 2 192 | ) 193 | 194 | XCTAssertFalse(try sut.canWriteToFile()) 195 | } 196 | 197 | func test_canWriteToFile_emptyFile_returnsFalse() throws { 198 | let sut = try createHeaderHandle( 199 | maximumBytes: 1_000, 200 | overwritesOldMessages: true, 201 | version: 2 202 | ) 203 | 204 | XCTAssertThrowsError(try sut.canWriteToFile()) { 205 | XCTAssertEqual($0 as? CacheAdvanceError, CacheAdvanceError.fileCorrupted) 206 | } 207 | } 208 | 209 | func test_checkFile_noMismatches_doesNotThrow() throws { 210 | let originalHeader = try createHeaderHandle( 211 | maximumBytes: 1_000, 212 | overwritesOldMessages: true, 213 | version: 1 214 | ) 215 | try originalHeader.synchronizeHeaderData() 216 | 217 | let sut = try createHeaderHandle( 218 | maximumBytes: 1_000, 219 | overwritesOldMessages: true, 220 | version: 1 221 | ) 222 | 223 | XCTAssertNoThrow(try sut.checkFile()) 224 | } 225 | 226 | // MARK: Private 227 | 228 | private func createHeaderHandle( 229 | maximumBytes: Bytes = 500, 230 | overwritesOldMessages: Bool = true, 231 | version: UInt8 = FileHeader.version 232 | ) 233 | throws 234 | -> CacheHeaderHandle 235 | { 236 | try CacheHeaderHandle( 237 | forReadingFrom: testFileLocation, 238 | maximumBytes: maximumBytes, 239 | overwritesOldMessages: overwritesOldMessages, 240 | version: version 241 | ) 242 | } 243 | 244 | private let testFileLocation = FileManager.default.temporaryDirectory.appendingPathComponent("CacheHeaderHandleTests") 245 | } 246 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/EncodableMessageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 11/9/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | import XCTest 20 | 21 | @testable import CacheAdvance 22 | 23 | final class EncodableMessageTests: XCTestCase { 24 | // MARK: Behavior Tests 25 | 26 | func test_encodedData_encodesCorrectSize() throws { 27 | let message = TestableMessage("This is a test") 28 | let data = try encoder.encode(message) 29 | let encodedMessage = EncodableMessage(message: message, encoder: encoder) 30 | let encodedData = try encodedMessage.encodedData() 31 | 32 | let prefix = encodedData.subdata(in: 0..(message: message, encoder: encoder) 40 | let encodedData = try encodedMessage.encodedData() 41 | XCTAssertEqual(encodedData.count, data.count + MessageSpan.storageLength) 42 | } 43 | 44 | func test_encodedData_hasDataPostfix() throws { 45 | let message = TestableMessage("This is a test") 46 | let data = try encoder.encode(message) 47 | let encodedMessage = EncodableMessage(message: message, encoder: encoder) 48 | let encodedData = try encodedMessage.encodedData() 49 | XCTAssertEqual(encodedData.advanced(by: MessageSpan.storageLength), data) 50 | } 51 | 52 | func test_encodedData_whenMessageDataTooLarge_throwsError() throws { 53 | let encodedMessage = EncodableMessage(message: Data(count: Int(UInt8.max)), encoder: encoder) 54 | XCTAssertThrowsError(try encodedMessage.encodedData()) 55 | } 56 | 57 | // MARK: Private 58 | 59 | private let encoder = JSONEncoder() 60 | } 61 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/FileHeaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/27/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS"BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | import XCTest 20 | 21 | @testable import CacheAdvance 22 | 23 | final class FileHeaderTests: XCTestCase { 24 | // MARK: Behavior Tests 25 | 26 | func test_initFromData_returnsNilWhenDataIsOfIncorrectLength() throws { 27 | XCTAssertNil(FileHeader(from: Data(repeating: 0, count: 8))) 28 | } 29 | 30 | func test_initFromData_readsZerodData() throws { 31 | let fileHeader = FileHeader(from: Data(repeating: 0, count: Int(FileHeader.expectedEndOfHeaderInFile))) 32 | XCTAssertEqual(fileHeader?.version, 0) 33 | XCTAssertEqual(fileHeader?.maximumBytes, 0) 34 | XCTAssertEqual(fileHeader?.overwritesOldMessages, false) 35 | XCTAssertEqual(fileHeader?.offsetInFileOfOldestMessage, 0) 36 | XCTAssertEqual(fileHeader?.offsetInFileAtEndOfNewestMessage, 0) 37 | } 38 | 39 | func test_initFromData_readsOutExpectedData() throws { 40 | let fileHeader = createFileHeader() 41 | 42 | let fileHeaderFromData = FileHeader(from: fileHeader.asData) 43 | XCTAssertEqual(fileHeader.version, fileHeaderFromData?.version) 44 | XCTAssertEqual(fileHeader.maximumBytes, fileHeaderFromData?.maximumBytes) 45 | XCTAssertEqual(fileHeader.overwritesOldMessages, fileHeaderFromData?.overwritesOldMessages) 46 | XCTAssertEqual(fileHeader.offsetInFileOfOldestMessage, fileHeaderFromData?.offsetInFileOfOldestMessage) 47 | XCTAssertEqual(fileHeader.offsetInFileAtEndOfNewestMessage, fileHeaderFromData?.offsetInFileAtEndOfNewestMessage) 48 | } 49 | 50 | func test_expectedEndOfHeaderInFile_hasCorrectLengthForHeaderVersion1() { 51 | XCTAssertEqual( 52 | FileHeader.expectedEndOfHeaderInFile, 53 | 64, 54 | "Header length has changed from expected 64 bytes for header version 1. This represents a breaking change." 55 | ) 56 | } 57 | 58 | func test_expectedEndOfHeaderInFile_hasExpectedLength() { 59 | XCTAssertEqual( 60 | UInt64(createFileHeader().asData.count), 61 | FileHeader.expectedEndOfHeaderInFile 62 | ) 63 | } 64 | 65 | // MARK: Private 66 | 67 | private func createFileHeader( 68 | version: UInt8 = FileHeader.version, 69 | maximumBytes: Bytes = 500, 70 | overwritesOldMessages: Bool = false, 71 | offsetInFileOfOldestMessage: UInt64 = 20, 72 | offsetInFileAtEndOfNewestMessage: UInt64 = 400 73 | ) 74 | -> FileHeader 75 | { 76 | FileHeader( 77 | version: version, 78 | maximumBytes: maximumBytes, 79 | overwritesOldMessages: overwritesOldMessages, 80 | offsetInFileOfOldestMessage: offsetInFileOfOldestMessage, 81 | offsetInFileAtEndOfNewestMessage: offsetInFileAtEndOfNewestMessage 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/FlushableCachePerformanceComparisonTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 4/26/20. 3 | // Copyright © 2020 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS"BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import XCTest 19 | 20 | final class FlushableCachePerformanceComparisonTests: XCTestCase { 21 | // MARK: XCTestCase 22 | 23 | override func setUp() { 24 | super.setUp() 25 | 26 | // Delete the existing cache. 27 | XCTAssertTrue(FileManager.default.createFile(atPath: testFileLocation.path, contents: nil, attributes: nil)) 28 | } 29 | 30 | // MARK: Behavior Tests 31 | 32 | func test_append_flushableCache_fillableCache_canMaintainMaxCount() throws { 33 | let cache = FlushableCache( 34 | location: testFileLocation, 35 | maxMessageCount: TestableMessage.lorumIpsum.count, 36 | shouldOverwriteMessages: false 37 | ) 38 | for message in TestableMessage.lorumIpsum + TestableMessage.lorumIpsum { 39 | try cache.appendMessage(message) 40 | } 41 | 42 | XCTAssertEqual(try cache.messages().count, TestableMessage.lorumIpsum.count) 43 | } 44 | 45 | func test_messages_flushableCache_fillableCache_canReadInsertedMessages() throws { 46 | let cache = FlushableCache( 47 | location: testFileLocation, 48 | maxMessageCount: TestableMessage.lorumIpsum.count, 49 | shouldOverwriteMessages: true 50 | ) 51 | for message in TestableMessage.lorumIpsum { 52 | try cache.appendMessage(message) 53 | } 54 | 55 | XCTAssertEqual(try cache.messages(), TestableMessage.lorumIpsum) 56 | } 57 | 58 | func test_append_flushableCache_overwritingCache_canMaintainMaxCount() throws { 59 | let cache = FlushableCache( 60 | location: testFileLocation, 61 | maxMessageCount: TestableMessage.lorumIpsum.count, 62 | shouldOverwriteMessages: true 63 | ) 64 | for message in TestableMessage.lorumIpsum + TestableMessage.lorumIpsum { 65 | try cache.appendMessage(message) 66 | } 67 | 68 | XCTAssertEqual(try cache.messages().count, TestableMessage.lorumIpsum.count) 69 | } 70 | 71 | func test_append_flushableCache_overwritingCache_storesOnlyMostRecentMessages() throws { 72 | let cache = FlushableCache( 73 | location: testFileLocation, 74 | maxMessageCount: TestableMessage.lorumIpsum.count, 75 | shouldOverwriteMessages: true 76 | ) 77 | for message in TestableMessage.lorumIpsum { 78 | try cache.appendMessage(message) 79 | } 80 | try cache.appendMessage(#function) 81 | 82 | XCTAssertEqual(try cache.messages(), Array(TestableMessage.lorumIpsum.dropFirst()) + [#function]) 83 | } 84 | 85 | func test_messages_flushableCache_overwritingCache_canReadInsertedMessages() throws { 86 | let cache = FlushableCache( 87 | location: testFileLocation, 88 | maxMessageCount: TestableMessage.lorumIpsum.count, 89 | shouldOverwriteMessages: true 90 | ) 91 | for message in TestableMessage.lorumIpsum { 92 | try cache.appendMessage(message) 93 | } 94 | 95 | XCTAssertEqual(try cache.messages(), TestableMessage.lorumIpsum) 96 | } 97 | 98 | // MARK: Performance Tests 99 | 100 | func test_performance_flushableCache_createCacheAndAppendSingleMessageAndFlush() { 101 | let cache = FlushableCache( 102 | location: testFileLocation, 103 | maxMessageCount: TestableMessage.lorumIpsum.count, 104 | shouldOverwriteMessages: false 105 | ) 106 | measure { 107 | try? cache.appendMessage("test message") 108 | try? cache.flushMessages() 109 | 110 | // Prepare ourselves for the next run. 111 | cache.dropMessages() 112 | } 113 | } 114 | 115 | func test_performance_flushableCache_appendAndFlush_fillableCache() { 116 | let cache = FlushableCache( 117 | location: testFileLocation, 118 | maxMessageCount: TestableMessage.lorumIpsum.count, 119 | shouldOverwriteMessages: false 120 | ) 121 | measure { 122 | for message in TestableMessage.lorumIpsum { 123 | try? cache.appendMessage(message) 124 | try? cache.flushMessages() 125 | } 126 | 127 | // Prepare ourselves for the next run. 128 | cache.dropMessages() 129 | } 130 | } 131 | 132 | func test_performance_flushableCache_appendAndFlush_overwritingCache() throws { 133 | let cache = FlushableCache( 134 | location: testFileLocation, 135 | maxMessageCount: TestableMessage.lorumIpsum.count, 136 | shouldOverwriteMessages: true 137 | ) 138 | 139 | // Fill the cache before the test starts. 140 | for message in TestableMessage.lorumIpsum { 141 | try cache.appendMessage(message) 142 | } 143 | measure { 144 | for message in TestableMessage.lorumIpsum { 145 | try? cache.appendMessage(message) 146 | try? cache.flushMessages() 147 | } 148 | } 149 | } 150 | 151 | func test_performance_flushableCache_appendAndFlushEvery50Messages_fillableCache() { 152 | let cache = FlushableCache( 153 | location: testFileLocation, 154 | maxMessageCount: TestableMessage.lorumIpsum.count, 155 | shouldOverwriteMessages: false 156 | ) 157 | measure { 158 | for (index, message) in TestableMessage.lorumIpsum.enumerated() { 159 | try? cache.appendMessage(message) 160 | if index % 50 == 0 { 161 | try? cache.flushMessages() 162 | } 163 | } 164 | 165 | // Prepare ourselves for the next run. 166 | cache.dropMessages() 167 | } 168 | } 169 | 170 | func test_performance_flushableCache_appendAndFlushEvery50Messages_overwritingCache() throws { 171 | let cache = FlushableCache( 172 | location: testFileLocation, 173 | maxMessageCount: TestableMessage.lorumIpsum.count, 174 | shouldOverwriteMessages: true 175 | ) 176 | 177 | // Fill the cache before the test starts. 178 | for message in TestableMessage.lorumIpsum { 179 | try cache.appendMessage(message) 180 | } 181 | measure { 182 | for (index, message) in TestableMessage.lorumIpsum.enumerated() { 183 | try? cache.appendMessage(message) 184 | if index % 50 == 0 { 185 | try? cache.flushMessages() 186 | } 187 | } 188 | } 189 | } 190 | 191 | func test_performance_flushableCache_messages_fillableCache() throws { 192 | let cache = FlushableCache( 193 | location: testFileLocation, 194 | maxMessageCount: TestableMessage.lorumIpsum.count, 195 | shouldOverwriteMessages: false 196 | ) 197 | for message in TestableMessage.lorumIpsum { 198 | try cache.appendMessage(message) 199 | } 200 | try cache.flushMessages() 201 | measure { 202 | let freshCache = FlushableCache( 203 | location: testFileLocation, 204 | maxMessageCount: TestableMessage.lorumIpsum.count, 205 | shouldOverwriteMessages: false 206 | ) 207 | _ = try? freshCache.messages() 208 | } 209 | } 210 | 211 | func test_performance_flushableCache_messages_overwritingCache() throws { 212 | let cache = FlushableCache( 213 | location: testFileLocation, 214 | maxMessageCount: TestableMessage.lorumIpsum.count, 215 | shouldOverwriteMessages: true 216 | ) 217 | for message in TestableMessage.lorumIpsum + TestableMessage.lorumIpsum { 218 | try? cache.appendMessage(message) 219 | } 220 | try cache.flushMessages() 221 | measure { 222 | let freshCache = FlushableCache( 223 | location: testFileLocation, 224 | maxMessageCount: TestableMessage.lorumIpsum.count, 225 | shouldOverwriteMessages: true 226 | ) 227 | _ = try? freshCache.messages() 228 | } 229 | } 230 | 231 | // MARK: Private 232 | 233 | private let testFileLocation = FileManager.default.temporaryDirectory.appendingPathComponent("SQLiteTests") 234 | } 235 | 236 | /// A simple in-memory cache that can be flushed to disk. 237 | class FlushableCache { 238 | // MARK: Initialization 239 | 240 | init( 241 | location: URL, 242 | maxMessageCount: Int, 243 | shouldOverwriteMessages: Bool 244 | ) { 245 | self.location = location 246 | self.maxMessageCount = maxMessageCount 247 | self.shouldOverwriteMessages = shouldOverwriteMessages 248 | } 249 | 250 | // MARK: Internal 251 | 252 | func appendMessage(_ message: T) throws { 253 | var updatedMessages = try messages() + [message] 254 | if updatedMessages.count > maxMessageCount { 255 | guard shouldOverwriteMessages else { 256 | // We do not have room for this message. 257 | return 258 | } 259 | updatedMessages = Array(updatedMessages.dropFirst()) 260 | } 261 | 262 | _messages = updatedMessages 263 | } 264 | 265 | func messages() throws -> [T] { 266 | if let _messages { 267 | return _messages 268 | } else { 269 | let persistedData = try Data(contentsOf: location) 270 | guard !persistedData.isEmpty else { 271 | _messages = [] 272 | return [] 273 | } 274 | let persistedMessages = try decoder.decode([T].self, from: persistedData) 275 | _messages = persistedMessages 276 | return persistedMessages 277 | } 278 | } 279 | 280 | func flushMessages() throws { 281 | try encoder.encode(messages()).write(to: location) 282 | } 283 | 284 | func dropMessages() { 285 | _messages = [] 286 | } 287 | 288 | // MARK: Private 289 | 290 | private var _messages: [T]? 291 | 292 | private let location: URL 293 | private let maxMessageCount: Int 294 | private let shouldOverwriteMessages: Bool 295 | private let encoder = JSONEncoder() 296 | private let decoder = JSONDecoder() 297 | } 298 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/SQLitePerformanceComparisonTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 4/23/20. 3 | // Copyright © 2020 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS"BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | #if !os(Linux) 19 | import SQLite3 20 | import XCTest 21 | 22 | @testable import CacheAdvance 23 | 24 | final class SQLitePerformanceComparisonTests: XCTestCase { 25 | // MARK: XCTestCase 26 | 27 | override func setUp() { 28 | super.setUp() 29 | 30 | // Delete the existing cache. 31 | try? FileManager.default.removeItem(at: testFileLocation) 32 | } 33 | 34 | // MARK: Behavior Tests 35 | 36 | func test_append_sqlite_fillableCache_canMaintainMaxCount() { 37 | let cache = SQLiteCache( 38 | location: testFileLocation, 39 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 40 | shouldOverwriteMessages: false 41 | ) 42 | for message in TestableMessage.lorumIpsum + TestableMessage.lorumIpsum { 43 | cache.appendMessage(message) 44 | } 45 | 46 | XCTAssertEqual(cache.messages().count, TestableMessage.lorumIpsum.count) 47 | } 48 | 49 | func test_messages_sqlite_fillableCache_canReadInsertedMessages() { 50 | let cache = SQLiteCache( 51 | location: testFileLocation, 52 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 53 | shouldOverwriteMessages: true 54 | ) 55 | for message in TestableMessage.lorumIpsum { 56 | cache.appendMessage(message) 57 | } 58 | 59 | XCTAssertEqual(cache.messages(), TestableMessage.lorumIpsum) 60 | } 61 | 62 | func test_append_sqlite_overwritingCache_canMaintainMaxCount() { 63 | let cache = SQLiteCache( 64 | location: testFileLocation, 65 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 66 | shouldOverwriteMessages: true 67 | ) 68 | for message in TestableMessage.lorumIpsum + TestableMessage.lorumIpsum { 69 | cache.appendMessage(message) 70 | } 71 | 72 | XCTAssertEqual(cache.messages().count, TestableMessage.lorumIpsum.count) 73 | } 74 | 75 | func test_append_sqlite_overwritingCache_storesOnlyMostRecentMessages() { 76 | let cache = SQLiteCache( 77 | location: testFileLocation, 78 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 79 | shouldOverwriteMessages: true 80 | ) 81 | for message in TestableMessage.lorumIpsum { 82 | cache.appendMessage(message) 83 | } 84 | cache.appendMessage(#function) 85 | 86 | XCTAssertEqual(cache.messages(), Array(TestableMessage.lorumIpsum.dropFirst()) + [#function]) 87 | } 88 | 89 | func test_messages_sqlite_overwritingCache_canReadInsertedMessages() { 90 | let cache = SQLiteCache( 91 | location: testFileLocation, 92 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 93 | shouldOverwriteMessages: true 94 | ) 95 | for message in TestableMessage.lorumIpsum { 96 | cache.appendMessage(message) 97 | } 98 | 99 | XCTAssertEqual(cache.messages(), TestableMessage.lorumIpsum) 100 | } 101 | 102 | // MARK: Performance Tests 103 | 104 | func test_performance_sqlite_createDatabaseAndAppendSingleMessage() { 105 | measure { 106 | // Delete any existing database. 107 | try? FileManager.default.removeItem(at: testFileLocation) 108 | 109 | let cache = SQLiteCache( 110 | location: testFileLocation, 111 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 112 | shouldOverwriteMessages: false 113 | ) 114 | 115 | cache.appendMessage("test message") 116 | } 117 | } 118 | 119 | func test_performance_sqlite_append_fillableCache() { 120 | // Create a cache that won't run out of room over multiple test runs. 121 | let cache = SQLiteCache( 122 | location: testFileLocation, 123 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count * 10), 124 | shouldOverwriteMessages: false 125 | ) 126 | cache.createDatabaseIfNecessay() 127 | 128 | measure { 129 | for message in TestableMessage.lorumIpsum { 130 | cache.appendMessage(message) 131 | } 132 | } 133 | } 134 | 135 | func test_performance_sqlite_append_overwritingCache() { 136 | let cache = SQLiteCache( 137 | location: testFileLocation, 138 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 139 | shouldOverwriteMessages: true 140 | ) 141 | 142 | // Fill the cache before the test starts. 143 | for message in TestableMessage.lorumIpsum { 144 | cache.appendMessage(message) 145 | } 146 | measure { 147 | for message in TestableMessage.lorumIpsum { 148 | cache.appendMessage(message) 149 | } 150 | } 151 | } 152 | 153 | func test_performance_sqlite_messages_fillableCache() { 154 | let cache = SQLiteCache( 155 | location: testFileLocation, 156 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 157 | shouldOverwriteMessages: false 158 | ) 159 | for message in TestableMessage.lorumIpsum { 160 | cache.appendMessage(message) 161 | } 162 | measure { 163 | _ = cache.messages() 164 | } 165 | } 166 | 167 | func test_performance_sqlite_messages_overwritingCache() { 168 | let cache = SQLiteCache( 169 | location: testFileLocation, 170 | maxMessageCount: Int32(TestableMessage.lorumIpsum.count), 171 | shouldOverwriteMessages: true 172 | ) 173 | for message in TestableMessage.lorumIpsum + TestableMessage.lorumIpsum { 174 | cache.appendMessage(message) 175 | } 176 | measure { 177 | _ = cache.messages() 178 | } 179 | } 180 | 181 | // MARK: Private 182 | 183 | private let testFileLocation = FileManager.default.temporaryDirectory.appendingPathComponent("SQLiteTests") 184 | } 185 | 186 | class SQLiteCache { 187 | // MARK: Initialization 188 | 189 | init( 190 | location: URL, 191 | maxMessageCount: Int32, 192 | shouldOverwriteMessages: Bool 193 | ) { 194 | self.location = location 195 | self.maxMessageCount = maxMessageCount 196 | self.shouldOverwriteMessages = shouldOverwriteMessages 197 | } 198 | 199 | deinit { 200 | if let db { 201 | sqlite3_close(db) 202 | } 203 | } 204 | 205 | // MARK: Internal 206 | 207 | func createDatabaseIfNecessay() { 208 | guard db == nil else { 209 | return 210 | } 211 | guard sqlite3_open(NSString(string: location.path).utf8String, &db) == SQLITE_OK else { 212 | XCTFail("Failed to open database") 213 | return 214 | } 215 | var createTableStatement: OpaquePointer? 216 | guard sqlite3_prepare_v2(db, "CREATE TABLE Messages(Message BLOB);", -1, &createTableStatement, nil) == SQLITE_OK else { 217 | XCTFail("Failed to prepare database") 218 | return 219 | } 220 | guard sqlite3_step(createTableStatement) == SQLITE_DONE else { 221 | XCTFail("Failed to create database") 222 | return 223 | } 224 | sqlite3_finalize(createTableStatement) 225 | } 226 | 227 | func appendMessage(_ message: T) { 228 | createDatabaseIfNecessay() 229 | guard let messageData = try? encoder.encode(message), 230 | let messageDataPointer = messageData.withUnsafeBytes({ $0.baseAddress }) 231 | else { 232 | XCTFail("Could not encode message") 233 | return 234 | } 235 | 236 | if messageCount() == maxMessageCount { 237 | guard shouldOverwriteMessages else { 238 | // We do not have room for this message. 239 | return 240 | } 241 | // Delete the first message to make room for this message. 242 | let rawDeleteQuery = "DELETE FROM Messages ORDER BY rowid LIMIT 1;" 243 | var deleteQuery: OpaquePointer? 244 | guard sqlite3_prepare_v2(db, rawDeleteQuery, -1, &deleteQuery, nil) == SQLITE_OK else { 245 | XCTFail("Failed to prepare delete") 246 | return 247 | } 248 | defer { 249 | sqlite3_finalize(deleteQuery) 250 | } 251 | 252 | guard sqlite3_step(deleteQuery) == SQLITE_DONE else { 253 | XCTFail("Failed to delete oldest message") 254 | return 255 | } 256 | } 257 | 258 | let insertStatementString = "INSERT INTO Messages(Message) VALUES (?);" 259 | var insertStatement: OpaquePointer? 260 | 261 | guard sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK else { 262 | XCTFail("Failed to prepare message") 263 | return 264 | } 265 | sqlite3_bind_blob(insertStatement, 1, messageDataPointer, Int32(messageData.count), nil) 266 | guard sqlite3_step(insertStatement) == SQLITE_DONE else { 267 | XCTFail("Failed to insert message") 268 | return 269 | } 270 | sqlite3_finalize(insertStatement) 271 | } 272 | 273 | func messages() -> [T] { 274 | createDatabaseIfNecessay() 275 | let rawQuery = "SELECT * FROM Messages;" 276 | var query: OpaquePointer? 277 | guard sqlite3_prepare_v2(db, rawQuery, -1, &query, nil) == SQLITE_OK else { 278 | XCTFail("Failed to prepare read") 279 | return [] 280 | } 281 | defer { 282 | sqlite3_finalize(query) 283 | } 284 | var logs = [T]() 285 | while sqlite3_step(query) == SQLITE_ROW { 286 | guard let messageDataPointer = sqlite3_column_blob(query, 0) else { 287 | XCTFail("Failed to retrieve message blob") 288 | return [] 289 | } 290 | let messageLength = sqlite3_column_bytes(query, 0) 291 | let messageData = Data(bytes: messageDataPointer, count: Int(messageLength)) 292 | guard let message = try? decoder.decode(T.self, from: messageData) else { 293 | XCTFail("Failed to decode message") 294 | return [] 295 | } 296 | logs.append(message) 297 | } 298 | 299 | return logs 300 | } 301 | 302 | // MARK: Private 303 | 304 | private var db: OpaquePointer? 305 | private let location: URL 306 | private let maxMessageCount: Int32 307 | private let shouldOverwriteMessages: Bool 308 | private let encoder = JSONEncoder() 309 | private let decoder = JSONDecoder() 310 | 311 | private func messageCount() -> Int32 { 312 | let rawCountQuery = "SELECT COUNT(*) FROM Messages;" 313 | var countQuery: OpaquePointer? 314 | guard sqlite3_prepare_v2(db, rawCountQuery, -1, &countQuery, nil) == SQLITE_OK else { 315 | XCTFail("Failed to prepare read") 316 | return Int32.max 317 | } 318 | defer { 319 | sqlite3_finalize(countQuery) 320 | } 321 | 322 | guard sqlite3_step(countQuery) == SQLITE_ROW else { 323 | XCTFail("Failed to get persisted message count") 324 | return Int32.max 325 | } 326 | 327 | return sqlite3_column_int(countQuery, 0) 328 | } 329 | } 330 | #endif 331 | -------------------------------------------------------------------------------- /Tests/CacheAdvanceTests/TestableMessage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Dan Federman on 12/20/19. 3 | // Copyright © 2019 Dan Federman. 4 | // 5 | // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // you may not use this file except in compliance with the License. 7 | // You may obtain a copy of the License at 8 | // 9 | //    http://www.apache.org/licenses/LICENSE-2.0 10 | // 11 | // Unless required by applicable law or agreed to in writing, software 12 | // distributed under the License is distributed on an "AS IS" BASIS, 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | // See the License for the specific language governing permissions and 15 | // limitations under the License. 16 | // 17 | 18 | import Foundation 19 | import LorumIpsum 20 | 21 | struct TestableMessage: Codable, ExpressibleByStringLiteral, Equatable { 22 | typealias StringLiteralType = String 23 | 24 | init(stringLiteral value: Self.StringLiteralType) { 25 | self.value = value 26 | } 27 | 28 | let value: String 29 | 30 | static let lorumIpsum = LorumIpsum.messages.map { TestableMessage(stringLiteral: $0) } 31 | } 32 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | comment: 5 | layout: "reach,diff,flags,tree" 6 | behavior: default 7 | require_changes: no 8 | 9 | coverage: 10 | status: 11 | project: 12 | default: 13 | threshold: 0.25% 14 | patch: off 15 | 16 | ignore: 17 | # Due to limitations in Github Actions' supported Xcode versions, we can't get good coverage on this file. 18 | - "Sources/CacheAdvance/FileHandleExtensions.swift" 19 | # This package is not shipped and only includes testing helpers. 20 | - "Sources/LorumIpsum" 21 | # This package is not shipped. 22 | - "Tests" 23 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | set -e 4 | 5 | pushd $(git rev-parse --show-toplevel) 6 | 7 | swift run --package-path CLI -c release swiftformat . 8 | 9 | popd 10 | --------------------------------------------------------------------------------