diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..46f1c90 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: bundler + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + ignore: + - dependency-name: "rubocop-lts" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..502c2a0 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, "*-stable" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main, "*-stable" ] + schedule: + - cron: '35 1 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'ruby' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..6306958 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,121 @@ +name: Test Coverage + +env: + K_SOUP_COV_MIN_BRANCH: 80 + K_SOUP_COV_MIN_LINE: 91 + K_SOUP_COV_MIN_HARD: true + K_SOUP_COV_DO: true + K_SOUP_COV_COMMAND_NAME: "RSpec Coverage" + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs with Coverage - Ruby ${{ matrix.ruby }} ${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + # Coverage + - ruby: "3.4" + appraisal: "coverage" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - uses: amancevice/setup-code-climate@v2 + name: CodeClimate Install + if: ${{ github.event_name != 'pull_request' }} + with: + cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} + + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: "${{ matrix.ruby }}" + rubygems: "${{ matrix.rubygems }}" + bundler: "${{ matrix.bundler }}" + bundler-cache: false + + - name: CodeClimate Pre-build Notification + run: cc-test-reporter before-build + if: ${{ github.event_name != 'pull_request' }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + # Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root) + # We need to do this first to get appraisal installed. + # NOTE: This does not use the main Gemfile at all. + - name: Bundle install for Appraisal ${{ matrix.appraisal }} + run: bundle + - name: Install Appraisal ${{ matrix.appraisal }} dependencies + run: bundle exec appraisal ${{ matrix.appraisal }} bundle + - name: Run ${{ matrix.appraisal }} tests via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} + + - name: CodeClimate Post-build Notification + run: cc-test-reporter after-build + if: ${{ github.event_name != 'pull_request' }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Code Coverage Summary Report + uses: irongut/CodeCoverageSummary@v1.3.0 + if: ${{ github.event_name == 'pull_request' }} + with: + filename: ./coverage/coverage.xml + badge: true + fail_below_min: true + format: markdown + hide_branch_rate: false + hide_complexity: true + indicators: true + output: both + thresholds: '69 80' + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Add Coverage PR Comment + uses: marocchino/sticky-pull-request-comment@v2 + if: ${{ github.event_name == 'pull_request' }} + with: + recreate: true + path: code-coverage-results.md + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: ${{ matrix.experimental != 'false' }} + + - name: Upload results to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..0d4a013 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/.github/workflows/heads.yml b/.github/workflows/heads.yml new file mode 100644 index 0000000..f528289 --- /dev/null +++ b/.github/workflows/heads.yml @@ -0,0 +1,84 @@ +name: Heads Compat Matrix + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{ matrix.appraisal }}${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: true + matrix: + include: + # ruby-head + - ruby: "ruby-head" + appraisal: "ruby-head" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + # truffleruby-head + - ruby: "truffleruby-head" + appraisal: "truffleruby-head" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + # jruby-head + # Error output has a significantly different format in JRuby :( + # - ruby: "jruby-head" + # appraisal: "jruby-head" + # exec_cmd: "turbo_tests -n4" + # gemfile: "Appraisal.root" + # rubygems: latest + # bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: false + + # Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root) + # We need to do this first to get appraisal installed. + # NOTE: This does not use the main Gemfile at all. + - name: Bundle install for Appraisal ${{ matrix.appraisal }} + run: bundle + - name: Install Appraisal ${{ matrix.appraisal }} dependencies + run: bundle exec appraisal ${{ matrix.appraisal }} bundle + - name: Run ${{ matrix.appraisal }} tests via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/.github/workflows/legacy.yml b/.github/workflows/legacy.yml new file mode 100644 index 0000000..f2dc030 --- /dev/null +++ b/.github/workflows/legacy.yml @@ -0,0 +1,67 @@ +name: Legacy Compat (EOL, Ruby 3.0) Matrix + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{ matrix.appraisal }}${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + # Ruby 3.0 + - ruby: "3.0" + appraisal: "ruby-3-0" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: '3.5.23' + bundler: '2.5.23' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: false + + # Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root) + # We need to do this first to get appraisal installed. + # NOTE: This does not use the main Gemfile at all. + - name: Bundle install for Appraisal ${{ matrix.appraisal }} + run: bundle + - name: Install Appraisal ${{ matrix.appraisal }} dependencies + run: bundle exec appraisal ${{ matrix.appraisal }} bundle + - name: Run ${{ matrix.appraisal }} tests via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml new file mode 100644 index 0000000..62cbe81 --- /dev/null +++ b/.github/workflows/style.yml @@ -0,0 +1,64 @@ +name: Ruby - Style + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + rubocop: + name: RuboCop Gradual + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + # Style + - ruby: "3.4" + appraisal: "style" + exec_cmd: "rake rubocop_gradual:check" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: false + + # Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root) + # We need to do this first to get appraisal installed. + # NOTE: This does not use the main Gemfile at all. + - name: Bundle install for Appraisal ${{ matrix.appraisal }} + run: bundle + - name: Install Appraisal ${{ matrix.appraisal }} dependencies + run: bundle exec appraisal ${{ matrix.appraisal }} bundle + - name: Run ${{ matrix.appraisal }} tests via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/.github/workflows/supported.yml b/.github/workflows/supported.yml new file mode 100644 index 0000000..062faf0 --- /dev/null +++ b/.github/workflows/supported.yml @@ -0,0 +1,90 @@ +name: Supported Compat Matrix + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{ matrix.appraisal }}${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + matrix: + include: + # Ruby 3.1 + - ruby: "3.1" + appraisal: "ruby-3-1" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + # Ruby 3.2 + - ruby: "3.2" + appraisal: "ruby-3-2" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + # Ruby 3.3 + - ruby: "3.3" + appraisal: "ruby-3-3" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + # Ruby 3.4 + - ruby: "3.4" + appraisal: "ruby-3-4" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: latest + bundler: latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: false + + # Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root) + # We need to do this first to get appraisal installed. + # NOTE: This does not use the main Gemfile at all. + - name: Bundle install for Appraisal ${{ matrix.appraisal }} + run: bundle + - name: Install Appraisal ${{ matrix.appraisal }} dependencies + run: bundle exec appraisal ${{ matrix.appraisal }} bundle + - name: Run ${{ matrix.appraisal }} tests via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/.github/workflows/tag_and_release.yml b/.github/workflows/tag_and_release.yml index 9d0264a..96712c2 100644 --- a/.github/workflows/tag_and_release.yml +++ b/.github/workflows/tag_and_release.yml @@ -20,7 +20,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby 2.7 uses: ruby/setup-ruby@v1 with: @@ -33,7 +33,7 @@ jobs: bundle install bundle exec turbo_tests - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: "ruby-gem" path: "pkg/*.gem" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 657d595..4f56ce7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,6 +2,8 @@ name: Tests on: pull_request: + branches: + - '!*' # Ignore this workflow; Remove workflow once done pulling in upstream PRs permissions: contents: read @@ -17,7 +19,7 @@ jobs: os: [ ubuntu-latest, windows-latest ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} diff --git a/.github/workflows/unsupported.yml b/.github/workflows/unsupported.yml new file mode 100644 index 0000000..494818a --- /dev/null +++ b/.github/workflows/unsupported.yml @@ -0,0 +1,67 @@ +name: Unsupported Compat (EOL, Ruby 2.7) Matrix + +env: + K_SOUP_COV_DO: false + +on: + push: + branches: + - 'main' + tags: + - '!*' # Do not execute on tags + pull_request: + branches: + - '*' + # Allow manually triggering the workflow. + workflow_dispatch: + +permissions: + contents: read + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + test: + name: Specs - Ruby ${{ matrix.ruby }} ${{ matrix.appraisal }}${{ matrix.name_extra || '' }} + if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')" + runs-on: ubuntu-22.04 + continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }} + env: # $BUNDLE_GEMFILE must be set at job level, so it is set for all steps + BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}.gemfile + strategy: + fail-fast: false + matrix: + include: + # Ruby 2.7 + - ruby: "2.7" + appraisal: "ruby-2-7" + exec_cmd: "turbo_tests -n4" + gemfile: "Appraisal.root" + rubygems: '3.4.22' + bundler: '2.4.22' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Ruby & RubyGems + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + rubygems: ${{ matrix.rubygems }} + bundler: ${{ matrix.bundler }} + bundler-cache: false + + # Raw `bundle` will use the BUNDLE_GEMFILE set to matrix.gemfile (i.e. Appraisal.root) + # We need to do this first to get appraisal installed. + # NOTE: This does not use the main Gemfile at all. + - name: Bundle install for Appraisal ${{ matrix.appraisal }} + run: bundle + - name: Install Appraisal ${{ matrix.appraisal }} dependencies + run: bundle exec appraisal ${{ matrix.appraisal }} bundle + - name: Run ${{ matrix.appraisal }} tests via ${{ matrix.exec_cmd }} + run: bundle exec appraisal ${{ matrix.appraisal }} bundle exec ${{ matrix.exec_cmd }} diff --git a/.gitignore b/.gitignore index 2a71158..2fde0d5 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,7 @@ tags # End of https://www.toptal.com/developers/gitignore/api/macos,vim +# Appraisals Bundles (only commit the coverage, audit, and style lockfiles) +gemfiles/ruby_*.gemfile.lock +gemfiles/*_head.gemfile.lock +Appraisal.root.gemfile.lock \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..eb389b5 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,5 @@ +inherit_gem: + rubocop-lts: config/rubygem_rspec.yml + +require: + - 'rubocop-packaging' diff --git a/.rubocop_gradual.lock b/.rubocop_gradual.lock new file mode 100644 index 0000000..eea178e --- /dev/null +++ b/.rubocop_gradual.lock @@ -0,0 +1,65 @@ +{ + "fixtures/rspec/errors_outside_of_examples_spec.rb:2409688709": [ + [1, 16, 54, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 3626965792], + [2, 35, 3, "RSpec/BeEql: Prefer `be` over `eql`.", 193405885] + ], + "fixtures/rspec/failing_spec.rb:1154565131": [ + [1, 16, 23, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 986317040], + [11, 12, 1, "RSpec/ExpectActual: Provide the actual value you are testing to `expect(...)`.", 177559] + ], + "fixtures/rspec/no_method_error_spec.rb:47614094": [ + [1, 16, 20, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 4112385446] + ], + "fixtures/rspec/pending_exceptions_spec.rb:2868747541": [ + [1, 16, 51, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 4038399051], + [5, 12, 1, "RSpec/ExpectActual: Provide the actual value you are testing to `expect(...)`.", 177559], + [9, 12, 3, "RSpec/ExpectActual: Provide the actual value you are testing to `expect(...)`.", 193358996], + [12, 3, 43, "RSpec/PendingWithoutReason: Give the reason for xit.", 1479957056], + [13, 12, 1, "RSpec/ExpectActual: Provide the actual value you are testing to `expect(...)`.", 177556] + ], + "lib/turbo_tests.rb:2945832194": [ + [23, 5, 323, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 166166262], + [50, 5, 269, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 4255608859], + [71, 5, 614, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 574527918] + ], + "lib/turbo_tests/runner.rb:3014787945": [ + [12, 5, 479, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1520723843], + [23, 5, 1400, "Style/ClassMethodsDefinitions: Use `class << self` to define a class method.", 1925027850], + [240, 11, 10, "ThreadSafety/NewThread: Avoid starting new threads.", 3411682361], + [259, 47, 6, "Style/GlobalStdStream: Use `$stderr` instead of `STDERR`.", 3356712163], + [261, 21, 10, "ThreadSafety/NewThread: Avoid starting new threads.", 3411682361], + [270, 7, 10, "ThreadSafety/NewThread: Avoid starting new threads.", 3411682361], + [330, 9, 6, "Style/GlobalStdStream: Use `$stdout` instead of `STDOUT`.", 3356722952] + ], + "spec/cli_spec.rb:3990998076": [ + [1, 1, 30, "RSpec/SpecFilePathFormat: Spec path should end with `turbo_tests/cli*_spec.rb`.", 965721356], + [11, 13, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2146945865], + [30, 7, 12, "RSpec/MultipleExpectations: Example has too many expectations [3/1].", 2388739333], + [31, 34, 3, "RSpec/BeEql: Prefer `be` over `eql`.", 193405885], + [38, 13, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 812172126], + [41, 7, 632, "RSpec/ExampleLength: Example has too many lines. [10/5]", 4177603105], + [42, 34, 3, "RSpec/BeEql: Prefer `be` over `eql`.", 193405885], + [60, 13, 28, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 2146945865], + [75, 7, 12, "RSpec/MultipleExpectations: Example has too many expectations [3/1].", 2388739333], + [76, 34, 3, "RSpec/BeEql: Prefer `be` over `eql`.", 193405885], + [87, 13, 20, "RSpec/ContextWording: Context description should match /^when\\b/, /^with\\b/, or /^without\\b/.", 812172126], + [90, 7, 600, "RSpec/ExampleLength: Example has too many lines. [10/5]", 1660729462], + [91, 34, 3, "RSpec/BeEql: Prefer `be` over `eql`.", 193405885], + [111, 5, 32, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 281328851], + [112, 32, 3, "RSpec/BeEql: Prefer `be` over `eql`.", 193405885], + [121, 5, 38, "RSpec/MultipleExpectations: Example has too many expectations [2/1].", 3463001811], + [121, 5, 409, "RSpec/ExampleLength: Example has too many lines. [9/5]", 4122755410], + [122, 32, 3, "RSpec/BeEql: Prefer `be` over `eql`.", 193405885] + ], + "spec/doc_formatter_spec.rb:233381593": [ + [5, 16, 19, "RSpec/DescribeClass: The first argument to describe should be the class or module being tested.", 879596202] + ], + "spec/turbo_tests_spec.rb:811756577": [ + [10, 15, 7, "RSpec/MessageSpies: Prefer `have_received` for setting message expectations. Setup `ParallelTests::Tasks` as a spy using `allow` or `instance_spy`.", 1384559950], + [20, 15, 7, "RSpec/MessageSpies: Prefer `have_received` for setting message expectations. Setup `ParallelTests::Tasks` as a spy using `allow` or `instance_spy`.", 1384559950] + ], + "turbo_tests.gemspec:2921004918": [ + [29, 16, 36, "ThreadSafety/DirChdir: Avoid using `Dir.chdir` due to its process-wide effect.", 3576345059], + [30, 5, 19, "Packaging/GemspecGit: Avoid using git to produce lists of files. Downstreams often need to build your package in an environment that does not have git (on purpose). Use some pure Ruby alternative, like `Dir` or `Dir.glob`.", 3879951891] + ] +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..041df9a --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 3.4.1 diff --git a/Appraisal.root.gemfile b/Appraisal.root.gemfile new file mode 100644 index 0000000..c5db382 --- /dev/null +++ b/Appraisal.root.gemfile @@ -0,0 +1,12 @@ +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +source "https://rubygems.org" + +# Appraisal Root Gemfile is for running appraisal to generate the Appraisal Gemfiles +# in gemfiles/*gemfile. +# On CI, we use it for the Appraisal-based builds. +# We do not load the standard Gemfile, as it is tailored for local development. + +gemspec + +gem "appraisal", github: "pboling/appraisal", branch: "eval_gemfile" diff --git a/Appraisals b/Appraisals new file mode 100644 index 0000000..f24e85e --- /dev/null +++ b/Appraisals @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +appraise "ruby-2-7" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "ruby-3-0" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "ruby-3-1" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "ruby-3-2" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "ruby-3-3" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "ruby-3-4" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +# Only run security audit on latest Ruby version +appraise "audit" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + eval_gemfile "modular/audit.gemfile" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +# Only run coverage on latest Ruby version +appraise "coverage" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + eval_gemfile "modular/coverage.gemfile" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +# Only run linter on latest Ruby version (but, in support of oldest supported Ruby version) +appraise "style" do + gem "mutex_m", "~> 0.2" + gem "stringio", "~> 3.0" + eval_gemfile "modular/style.gemfile" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "ruby-head" do + gem "mutex_m", ">= 0.2" + gem "stringio", ">= 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "truffleruby-head" do + gem "mutex_m", ">= 0.2" + gem "stringio", ">= 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end + +appraise "jruby-head" do + gem "mutex_m", ">= 0.2" + gem "stringio", ">= 3.0" + remove_gem "appraisal" # only present because it must be in the gemfile because we target a git branch +end diff --git a/Gemfile b/Gemfile index 3085f59..7203c89 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,27 @@ source "https://rubygems.org" -# Specify your gem's dependencies in turbo_tests.gemspec +#### IMPORTANT ####################################################### +# Gemfile is for local development ONLY; Gemfile is NOT loaded in CI # +####################################################### IMPORTANT #### + +# Specify your gem's general development dependencies in turbo_tests.gemspec gemspec -gem "rake", "~> 13.0" +# Security Audit +if RUBY_VERSION >= "3" + # NOTE: Audit fails on Ruby 2.7 because nokogiri has dropped support for Ruby < 3 + # See: https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-r95h-9x8f-r3f7 + # We can't add upgraded nokogiri here unless we are developing on Ruby 3+ + eval_gemfile "gemfiles/modular/audit.gemfile" +end + +# Code Coverage +eval_gemfile "gemfiles/modular/coverage.gemfile" + +# Linting +eval_gemfile "gemfiles/modular/style.gemfile" + +# Documentation +eval_gemfile "gemfiles/modular/documentation.gemfile" + +gem "appraisal", path: "/Users/pboling/src/forks/appraisal" diff --git a/Gemfile.lock b/Gemfile.lock index a95169f..f6383e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,40 +5,191 @@ PATH parallel_tests (>= 3.3.0, < 5) rspec (>= 3.10) +PATH + remote: /Users/pboling/src/forks/appraisal + specs: + appraisal (3.0.0.rc1) + bundler + rake + thor (>= 0.14.0) + GEM remote: https://rubygems.org/ specs: + ansi (1.5.0) + ast (2.4.2) + backports (3.25.0) + benchmark (0.4.0) + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) coderay (1.1.3) - diff-lcs (1.5.0) - method_source (1.0.0) - parallel (1.22.1) - parallel_tests (4.2.0) + date (3.4.1) + diff-lcs (1.5.1) + diffy (3.4.3) + docile (1.4.1) + github-markup (5.0.1) + json (2.9.1) + kettle-soup-cover (1.0.4) + simplecov (~> 0.22) + simplecov-cobertura (~> 2.1) + simplecov-console (~> 0.9, >= 0.9.1) + simplecov-html (~> 0.12) + simplecov-lcov (~> 0.8) + simplecov-rcov (~> 0.3, >= 0.3.3) + simplecov_json_formatter (~> 0.1, >= 0.1.4) + version_gem (~> 1.1, >= 1.1.4) + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) + method_source (1.1.0) + ostruct (0.6.1) + parallel (1.26.3) + parallel_tests (4.9.0) parallel - pry (0.14.2) + parser (3.3.7.0) + ast (~> 2.4.1) + racc + pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) - rake (13.0.6) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.1) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.2) + psych (5.2.3) + date + stringio + racc (1.8.1) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.11.0) + psych (>= 4.0.0) + regexp_parser (2.10.0) + rexml (3.4.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-block_is_expected (1.0.6) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.4) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + rubocop (1.70.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.38.0) + parser (>= 3.3.1.0) + rubocop-gradual (0.3.6) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-support (3.12.0) + diffy (~> 3.0) + parallel (~> 1.10) + rainbow (>= 2.2.2, < 4.0) + rubocop (~> 1.0) + rubocop-lts (18.2.1) + rubocop-ruby2_7 (>= 2.0.4, < 3) + standard-rubocop-lts (>= 1.0.3, < 3) + version_gem (>= 1.1.2, < 3) + rubocop-md (1.2.4) + rubocop (>= 1.45) + rubocop-packaging (0.5.2) + rubocop (>= 1.33, < 2.0) + rubocop-performance (1.23.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + rubocop-rspec (3.4.0) + rubocop (~> 1.61) + rubocop-ruby2_7 (2.0.6) + rubocop-gradual (~> 0.3, >= 0.3.1) + rubocop-md (~> 1.2) + rubocop-rake (~> 0.6) + rubocop-shopify (~> 2.14) + rubocop-thread_safety (~> 0.5, >= 0.5.1) + standard-rubocop-lts (~> 1.0, >= 1.0.7) + version_gem (>= 1.1.3, < 3) + rubocop-shopify (2.15.1) + rubocop (~> 1.51) + rubocop-thread_safety (0.6.0) + rubocop (>= 1.48.1) + ruby-progressbar (1.13.0) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-cobertura (2.1.0) + rexml + simplecov (~> 0.19) + simplecov-console (0.9.2) + ansi + simplecov + terminal-table + simplecov-html (0.13.1) + simplecov-lcov (0.8.0) + simplecov-rcov (0.3.7) + simplecov (>= 0.4.1) + simplecov_json_formatter (0.1.4) + standard (1.44.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.70.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.6) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.6.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.23.0) + standard-rubocop-lts (1.0.10) + rspec-block_is_expected (~> 1.0, >= 1.0.5) + standard (>= 1.35.1, < 2) + standard-custom (>= 1.0.2, < 2) + standard-performance (>= 1.3.1, < 2) + version_gem (>= 1.1.4, < 3) + stringio (3.1.2) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.3.2) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + version_gem (1.1.4) + yard (0.9.37) + yard-junk (0.0.10) + backports (>= 3.18) + ostruct + rainbow + yard PLATFORMS ruby DEPENDENCIES + appraisal! + benchmark (~> 0.4) + bundler-audit (~> 0.9.2) + github-markup + kettle-soup-cover (~> 1.0, >= 1.0.4) pry (~> 0.14) rake (~> 13.0) + rdoc (~> 6.10) + rubocop-lts (~> 18.2, >= 18.2.1) + rubocop-packaging (~> 0.5, >= 0.5.2) + rubocop-rspec (~> 3.2) + standard (>= 1.35.1, != 1.42.0, != 1.41.1) turbo_tests! + yard (~> 0.9.34) + yard-junk (~> 0.0.10) BUNDLED WITH - 2.4.19 + 2.6.2 diff --git a/README.md b/README.md index f5a285a..c23b7c2 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ -`turbo_tests` is a drop-in replacement for [`grosser/parallel_tests`](https://github.com/grosser/parallel_tests) with incremental summarized output. Source code of this gem is based on [Discourse](https://github.com/discourse/discourse/blob/6b9784cf8a18636bce281a7e4d18e65a0cbc6290/lib/turbo_tests.rb) and [RubyGems](https://github.com/rubygems/rubygems/tree/390335ceb351668cd433bd5bb9823dd021f82533/bundler/tool) work in this area. +`turbo_tests` is a drop-in replacement for [grosser/parallel_tests](https://github.com/grosser/parallel_tests) with incremental summarized output. Source code of this gem is based on [Discourse](https://github.com/discourse/discourse/blob/6b9784cf8a18636bce281a7e4d18e65a0cbc6290/lib/turbo_tests.rb) and [RubyGems](https://github.com/rubygems/rubygems/tree/390335ceb351668cd433bd5bb9823dd021f82533/bundler/tool) work in this area. -Incremental summarized output [doesn't fit vision of `parallel_tests` author](https://github.com/grosser/parallel_tests/issues/708) and [RSpec doesn't support built-in parallel testing yet](https://github.com/rspec/rspec-rails/issues/2104#issuecomment-658474900). This gem will not be useful once one of the issues above will be implemented. +Incremental summarized output doesn't [fit vision](https://github.com/grosser/parallel_tests/issues/708) of `parallel_tests` author and [RSpec doesn't support built-in parallel testing yet](https://github.com/rspec/rspec-rails/issues/2104#issuecomment-658474900). This gem will not be useful once one of the issues above will be implemented. ## Why incremental output? @@ -55,7 +55,7 @@ Finished in 2 minute 25.15 seconds (files took 0 seconds to load) Add this line to your application's `Gemfile`: ```ruby -gem 'turbo_tests' +gem "turbo_tests" ``` And then execute: @@ -70,6 +70,14 @@ Or install it yourself as: $ gem install turbo_tests ``` +## Setup + +Create test databases + +```bash +$ bundle exec turbo_tests --create +``` + ## Usage Execute tests: @@ -97,6 +105,20 @@ Options: -v, --verbose More output --fail-fast=[N] --seed SEED Seed for rspec + --create Create test databases + --print_failed_group Prints group that had failures in it +``` + +To pass any options supported by paralell_tests, use `--`: + +```bash +bundle exec turbo_tests -n 4 -- --only-group 1 --pattern spec/system +``` + +`turbo_tests` supports custom formatter such as Fuubar, but you might need to require it: + +```bash +bundle exec turbo_tests -r fuubar -f Fuubar spec/whatever ``` ## Development @@ -105,6 +127,22 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). +### Appraisals + +From time to time the appraisal gemfiles in `gemfiles/` will need to be updated. +They are created and updated with the command: + +```shell +BUNDLE_GEMFILE=Appraisal.root.gemfile bundle install +BUNDLE_GEMFILE=Appraisal.root.gemfile bundle exec appraisal update +``` + +NOTE: Once `eval_gemfile` support is [merged into appraisal](https://github.com/thoughtbot/appraisal/pull/248) the above commands will be simplified to: + +```shell +BUNDLE_GEMFILE=Appraisal.root.gemfile appraisal update +``` + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/serpapi/turbo_tests. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/serpapi/turbo_tests/blob/master/CODE_OF_CONDUCT.md). diff --git a/Rakefile b/Rakefile index dbd59ee..c656deb 100644 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,61 @@ -require "bundler/setup" +# frozen_string_literal: true + require "bundler/gem_tasks" -require "rspec/core/rake_task" -RSpec::Core::RakeTask.new(:spec) +begin + require "rspec/core/rake_task" + + RSpec::Core::RakeTask.new(:spec) +rescue LoadError + task(:spec) do + warn("RSpec is disabled") + end +end + +desc "alias test task to spec" +task test: :spec + +begin + require "reek/rake/task" + Reek::Rake::Task.new do |t| + t.fail_on_error = true + t.verbose = false + t.source_files = "{spec,spec_ignored,spec_orms,lib}/**/*.rb" + end +rescue LoadError + task(:reek) do + warn("reek is disabled") + end +end + +begin + require "yard-junk/rake" + + YardJunk::Rake.define_task +rescue LoadError + task("yard:junk") do + warn("yard:junk is disabled") + end +end + +begin + require "yard" + + YARD::Rake::YardocTask.new(:yard) +rescue LoadError + task(:yard) do + warn("yard is disabled") + end +end + +begin + require "rubocop/lts" + Rubocop::Lts.install_tasks +rescue LoadError + task(:rubocop_gradual) do + warn("RuboCop (Gradual) is disabled") + end +end -task default: :spec +# These tests do not require any services to be running, so this is what we run as default +task default: %i[spec rubocop_gradual:autocorrect yard yard:junk] diff --git a/bin/turbo_tests b/bin/turbo_tests index b67291c..91d4f9d 100755 --- a/bin/turbo_tests +++ b/bin/turbo_tests @@ -3,7 +3,7 @@ # frozen_string_literal: true # Enable local usage from cloned repo -root = File.expand_path("../..", __FILE__) +root = File.expand_path("..", __dir__) $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile") require "turbo_tests" diff --git a/fixtures/rspec/errors_outside_of_examples_spec.rb b/fixtures/rspec/errors_outside_of_examples_spec.rb index 48738ea..9e23f50 100644 --- a/fixtures/rspec/errors_outside_of_examples_spec.rb +++ b/fixtures/rspec/errors_outside_of_examples_spec.rb @@ -1,5 +1,5 @@ -RSpec.describe "Fixture of spec file with errors outside of examples" do - it("passes") { expect(2 * 2).to eql(4) } +RSpec.describe("Fixture of spec file with errors outside of examples") do + it("passes") { expect(2 * 2).to(eql(4)) } 1 / 0 end diff --git a/fixtures/rspec/failing_spec.rb b/fixtures/rspec/failing_spec.rb index 73a8240..4539b59 100644 --- a/fixtures/rspec/failing_spec.rb +++ b/fixtures/rspec/failing_spec.rb @@ -1,5 +1,5 @@ -RSpec.describe "Failing example group" do - after(:each) do |example| +RSpec.describe("Failing example group") do + after do |example| example.metadata[:extra_failure_lines] ||= [] lines = example.metadata[:extra_failure_lines] @@ -8,6 +8,6 @@ end it "fails" do - expect(2).to eq(3) + expect(2).to(eq(3)) end end diff --git a/fixtures/rspec/no_method_error_spec.rb b/fixtures/rspec/no_method_error_spec.rb index c8c9689..86bdbbb 100644 --- a/fixtures/rspec/no_method_error_spec.rb +++ b/fixtures/rspec/no_method_error_spec.rb @@ -1,3 +1,3 @@ -RSpec.describe "NoMethodError spec" do - it("fails") { expect(nil[:key]).to eql("value") } -end \ No newline at end of file +RSpec.describe("NoMethodError spec") do + it("fails") { expect(nil[:key]).to(eql("value")) } +end diff --git a/fixtures/rspec/pending_exceptions_spec.rb b/fixtures/rspec/pending_exceptions_spec.rb index 737005f..a825dc6 100644 --- a/fixtures/rspec/pending_exceptions_spec.rb +++ b/fixtures/rspec/pending_exceptions_spec.rb @@ -1,15 +1,15 @@ -RSpec.describe "Fixture of spec file with pending failed examples" do +RSpec.describe("Fixture of spec file with pending failed examples") do it "is implemented but skipped with 'pending'" do pending("TODO: skipped with 'pending'") - expect(2).to eq(3) + expect(2).to(eq(3)) end it "is implemented but skipped with 'skip'", skip: "TODO: skipped with 'skip'" do - expect(100).to eq(500) + expect(100).to(eq(500)) end xit "is implemented but skipped with 'xit'" do - expect(1).to eq(42) + expect(1).to(eq(42)) end end diff --git a/gemfiles/audit.gemfile b/gemfiles/audit.gemfile new file mode 100644 index 0000000..b93d318 --- /dev/null +++ b/gemfiles/audit.gemfile @@ -0,0 +1,10 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" + +eval_gemfile("modular/audit.gemfile") diff --git a/gemfiles/audit.gemfile.lock b/gemfiles/audit.gemfile.lock new file mode 100644 index 0000000..f614ea5 --- /dev/null +++ b/gemfiles/audit.gemfile.lock @@ -0,0 +1,60 @@ +PATH + remote: .. + specs: + turbo_tests (2.2.4) + parallel_tests (>= 3.3.0, < 5) + rspec (>= 3.10) + +GEM + remote: https://rubygems.org/ + specs: + bundler-audit (0.9.2) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) + coderay (1.1.3) + diff-lcs (1.5.1) + method_source (1.1.0) + mutex_m (0.3.0) + parallel (1.26.3) + parallel_tests (4.9.0) + parallel + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + rake (13.2.1) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + stringio (3.1.2) + thor (1.3.2) + +PLATFORMS + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86_64-darwin + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + bundler-audit (~> 0.9.2) + mutex_m (~> 0.2) + pry (~> 0.14) + rake (~> 13.0) + stringio (~> 3.0) + turbo_tests! + +BUNDLED WITH + 2.6.2 diff --git a/gemfiles/coverage.gemfile b/gemfiles/coverage.gemfile new file mode 100644 index 0000000..ec3037b --- /dev/null +++ b/gemfiles/coverage.gemfile @@ -0,0 +1,10 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" + +eval_gemfile("modular/coverage.gemfile") diff --git a/gemfiles/coverage.gemfile.lock b/gemfiles/coverage.gemfile.lock new file mode 100644 index 0000000..2bfc00b --- /dev/null +++ b/gemfiles/coverage.gemfile.lock @@ -0,0 +1,84 @@ +PATH + remote: .. + specs: + turbo_tests (2.2.4) + parallel_tests (>= 3.3.0, < 5) + rspec (>= 3.10) + +GEM + remote: https://rubygems.org/ + specs: + ansi (1.5.0) + coderay (1.1.3) + diff-lcs (1.5.1) + docile (1.4.1) + kettle-soup-cover (1.0.4) + simplecov (~> 0.22) + simplecov-cobertura (~> 2.1) + simplecov-console (~> 0.9, >= 0.9.1) + simplecov-html (~> 0.12) + simplecov-lcov (~> 0.8) + simplecov-rcov (~> 0.3, >= 0.3.3) + simplecov_json_formatter (~> 0.1, >= 0.1.4) + version_gem (~> 1.1, >= 1.1.4) + method_source (1.1.0) + mutex_m (0.3.0) + parallel (1.26.3) + parallel_tests (4.9.0) + parallel + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + rake (13.2.1) + rexml (3.4.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-cobertura (2.1.0) + rexml + simplecov (~> 0.19) + simplecov-console (0.9.2) + ansi + simplecov + terminal-table + simplecov-html (0.13.1) + simplecov-lcov (0.8.0) + simplecov-rcov (0.3.7) + simplecov (>= 0.4.1) + simplecov_json_formatter (0.1.4) + stringio (3.1.2) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + version_gem (1.1.4) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + kettle-soup-cover (~> 1.0, >= 1.0.4) + mutex_m (~> 0.2) + pry (~> 0.14) + rake (~> 13.0) + stringio (~> 3.0) + turbo_tests! + +BUNDLED WITH + 2.6.2 diff --git a/gemfiles/jruby_head.gemfile b/gemfiles/jruby_head.gemfile new file mode 100644 index 0000000..49b0671 --- /dev/null +++ b/gemfiles/jruby_head.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", ">= 0.2" +gem "stringio", ">= 3.0" + +gemspec path: "../" diff --git a/gemfiles/modular/audit.gemfile b/gemfiles/modular/audit.gemfile new file mode 100644 index 0000000..e5cc919 --- /dev/null +++ b/gemfiles/modular/audit.gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Many gems are dropping support for Ruby < 3, +# so we only want to run our security audit in CI on Ruby 3+ +gem "bundler-audit", "~> 0.9.2" diff --git a/gemfiles/modular/coverage.gemfile b/gemfiles/modular/coverage.gemfile new file mode 100644 index 0000000..c7756b9 --- /dev/null +++ b/gemfiles/modular/coverage.gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# We run code coverage on the latest version of Ruby only. + +# Coverage +gem "kettle-soup-cover", "~> 1.0", ">= 1.0.4" diff --git a/gemfiles/modular/documentation.gemfile b/gemfiles/modular/documentation.gemfile new file mode 100644 index 0000000..c985345 --- /dev/null +++ b/gemfiles/modular/documentation.gemfile @@ -0,0 +1,5 @@ +### Documentation +gem "yard", "~> 0.9.34", require: false +gem "yard-junk", "~> 0.0.10" +gem "github-markup" +gem "rdoc", "~> 6.10" diff --git a/gemfiles/modular/style.gemfile b/gemfiles/modular/style.gemfile new file mode 100644 index 0000000..d222e96 --- /dev/null +++ b/gemfiles/modular/style.gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# We run rubocop on the latest version of Ruby, +# but in support of the oldest supported version of Ruby + +gem "rubocop-lts", "~> 18.2", ">= 18.2.1" # For Ruby 2.7+ +gem "rubocop-packaging", "~> 0.5", ">= 0.5.2" +gem "rubocop-rspec", "~> 3.2" +gem "standard", ">= 1.35.1", "!= 1.41.1", "!= 1.42.0" + +# Std Lib extractions +gem "benchmark", "~> 0.4" # Removed from Std Lib in Ruby 3.5 diff --git a/gemfiles/ruby_2_7.gemfile b/gemfiles/ruby_2_7.gemfile new file mode 100644 index 0000000..ed1eb63 --- /dev/null +++ b/gemfiles/ruby_2_7.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" diff --git a/gemfiles/ruby_3_0.gemfile b/gemfiles/ruby_3_0.gemfile new file mode 100644 index 0000000..ed1eb63 --- /dev/null +++ b/gemfiles/ruby_3_0.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" diff --git a/gemfiles/ruby_3_1.gemfile b/gemfiles/ruby_3_1.gemfile new file mode 100644 index 0000000..ed1eb63 --- /dev/null +++ b/gemfiles/ruby_3_1.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" diff --git a/gemfiles/ruby_3_2.gemfile b/gemfiles/ruby_3_2.gemfile new file mode 100644 index 0000000..ed1eb63 --- /dev/null +++ b/gemfiles/ruby_3_2.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" diff --git a/gemfiles/ruby_3_3.gemfile b/gemfiles/ruby_3_3.gemfile new file mode 100644 index 0000000..ed1eb63 --- /dev/null +++ b/gemfiles/ruby_3_3.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" diff --git a/gemfiles/ruby_3_4.gemfile b/gemfiles/ruby_3_4.gemfile new file mode 100644 index 0000000..ed1eb63 --- /dev/null +++ b/gemfiles/ruby_3_4.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" diff --git a/gemfiles/ruby_head.gemfile b/gemfiles/ruby_head.gemfile new file mode 100644 index 0000000..49b0671 --- /dev/null +++ b/gemfiles/ruby_head.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", ">= 0.2" +gem "stringio", ">= 3.0" + +gemspec path: "../" diff --git a/gemfiles/style.gemfile b/gemfiles/style.gemfile new file mode 100644 index 0000000..f75762c --- /dev/null +++ b/gemfiles/style.gemfile @@ -0,0 +1,10 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", "~> 0.2" +gem "stringio", "~> 3.0" + +gemspec path: "../" + +eval_gemfile("modular/style.gemfile") diff --git a/gemfiles/style.gemfile.lock b/gemfiles/style.gemfile.lock new file mode 100644 index 0000000..d4ff575 --- /dev/null +++ b/gemfiles/style.gemfile.lock @@ -0,0 +1,135 @@ +PATH + remote: .. + specs: + turbo_tests (2.2.4) + parallel_tests (>= 3.3.0, < 5) + rspec (>= 3.10) + +GEM + remote: https://rubygems.org/ + specs: + ast (2.4.2) + benchmark (0.4.0) + coderay (1.1.3) + diff-lcs (1.5.1) + diffy (3.4.3) + json (2.9.1) + language_server-protocol (3.17.0.4) + lint_roller (1.1.0) + method_source (1.1.0) + mutex_m (0.3.0) + parallel (1.26.3) + parallel_tests (4.9.0) + parallel + parser (3.3.7.0) + ast (~> 2.4.1) + racc + pry (0.15.2) + coderay (~> 1.1) + method_source (~> 1.0) + racc (1.8.1) + rainbow (3.1.1) + rake (13.2.1) + regexp_parser (2.10.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-block_is_expected (1.0.6) + rspec-core (3.13.2) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.2) + rubocop (1.70.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.38.0) + parser (>= 3.3.1.0) + rubocop-gradual (0.3.6) + diff-lcs (>= 1.2.0, < 2.0) + diffy (~> 3.0) + parallel (~> 1.10) + rainbow (>= 2.2.2, < 4.0) + rubocop (~> 1.0) + rubocop-lts (18.2.1) + rubocop-ruby2_7 (>= 2.0.4, < 3) + standard-rubocop-lts (>= 1.0.3, < 3) + version_gem (>= 1.1.2, < 3) + rubocop-md (1.2.4) + rubocop (>= 1.45) + rubocop-packaging (0.5.2) + rubocop (>= 1.33, < 2.0) + rubocop-performance (1.23.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rake (0.6.0) + rubocop (~> 1.0) + rubocop-rspec (3.4.0) + rubocop (~> 1.61) + rubocop-ruby2_7 (2.0.6) + rubocop-gradual (~> 0.3, >= 0.3.1) + rubocop-md (~> 1.2) + rubocop-rake (~> 0.6) + rubocop-shopify (~> 2.14) + rubocop-thread_safety (~> 0.5, >= 0.5.1) + standard-rubocop-lts (~> 1.0, >= 1.0.7) + version_gem (>= 1.1.3, < 3) + rubocop-shopify (2.15.1) + rubocop (~> 1.51) + rubocop-thread_safety (0.6.0) + rubocop (>= 1.48.1) + ruby-progressbar (1.13.0) + standard (1.44.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.70.0) + standard-custom (~> 1.0.0) + standard-performance (~> 1.6) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.6.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.23.0) + standard-rubocop-lts (1.0.10) + rspec-block_is_expected (~> 1.0, >= 1.0.5) + standard (>= 1.35.1, < 2) + standard-custom (>= 1.0.2, < 2) + standard-performance (>= 1.3.1, < 2) + version_gem (>= 1.1.4, < 3) + stringio (3.1.2) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + version_gem (1.1.4) + +PLATFORMS + arm64-darwin-24 + ruby + +DEPENDENCIES + benchmark (~> 0.4) + mutex_m (~> 0.2) + pry (~> 0.14) + rake (~> 13.0) + rubocop-lts (~> 18.2, >= 18.2.1) + rubocop-packaging (~> 0.5, >= 0.5.2) + rubocop-rspec (~> 3.2) + standard (>= 1.35.1, != 1.42.0, != 1.41.1) + stringio (~> 3.0) + turbo_tests! + +BUNDLED WITH + 2.6.2 diff --git a/gemfiles/truffleruby_head.gemfile b/gemfiles/truffleruby_head.gemfile new file mode 100644 index 0000000..49b0671 --- /dev/null +++ b/gemfiles/truffleruby_head.gemfile @@ -0,0 +1,8 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "mutex_m", ">= 0.2" +gem "stringio", ">= 3.0" + +gemspec path: "../" diff --git a/lib/turbo_tests.rb b/lib/turbo_tests.rb index afb362b..5e2df1d 100644 --- a/lib/turbo_tests.rb +++ b/lib/turbo_tests.rb @@ -21,24 +21,31 @@ module TurboTests FakeException = Struct.new(:backtrace, :message, :cause) class FakeException def self.from_obj(obj) - if obj - klass = - Class.new(FakeException) { - define_singleton_method(:name) do - obj[:class_name] - end - } + return unless obj - klass.new( - obj[:backtrace], - obj[:message], - FakeException.from_obj(obj[:cause]) - ) - end + klass = + Class.new(FakeException) do + define_singleton_method(:name) do + obj[:class_name] + end + end + + klass.new( + obj[:backtrace], + obj[:message], + FakeException.from_obj(obj[:cause]), + ) end end - FakeExecutionResult = Struct.new(:example_skipped?, :pending_message, :status, :pending_fixed?, :exception, :pending_exception) + FakeExecutionResult = Struct.new( + :example_skipped?, + :pending_message, + :status, + :pending_fixed?, + :exception, + :pending_exception, + ) class FakeExecutionResult def self.from_obj(obj) new( @@ -47,12 +54,19 @@ def self.from_obj(obj) obj[:status].to_sym, obj[:pending_fixed?], FakeException.from_obj(obj[:exception]), - FakeException.from_obj(obj[:exception]) + FakeException.from_obj(obj[:exception]), ) end end - FakeExample = Struct.new(:execution_result, :location, :description, :full_description, :metadata, :location_rerun_argument) + FakeExample = Struct.new( + :execution_result, + :location, + :description, + :full_description, + :metadata, + :location_rerun_argument, + ) class FakeExample def self.from_obj(obj) metadata = obj[:metadata] @@ -60,7 +74,7 @@ def self.from_obj(obj) metadata[:shared_group_inclusion_backtrace].map! do |frame| RSpec::Core::SharedExampleGroupInclusionStackFrame.new( frame[:shared_group_name], - frame[:inclusion_location] + frame[:inclusion_location], ) end @@ -72,13 +86,13 @@ def self.from_obj(obj) obj[:description], obj[:full_description], metadata, - obj[:location_rerun_argument] + obj[:location_rerun_argument], ) end def notification RSpec::Core::Notifications::ExampleNotification.for( - self + self, ) end end diff --git a/lib/turbo_tests/cli.rb b/lib/turbo_tests/cli.rb index 6f033cc..ae6f445 100644 --- a/lib/turbo_tests/cli.rb +++ b/lib/turbo_tests/cli.rb @@ -17,8 +17,11 @@ def run verbose = false fail_fast = nil seed = nil + print_failed_group = false + create = false + nice = false - OptionParser.new { |opts| + OptionParser.new do |opts| opts.banner = <<~BANNER Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('1', '2', '3', ...). @@ -40,10 +43,14 @@ def run requires << filename end - opts.on("-f", "--format FORMATTER", "Choose a formatter. Available formatters: progress (p), documentation (d). Default: progress") do |name| + opts.on( + "-f", + "--format FORMATTER", + "Choose a formatter. Available formatters: progress (p), documentation (d). Default: progress", + ) do |name| formatters << { name: name, - outputs: [] + outputs: [], } end @@ -55,7 +62,7 @@ def run if formatters.empty? formatters << { name: "progress", - outputs: [] + outputs: [], } end formatters.last[:outputs] << filename @@ -72,45 +79,65 @@ def run opts.on("--fail-fast=[N]") do |n| n = begin Integer(n) - rescue + rescue StandardError nil end - fail_fast = n.nil? || n < 1 ? 1 : n + fail_fast = (n.nil? || n < 1) ? 1 : n end opts.on("--seed SEED", "Seed for rspec") do |s| seed = s end - }.parse!(@argv) + + opts.on("--create", "Create databases") do + create = true + end + + opts.on("--print_failed_group", "Prints group that had failures in it") do + print_failed_group = true + end + + opts.on("--nice", "execute test commands with low priority") do + nice = true + end + end.parse!(@argv) + + if create + return TurboTests::Runner.create(count) + end requires.each { |f| require(f) } if formatters.empty? formatters << { name: "progress", - outputs: [] + outputs: [], } end formatters.each do |formatter| - if formatter[:outputs].empty? - formatter[:outputs] << "-" - end + formatter[:outputs] << "-" if formatter[:outputs].empty? end + parallel_options = ParallelTests::CLI.new.send(:parse_options!, @argv.unshift("--type", "rspec")) + files = parallel_options.fetch(:files, ["spec"]) + exitstatus = TurboTests::Runner.run( formatters: formatters, tags: tags, - files: @argv.empty? ? ["spec"] : @argv, + files: files, runtime_log: runtime_log, verbose: verbose, fail_fast: fail_fast, count: count, - seed: seed + seed: seed, + nice: nice, + print_failed_group: print_failed_group, + parallel_options: parallel_options, ) # From https://github.com/serpapi/turbo_tests/pull/20/ - exit exitstatus + exit(exitstatus) end end end diff --git a/lib/turbo_tests/json_rows_formatter.rb b/lib/turbo_tests/json_rows_formatter.rb index d0244ca..276d945 100644 --- a/lib/turbo_tests/json_rows_formatter.rb +++ b/lib/turbo_tests/json_rows_formatter.rb @@ -30,7 +30,7 @@ class JsonRowsFormatter :example_group_started, :example_group_finished, :message, - :seed + :seed, ) attr_reader :output @@ -42,76 +42,76 @@ def initialize(output) def start(notification) output_row( type: :load_summary, - summary: load_summary_to_json(notification) + summary: load_summary_to_json(notification), ) end def example_group_started(notification) output_row( type: :group_started, - group: group_to_json(notification) + group: group_to_json(notification), ) end def example_group_finished(notification) output_row( type: :group_finished, - group: group_to_json(notification) + group: group_to_json(notification), ) end def example_passed(notification) output_row( type: :example_passed, - example: example_to_json(notification.example) + example: example_to_json(notification.example), ) end def example_pending(notification) output_row( type: :example_pending, - example: example_to_json(notification.example) + example: example_to_json(notification.example), ) end def example_failed(notification) output_row( type: :example_failed, - example: example_to_json(notification.example) + example: example_to_json(notification.example), ) end def seed(notification) output_row( type: :seed, - seed: notification.seed + seed: notification.seed, ) end - def close(notification) + def close(_notification) output_row( - type: :close + type: :close, ) end def message(notification) output_row( type: :message, - message: notification.message + message: notification.message, ) end private def exception_to_json(exception) - if exception - { - class_name: exception.class.name.to_s, - backtrace: exception.backtrace, - message: exception.message, - cause: exception_to_json(exception.cause) - } - end + return unless exception + + { + class_name: exception.class.name.to_s, + backtrace: exception.backtrace, + message: exception.message, + cause: exception_to_json(exception.cause), + } end def execution_result_to_json(result) @@ -120,14 +120,14 @@ def execution_result_to_json(result) pending_message: result.pending_message, status: result.status, pending_fixed?: result.pending_fixed?, - exception: exception_to_json(result.exception || result.pending_exception) + exception: exception_to_json(result.exception || result.pending_exception), } end def stack_frame_to_json(frame) { shared_group_name: frame.shared_group_name, - inclusion_location: frame.inclusion_location + inclusion_location: frame.inclusion_location, } end @@ -158,8 +158,8 @@ def load_summary_to_json(notification) def group_to_json(notification) { group: { - description: notification.group.description - } + description: notification.group.description, + }, } end diff --git a/lib/turbo_tests/reporter.rb b/lib/turbo_tests/reporter.rb index 417f06a..acd2f76 100644 --- a/lib/turbo_tests/reporter.rb +++ b/lib/turbo_tests/reporter.rb @@ -3,27 +3,27 @@ module TurboTests class Reporter attr_writer :load_time + attr_reader :pending_examples, :failed_examples - def self.from_config(formatter_config, start_time, seed, seed_used) - reporter = new(start_time, seed, seed_used) + class << self + def from_config(formatter_config, start_time, seed, seed_used, files, parallel_options) + reporter = new(start_time, seed, seed_used, files, parallel_options) - formatter_config.each do |config| - name, outputs = config.values_at(:name, :outputs) + formatter_config.each do |config| + name, outputs = config.values_at(:name, :outputs) - outputs.map! do |filename| - filename == "-" ? $stdout : File.open(filename, "w") + outputs.map! do |filename| + (filename == "-") ? $stdout : File.open(filename, "w") + end + + reporter.add(name, outputs) end - reporter.add(name, outputs) + reporter end - - reporter end - attr_reader :pending_examples - attr_reader :failed_examples - - def initialize(start_time, seed, seed_used) + def initialize(start_time, seed, seed_used, files, parallel_options) @formatters = [] @pending_examples = [] @failed_examples = [] @@ -34,6 +34,8 @@ def initialize(start_time, seed, seed_used) @seed_used = seed_used @load_time = 0 @errors_outside_of_examples_count = 0 + @files = files + @parallel_options = parallel_options end def add(name, outputs) @@ -63,7 +65,7 @@ def report(example_groups) end end - def start(example_groups, time=RSpec::Core::Time.now) + def start(example_groups, time = RSpec::Core::Time.now) @start = time @load_time = (@start - @start_time).to_f @@ -71,7 +73,10 @@ def start(example_groups, time=RSpec::Core::Time.now) expected_example_count = example_groups.flatten(1).count delegate_to_formatters(:seed, RSpec::Core::Notifications::SeedNotification.new(@seed, @seed_used)) - delegate_to_formatters(:start, RSpec::Core::Notifications::StartNotification.new(expected_example_count, @load_time)) + delegate_to_formatters( + :start, + RSpec::Core::Notifications::StartNotification.new(expected_example_count, @load_time), + ) end def report_number_of_tests(groups) @@ -79,7 +84,7 @@ def report_number_of_tests(groups) num_processes = groups.size num_tests = groups.map(&:size).sum - tests_per_process = (num_processes == 0 ? 0 : num_tests.to_f / num_processes).round + tests_per_process = ((num_processes == 0) ? 0 : num_tests.to_f / num_processes).round puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{tests_per_process} #{name}s per process" end @@ -119,40 +124,48 @@ def message(message) def error_outside_of_examples(error_message) @errors_outside_of_examples_count += 1 - message error_message + message(error_message) end def finish end_time = RSpec::Core::Time.now @duration = end_time - @start_time - delegate_to_formatters :stop, RSpec::Core::Notifications::ExamplesNotification.new(self) + delegate_to_formatters(:stop, RSpec::Core::Notifications::ExamplesNotification.new(self)) - delegate_to_formatters :start_dump, RSpec::Core::Notifications::NullNotification - delegate_to_formatters(:dump_pending, + delegate_to_formatters(:start_dump, RSpec::Core::Notifications::NullNotification) + delegate_to_formatters( + :dump_pending, RSpec::Core::Notifications::ExamplesNotification.new( - self - )) - delegate_to_formatters(:dump_failures, + self, + ), + ) + delegate_to_formatters( + :dump_failures, RSpec::Core::Notifications::ExamplesNotification.new( - self - )) - delegate_to_formatters(:dump_summary, + self, + ), + ) + delegate_to_formatters( + :dump_summary, RSpec::Core::Notifications::SummaryNotification.new( end_time - @start_time, @all_examples, @failed_examples, @pending_examples, @load_time, - @errors_outside_of_examples_count - )) - delegate_to_formatters(:seed, + @errors_outside_of_examples_count, + ), + ) + delegate_to_formatters( + :seed, RSpec::Core::Notifications::SeedNotification.new( @seed, @seed_used, - )) + ), + ) ensure - delegate_to_formatters :close, RSpec::Core::Notifications::NullNotification + delegate_to_formatters(:close, RSpec::Core::Notifications::NullNotification) end protected diff --git a/lib/turbo_tests/runner.rb b/lib/turbo_tests/runner.rb index 9db297d..d428512 100644 --- a/lib/turbo_tests/runner.rb +++ b/lib/turbo_tests/runner.rb @@ -9,27 +9,49 @@ module TurboTests class Runner using CoreExtensions + def self.create(count) + # We are unable to load parallel tests' tasks in the normal way (top of file) + # because it requires that the Rails.application instance already be configured + require "parallel_tests/tasks" + + ENV["PARALLEL_TEST_FIRST_IS_1"] = "true" + command = ["bundle", "exec", "rake", "db:create", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"] + args = {count: count.to_s} + ParallelTests::Tasks.run_in_parallel(command, args) + end + def self.run(opts = {}) files = opts[:files] formatters = opts[:formatters] tags = opts[:tags] + parallel_options = opts[:parallel_options] || {} start_time = opts.fetch(:start_time) { RSpec::Core::Time.now } runtime_log = opts.fetch(:runtime_log, nil) verbose = opts.fetch(:verbose, false) fail_fast = opts.fetch(:fail_fast, nil) count = opts.fetch(:count, nil) - seed = opts.fetch(:seed) + seed = opts.fetch(:seed, nil) seed_used = !seed.nil? + print_failed_group = opts.fetch(:print_failed_group, false) + nice = opts.fetch(:nice, false) + + use_runtime_info = files == ["spec"] - if verbose - warn "VERBOSE" + if use_runtime_info + parallel_options[:runtime_log] = runtime_log + else + parallel_options[:group_by] = :filesize end - reporter = Reporter.from_config(formatters, start_time, seed, seed_used) + warn("VERBOSE") if verbose + + reporter = Reporter.from_config(formatters, start_time, seed, seed_used, files, parallel_options) new( reporter: reporter, + formatters: formatters, + start_time: start_time, files: files, tags: tags, runtime_log: runtime_log, @@ -38,85 +60,102 @@ def self.run(opts = {}) count: count, seed: seed, seed_used: seed_used, + print_failed_group: print_failed_group, + use_runtime_info: use_runtime_info, + parallel_options: parallel_options, + nice: nice, ).run end - def initialize(opts) + def initialize(**opts) + @formatters = opts[:formatters] @reporter = opts[:reporter] @files = opts[:files] @tags = opts[:tags] - @runtime_log = opts[:runtime_log] || "tmp/turbo_rspec_runtime.log" @verbose = opts[:verbose] @fail_fast = opts[:fail_fast] + @start_time = opts[:start_time] @count = opts[:count] @seed = opts[:seed] @seed_used = opts[:seed_used] + @nice = opts[:nice] + @use_runtime_info = opts[:use_runtime_info] @load_time = 0 @load_count = 0 @failure_count = 0 + # Supports runtime_log as a top level option, + # but also nested inside parallel_options + @runtime_log = opts[:runtime_log] || "tmp/turbo_rspec_runtime.log" + @parallel_options = opts.fetch(:parallel_options, {}) + @parallel_options[:runtime_log] ||= @runtime_log + @record_runtime = @parallel_options[:group_by] == :runtime + @messages = Thread::Queue.new @threads = [] + @wait_threads = [] @error = false + @print_failed_group = opts[:print_failed_group] end def run @num_processes = [ ParallelTests.determine_number_of_processes(@count), - ParallelTests::RSpec::Runner.tests_with_size(@files, {}).size + ParallelTests::RSpec::Runner.tests_with_size(@files, {}).size, ].min - use_runtime_info = @files == ["spec"] - - group_opts = {} - - if use_runtime_info - group_opts[:runtime_log] = @runtime_log - else - group_opts[:group_by] = :filesize - end - tests_in_groups = ParallelTests::RSpec::Runner.tests_in_groups( @files, @num_processes, - **group_opts + **@parallel_options, ) - setup_tmp_dir - subprocess_opts = { - record_runtime: use_runtime_info, + record_runtime: @record_runtime, } - @reporter.report(tests_in_groups) do |reporter| - wait_threads = tests_in_groups.map.with_index do |tests, process_id| + @reporter.report(tests_in_groups) do |_reporter| + old_signal = Signal.trap(:INT) { handle_interrupt } + + @wait_threads = tests_in_groups.map.with_index do |tests, process_id| start_regular_subprocess(tests, process_id + 1, **subprocess_opts) - end + end.compact + @interrupt_handled = false handle_messages @threads.each(&:join) - if @reporter.failed_examples.empty? && wait_threads.map(&:value).all?(&:success?) + report_failed_group(tests_in_groups) if @print_failed_group + + Signal.trap(:INT, old_signal) + + if @reporter.failed_examples.empty? && @wait_threads.map(&:value).all?(&:success?) 0 else # From https://github.com/serpapi/turbo_tests/pull/20/ - wait_threads.map { |thread| thread.value.exitstatus }.max + @wait_threads.map { |thread| thread.value.exitstatus }.max end end end private - def setup_tmp_dir - begin - FileUtils.rm_r("tmp/test-pipes") - rescue Errno::ENOENT + def handle_interrupt + if @interrupt_handled + Kernel.exit + else + puts "\nShutting down subprocesses..." + @wait_threads.each do |wait_thr| + child_pid = wait_thr.pid + pgid = Process.respond_to?(:getpgid) ? Process.getpgid(child_pid) : 0 + Process.kill(:INT, child_pid) if Process.pid != pgid + rescue Errno::ESRCH, Errno::ENOENT + end + @interrupt_handled = true end - - FileUtils.mkdir_p("tmp/test-pipes/") end def start_regular_subprocess(tests, process_id, **opts) @@ -125,7 +164,7 @@ def start_regular_subprocess(tests, process_id, **opts) @tags.map { |tag| "--tag=#{tag}" }, tests, process_id, - **opts + **opts, ) end @@ -135,22 +174,29 @@ def start_subprocess(env, extra_args, tests, process_id, record_runtime:) type: "exit", process_id: process_id, } + + nil else env["RSPEC_FORMATTER_OUTPUT_ID"] = SecureRandom.uuid env["RUBYOPT"] = ["-I#{File.expand_path("..", __dir__)}", ENV["RUBYOPT"]].compact.join(" ") env["RSPEC_SILENCE_FILTER_ANNOUNCEMENTS"] = "1" - if ENV["BUNDLE_BIN_PATH"] - command_name = [ENV["BUNDLE_BIN_PATH"], "exec", "rspec"] - else - command_name = "rspec" - end + command_name = + if ENV["RSPEC_EXECUTABLE"] + ENV["RSPEC_EXECUTABLE"].split + elsif ENV["BUNDLE_BIN_PATH"] + [ENV["BUNDLE_BIN_PATH"], "exec", "rspec"] + else + "rspec" + end record_runtime_options = if record_runtime [ - "--format", "ParallelTests::RSpec::RuntimeLogger", - "--out", @runtime_log, + "--format", + "ParallelTests::RSpec::RuntimeLogger", + "--out", + @runtime_log, ] else [] @@ -164,14 +210,19 @@ def start_subprocess(env, extra_args, tests, process_id, record_runtime:) [] end + spec_opts = ParallelTests::RSpec::Runner.send(:spec_opts) + command = [ *command_name, *extra_args, *seed_option, - "--format", "TurboTests::JsonRowsFormatter", + "--format", + "TurboTests::JsonRowsFormatter", *record_runtime_options, + *spec_opts, *tests, ] + command.unshift("nice") if @nice if @verbose command_str = [ @@ -179,7 +230,7 @@ def start_subprocess(env, extra_args, tests, process_id, record_runtime:) command.join(" "), ].select { |x| x.size > 0 }.join(" ") - warn "Process #{process_id}: #{command_str}" + warn("Process #{process_id}: #{command_str}") end stdin, stdout, stderr, wait_thr = Open3.popen3(env, *command) @@ -190,26 +241,25 @@ def start_subprocess(env, extra_args, tests, process_id, record_runtime:) stdout.each_line do |line| result = line.split(env["RSPEC_FORMATTER_OUTPUT_ID"]) - output = result.shift - print(output) unless output.empty? + initial = result.shift + print(initial) unless initial.empty? message = result.shift next unless message message = JSON.parse(message, symbolize_names: true) + message[:process_id] = process_id @messages << message end - @messages << { type: "exit", process_id: process_id } + @messages << {type: "exit", process_id: process_id} end @threads << start_copy_thread(stderr, STDERR) @threads << Thread.new do - unless wait_thr.value.success? - @messages << { type: "error" } - end + @messages << {type: "error"} unless wait_thr.value.success? end wait_thr @@ -272,11 +322,9 @@ def handle_messages nil when "exit" exited += 1 - if exited == @num_processes - break - end + break if exited == @num_processes else - STDERR.puts("Unhandled message in main process: #{message}") + warn("Unhandled message in main process: #{message}") end STDOUT.flush @@ -287,5 +335,14 @@ def handle_messages def fail_fast_met !@fail_fast.nil? && @failure_count >= @fail_fast end + + def report_failed_group(tests_in_groups) + @wait_threads.map(&:value).each_with_index do |value, index| + next if value.success? + + failing_group = tests_in_groups[index].join(" ") + puts "Group that failed: #{failing_group}" + end + end end end diff --git a/lib/utils/hash_extension.rb b/lib/utils/hash_extension.rb index c00bf6e..0b1da04 100644 --- a/lib/utils/hash_extension.rb +++ b/lib/utils/hash_extension.rb @@ -1,7 +1,7 @@ module CoreExtensions refine Hash do def to_struct - Struct.new(*self.keys).new(*self.values.map { |value| value.is_a?(Hash) ? value.to_struct : value }) + Struct.new(*keys).new(*values.map { |value| value.is_a?(Hash) ? value.to_struct : value }) end end -end \ No newline at end of file +end diff --git a/spec/cli_spec.rb b/spec/cli_spec.rb index 93fa132..06d74fd 100644 --- a/spec/cli_spec.rb +++ b/spec/cli_spec.rb @@ -1,35 +1,28 @@ RSpec.describe TurboTests::CLI do - before { output } + subject(:output) { %x(bundle exec turbo_tests -f d #{fixture}).strip } - subject(:output) { `bundle exec turbo_tests -f d #{fixture}`.strip } + before { output } context "when the 'seed' parameter was used" do - let(:seed) { 1234 } + subject(:output) { %x(bundle exec turbo_tests -f d #{fixture} --seed #{seed}).strip } - subject(:output) { `bundle exec turbo_tests -f d #{fixture} --seed #{seed}`.strip } + let(:seed) { 1234 } context "errors outside of examples" do - let(:expected_start_of_output) { -%( + let(:expected_start_of_output) do + %( 1 processes for 1 specs, ~ 1 specs per process Randomized with seed #{seed} An error occurred while loading #{fixture}. -\e[31mFailure/Error: \e[0m\e[1;34m1\e[0m / \e[1;34m0\e[0m\e[0m -\e[31m\e[0m -\e[31mZeroDivisionError:\e[0m -\e[31m divided by 0\e[0m -\e[36m# #{fixture}:4:in `/'\e[0m -\e[36m# #{fixture}:4:in `block in '\e[0m -\e[36m# #{fixture}:1:in `'\e[0m ).strip - } + end let(:expected_end_of_output) do - "0 examples, 0 failures, 1 error occurred outside of examples\n"\ - "\n"\ - "Randomized with seed #{seed}" + "0 examples, 0 failures, 1 error occurred outside of examples\n" \ + "\n" \ + "Randomized with seed #{seed}" end let(:fixture) { "./fixtures/rspec/errors_outside_of_examples_spec.rb" } @@ -65,20 +58,13 @@ context "when 'seed' parameter was not used" do context "errors outside of examples" do - let(:expected_start_of_output) { -%( + let(:expected_start_of_output) do + %( 1 processes for 1 specs, ~ 1 specs per process An error occurred while loading #{fixture}. -\e[31mFailure/Error: \e[0m\e[1;34m1\e[0m / \e[1;34m0\e[0m\e[0m -\e[31m\e[0m -\e[31mZeroDivisionError:\e[0m -\e[31m divided by 0\e[0m -\e[36m# #{fixture}:4:in `/'\e[0m -\e[36m# #{fixture}:4:in `block in '\e[0m -\e[36m# #{fixture}:1:in `'\e[0m ).strip - } + end let(:expected_end_of_output) do "0 examples, 0 failures, 1 error occurred outside of examples" @@ -94,7 +80,7 @@ end it "exludes the seed message from the output" do - expect(output).to_not include("seed") + expect(output).not_to include("seed") end end @@ -136,12 +122,12 @@ expect($?.exitstatus).to eql(1) [ - "undefined method `[]' for nil", - 'it("fails") { expect(nil[:key]).to eql("value") }', - "# #{fixture}:2:in `block (2 levels) in '", - "1 example, 1 failure", + /undefined method [`']\[\]' for nil/, + /it\("fails"\) \{ expect\(nil\[:key\]\).to\(eql\("value"\)\) \}/, + /# #{Regexp.escape(fixture)}:2:in [`']block \(2 levels\) in '/, + /1 example, 1 failure/, ].each do |part| - expect(output).to include(part) + expect(output).to match(part) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3e568ab..6bad508 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ -require "bundler/setup" require "turbo_tests" +require "parallel_tests/tasks" + RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status" diff --git a/spec/turbo_tests_spec.rb b/spec/turbo_tests_spec.rb index 0ee2784..438ccb1 100644 --- a/spec/turbo_tests_spec.rb +++ b/spec/turbo_tests_spec.rb @@ -1,5 +1,27 @@ RSpec.describe TurboTests do it "has a version number" do - expect(TurboTests::VERSION).not_to be nil + expect(TurboTests::VERSION).not_to be_nil + end + + describe "create" do + context "with nil count" do + it "creates databases" do + expect(ParallelTests::Tasks) + .to receive(:run_in_parallel) + .with(["bundle", "exec", "rake", "db:create", "RAILS_ENV=test"], {count: ""}) + + TurboTests::Runner.create(nil) + end + end + + context "with count" do + it "creates databases" do + expect(ParallelTests::Tasks) + .to receive(:run_in_parallel) + .with(["bundle", "exec", "rake", "db:create", "RAILS_ENV=test"], {count: "4"}) + + TurboTests::Runner.create(4) + end + end end end diff --git a/turbo_tests.gemspec b/turbo_tests.gemspec index 52064e1..1556a89 100644 --- a/turbo_tests.gemspec +++ b/turbo_tests.gemspec @@ -4,7 +4,6 @@ Gem::Specification.new do |spec| spec.name = "turbo_tests" spec.version = TurboTests::VERSION spec.platform = Gem::Platform::RUBY - spec.date = Time.now.strftime('%Y-%m-%d') spec.summary = "`turbo_tests` is a drop-in replacement for `grosser/parallel_tests` with incremental summarized output. Source code of `turbo_test` gem is based on Discourse and Rubygems work in this area (see README file of the source repository)." spec.homepage = "https://github.com/serpapi/turbo_tests" @@ -19,15 +18,16 @@ Gem::Specification.new do |spec| spec.required_ruby_version = ">= 2.7" - spec.add_dependency "rspec", ">= 3.10" - spec.add_dependency "parallel_tests", ">= 3.3.0", "< 5" + spec.add_dependency("parallel_tests", ">= 3.3.0", "< 5") + spec.add_dependency("rspec", ">= 3.10") - spec.add_development_dependency "pry", "~> 0.14" + spec.add_development_dependency("pry", "~> 0.14") + spec.add_development_dependency("rake", "~> 13.0") # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.files = Dir.chdir(File.expand_path(__dir__)) do + %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end spec.executables = ["turbo_tests"]