├── .editorconfig ├── .github └── workflows │ ├── async-v1.yaml │ ├── async-v2.yaml │ ├── coverage.yaml │ ├── documentation.yaml │ └── test.yaml ├── .gitignore ├── .rspec ├── async-rspec.gemspec ├── conduct.md ├── gems.rb ├── gems ├── async-v1.rb └── async-v2.rb ├── lib └── async │ ├── rspec.rb │ └── rspec │ ├── buffer.rb │ ├── leaks.rb │ ├── memory.rb │ ├── profile.rb │ ├── reactor.rb │ ├── ssl.rb │ └── version.rb ├── license.md ├── readme.md ├── release.cert └── spec ├── async └── rspec │ ├── buffer_spec.rb │ ├── leaks_spec.rb │ ├── memory_spec.rb │ ├── profile_spec.rb │ ├── reactor_spec.rb │ └── ssl_spec.rb └── spec_helper.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.github/workflows/async-v1.yaml: -------------------------------------------------------------------------------- 1 | name: Async v1 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{matrix.os}}-latest 8 | 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu 13 | 14 | ruby: 15 | - "2.7" 16 | - "3.0" 17 | - "3.1" 18 | - "3.2" 19 | 20 | env: 21 | BUNDLE_GEMFILE: gems/async-v1.rb 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{matrix.ruby}} 28 | bundler-cache: true 29 | 30 | - name: Run tests 31 | timeout-minutes: 5 32 | run: bundle exec rspec 33 | -------------------------------------------------------------------------------- /.github/workflows/async-v2.yaml: -------------------------------------------------------------------------------- 1 | name: Async v2 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{matrix.os}}-latest 8 | 9 | strategy: 10 | matrix: 11 | os: 12 | - ubuntu 13 | 14 | ruby: 15 | - "3.1" 16 | - "3.2" 17 | 18 | env: 19 | BUNDLE_GEMFILE: gems/async-v2.rb 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{matrix.ruby}} 26 | bundler-cache: true 27 | 28 | - name: Run tests 29 | timeout-minutes: 5 30 | run: bundle exec rspec 31 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | COVERAGE: PartialSummary 11 | 12 | jobs: 13 | test: 14 | name: ${{matrix.ruby}} on ${{matrix.os}} 15 | runs-on: ${{matrix.os}}-latest 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "3.2" 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{matrix.ruby}} 31 | bundler-cache: true 32 | 33 | - name: Run tests 34 | timeout-minutes: 5 35 | run: bundle exec bake test 36 | 37 | - uses: actions/upload-artifact@v2 38 | with: 39 | name: coverage-${{matrix.os}}-${{matrix.ruby}} 40 | path: .covered.db 41 | 42 | validate: 43 | needs: test 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: ruby/setup-ruby@v1 49 | with: 50 | ruby-version: "3.2" 51 | bundler-cache: true 52 | 53 | - uses: actions/download-artifact@v3 54 | 55 | - name: Validate coverage 56 | timeout-minutes: 5 57 | run: bundle exec bake covered:validate --paths */.covered.db \; 58 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | # Allows you to run this workflow manually from the Actions tab: 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages: 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow one concurrent deployment: 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: true 21 | 22 | env: 23 | CONSOLE_OUTPUT: XTerm 24 | BUNDLE_WITH: maintenance 25 | 26 | jobs: 27 | generate: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | 33 | - uses: ruby/setup-ruby@v1 34 | with: 35 | ruby-version: "3.2" 36 | bundler-cache: true 37 | 38 | - name: Installing packages 39 | run: sudo apt-get install wget 40 | 41 | - name: Generate documentation 42 | timeout-minutes: 5 43 | run: bundle exec bake utopia:project:static --force no 44 | 45 | - name: Upload documentation artifact 46 | uses: actions/upload-pages-artifact@v1 47 | with: 48 | path: docs 49 | 50 | deploy: 51 | runs-on: ubuntu-latest 52 | 53 | environment: 54 | name: github-pages 55 | url: ${{steps.deployment.outputs.page_url}} 56 | 57 | needs: generate 58 | steps: 59 | - name: Deploy to GitHub Pages 60 | id: deployment 61 | uses: actions/deploy-pages@v1 62 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: 6 | contents: read 7 | 8 | env: 9 | CONSOLE_OUTPUT: XTerm 10 | 11 | jobs: 12 | test: 13 | name: ${{matrix.ruby}} on ${{matrix.os}} 14 | runs-on: ${{matrix.os}}-latest 15 | continue-on-error: ${{matrix.experimental}} 16 | 17 | strategy: 18 | matrix: 19 | os: 20 | - ubuntu 21 | - macos 22 | 23 | ruby: 24 | - "2.7" 25 | - "3.0" 26 | - "3.1" 27 | - "3.2" 28 | 29 | experimental: [false] 30 | 31 | include: 32 | - os: ubuntu 33 | ruby: truffleruby 34 | experimental: true 35 | - os: ubuntu 36 | ruby: jruby 37 | experimental: true 38 | - os: ubuntu 39 | ruby: head 40 | experimental: true 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | - uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: ${{matrix.ruby}} 47 | bundler-cache: true 48 | 49 | - name: Run tests 50 | timeout-minutes: 10 51 | run: bundle exec bake test 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /pkg/ 3 | /gems.locked 4 | /.covered.db 5 | /external 6 | 7 | /.rspec 8 | /.rspec_status 9 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --warnings 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /async-rspec.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/async/rspec/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "async-rspec" 7 | spec.version = Async::RSpec::VERSION 8 | 9 | spec.summary = "Helpers for writing specs against the async gem." 10 | spec.authors = ["Samuel Williams", "Janko Marohnić", "Olle Jonsson", "Cyril Roelandt", "Jeremy Jung", "Robin Goos"] 11 | spec.license = "MIT" 12 | 13 | spec.cert_chain = ['release.cert'] 14 | spec.signing_key = File.expand_path('~/.gem/release.pem') 15 | 16 | spec.homepage = "https://github.com/socketry/async-rspec" 17 | 18 | spec.files = Dir.glob(['{lib}/**/*', '*.md'], File::FNM_DOTMATCH, base: __dir__) 19 | 20 | spec.add_dependency "rspec", "~> 3.0" 21 | spec.add_dependency "rspec-files", "~> 1.0" 22 | spec.add_dependency "rspec-memory", "~> 1.0" 23 | 24 | spec.add_development_dependency "async" 25 | spec.add_development_dependency "bundler" 26 | spec.add_development_dependency "covered" 27 | end 28 | -------------------------------------------------------------------------------- /conduct.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /gems.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | source 'https://rubygems.org' 7 | 8 | gemspec 9 | 10 | # gem "async", path: "../async" 11 | 12 | group :maintenance, optional: true do 13 | gem "bake-modernize" 14 | gem "bake-gem" 15 | 16 | gem "utopia-project" 17 | end 18 | 19 | group :test do 20 | gem "bake-test" 21 | end 22 | -------------------------------------------------------------------------------- /gems/async-v1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2023, by Samuel Williams. 5 | 6 | source 'https://rubygems.org' 7 | 8 | gemspec path: "../" 9 | 10 | gem 'async', '~> 1.0' 11 | -------------------------------------------------------------------------------- /gems/async-v2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2021-2023, by Samuel Williams. 5 | 6 | source 'https://rubygems.org' 7 | 8 | gemspec path: "../" 9 | 10 | gem 'async', '~> 2.0' 11 | -------------------------------------------------------------------------------- /lib/async/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require_relative "rspec/version" 7 | require_relative "rspec/reactor" 8 | require_relative "rspec/memory" 9 | -------------------------------------------------------------------------------- /lib/async/rspec/buffer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'securerandom' 7 | 8 | module Async 9 | module RSpec 10 | module Buffer 11 | TMP = "/tmp" 12 | 13 | def self.open(mode = 'w+', root: TMP) 14 | path = File.join(root, SecureRandom.hex(32)) 15 | file = File.open(path, mode) 16 | 17 | File.unlink(path) 18 | 19 | return file unless block_given? 20 | 21 | begin 22 | yield file 23 | ensure 24 | file.close 25 | end 26 | end 27 | end 28 | 29 | ::RSpec.shared_context Buffer do 30 | let(:buffer) {Buffer.open} 31 | after(:each) {buffer.close} 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/async/rspec/leaks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'rspec/files' 7 | 8 | module Async 9 | module RSpec 10 | Leaks = ::RSpec::Files::Leaks 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/async/rspec/memory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | # Copyright, 2018, by Janko Marohnić. 6 | 7 | require 'rspec/memory' 8 | 9 | module Async 10 | module RSpec 11 | Memory = ::RSpec::Memory 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/async/rspec/profile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | module Async 7 | module RSpec 8 | module Profile 9 | end 10 | 11 | ::RSpec.shared_context Profile do 12 | before(:all) do 13 | warn "Profiling not enabled/supported." 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/async/rspec/reactor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2023, by Robin Goos. 6 | 7 | require_relative 'leaks' 8 | 9 | require 'kernel/sync' 10 | require 'kernel/async' 11 | require 'async/reactor' 12 | require 'async/task' 13 | 14 | module Async 15 | module RSpec 16 | module Reactor 17 | def notify_failure(exception = $!) 18 | ::RSpec::Support.notify_failure(exception) 19 | end 20 | 21 | def run_in_reactor(reactor, duration = nil) 22 | result = nil 23 | timer_task = nil 24 | 25 | if duration 26 | timer_task = reactor.async do |task| 27 | # Wait for the timeout, at any point this task might be cancelled if the user code completes: 28 | task.annotate("Timer task duration=#{duration}.") 29 | task.sleep(duration) 30 | 31 | # The timeout expired, so generate an error: 32 | buffer = StringIO.new 33 | reactor.print_hierarchy(buffer) 34 | 35 | # Raise an error so it is logged: 36 | raise TimeoutError, "Run time exceeded duration #{duration}s:\n#{buffer.string}" 37 | end 38 | end 39 | 40 | spec_task = reactor.async do |spec_task| 41 | spec_task.annotate("running example") 42 | 43 | result = yield(spec_task) 44 | 45 | # We are finished, so stop the timer task if it was started: 46 | timer_task&.stop 47 | 48 | # Now stop the entire reactor: 49 | raise Async::Stop 50 | end 51 | 52 | begin 53 | timer_task&.wait 54 | spec_task.wait 55 | ensure 56 | spec_task.stop 57 | end 58 | 59 | return result 60 | end 61 | end 62 | 63 | ::RSpec.shared_context Reactor do 64 | include Reactor 65 | let(:reactor) {@reactor} 66 | 67 | # This is fiber local: 68 | rspec_context = Thread.current[:__rspec] 69 | 70 | include_context Async::RSpec::Leaks 71 | 72 | around(:each) do |example| 73 | duration = example.metadata.fetch(:timeout, 60) 74 | 75 | begin 76 | Sync do |task| 77 | @reactor = task.reactor 78 | 79 | task.annotate(self.class) 80 | 81 | run_in_reactor(@reactor, duration) do 82 | Thread.current[:__rspec] = rspec_context 83 | example.run 84 | end 85 | ensure 86 | @reactor = nil 87 | end 88 | end 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/async/rspec/ssl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'openssl' 7 | 8 | module Async 9 | module RSpec 10 | module SSL 11 | module CertificateAuthority 12 | end 13 | 14 | module ValidCertificate 15 | end 16 | 17 | module InvalidCertificate 18 | end 19 | 20 | module VerifiedContexts 21 | end 22 | 23 | module HostCertificates 24 | end 25 | end 26 | 27 | ::RSpec.shared_context SSL::CertificateAuthority do 28 | # This key size is generally considered insecure, but it's fine for testing. 29 | let(:certificate_authority_key) {OpenSSL::PKey::RSA.new(2048)} 30 | let(:certificate_authority_name) {OpenSSL::X509::Name.parse("O=TestCA/CN=localhost")} 31 | 32 | # The certificate authority is used for signing and validating the certificate which is used for communciation: 33 | let(:certificate_authority) do 34 | certificate = OpenSSL::X509::Certificate.new 35 | 36 | certificate.subject = certificate_authority_name 37 | # We use the same issuer as the subject, which makes this certificate self-signed: 38 | certificate.issuer = certificate_authority_name 39 | 40 | certificate.public_key = certificate_authority_key.public_key 41 | 42 | certificate.serial = 1 43 | certificate.version = 2 44 | 45 | certificate.not_before = Time.now 46 | certificate.not_after = Time.now + 3600 47 | 48 | extension_factory = OpenSSL::X509::ExtensionFactory.new 49 | extension_factory.subject_certificate = certificate 50 | extension_factory.issuer_certificate = certificate 51 | certificate.add_extension extension_factory.create_extension("basicConstraints", "CA:TRUE", true) 52 | certificate.add_extension extension_factory.create_extension("keyUsage", "keyCertSign, cRLSign", true) 53 | certificate.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash") 54 | certificate.add_extension extension_factory.create_extension("authorityKeyIdentifier", "keyid:always", false) 55 | 56 | certificate.sign certificate_authority_key, OpenSSL::Digest::SHA256.new 57 | end 58 | 59 | let(:certificate_store) do 60 | # The certificate store which is used for validating the server certificate: 61 | OpenSSL::X509::Store.new.tap do |certificates| 62 | certificates.add_cert(certificate_authority) 63 | end 64 | end 65 | end 66 | 67 | ::RSpec.shared_context SSL::ValidCertificate do 68 | include_context SSL::CertificateAuthority 69 | 70 | # The private key to use on the server side: 71 | let(:key) {OpenSSL::PKey::RSA.new(2048)} 72 | let(:certificate_name) {OpenSSL::X509::Name.parse("O=Test/CN=localhost")} 73 | 74 | # The certificate used for actual communication: 75 | let(:certificate) do 76 | certificate = OpenSSL::X509::Certificate.new 77 | certificate.subject = certificate_name 78 | certificate.issuer = certificate_authority.subject 79 | 80 | certificate.public_key = key.public_key 81 | 82 | certificate.serial = 2 83 | certificate.version = 2 84 | 85 | certificate.not_before = Time.now 86 | certificate.not_after = Time.now + 3600 87 | 88 | extension_factory = OpenSSL::X509::ExtensionFactory.new() 89 | extension_factory.subject_certificate = certificate 90 | extension_factory.issuer_certificate = certificate_authority 91 | certificate.add_extension extension_factory.create_extension("keyUsage", "digitalSignature", true) 92 | certificate.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash") 93 | 94 | certificate.sign certificate_authority_key, OpenSSL::Digest::SHA256.new 95 | end 96 | end 97 | 98 | ::RSpec.shared_context SSL::HostCertificates do 99 | include_context SSL::CertificateAuthority 100 | 101 | let(:keys) do 102 | Hash[ 103 | hosts.collect{|name| [name, OpenSSL::PKey::RSA.new(2048)]} 104 | ] 105 | end 106 | 107 | # The certificate used for actual communication: 108 | let(:certificates) do 109 | Hash[ 110 | hosts.collect do |name| 111 | certificate_name = OpenSSL::X509::Name.parse("O=Test/CN=#{name}") 112 | 113 | certificate = OpenSSL::X509::Certificate.new 114 | certificate.subject = certificate_name 115 | certificate.issuer = certificate_authority.subject 116 | 117 | certificate.public_key = keys[name].public_key 118 | 119 | certificate.serial = 2 120 | certificate.version = 2 121 | 122 | certificate.not_before = Time.now 123 | certificate.not_after = Time.now + 3600 124 | 125 | extension_factory = OpenSSL::X509::ExtensionFactory.new 126 | extension_factory.subject_certificate = certificate 127 | extension_factory.issuer_certificate = certificate_authority 128 | certificate.add_extension extension_factory.create_extension("keyUsage", "digitalSignature", true) 129 | certificate.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash") 130 | 131 | certificate.sign certificate_authority_key, OpenSSL::Digest::SHA256.new 132 | 133 | [name, certificate] 134 | end 135 | ] 136 | end 137 | 138 | let(:server_context) do 139 | OpenSSL::SSL::SSLContext.new.tap do |context| 140 | context.servername_cb = Proc.new do |socket, name| 141 | if hosts.include? name 142 | socket.hostname = name 143 | 144 | OpenSSL::SSL::SSLContext.new.tap do |context| 145 | context.cert = certificates[name] 146 | context.key = keys[name] 147 | end 148 | end 149 | end 150 | end 151 | end 152 | 153 | let(:client_context) do 154 | OpenSSL::SSL::SSLContext.new.tap do |context| 155 | context.cert_store = certificate_store 156 | context.verify_mode = OpenSSL::SSL::VERIFY_PEER 157 | end 158 | end 159 | end 160 | 161 | ::RSpec.shared_context SSL::InvalidCertificate do 162 | include_context SSL::CertificateAuthority 163 | 164 | # The private key to use on the server side: 165 | let(:key) {OpenSSL::PKey::RSA.new(2048)} 166 | let(:invalid_key) {OpenSSL::PKey::RSA.new(2048)} 167 | let(:certificate_name) {OpenSSL::X509::Name.parse("O=Test/CN=localhost")} 168 | 169 | # The certificate used for actual communication: 170 | let(:certificate) do 171 | certificate = OpenSSL::X509::Certificate.new 172 | certificate.subject = certificate_name 173 | certificate.issuer = certificate_authority.subject 174 | 175 | certificate.public_key = key.public_key 176 | 177 | certificate.serial = 2 178 | certificate.version = 2 179 | 180 | certificate.not_before = Time.now - 3600 181 | certificate.not_after = Time.now 182 | 183 | extension_factory = OpenSSL::X509::ExtensionFactory.new() 184 | extension_factory.subject_certificate = certificate 185 | extension_factory.issuer_certificate = certificate_authority 186 | certificate.add_extension extension_factory.create_extension("keyUsage", "digitalSignature", true) 187 | certificate.add_extension extension_factory.create_extension("subjectKeyIdentifier", "hash") 188 | 189 | certificate.sign invalid_key, OpenSSL::Digest::SHA256.new 190 | end 191 | end 192 | 193 | ::RSpec.shared_context SSL::VerifiedContexts do 194 | let(:server_context) do 195 | OpenSSL::SSL::SSLContext.new.tap do |context| 196 | context.cert = certificate 197 | context.key = key 198 | end 199 | end 200 | 201 | let(:client_context) do 202 | OpenSSL::SSL::SSLContext.new.tap do |context| 203 | context.cert_store = certificate_store 204 | context.verify_mode = OpenSSL::SSL::VERIFY_PEER 205 | end 206 | end 207 | end 208 | end 209 | end 210 | -------------------------------------------------------------------------------- /lib/async/rspec/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | module Async 7 | module RSpec 8 | VERSION = "1.17.1" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright, 2017-2023, by Samuel Williams. 4 | Copyright, 2018, by Janko Marohnić. 5 | Copyright, 2019, by Jeremy Jung. 6 | Copyright, 2019, by Cyril Roelandt. 7 | Copyright, 2020-2021, by Olle Jonsson. 8 | Copyright, 2023, by Robin Goos. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Async::RSpec 2 | 3 | Provides useful `RSpec.shared_context`s for testing code that builds on top of [async](https://github.com/socketry/async). 4 | 5 | [![Development Status](https://github.com/socketry/async-rspec/workflows/Test/badge.svg)](https://github.com/socketry/async-rspec/actions?workflow=Test) 6 | 7 | ## Installation 8 | 9 | ``` shell 10 | $ bundle add async-rspec 11 | ``` 12 | 13 | Then add this require statement to the top of `spec/spec_helper.rb` 14 | 15 | ``` ruby 16 | require 'async/rspec' 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### Async Reactor 22 | 23 | Many specs need to run within a reactor. A shared context is provided which includes all the relevant bits, including the above leaks checks. If your spec fails to run in less than 10 seconds, an `Async::TimeoutError` raises to prevent your test suite from hanging. 24 | 25 | ``` ruby 26 | require 'async/io' 27 | 28 | RSpec.describe Async::IO do 29 | include_context Async::RSpec::Reactor 30 | 31 | let(:pipe) {IO.pipe} 32 | let(:input) {Async::IO::Generic.new(pipe.first)} 33 | let(:output) {Async::IO::Generic.new(pipe.last)} 34 | 35 | it "should send and receive data within the same reactor" do 36 | message = nil 37 | 38 | output_task = reactor.async do 39 | message = input.read(1024) 40 | end 41 | 42 | reactor.async do 43 | output.write("Hello World") 44 | end 45 | 46 | output_task.wait 47 | expect(message).to be == "Hello World" 48 | 49 | input.close 50 | output.close 51 | end 52 | end 53 | ``` 54 | 55 | ### Changing Timeout 56 | 57 | You can change the timeout by specifying it as an option: 58 | 59 | ``` ruby 60 | RSpec.describe MySlowThing, timeout: 60 do 61 | # ... 62 | end 63 | ``` 64 | 65 | ### File Descriptor Leaks 66 | 67 | Leaking sockets and other kinds of IOs are a problem for long running services. `Async::RSpec::Leaks` tracks all open sockets both before and after the spec. If any are left open, a `RuntimeError` is raised and the spec fails. 68 | 69 | ``` ruby 70 | RSpec.describe "leaky ios" do 71 | include_context Async::RSpec::Leaks 72 | 73 | # The following fails: 74 | it "leaks io" do 75 | @input, @output = IO.pipe 76 | end 77 | end 78 | ``` 79 | 80 | In some cases, the Ruby garbage collector will close IOs. In the above case, it's possible that just writing `IO.pipe` will not leak as Ruby will garbage collect the resulting IOs immediately. It's still incorrect to not close IOs, so don't depend on this behaviour. 81 | 82 | ### Allocations 83 | 84 | This functionality was moved to [`rspec-memory`](https://github.com/socketry/rspec-memory). 85 | 86 | ## Contributing 87 | 88 | We welcome contributions to this project. 89 | 90 | 1. Fork it. 91 | 2. Create your feature branch (`git checkout -b my-new-feature`). 92 | 3. Commit your changes (`git commit -am 'Add some feature'`). 93 | 4. Push to the branch (`git push origin my-new-feature`). 94 | 5. Create new Pull Request. 95 | -------------------------------------------------------------------------------- /release.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11 3 | ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK 4 | CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz 5 | MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd 6 | MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj 7 | bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB 8 | igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2 9 | 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW 10 | sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE 11 | e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN 12 | XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss 13 | RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn 14 | tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM 15 | zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW 16 | xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O 17 | BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs 18 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs 19 | aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE 20 | cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl 21 | xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/ 22 | c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp 23 | 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws 24 | JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP 25 | eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt 26 | Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8 27 | voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /spec/async/rspec/buffer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/rspec/buffer' 7 | 8 | RSpec.describe Async::RSpec::Buffer do 9 | include_context Async::RSpec::Buffer 10 | 11 | it "behaves like a file" do 12 | expect(buffer).to be_instance_of(File) 13 | end 14 | 15 | it "should not exist on disk" do 16 | expect(File).to_not be_exist(buffer.path) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/async/rspec/leaks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async/rspec/leaks' 7 | 8 | RSpec.describe "leaks context" do 9 | include_context Async::RSpec::Leaks 10 | 11 | it "leaks io" do 12 | expect(before_ios).to be == current_ios 13 | 14 | input, output = IO.pipe 15 | 16 | expect(before_ios).to_not be == current_ios 17 | 18 | input.close 19 | output.close 20 | 21 | expect(before_ios).to be == current_ios 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/async/rspec/memory_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | # Copyright, 2018, by Janko Marohnić. 6 | 7 | require 'async/rspec/memory' 8 | 9 | RSpec.describe Async::RSpec::Memory do 10 | include_context Async::RSpec::Memory 11 | 12 | it "should execute code in block" do 13 | string = nil 14 | 15 | expect do 16 | string = String.new 17 | end.to limit_allocations(String => 1) 18 | 19 | expect(string).to_not be_nil 20 | end 21 | 22 | context "on supported platform", if: Async::RSpec::Memory::Trace.supported? do 23 | it "should not exceed specified count limit" do 24 | expect do 25 | 2.times{String.new} 26 | end.to limit_allocations(String => 2) 27 | 28 | expect do 29 | 2.times{String.new} 30 | end.to limit_allocations.of(String, count: 2) 31 | end 32 | 33 | it "should fail if there are untracked allocations" do 34 | expect do 35 | expect do 36 | Array.new 37 | end.to limit_allocations 38 | end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /it was not specified/) 39 | end 40 | 41 | it "should exceed specified count limit" do 42 | expect do 43 | expect do 44 | 6.times{String.new} 45 | end.to limit_allocations(String => 4) 46 | end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /expected exactly 4 instances/) 47 | end if Async::RSpec::Memory::Trace.supported? 48 | 49 | it "should be within specified count range" do 50 | expect do 51 | 2.times{String.new} 52 | end.to limit_allocations(String => 1..3) 53 | 54 | expect do 55 | 2.times{String.new} 56 | end.to limit_allocations.of(String, count: 1..3) 57 | end 58 | 59 | it "should exceed specified count range" do 60 | expect do 61 | expect do 62 | 6.times{String.new} 63 | end.to limit_allocations(String => 1..3) 64 | end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /expected within 1..3 instances/) 65 | end 66 | 67 | it "should not exceed specified size limit" do 68 | expect do 69 | "a" * 100_000 70 | end.to limit_allocations.of(String, size: 100_001) 71 | end 72 | 73 | it "should exceed specified size limit" do 74 | expect do 75 | expect do 76 | "a" * 120_000 77 | end.to limit_allocations(size: 100_000) 78 | end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /expected exactly 100000 bytes/) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/async/rspec/profile_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'async' 7 | require 'async/rspec/profile' 8 | 9 | RSpec.describe Async::RSpec::Profile do 10 | include_context Async::RSpec::Profile 11 | 12 | it "profiles the function" do 13 | Async do |parent| 14 | Async do |child| 15 | child.sleep(1) 16 | end.wait 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/async/rspec/reactor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | # Copyright, 2023, by Robin Goos. 6 | 7 | require 'async/rspec/reactor' 8 | 9 | RSpec.describe Async::RSpec::Reactor do 10 | context "with shared context", timeout: 1 do 11 | include_context Async::RSpec::Reactor 12 | 13 | # The following fails: 14 | it "has reactor" do 15 | expect(reactor).to be_kind_of Async::Reactor 16 | end 17 | 18 | it "doesn't time out" do 19 | reactor.async do |task| 20 | expect do 21 | task.sleep(0.1) 22 | end.to_not raise_error 23 | end.wait 24 | end 25 | 26 | # it "times out" do 27 | # reactor.async do |task| 28 | # task.sleep(2) 29 | # end.wait 30 | # end 31 | # 32 | # it "propagates errors" do 33 | # reactor.async do |task| 34 | # raise "Boom!" 35 | # end.wait 36 | # end 37 | end 38 | 39 | context "timeouts", timeout: 1 do 40 | include Async::RSpec::Reactor 41 | 42 | it "times out" do 43 | expect do 44 | Sync do |task| 45 | run_in_reactor(task.reactor, 0.05) do |spec_task| 46 | spec_task.sleep(0.1) 47 | end 48 | end 49 | end.to raise_error(Async::TimeoutError) 50 | end 51 | 52 | it "doesn't time out" do 53 | expect do 54 | Sync do |task| 55 | run_in_reactor(task.reactor, 0.05) do |spec_task| 56 | spec_task.sleep(0.01) 57 | end 58 | end 59 | end.to_not raise_error 60 | end 61 | 62 | # it "propagates errors" do 63 | # expect do 64 | # run_in_reactor(reactor, 0.05) do 65 | # raise "Boom!" 66 | # end 67 | # end.to raise_error("Boom!") 68 | # end 69 | end 70 | 71 | context "rspec metadata", timeout: 1 do 72 | include_context Async::RSpec::Reactor 73 | 74 | it "should have access to example metadata" do 75 | expect(RSpec.current_example).not_to be_nil 76 | expect(RSpec.current_example.metadata[:described_class]).to eq(Async::RSpec::Reactor) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/async/rspec/ssl_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2018-2023, by Samuel Williams. 5 | 6 | require 'async/rspec/ssl' 7 | 8 | RSpec.describe Async::RSpec::SSL do 9 | context Async::RSpec::SSL::CertificateAuthority do 10 | include_context Async::RSpec::SSL::CertificateAuthority 11 | 12 | it "has a valid certificate authority" do 13 | expect(certificate_authority.verify(certificate_authority_key)).to be_truthy 14 | end 15 | end 16 | 17 | context Async::RSpec::SSL::ValidCertificate do 18 | include_context Async::RSpec::SSL::ValidCertificate 19 | 20 | it "can validate client certificate" do 21 | expect(certificate_store.verify(certificate)).to be_truthy 22 | end 23 | end 24 | 25 | context Async::RSpec::SSL::InvalidCertificate do 26 | include_context Async::RSpec::SSL::InvalidCertificate 27 | 28 | it "fails to validate certificate" do 29 | expect(certificate_store.verify(certificate)).to be_falsey 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Released under the MIT License. 4 | # Copyright, 2017-2023, by Samuel Williams. 5 | 6 | require 'covered/rspec' 7 | 8 | RSpec.configure do |config| 9 | # Enable flags like --only-failures and --next-failure 10 | config.example_status_persistence_file_path = ".rspec_status" 11 | 12 | # Disable RSpec exposing methods globally on `Module` and `main` 13 | config.disable_monkey_patching! 14 | 15 | config.expect_with :rspec do |c| 16 | c.syntax = :expect 17 | end 18 | end 19 | --------------------------------------------------------------------------------