pax_global_header00006660000000000000000000000064150433162740014517gustar00rootroot0000000000000052 comment=2c97ebe614d9a389766b3e485a4ef85f4acc87b8 parallel_tests-5.4.0/000077500000000000000000000000001504331627400145435ustar00rootroot00000000000000parallel_tests-5.4.0/.github/000077500000000000000000000000001504331627400161035ustar00rootroot00000000000000parallel_tests-5.4.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000004701504331627400217050ustar00rootroot00000000000000Thank you for your contribution! ## Checklist - [ ] Feature branch is up-to-date with `master` (if not - rebase it). - [ ] Added tests. - [ ] Added an entry to the [Changelog](../blob/master/CHANGELOG.md) if the new code introduces user-observable changes. - [ ] Update Readme.md when cli options are changed parallel_tests-5.4.0/.github/workflows/000077500000000000000000000000001504331627400201405ustar00rootroot00000000000000parallel_tests-5.4.0/.github/workflows/test.yml000066400000000000000000000020611504331627400216410ustar00rootroot00000000000000name: test on: push: branches: [master] pull_request: branches: [master] jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false # run all tests so we see which gem/ruby combinations break matrix: ruby: ['3.1', '3.2', '3.3', '3.4', head, jruby-head] os: [ubuntu-latest, windows-latest] task: [spec] include: - ruby: '3.1' # lowest supported version, same as gemspec and .rubocop.yml os: ubuntu-latest task: rubocop steps: - uses: actions/checkout@master - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: rake ${{ matrix.task }} # allow ruby/jruby-head to fail since they are moving targets # TODO: this will always show green, fix once https://github.com/actions/toolkit/issues/399 is resolved continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} run: bundle exec rake ${{ matrix.task }} parallel_tests-5.4.0/.gitignore000066400000000000000000000000511504331627400165270ustar00rootroot00000000000000*.sh tmp .bundle **/vendor/bundle /.idea parallel_tests-5.4.0/.rspec000066400000000000000000000000741504331627400156610ustar00rootroot00000000000000--color --order random --exclude-pattern spec/fixtures/**/* parallel_tests-5.4.0/.rubocop.yml000066400000000000000000000043751504331627400170260ustar00rootroot00000000000000AllCops: NewCops: enable TargetRubyVersion: 3.1 SuggestExtensions: false Exclude: - '**/vendor/bundle/**/*' - 'spec/fixtures/*/db/schema.rb' Style/FrozenStringLiteralComment: Enabled: false Style/StringLiterals: Enabled: false Style/StringLiteralsInInterpolation: Enabled: false Lint/AmbiguousRegexpLiteral: Enabled: false Bundler/OrderedGems: Enabled: false Metrics: Enabled: false Style/Documentation: Enabled: false Layout/EmptyLineAfterMagicComment: Enabled: false Layout/EndAlignment: EnforcedStyleAlignWith: variable Layout/MultilineOperationIndentation: Enabled: false Layout/MultilineMethodCallIndentation: EnforcedStyle: indented Style/NumericPredicate: EnforcedStyle: comparison Layout/EmptyLineAfterGuardClause: Enabled: false Layout/FirstHashElementLineBreak: Enabled: true # Opt-in # Opt-in Layout/FirstMethodArgumentLineBreak: Enabled: true # Opt-in Layout/FirstMethodParameterLineBreak: Enabled: true # Opt-in # https://github.com/rubocop-hq/rubocop/issues/5891 Style/SpecialGlobalVars: Enabled: false Style/GlobalStdStream: Enabled: false Style/WordArray: EnforcedStyle: brackets Style/SymbolArray: EnforcedStyle: brackets Style/DoubleNegation: Enabled: false Style/NumericLiterals: Enabled: false Layout/LineLength: Enabled: false Max: 120 Style/RedundantConstantBase: Enabled: false Style/RegexpLiteral: Enabled: false Style/Lambda: EnforcedStyle: literal Style/IfUnlessModifier: Enabled: false Style/FormatString: Enabled: false Naming/VariableNumber: Enabled: false Naming/MethodParameterName: Enabled: false Style/GuardClause: Enabled: false Lint/AssignmentInCondition: Enabled: false Style/Next: Enabled: false Naming/HeredocDelimiterNaming: Enabled: false Style/Semicolon: Enabled: false Lint/InterpolationCheck: Enabled: false Style/ClassAndModuleChildren: Enabled: false Style/TrivialAccessors: Enabled: false Style/ClassVars: Enabled: false Style/CaseEquality: Enabled: false Lint/EmptyClass: Enabled: false # ENV.fetch('FOO', nil) is the same as ENV['FOO'] Style/FetchEnvVar: Enabled: false # &block is pretty readable Naming/BlockForwarding: Enabled: false # &block is pretty readable Style/ArgumentsForwarding: Enabled: false parallel_tests-5.4.0/CHANGELOG.md000066400000000000000000000270761504331627400163700ustar00rootroot00000000000000# Changelog ## Unreleased only add here if you are working on a PR ### Breaking Changes ### Added ### Fixed ## 5.4.0 - 2025-08-01 ### Added - Rake tasks will prioritize the `PARALLEL_RAILS_ENV` value over the default `test` environment ## 5.3.1 - 2025-07-23 ### Fixed - The `--multiply-processes` option was being parsed into `options[:multiply-processes]` but was being referenced as `options[:multiply]` in the code ## 5.3.0 - 2025-05-30 ### Added - The `--exec-args` option, which allows users to run shell commands in parallel with test files as arguments ## 5.2.0 - 2025-05-08 ### Added - The `specify-groups` option supports reading from STDIN when set to `-` ## 5.1.0 - 2025-03-09 ### Fixed - Restored jruby support by restoring ruby 3.1 support ## 5.0.1 - 2025-03-05 ### Fixed - Fix Cucumber failures logger when a runner doesn't have any failed examples ## 5.0.0 - 2025-03-01 ### Breaking Changes - dropped ruby 3.0 and 3.1, added ruby 3.4 ## 4.10.1 - 2025-03-01 ### Fixed - reverted determine_number_of_processes rename since that broke dependencies ## 4.10.0 - 2025-02-28 ### Added - Allow processor multiplier (flag: `-m` or `--multiply-processes`) to be set via the environment variable `PARALLEL_TEST_MULTIPLY_PROCESSES` ## 4.9.1 - 2025-02-19 ### Fixed - Fix output of Cucumber failures logger. Previously, an event handler inherited from `Cucumber::Formatter::Rerun` would improperly join failures (e.g. `feature/one.feature:1feature/two.feature:1`). Now failures are separated with a single space. ## 4.9.0 - 2025-01-09 ### Fixed - check ActiveRecord version instead of Rails ## 4.8.0 - 2025-01-03 ### Added - add --test-file-limit option for huge windows setups that breaks command length limit ## 4.7.2 - 2024-09-09 ### Fixed - Restore support for passing custom command lines as PARALLEL_TESTS_EXECUTABLE. - dropped ruby 2.7 support ## 4.7.1 - 2024-04-25 ### Added - Restored the `--verbose-process-command` and `--verbose-rerun-command` options, removed in version 4.0.0. See [#952](https://github.com/grosser/parallel_tests/pull/952). `--verbose-command` continues to be supported and is equivalent to set the 2 options above. ## 4.7.0 - 2024-04-23 ### Added - Added `--failure-exit-code [INT]` flag to specify a custom exit code when tests fail. This option allows users to define a specific exit code that the test suite should return if any tests fail. ## 4.6.1 - 2024-04-03 ### Fixed - The `--allow-duplicates` flag now runs duplicate tests in different groups ## 4.6.0 - 2024-03-25 ## Added - Add `--allow-duplicates` flag to support re-running 1 spec multiple times ## 4.5.2 - 2024-02-16 ### Fixed - do not crash when a pid file was already deleted when trying to delete it ## 4.5.1 - 2024-02-16 ### Fixed Rails 5.2 and gherkin fixes ## 4.5.0 - 2024-02-06 ### Added - Support for running tasks against individual databases in a multi-database setup with Rails >= 6.1 ([#930](https://github.com/grosser/parallel_tests/pull/930)) ## 4.4.0 - 2023-12-24 ### Added - Sort the output of `runtime_logger` for RSpec to show slowest tests first - Add new `ParallelTests::RSpec::VerboseLogger` to output detailed information about each example and it's process as it starts and finishes. ## 4.3.0 - 2023-10-08 ### Added - Support for RSpec turnip feature files. ## 4.2.2 - 2023-09-05 ### Breaking Changes - Drop support for RSpec 2. ### Added - Document unexpected behavior where the `--only-group` flag will also set a grouping strategy. ## 4.2.1 - 2023-05-05 ### Fixed - Fix $TEST_ENV_NUMBER replacing code to not affect all processes (#905) - Remove duplicate raise codes. (#897) ## 4.2.0 - 2023-02-06 ### Fixed - Avoid double sending int while also not breaking debugging [#891](https://github.com/grosser/parallel_tests/pull/891) ## 4.1.0 - 2023-01-14 ### Fixed - Avoid double sending of SIGINT to subprocesses [#889](https://github.com/grosser/parallel_tests/pull/889) ## 4.0.0 - 2022-11-05 ### Breaking Changes - The `--verbose-process-command` and `--verbose-rerun-command` are combined into `--verbose-command`. See [#884](https://github.com/grosser/parallel_tests/pull/884). - Drop ruby 2.6 support ## 3.13.0 - 2022-09-23 ### Changed - Drop support for ruby 2.5 ## v3.12.1 - 2022-09-12 ### Fixed - `--quiet` no longer prints 'Using recorded test runtime' ## v3.12.0 - 2022-08-30 ### Fixed - Grouping by scenarios now works for tests that are nested under Rules. ## 3.11.0 - 2022-05-27 ### Changed - Raise a custom `RuntimeLogTooSmallError` exception when the runtime log is too small instead of a generic `RuntimeError`. ## 3.10.1 - 2022-05-23 ### Fixed - Running rake tasks with number of processes or extra args ## 3.10.0 - 2022-05-23 ### Added - Changed Rake subtasks to always use the same Rake executable as the parent process. ## 3.9.1 - 2022-05-23 ### Fixed - Fixed `NoMethodError` exception when running Rake task `parallel:setup`. ## 3.9.0 - 2022-05-22 ### Added - Subprocesses execute without a shell. ## 3.8.1 - 2022-03-28 ### Added - Support Ruby 2.5 / 2.6 ## 3.8.0 - 2022-03-26 ### Breaking Changes - Drop support for ruby 2.5 / 2.6 ### Added - Tested on ruby 3.0 and 3.1 ### Fixed - Added Rails 7.0 to fixtures - Fixes deprecation warning around the usage of `ActiveRecord::Base.schema_format` and deprecation in Rails 7.1 ## v3.7.1 - 2021-08-14 ### Breaking Changes - None ### Added - None ### Fixed - All cucumber options are now pushed to the end of the command invocation - Fixes an issue where the `--retry` flag wouldn't work correctly ## v3.7.0 - 2021-04-08 ### Breaking Changes - None ### Added - Added `--highest-exit-status` option to return the highest exit status to allow sub-processes to send things other than 1 ### Fixed - None ## v3.6.0 - 2021-03-25 ### Breaking Changes - Drop ruby 2.4 support ### Added - Run default test folder if no arguments are passed. ### Fixed - None ## v3.5.1 - 2021-03-07 ### Breaking Changes - None ### Added - None ### Fixed - Do not use db:structure for rails 6.1 ## v3.5.0 - 2021-02-24 ### Breaking Changes - None ### Added - Add support for specifying exactly how isolated processes run tests with 'specify-groups' option. - Refactorings for rubocop ### Fixed - None ## v3.4.0 - 2020-12-24 ### Breaking Changes - None ### Added - Colorize summarized RSpec results.([#787](https://github.com/grosser/parallel_tests/pull/787)). ### Fixed - replace deprecated db:structure by db:schema (#801). ## 3.3.0 - 2020-09-16 ### Added - Added support for multiple isolated processes. ## 3.2.0 - 2020-08-27 ### Breaking Changes - RAILS_ENV cannot be specified for rake tasks (#776). ### Added - None ### Fixed - Rake tasks will no longer run against development environment when using a Spring-ified rake binstub (#776). ## 3.1.0 - 2020-07-23 ### Added - `--fail-fast` stops all groups if one group fails. Can be used to stop all groups if one test failed by using `fail-fast` in the test-framework too (for example rspec via `--test-options '--fail-fast'` or in `.rspec_parallel`). ## 3.0.0 - 2020-06-10 ### Breaking Changes - The `--group-by` flag with value `steps` and `features` now requires end users to add the `cuke_modeler` gem to their Gemfile (#762). ### Added - Cucumber 4 support (#762) ### Fixed - Fix a bundler deprecation when running specs (#761) - remove name override logic that never worked (#758) ### Dependencies - Drop ruby 2.3 support (#760) - Drop ruby 2.2 support (#759) ## 2.32.0 - 2020-03-15 ### Fixed - Calculate unknown runtimes lazily when running tests grouped by runtime ([#750](https://github.com/grosser/parallel_tests/pull/750)). ## 2.31.0 - 2020-01-31 ### Fixed - File paths passed from the CLI are now cleaned (consecutive slashes and useless dots removed) ([#748](https://github.com/grosser/parallel_tests/pull/748)). ## 2.30.1 - 2020-01-14 ### Added - Add project metadata to gemspec ([#739](https://github.com/grosser/parallel_tests/pull/739)). ## Fixed - Fix bundler deprecation warning related to `bundle show`) ([#744](https://github.com/grosser/parallel_tests/pull/744)). - Fix numerous flakey tests ([#736](https://github.com/grosser/parallel_tests/pull/736), [#741](https://github.com/grosser/parallel_tests/pull/741)). ## 2.30.0 - 2019-12-10 ### Added - Support db:structure:dump and load structure in parallel ([#732](ht.tps://github.com/grosser/parallel_tests/pull/732)). - Add note to the README about using the spring-commands-parallel-tests gem to automatically patch and enable Spring ([#731](https://github.com/grosser/parallel_tests/pull/731)). ### Fixed - Refactor logic in the `parallel:prepare` task ([#737](https://github.com/grosser/parallel_tests/pull/737)). - Update README to use :sql schema format. - Fix loading of the `version` file when using a local git repo with Bundler ([#730](https://github.com/grosser/parallel_tests/pull/730)). ## 2.29.2 - 2019-08-06 ### Fixed - Eliminate some ruby warnings relating to ambiguous arguments, unused variables, a redefined method, and uninitialized instance variables ([#712](https://github.com/grosser/parallel_tests/pull/712)). ## 2.29.1 - 2019-06-13 ### Fixed - Fix NameError due to not requiring `shellwords` ([#707](https://github.com/grosser/parallel_tests/pull/707)). ## 2.29.0 - 2019-05-04 ### Added - `--verbose-process-command`, which prints the command that will be executed by each process before it begins ([#697](https://github.com/grosser/parallel_tests/pull/697/files)). - `--verbose-rerun-command`, which prints the command executed by that process after a process fails ([#697](https://github.com/grosser/parallel_tests/pull/697/files)). ## 2.28.0 - 2019-02-07 ### Added - `exclude-pattern`, which excludes tests matching the passed in regex pattern ([#682](https://github.com/grosser/parallel_tests/pull/682), [#683](https://github.com/grosser/parallel_tests/pull/683)). ## 2.27.1 - 2019-01-01 ### Changed - `simulate_output_for_ci` now outputs dots (`.`) even after the first parallel thread finishes ([#673](https://github.com/grosser/parallel_tests/pull/673)). ### Fixed - Typo in CLI options ([#672](https://github.com/grosser/parallel_tests/pull/672)). ## 2.27.0 - 2018-11-09 ### Added - Support for new Cucumber tag expressions syntax ([#668](https://github.com/grosser/parallel_tests/pull/668)). ## 2.26.2 - 2018-10-29 ### Added - `db:test:purge` is now `db:purge` so it can be used in any environment, not just the `test` environment. This change is backwards compatible. ([#665](https://github.com/grosser/parallel_tests/pull/665)). - Tests against Rails 5.1 and 5.2 ([#663])(https://github.com/grosser/parallel_tests/pull/663)). ## 2.26.0 - 2018-10-25 ### Fixed - Update formatter to use Cucumber events API instead of deprecated API ([#664](https://github.com/grosser/parallel_tests/pull/664)) ## 2.25.0 - 2018-10-24 ### Fixed - Commands and their respective outputs are now grouped together when using the `verbose` and `serialize-output` flags together ([#660](https://github.com/grosser/parallel_tests/pull/660)). ### Dependencies - Dropped support for MiniTest 4 and Test-Unit ([#662](https://github.com/grosser/parallel_tests/pull/662)). - Dropped support for Ruby 2.1 ([#659](https://github.com/grosser/parallel_tests/pull/659)) ## 2.24.0 - 2018-10-24 ### Fixed - Improve accuracy when recording example times ([#661](https://github.com/grosser/parallel_tests/pull/661)). ### Dependencies - Dropped support for Ruby 2.0 ([#661](https://github.com/grosser/parallel_tests/pull/661)). ## 2.23.0 - 2018-09-14 ### Added - Rake task now passes through additional arguments to the CLI ([#656](https://github.com/grosser/parallel_tests/pull/656)). ## Previous versions No docs yet. Contributions welcome! parallel_tests-5.4.0/Gemfile000066400000000000000000000006351504331627400160420ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'bump' gem 'test-unit' gem 'minitest' gem 'rspec' gem 'cucumber' gem 'cuke_modeler' gem 'spinach' gem 'racc' # need for spinach on 3.3+ https://github.com/codegram/spinach/issues/256 gem 'rake' gem 'rubocop', '~> 1.73.1' # lock minor so we do not get accidental violations gem 'logger' # to silence warnings in tests, not a real dependency parallel_tests-5.4.0/Gemfile.lock000066400000000000000000000055201504331627400167670ustar00rootroot00000000000000PATH remote: . specs: parallel_tests (5.4.0) parallel GEM remote: https://rubygems.org/ specs: ast (2.4.2) base64 (0.3.0) bigdecimal (3.2.2) bigdecimal (3.2.2-java) builder (3.3.0) bump (0.10.0) colorize (1.1.0) cucumber (10.0.0) base64 (~> 0.2) builder (~> 3.2) cucumber-ci-environment (> 9, < 11) cucumber-core (> 15, < 17) cucumber-cucumber-expressions (> 17, < 19) cucumber-html-formatter (> 20.3, < 22) diff-lcs (~> 1.5) logger (~> 1.6) mini_mime (~> 1.1) multi_test (~> 1.1) sys-uname (~> 1.3) cucumber-ci-environment (10.0.1) cucumber-core (15.1.0) cucumber-gherkin (> 27, < 31) cucumber-messages (> 26, < 29) cucumber-tag-expressions (> 5, < 7) cucumber-cucumber-expressions (18.0.1) bigdecimal cucumber-gherkin (30.0.4) cucumber-messages (> 25, < 28) cucumber-html-formatter (21.13.0) cucumber-messages (> 19, < 28) cucumber-messages (27.2.0) cucumber-tag-expressions (6.1.2) cuke_modeler (3.24.0) cucumber-gherkin (< 33.0) diff-lcs (1.6.2) ffi (1.17.2) ffi (1.17.2-java) gherkin-ruby (0.3.2) json (2.10.1) json (2.10.1-java) language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) mini_mime (1.1.5) minitest (5.25.4) multi_test (1.1.0) parallel (1.26.3) parser (3.3.7.1) ast (~> 2.4.1) racc power_assert (2.0.5) racc (1.8.1) racc (1.8.1-java) 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-core (3.13.3) 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.73.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.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.38.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.38.1) parser (>= 3.3.1.0) ruby-progressbar (1.13.0) spinach (0.12.0) colorize gherkin-ruby (>= 0.3.2) sys-uname (1.3.1) ffi (~> 1.1) test-unit (3.6.7) power_assert unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) PLATFORMS java ruby x64-mingw32 x86_64-linux DEPENDENCIES bump cucumber cuke_modeler logger minitest parallel_tests! racc rake rspec rubocop (~> 1.73.1) spinach test-unit BUNDLED WITH 2.6.2 parallel_tests-5.4.0/Rakefile000066400000000000000000000020331504331627400162060ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler/setup' require 'bump/tasks' require 'bundler/gem_tasks' # update all versions so bundling does not fail on CI Bump.replace_in_default = Dir["spec/fixtures/rails*/Gemfile.lock"] task default: [:spec, :rubocop] task :spec do sh "rspec spec/" end desc "Run rubocop" task :rubocop do sh "rubocop --parallel" end desc "bundle all gemfiles [EXTRA=]" task :bundle_all do extra = ENV["EXTRA"] || "install" gemfiles = (["Gemfile"] + Dir["spec/fixtures/rails*/Gemfile"]) raise if gemfiles.size < 3 gemfiles.each do |gemfile| Bundler.with_unbundled_env do sh "GEMFILE=#{gemfile} bundle #{extra}" end end end desc "render the README option section" task :readme do output = `bundle exec ./bin/parallel_test -h` abort "Command failed: #{output}" unless $?.success? output.sub!(/.*Options are:/m, "") || raise file = "README.md" separator = "" parts = File.read(file).split(separator) parts[1] = output File.write file, parts.join(separator) end parallel_tests-5.4.0/Readme.md000066400000000000000000000525021504331627400162660ustar00rootroot00000000000000# parallel_tests [![Gem Version](https://badge.fury.io/rb/parallel_tests.svg)](https://rubygems.org/gems/parallel_tests) [![Build status](https://github.com/grosser/parallel_tests/workflows/test/badge.svg)](https://github.com/grosser/parallel_tests/actions?query=workflow%3Atest&branch=master) Speedup Minitest + RSpec + Turnip + Cucumber + Spinach by running parallel on multiple CPU cores.
ParallelTests splits tests into balanced groups (by number of lines or runtime) and runs each group in a process with its own database. Setup for Rails =============== [RailsCasts episode #413 Fast Tests](http://railscasts.com/episodes/413-fast-tests) ### Install `Gemfile`: ```ruby gem 'parallel_tests', group: [:development, :test] ``` ### Add to `config/database.yml` ParallelTests uses 1 database per test-process.
Process number123
ENV['TEST_ENV_NUMBER']'''2''3'
```yaml test: database: yourproject_test<%= ENV['TEST_ENV_NUMBER'] %> ``` ### Create additional database(s) rake parallel:create ### (Multi-DB) Create individual database rake parallel:create: rake parallel:create:secondary ### Copy development schema (repeat after migrations) rake parallel:prepare ### Run migrations in additional database(s) (repeat after migrations) rake parallel:migrate ### (Multi-DB) Run migrations in individual database rake parallel:migrate: ### Setup environment from scratch (create db and loads schema, useful for CI) rake parallel:setup ### Drop all test databases rake parallel:drop ### (Multi-DB) Drop individual test database rake parallel:drop: ### Run! rake parallel:test # Minitest rake parallel:spec # RSpec rake parallel:features # Cucumber rake parallel:features-spinach # Spinach rake "parallel:test[1]" --> force 1 CPU --> 86 seconds rake parallel:test --> got 2 CPUs? --> 47 seconds rake parallel:test --> got 4 CPUs? --> 26 seconds ... Test by pattern with Regex (e.g. use one integration server per subfolder / see if you broke any 'user'-related tests) rake "parallel:test[^test/unit]" # every test file in test/unit folder rake "parallel:test[user]" # run users_controller + user_helper + user tests rake "parallel:test['user|product']" # run user and product related tests rake "parallel:spec['spec\/(?!features)']" # run RSpec tests except the tests in spec/features ### Example output 2 processes for 210 specs, ~ 105 specs per process ... test output ... 843 examples, 0 failures, 1 pending Took 29.925333 seconds ### Run an arbitrary task in parallel ```Bash RAILS_ENV=test parallel_test -e "rake my:custom:task" # or rake "parallel:rake[my:custom:task]" # limited parallelism rake "parallel:rake[my:custom:task,2]" ``` Running setup or teardown once =================== ```Ruby require "parallel_tests" # preparation: # affected by race-condition: first process may boot slower than the second # the Process.ppid will be the pod of the process that started the parallel tests # when not using TEST_ENV_NUMBER we use a unique file per process because ppid would be the users shell done = "/tmp/parallel-setup-done-#{ENV['TEST_ENV_NUMBER'] ? Process.ppid : Process.pid}" if ParallelTests.first_process? do_something File.write done, "true" else sleep 0.1 until File.exist?(done) end # cleanup: # could also use last_process? but that is just the last process to start, not the last to finish at_exit do if ParallelTests.first_process? File.unlink done ParallelTests.wait_for_other_processes_to_finish undo_something end end ``` Even test group runtimes ======================== Test groups will often run for different times, making the full test run as slow as the slowest group. **Step 1**: Use these loggers (see below) to record test runtime **Step 2**: The next test run will use the recorded test runtimes (use `--runtime-log ` if you picked a location different from below) **Step 3**: Automate upload/download of test runtime from your CI system [example](https://github.com/grosser/parallel_rails_example/blob/master/.github/workflows/test.yml) (chunks need to be combined, an alternative is [amend](https://github.com/grosser/amend)) ### RSpec Rspec: Add to your `.rspec_parallel` (or `.rspec`), but can also be used via `--test-options='--format x'`: --format progress --format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log To use a custom logfile location (default: `tmp/parallel_runtime_rspec.log`), use the CLI: `parallel_test spec -t rspec --runtime-log my.log` ### Minitest Add to your `test_helper.rb`: ```ruby require 'parallel_tests/test/runtime_logger' if ENV['RECORD_RUNTIME'] ``` results will be logged to `tmp/parallel_runtime_test.log` when `RECORD_RUNTIME` is set, so it is not always required or overwritten. Loggers ======= RSpec: SummaryLogger -------------------- Log the test output without the different processes overwriting each other. Add the following to your `.rspec_parallel` (or `.rspec`), but can also be used via `--test-options='--format x'`: --format progress --format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log RSpec: FailuresLogger ----------------------- Produce pasteable command-line snippets for each failed example. For example: ```bash rspec /path/to/my_spec.rb:123 # should do something ``` Add the following to your `.rspec_parallel` (or `.rspec`), but can also be used via `--test-options='--format x'`: --format progress --format ParallelTests::RSpec::FailuresLogger --out tmp/failing_specs.log (Not needed to retry failures, for that pass [--only-failures](https://relishapp.com/rspec/rspec-core/docs/command-line/only-failures) to rspec) RSpec: VerboseLogger ----------------------- Prints a single line for starting and finishing each example, to see what is currently running in each process. ``` # PID, parallel process number, spec status, example description [14403] [2] [STARTED] Foo foo [14402] [1] [STARTED] Bar bar [14402] [1] [PASSED] Bar bar ``` Add the following to your `.rspec_parallel` (or `.rspec`), but can also be used via `--test-options='--format x'`: --format ParallelTests::RSpec::VerboseLogger Cucumber: FailuresLogger ----------------------- Log failed cucumber scenarios to the specified file. The filename can be passed to cucumber, prefixed with '@' to rerun failures. Usage: cucumber --format ParallelTests::Cucumber::FailuresLogger --out tmp/cucumber_failures.log Or add the formatter to the `parallel:` profile of your `cucumber.yml`: parallel: --format progress --format ParallelTests::Cucumber::FailuresLogger --out tmp/cucumber_failures.log but can also be used via `--test-options='--format x'`: Note if your `cucumber.yml` default profile uses `<%= std_opts %>` you may need to insert this as follows `parallel: <%= std_opts %> --format progress...` To rerun failures: cucumber @tmp/cucumber_failures.log Setup for non-rails =================== gem install parallel_tests # go to your project dir parallel_test parallel_rspec parallel_cucumber parallel_spinach - use `ENV['TEST_ENV_NUMBER']` inside your tests to select separate db/memcache/etc. (docker compose: expose it) - Only run a subset of files / folders: `parallel_test test/bar test/baz/foo_text.rb` - Pass test-options and files via `--`: `parallel_rspec -- -t acceptance -f progress -- spec/foo_spec.rb spec/acceptance` - Pass in test options, by using the -o flag (wrap everything in quotes): `parallel_cucumber -n 2 -o '-p foo_profile --tags @only_this_tag or @only_that_tag --format summary'` Options are: -n PROCESSES How many processes to use, default: available CPUs -p, --pattern PATTERN run tests matching this regex pattern --exclude-pattern PATTERN exclude tests matching this regex pattern --group-by TYPE group tests by: found - order of finding files steps - number of cucumber/spinach steps scenarios - individual cucumber scenarios filesize - by size of the file runtime - info from runtime log default - runtime when runtime log is filled otherwise filesize -m, --multiply-processes COUNT use given number as a multiplier of processes to run -s, --single PATTERN Run all matching files in the same process -i, --isolate Do not run any other tests in the group used by --single(-s) --isolate-n PROCESSES Use 'isolate' singles with number of processes, default: 1 --highest-exit-status Exit with the highest exit status provided by test run(s) --failure-exit-code INT Specify the exit code to use when tests fail --specify-groups SPECS Use 'specify-groups' if you want to specify multiple specs running in multiple processes in a specific formation. Commas indicate specs in the same process, pipes indicate specs in a new process. If SPECS is a '-' the value for this option is read from STDIN instead. Cannot use with --single, --isolate, or --isolate-n. Ex. $ parallel_tests -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb' Process 1 will contain 1_spec.rb and 2_spec.rb Process 2 will contain 3_spec.rb Process 3 will contain all other specs --only-group GROUP_INDEX[,GROUP_INDEX] Only run the given group numbers. Changes `--group-by` default to 'filesize'. -e, --exec COMMAND execute COMMAND in parallel and with ENV['TEST_ENV_NUMBER'] --exec-args COMMAND execute COMMAND in parallel with test files as arguments, for example: $ parallel_tests --exec-args echo > echo spec/a_spec.rb spec/b_spec.rb -o, --test-options 'OPTIONS' execute test commands with those options -t, --type TYPE test(default) / rspec / cucumber / spinach --suffix PATTERN override built in test file pattern (should match suffix): '_spec.rb$' - matches rspec files '_(test|spec).rb$' - matches test or spec files --serialize-stdout Serialize stdout output, nothing will be written until everything is done --prefix-output-with-test-env-number Prefixes test env number to the output when not using --serialize-stdout --combine-stderr Combine stderr into stdout, useful in conjunction with --serialize-stdout --non-parallel execute same commands but do not in parallel, needs --exec --no-symlinks Do not traverse symbolic links to find test files --ignore-tags PATTERN When counting steps ignore scenarios with tags that match this pattern --nice execute test commands with low priority. --runtime-log PATH Location of previously recorded test runtimes --allowed-missing COUNT Allowed percentage of missing runtimes (default = 50) --allow-duplicates When detecting files to run, allow duplicates --unknown-runtime SECONDS Use given number as unknown runtime (otherwise use average time) --first-is-1 Use "1" as TEST_ENV_NUMBER to not reuse the default test environment --fail-fast Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported --test-file-limit LIMIT Limit to this number of files per test run by batching (for windows set to ~100 to stay below 8192 max command limit, might have bugs from reusing test-env-number and summarizing partial results) --verbose Print debug output --verbose-command Combines options --verbose-process-command and --verbose-rerun-command --verbose-process-command Print the command that will be executed by each process before it begins --verbose-rerun-command After a process fails, print the command executed by that process --quiet Print only tests output -v, --version Show Version -h, --help Show this. You can run any command in parallel with `-e` / `--exec` ```bash parallel_test -n 3 -e 'ruby -e "puts %[hello from process #{ENV[:TEST_ENV_NUMBER.to_s].inspect}]"' hello from process "2" hello from process "" hello from process "3" ``` and pass arguments to a command with `--exec-args` ```bash parallel_test -n 3 --exec-args echo spec/a_spec.rb spec/b_spec.rb spec/c_spec.rb spec/d_spec.rb spec/e_spec.rb ``` and run multiple commands by using `sh` and `--exec-args` ```bash parallel_test -n 3 --exec-args "sh -c \"echo 'hello world' && rspec \$@\" --" ``` TIPS ==== ### RSpec - Add a `.rspec_parallel` to use different options, e.g. **no --drb** - Remove `--loadby` from `.rspec` - Instantly see failures (instead of just a red F) with [rspec-instafail](https://github.com/grosser/rspec-instafail) - Use [rspec-retry](https://github.com/NoRedInk/rspec-retry) (not rspec-rerun) to rerun failed tests. - [JUnit formatter configuration](https://github.com/grosser/parallel_tests/wiki#with-rspec_junit_formatter----by-jgarber) - Use [parallel_split_test](https://github.com/grosser/parallel_split_test) to run multiple scenarios in a single spec file, concurrently. (`parallel_tests` [works at the file-level and intends to stay that way](https://github.com/grosser/parallel_tests/issues/747#issuecomment-580216980)) ### Cucumber - Add a `parallel: foo` profile to your `config/cucumber.yml` and it will be used to run parallel tests - [ReportBuilder](https://github.com/rajatthareja/ReportBuilder) can help with combining parallel test results - Supports Cucumber 2.0+ and is actively maintained - Combines many JSON files into a single file - Builds a HTML report from JSON with support for debug msgs & embedded Base64 images. ### General - [ZSH] use quotes to use rake arguments `rake "parallel:prepare[3]"` - [Memcached] use different namespaces
e.g. `config.cache_store = ..., namespace: "test_#{ENV['TEST_ENV_NUMBER']}"` - Debug errors that only happen with multiple files using `--verbose` and [cleanser](https://github.com/grosser/cleanser) - `export PARALLEL_TEST_PROCESSORS=13` to override default processor count - `export PARALLEL_TEST_MULTIPLY_PROCESSES=.5` to override default processor multiplier - `export PARALLEL_RAILS_ENV=environment_name` to override the default `test` environment - Shell alias: `alias prspec='parallel_rspec -m 2 --'` - [Spring] Add the [spring-commands-parallel-tests](https://github.com/DocSpring/spring-commands-parallel-tests) gem to your `Gemfile` to get `parallel_tests` working with Spring. - `--first-is-1` will make the first environment be `1`, so you can test while running your full suite.
`export PARALLEL_TEST_FIRST_IS_1=true` will provide the same result - [email_spec and/or action_mailer_cache_delivery](https://github.com/grosser/parallel_tests/wiki) - [zeus-parallel_tests](https://github.com/sevos/zeus-parallel_tests) - [Distributed Parallel Tests on CI systems)](https://github.com/grosser/parallel_tests/wiki/Distributed-Parallel-Tests-on-CI-systems) learn how `parallel_tests` can run on distributed servers such as Travis and GitLab-CI. Also shows you how to use parallel_tests without adding `TEST_ENV_NUMBER`-backends - [Capybara setup](https://github.com/grosser/parallel_tests/wiki) - [Sphinx setup](https://github.com/grosser/parallel_tests/wiki) - [Capistrano setup](https://github.com/grosser/parallel_tests/wiki/Remotely-with-capistrano) let your tests run on a big box instead of your laptop - Rails vs `ArgumentError: secret_key_base`: use `config.secret_key_base = Random.hex(64)`, see [rails issue](https://github.com/rails/rails/issues/53661) Contribute your own gotchas to the [Wiki](https://github.com/grosser/parallel_tests/wiki) or even better open a PR :) Authors ==== inspired by [pivotal labs](https://blog.pivotal.io/labs/labs/parallelize-your-rspec-suite) ### [Contributors](https://github.com/grosser/parallel_tests/contributors) - [Charles Finkel](http://charlesfinkel.com/) - [Indrek Juhkam](http://urgas.eu) - [Jason Morrison](http://jayunit.net) - [jinzhu](http://github.com/jinzhu) - [Joakim Kolsjö](http://www.rubyblocks.se) - [Kevin Scaldeferri](http://kevin.scaldeferri.com/blog/) - [Kpumuk](http://kpumuk.info/) - [Maksim Horbul](http://github.com/mhorbul) - [Pivotal Labs](http://www.pivotallabs.com) - [Rohan Deshpande](http://github.com/rdeshpande) - [Tchandy](http://thiagopradi.net/) - [Terence Lee](http://hone.heroku.com/) - [Will Bryant](http://willbryant.net/) - [Fred Wu](http://fredwu.me) - [xxx](https://github.com/xxx) - [Levent Ali](http://purebreeze.com/) - [Michael Kintzer](https://github.com/rockrep) - [nathansobo](https://github.com/nathansobo) - [Joe Yates](http://titusd.co.uk) - [asmega](http://www.ph-lee.com) - [Doug Barth](https://github.com/dougbarth) - [Geoffrey Hichborn](https://github.com/phene) - [Trae Robrock](https://github.com/trobrock) - [Lawrence Wang](https://github.com/levity) - [Sean Walbran](https://github.com/seanwalbran) - [Lawrence Wang](https://github.com/levity) - [Potapov Sergey](https://github.com/greyblake) - [Łukasz Tackowiak](https://github.com/lukasztackowiak) - [Pedro Carriço](https://github.com/pedrocarrico) - [Pablo Manrubia Díez](https://github.com/pmanrubia) - [Slawomir Smiechura](https://github.com/ssmiech) - [Georg Friedrich](https://github.com/georg) - [R. Tyler Croy](https://github.com/rtyler) - [Ulrich Berkmüller](https://github.com/ulrich-berkmueller) - [Grzegorz Derebecki](https://github.com/madmax) - [Florian Motlik](https://github.com/flomotlik) - [Artem Kuzko](https://github.com/akuzko) - [Zeke Fast](https://github.com/zekefast) - [Joseph Shraibman](https://github.com/jshraibman-mdsol) - [David Davis](https://github.com/daviddavis) - [Ari Pollak](https://github.com/aripollak) - [Aaron Jensen](https://github.com/aaronjensen) - [Artur Roszczyk](https://github.com/sevos) - [Caleb Tomlinson](https://github.com/calebTomlinson) - [Jawwad Ahmad](https://github.com/jawwad) - [Iain Beeston](https://github.com/iainbeeston) - [Alejandro Pulver](https://github.com/alepulver) - [Felix Clack](https://github.com/felixclack) - [Izaak Alpert](https://github.com/karlhungus) - [Micah Geisel](https://github.com/botandrose) - [Exoth](https://github.com/Exoth) - [sidfarkus](https://github.com/sidfarkus) - [Colin Harris](https://github.com/aberant) - [Wataru MIYAGUNI](https://github.com/gongo) - [Brandon Turner](https://github.com/blt04) - [Matt Hodgson](https://github.com/mhodgson) - [bicarbon8](https://github.com/bicarbon8) - [seichner](https://github.com/seichner) - [Matt Southerden](https://github.com/mattsoutherden) - [Stanislaw Wozniak](https://github.com/sponte) - [Dmitry Polushkin](https://github.com/dmitry) - [Samer Masry](https://github.com/smasry) - [Volodymyr Mykhailyk](https:/github.com/volodymyr-mykhailyk) - [Mike Mueller](https://github.com/mmueller) - [Aaron Jensen](https://github.com/aaronjensen) - [Ed Slocomb](https://github.com/edslocomb) - [Cezary Baginski](https://github.com/e2) - [Marius Ioana](https://github.com/mariusioana) - [Lukas Oberhuber](https://github.com/lukaso) - [Ryan Zhang](https://github.com/ryanus) - [Rhett Sutphin](https://github.com/rsutphin) - [Doc Ritezel](https://github.com/ohrite) - [Alexandre Wilhelm](https://github.com/dogild) - [Jerry](https://github.com/boblington) - [Aleksei Gusev](https://github.com/hron) - [Scott Olsen](https://github.com/scottolsen) - [Andrei Botalov](https://github.com/abotalov) - [Zachary Attas](https://github.com/snackattas) - [David Rodríguez](https://github.com/deivid-rodriguez) - [Justin Doody](https://github.com/justindoody) - [Sandeep Singh](https://github.com/sandeepnagra) - [Calaway](https://github.com/calaway) - [alboyadjian](https://github.com/alboyadjian) - [Nathan Broadbent](https://github.com/ndbroadbent) - [Vikram B Kumar](https://github.com/v-kumar) - [Joshua Pinter](https://github.com/joshuapinter) - [Zach Dennis](https://github.com/zdennis) - [Jon Dufresne](https://github.com/jdufresne) - [Eric Kessler](https://github.com/enkessler) - [Adis Osmonov](https://github.com/adis-io) - [Josh Westbrook](https://github.com/joshwestbrook) - [Jay Dorsey](https://github.com/jaydorsey) - [hatsu](https://github.com/hatsu38) - [Mark Huk](https://github.com/vimutter) - [Johannes Vetter](https://github.com/johvet) - [Michel Filipe](https://github.com/mfilipe) [Michael Grosser](http://grosser.it)
michael@grosser.it
License: MIT parallel_tests-5.4.0/bin/000077500000000000000000000000001504331627400153135ustar00rootroot00000000000000parallel_tests-5.4.0/bin/parallel_cucumber000077500000000000000000000004231504331627400207210ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # enable local usage from cloned repo root = File.expand_path('..', __dir__) $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile") require "parallel_tests" ParallelTests::CLI.new.run(["--type", "cucumber"] + ARGV) parallel_tests-5.4.0/bin/parallel_rspec000077500000000000000000000004201504331627400202250ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # enable local usage from cloned repo root = File.expand_path('..', __dir__) $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile") require "parallel_tests" ParallelTests::CLI.new.run(["--type", "rspec"] + ARGV) parallel_tests-5.4.0/bin/parallel_spinach000077500000000000000000000004221504331627400205400ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # enable local usage from cloned repo root = File.expand_path('..', __dir__) $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile") require "parallel_tests" ParallelTests::CLI.new.run(["--type", "spinach"] + ARGV) parallel_tests-5.4.0/bin/parallel_test000077500000000000000000000004171504331627400200760ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # enable local usage from cloned repo root = File.expand_path('..', __dir__) $LOAD_PATH << "#{root}/lib" if File.exist?("#{root}/Gemfile") require "parallel_tests" ParallelTests::CLI.new.run(["--type", "test"] + ARGV) parallel_tests-5.4.0/lib/000077500000000000000000000000001504331627400153115ustar00rootroot00000000000000parallel_tests-5.4.0/lib/parallel_tests.rb000066400000000000000000000061261504331627400206610ustar00rootroot00000000000000# frozen_string_literal: true require "parallel" require "parallel_tests/railtie" if defined? Rails::Railtie require "rbconfig" module ParallelTests WINDOWS = (RbConfig::CONFIG['host_os'] =~ /cygwin|mswin|mingw|bccwin|wince|emx/) RUBY_BINARY = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) DEFAULT_MULTIPLY_PROCESSES = 1.0 autoload :CLI, "parallel_tests/cli" autoload :VERSION, "parallel_tests/version" autoload :Grouper, "parallel_tests/grouper" autoload :Pids, "parallel_tests/pids" class << self # used by external libraries, do not rename or change api def determine_number_of_processes(count) Integer( [ count, ENV["PARALLEL_TEST_PROCESSORS"], Parallel.processor_count ].detect { |c| !c.to_s.strip.empty? } ) end def determine_multiple(multiple) Float( [ multiple, ENV["PARALLEL_TEST_MULTIPLY_PROCESSES"], DEFAULT_MULTIPLY_PROCESSES ].detect { |c| !c.to_s.strip.empty? } ) end def with_pid_file Tempfile.open('parallel_tests-pidfile') do |f| ENV['PARALLEL_PID_FILE'] = f.path # Pids object should be created before threads will start adding pids to it # Otherwise we would have to use Mutex to prevent creation of several instances @pids = pids yield ensure ENV['PARALLEL_PID_FILE'] = nil @pids = nil end end def pids @pids ||= Pids.new(pid_file_path) end def pid_file_path ENV.fetch('PARALLEL_PID_FILE') end def stop_all_processes pids.all.each { |pid| Process.kill(:INT, pid) } rescue Errno::ESRCH, Errno::EPERM # Process already terminated, do nothing end # copied from http://github.com/carlhuda/bundler Bundler::SharedHelpers#find_gemfile def bundler_enabled? return true if Object.const_defined?(:Bundler) previous = nil current = File.expand_path(Dir.pwd) until !File.directory?(current) || current == previous filename = File.join(current, "Gemfile") return true if File.exist?(filename) previous = current current = File.expand_path("..", current) end false end def first_process? ENV["TEST_ENV_NUMBER"].to_i <= 1 end def last_process? current_process_number = ENV['TEST_ENV_NUMBER'] total_processes = ENV['PARALLEL_TEST_GROUPS'] return true if current_process_number.nil? && total_processes.nil? current_process_number = '1' if current_process_number.nil? current_process_number == total_processes end def with_ruby_binary(command) WINDOWS ? [RUBY_BINARY, '--', command] : [command] end def wait_for_other_processes_to_finish return unless ENV["TEST_ENV_NUMBER"] sleep 1 until number_of_running_processes <= 1 end def number_of_running_processes pids.count end def now Process.clock_gettime(Process::CLOCK_MONOTONIC) end def delta before = now.to_f yield now.to_f - before end end end parallel_tests-5.4.0/lib/parallel_tests/000077500000000000000000000000001504331627400203275ustar00rootroot00000000000000parallel_tests-5.4.0/lib/parallel_tests/cli.rb000066400000000000000000000453171504331627400214350ustar00rootroot00000000000000# frozen_string_literal: true require 'optparse' require 'tempfile' require 'parallel_tests' require 'shellwords' require 'pathname' module ParallelTests class CLI def run(argv) Signal.trap("INT") { handle_interrupt } options = parse_options!(argv) ENV['DISABLE_SPRING'] ||= '1' num_processes = ParallelTests.determine_number_of_processes(options[:count]) num_processes = (num_processes * ParallelTests.determine_multiple(options[:multiply_processes])).round options[:first_is_1] ||= first_is_1? if options[:execute] execute_command_in_parallel(options[:execute], num_processes, options) else run_tests_in_parallel(num_processes, options) end end private def handle_interrupt @graceful_shutdown_attempted ||= false Kernel.exit if @graceful_shutdown_attempted # In a shell, all sub-processes also get an interrupt, so they shut themselves down. # In a background process this does not happen and we need to do it ourselves. # We cannot always send the interrupt since then the sub-processes would get interrupted twice when in foreground # and that messes with interrupt handling. # # (can simulate detached with `(bundle exec parallel_rspec test/a_spec.rb -n 2 &)`) # also the integration test "passes on int signal to child processes" is detached. # # On windows getpgid does not work so we resort to always killing which is the smaller bug. # # The ParallelTests::Pids `synchronize` method can't be called directly from a trap, # using Thread workaround https://github.com/ddollar/foreman/issues/332 Thread.new do if Gem.win_platform? || ((child_pid = ParallelTests.pids.all.first) && Process.getpgid(child_pid) != Process.pid) ParallelTests.stop_all_processes end end @graceful_shutdown_attempted = true end def execute_in_parallel(items, num_processes, options) Tempfile.open 'parallel_tests-lock' do |lock| ParallelTests.with_pid_file do simulate_output_for_ci options[:serialize_stdout] do Parallel.map_with_index(items, in_threads: num_processes) do |item, index| result = yield(item, index) reprint_output(result, lock.path) if options[:serialize_stdout] ParallelTests.stop_all_processes if options[:fail_fast] && result[:exit_status] != 0 result end end end end end def run_tests_in_parallel(num_processes, options) test_results = nil run_tests_proc = -> do groups = @runner.tests_in_groups(options[:files], num_processes, options) groups.reject!(&:empty?) if options[:only_group] groups = options[:only_group].map { |i| groups[i - 1] }.compact num_processes = 1 end report_number_of_tests(groups) unless options[:quiet] test_results = execute_in_parallel(groups, groups.size, options) do |group, index| run_tests(group, index, num_processes, options) end report_results(test_results, options) unless options[:quiet] end if options[:quiet] run_tests_proc.call else report_time_taken(&run_tests_proc) end if any_test_failed?(test_results) warn final_fail_message exit_status = if options[:failure_exit_code] options[:failure_exit_code] elsif options[:highest_exit_status] test_results.map { |data| data.fetch(:exit_status) }.max else 1 end exit exit_status end end def run_tests(group, process_number, num_processes, options) if (limit = options[:test_file_limit]) # TODO: will have some bugs with summarizing results and last process results = group.each_slice(limit).map do |slice| @runner.run_tests(slice, process_number, num_processes, options) end result = results[0] results[1..].each do |res| result[:stdout] = result[:stdout].to_s + res[:stdout].to_s result[:exit_status] = [res[:exit_status], result[:exit_status]].max # adding all files back in, not using original cmd to show what was actually run result[:command] |= res[:command] end result else @runner.run_tests(group, process_number, num_processes, options) end end def reprint_output(result, lockfile) lock(lockfile) do $stdout.puts $stdout.puts result[:stdout] $stdout.flush end end def lock(lockfile) File.open(lockfile) do |lock| lock.flock File::LOCK_EX yield ensure # This shouldn't be necessary, but appears to be lock.flock File::LOCK_UN end end def report_results(test_results, options) results = @runner.find_results(test_results.map { |result| result[:stdout] } * "") puts "" puts @runner.summarize_results(results) report_failure_rerun_commmand(test_results, options) end def report_failure_rerun_commmand(test_results, options) failing_sets = test_results.reject { |r| r[:exit_status] == 0 } return if failing_sets.none? if options[:verbose] || options[:verbose_rerun_command] puts "\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\n" failing_sets.each do |failing_set| command = failing_set[:command] command = @runner.command_with_seed(command, failing_set[:seed]) if failing_set[:seed] @runner.print_command(command, failing_set[:env] || {}) end end end def report_number_of_tests(groups) name = @runner.test_file_name num_processes = groups.size num_tests = groups.map(&:size).sum tests_per_process = (num_processes == 0 ? 0 : num_tests / num_processes) puts "#{pluralize(num_processes, 'process')} for #{pluralize(num_tests, name)}, ~ #{pluralize(tests_per_process, name)} per process" end def pluralize(n, singular) if n == 1 "1 #{singular}" elsif singular.end_with?('s', 'sh', 'ch', 'x', 'z') "#{n} #{singular}es" else "#{n} #{singular}s" end end # exit with correct status code so rake parallel:test && echo 123 works def any_test_failed?(test_results) test_results.any? { |result| result[:exit_status] != 0 } end def parse_options!(argv) newline_padding = 37 # poor man's way of getting a decent table like layout for -h output on 120 char width terminal options = {} OptionParser.new do |opts| opts.banner = <<~BANNER Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('', '2', '3', ...) [optional] Only selected files & folders: parallel_test test/bar test/baz/xxx_text.rb [optional] Pass test-options and files via `--`: parallel_test -- -t acceptance -f progress -- spec/foo_spec.rb spec/acceptance Options are: BANNER opts.on("-n PROCESSES", Integer, "How many processes to use, default: available CPUs") { |n| options[:count] = n } opts.on("-p", "--pattern PATTERN", "run tests matching this regex pattern") { |pattern| options[:pattern] = /#{pattern}/ } opts.on("--exclude-pattern", "--exclude-pattern PATTERN", "exclude tests matching this regex pattern") { |pattern| options[:exclude_pattern] = /#{pattern}/ } opts.on( "--group-by TYPE", heredoc(<<~TEXT, newline_padding) group tests by: found - order of finding files steps - number of cucumber/spinach steps scenarios - individual cucumber scenarios filesize - by size of the file runtime - info from runtime log default - runtime when runtime log is filled otherwise filesize TEXT ) { |type| options[:group_by] = type.to_sym } opts.on("-m COUNT", "--multiply-processes COUNT", Float, "use given number as a multiplier of processes to run") do |m| options[:multiply_processes] = m end opts.on("-s PATTERN", "--single PATTERN", "Run all matching files in the same process") do |pattern| (options[:single_process] ||= []) << /#{pattern}/ end opts.on("-i", "--isolate", "Do not run any other tests in the group used by --single(-s)") do options[:isolate] = true end opts.on( "--isolate-n PROCESSES", Integer, "Use 'isolate' singles with number of processes, default: 1" ) { |n| options[:isolate_count] = n } opts.on( "--highest-exit-status", "Exit with the highest exit status provided by test run(s)" ) { options[:highest_exit_status] = true } opts.on( "--failure-exit-code INT", Integer, "Specify the exit code to use when tests fail" ) { |code| options[:failure_exit_code] = code } opts.on( "--specify-groups SPECS", heredoc(<<~TEXT, newline_padding) Use 'specify-groups' if you want to specify multiple specs running in multiple processes in a specific formation. Commas indicate specs in the same process, pipes indicate specs in a new process. If SPECS is a '-' the value for this option is read from STDIN instead. Cannot use with --single, --isolate, or --isolate-n. Ex. $ parallel_tests -n 3 . --specify-groups '1_spec.rb,2_spec.rb|3_spec.rb' Process 1 will contain 1_spec.rb and 2_spec.rb Process 2 will contain 3_spec.rb Process 3 will contain all other specs TEXT ) { |groups| options[:specify_groups] = groups } opts.on( "--only-group GROUP_INDEX[,GROUP_INDEX]", Array, heredoc(<<~TEXT, newline_padding) Only run the given group numbers. Changes `--group-by` default to 'filesize'. TEXT ) { |groups| options[:only_group] = groups.map(&:to_i) } opts.on("-e", "--exec COMMAND", "execute COMMAND in parallel and with ENV['TEST_ENV_NUMBER']") { |arg| options[:execute] = Shellwords.shellsplit(arg) } opts.on( "--exec-args COMMAND", heredoc(<<~TEXT, newline_padding) execute COMMAND in parallel with test files as arguments, for example: $ parallel_tests --exec-args echo > echo spec/a_spec.rb spec/b_spec.rb TEXT ) { |arg| options[:execute_args] = Shellwords.shellsplit(arg) } opts.on("-o", "--test-options 'OPTIONS'", "execute test commands with those options") { |arg| options[:test_options] = Shellwords.shellsplit(arg) } opts.on("-t", "--type TYPE", "test(default) / rspec / cucumber / spinach") do |type| @runner = load_runner(type) rescue NameError, LoadError => e puts "Runner for `#{type}` type has not been found! (#{e})" abort end opts.on( "--suffix PATTERN", heredoc(<<~TEXT, newline_padding) override built in test file pattern (should match suffix): '_spec.rb$' - matches rspec files '_(test|spec).rb$' - matches test or spec files TEXT ) { |pattern| options[:suffix] = /#{pattern}/ } opts.on("--serialize-stdout", "Serialize stdout output, nothing will be written until everything is done") { options[:serialize_stdout] = true } opts.on("--prefix-output-with-test-env-number", "Prefixes test env number to the output when not using --serialize-stdout") { options[:prefix_output_with_test_env_number] = true } opts.on("--combine-stderr", "Combine stderr into stdout, useful in conjunction with --serialize-stdout") { options[:combine_stderr] = true } opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec") { options[:non_parallel] = true } opts.on("--no-symlinks", "Do not traverse symbolic links to find test files") { options[:symlinks] = false } opts.on('--ignore-tags PATTERN', 'When counting steps ignore scenarios with tags that match this pattern') { |arg| options[:ignore_tag_pattern] = arg } opts.on("--nice", "execute test commands with low priority.") { options[:nice] = true } opts.on("--runtime-log PATH", "Location of previously recorded test runtimes") { |path| options[:runtime_log] = path } opts.on("--allowed-missing COUNT", Integer, "Allowed percentage of missing runtimes (default = 50)") { |percent| options[:allowed_missing_percent] = percent } opts.on('--allow-duplicates', 'When detecting files to run, allow duplicates') { options[:allow_duplicates] = true } opts.on("--unknown-runtime SECONDS", Float, "Use given number as unknown runtime (otherwise use average time)") { |time| options[:unknown_runtime] = time } opts.on("--first-is-1", "Use \"1\" as TEST_ENV_NUMBER to not reuse the default test environment") { options[:first_is_1] = true } opts.on("--fail-fast", "Stop all groups when one group fails (best used with --test-options '--fail-fast' if supported") { options[:fail_fast] = true } opts.on( "--test-file-limit LIMIT", Integer, heredoc(<<~TEXT, newline_padding) Limit to this number of files per test run by batching (for windows set to ~100 to stay below 8192 max command limit, might have bugs from reusing test-env-number and summarizing partial results) TEXT ) { |limit| options[:test_file_limit] = limit } opts.on("--verbose", "Print debug output") { options[:verbose] = true } opts.on("--verbose-command", "Combines options --verbose-process-command and --verbose-rerun-command") { options.merge! verbose_process_command: true, verbose_rerun_command: true } opts.on("--verbose-process-command", "Print the command that will be executed by each process before it begins") { options[:verbose_process_command] = true } opts.on("--verbose-rerun-command", "After a process fails, print the command executed by that process") { options[:verbose_rerun_command] = true } opts.on("--quiet", "Print only tests output") { options[:quiet] = true } opts.on("-v", "--version", "Show Version") do puts ParallelTests::VERSION exit 0 end opts.on("-h", "--help", "Show this.") do puts opts exit 0 end end.parse!(argv) raise "Both options are mutually exclusive: verbose & quiet" if options[:verbose] && options[:quiet] if options[:count] == 0 options.delete(:count) options[:non_parallel] = true end files, remaining = extract_file_paths(argv) unless options[:execute] if files.empty? default_test_folder = @runner.default_test_folder if File.directory?(default_test_folder) files = [default_test_folder] else abort "Pass files or folders to run" end end options[:files] = files.map { |file_path| Pathname.new(file_path).cleanpath.to_s } end append_test_options(options, remaining) options[:group_by] ||= :filesize if options[:only_group] if options[:group_by] == :found && options[:single_process] raise "--group-by found and --single-process are not supported" end allowed = [:filesize, :runtime, :found] if !allowed.include?(options[:group_by]) && options[:only_group] raise "--group-by #{allowed.join(" or ")} is required for --only-group" end if options[:specify_groups] && options.keys.intersect?([:single_process, :isolate, :isolate_count]) raise "Can't pass --specify-groups with any of these keys: --single, --isolate, or --isolate-n" end if options[:failure_exit_code] && options[:highest_exit_status] raise "Can't pass --failure-exit-code and --highest-exit-status" end options end def extract_file_paths(argv) dash_index = argv.rindex("--") file_args_at = (dash_index || -1) + 1 [argv[file_args_at..], argv[0...(dash_index || 0)]] end def extract_test_options(argv) dash_index = argv.index("--") || -1 argv[dash_index + 1..] end def append_test_options(options, argv) new_opts = extract_test_options(argv) return if new_opts.empty? options[:test_options] ||= [] options[:test_options] += new_opts end def load_runner(type) require "parallel_tests/#{type}/runner" runner_classname = type.split("_").map(&:capitalize).join.sub("Rspec", "RSpec") klass_name = "ParallelTests::#{runner_classname}::Runner" klass_name.split('::').inject(Object) { |x, y| x.const_get(y) } end def execute_command_in_parallel(command, num_processes, options) runs = if options[:only_group] options[:only_group].map { |g| g - 1 } else (0...num_processes).to_a end results = if options[:non_parallel] ParallelTests.with_pid_file do runs.map do |i| ParallelTests::Test::Runner.execute_command(command, i, num_processes, options) end end else execute_in_parallel(runs, runs.size, options) do |i| ParallelTests::Test::Runner.execute_command(command, i, num_processes, options) end end.flatten abort if results.any? { |r| r[:exit_status] != 0 } end def report_time_taken(&block) seconds = ParallelTests.delta(&block).to_i puts "\nTook #{seconds} seconds#{detailed_duration(seconds)}" end def detailed_duration(seconds) parts = [seconds / 3600, seconds % 3600 / 60, seconds % 60].drop_while(&:zero?) return if parts.size < 2 parts = parts.map { |i| "%02d" % i }.join(':').sub(/^0/, '') " (#{parts})" end def final_fail_message fail_message = "Tests Failed" fail_message = "\e[31m#{fail_message}\e[0m" if use_colors? fail_message end def use_colors? $stdout.tty? end def first_is_1? val = ENV["PARALLEL_TEST_FIRST_IS_1"] ['1', 'true'].include?(val) end # CI systems often fail when there is no output for a long time, so simulate some output def simulate_output_for_ci(simulate) if simulate progress_indicator = Thread.new do interval = Float(ENV['PARALLEL_TEST_HEARTBEAT_INTERVAL'] || 60) loop do sleep interval print '.' end end test_results = yield progress_indicator.exit test_results else yield end end def heredoc(text, newline_padding) text.rstrip.gsub("\n", "\n#{' ' * newline_padding}") end end end parallel_tests-5.4.0/lib/parallel_tests/cucumber/000077500000000000000000000000001504331627400221345ustar00rootroot00000000000000parallel_tests-5.4.0/lib/parallel_tests/cucumber/failures_logger.rb000066400000000000000000000016741504331627400256420ustar00rootroot00000000000000# frozen_string_literal: true require 'cucumber/formatter/rerun' require 'parallel_tests/gherkin/io' require 'cucumber/events' module ParallelTests module Cucumber class FailuresLogger < ::Cucumber::Formatter::Rerun include ParallelTests::Gherkin::Io def initialize(config) super @io = prepare_io(config.out_stream) # Remove handler inherited from Cucumber::Formatter::Rerun that does not # properly join file failures handlers = config.event_bus.instance_variable_get(:@handlers) handlers[::Cucumber::Events::TestRunFinished.to_s].pop # Add our own handler config.on_event :test_run_finished do next if @failures.empty? lock_output do @failures.each do |file, lines| lines.each do |line| @io.print "#{file}:#{line} " end end end end end end end end parallel_tests-5.4.0/lib/parallel_tests/cucumber/features_with_steps.rb000066400000000000000000000023471504331627400265560ustar00rootroot00000000000000# frozen_string_literal: true begin gem "cuke_modeler", "~> 3.0" require 'cuke_modeler' rescue LoadError raise 'Grouping by number of cucumber steps requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.' end module ParallelTests module Cucumber class FeaturesWithSteps class << self def all(tests, options) ignore_tag_pattern = options[:ignore_tag_pattern].nil? ? nil : Regexp.compile(options[:ignore_tag_pattern]) # format of hash will be FILENAME => NUM_STEPS steps_per_file = tests.each_with_object({}) do |file, steps| feature = ::CukeModeler::FeatureFile.new(file).feature # skip feature if it matches tag regex next if feature.tags.grep(ignore_tag_pattern).any? # count the number of steps in the file # will only include a feature if the regex does not match all_steps = feature.scenarios.map { |a| a.steps.count if a.tags.grep(ignore_tag_pattern).empty? }.compact steps[file] = all_steps.sum end steps_per_file.sort_by { |_, value| -value } end end end end end parallel_tests-5.4.0/lib/parallel_tests/cucumber/runner.rb000066400000000000000000000022271504331627400237750ustar00rootroot00000000000000# frozen_string_literal: true require "parallel_tests/gherkin/runner" module ParallelTests module Cucumber class Runner < ParallelTests::Gherkin::Runner SCENARIOS_RESULTS_BOUNDARY_REGEX = /^(Failing|Flaky) Scenarios:$/ SCENARIO_REGEX = %r{^cucumber features/.+:\d+} class << self def name 'cucumber' end def default_test_folder 'features' end def line_is_result?(line) super || line =~ SCENARIO_REGEX || line =~ SCENARIOS_RESULTS_BOUNDARY_REGEX end def summarize_results(results) output = [] scenario_groups = results.slice_before(SCENARIOS_RESULTS_BOUNDARY_REGEX).group_by(&:first) scenario_groups.each do |header, group| scenarios = group.flatten.grep(SCENARIO_REGEX) output << ([header] + scenarios).join("\n") if scenarios.any? end output << super output.join("\n\n") end def command_with_seed(cmd, seed) clean = remove_command_arguments(cmd, '--order') [*clean, '--order', "random:#{seed}"] end end end end end parallel_tests-5.4.0/lib/parallel_tests/cucumber/scenario_line_logger.rb000066400000000000000000000033031504331627400266310ustar00rootroot00000000000000# frozen_string_literal: true module ParallelTests module Cucumber module Formatters class ScenarioLineLogger attr_reader :scenarios def initialize(tag_expression = nil) @scenarios = [] @tag_expression = tag_expression end def visit_feature_element(uri, feature_element, feature_tags, line_numbers: []) scenario_tags = feature_element.tags.map(&:name) scenario_tags = feature_tags + scenario_tags if feature_element.is_a?(CukeModeler::Scenario) # :Scenario test_line = feature_element.source_line # We don't accept the feature_element if the current tags are not valid return unless matches_tags?(scenario_tags) # or if it is not at the correct location return if line_numbers.any? && !line_numbers.include?(test_line) @scenarios << [uri, feature_element.source_line].join(":") else # :ScenarioOutline feature_element.examples.each do |example| example_tags = example.tags.map(&:name) example_tags = scenario_tags + example_tags next unless matches_tags?(example_tags) example.rows[1..].each do |row| test_line = row.source_line next if line_numbers.any? && !line_numbers.include?(test_line) @scenarios << [uri, test_line].join(':') end end end end def method_missing(*); end # # rubocop:disable Style/MissingRespondToMissing private def matches_tags?(tags) @tag_expression.nil? || @tag_expression.evaluate(tags) end end end end end parallel_tests-5.4.0/lib/parallel_tests/cucumber/scenarios.rb000066400000000000000000000056101504331627400244510ustar00rootroot00000000000000# frozen_string_literal: true require 'cucumber/tag_expressions/parser' require 'cucumber/runtime' require 'cucumber' require 'parallel_tests/cucumber/scenario_line_logger' require 'parallel_tests/gherkin/listener' begin gem "cuke_modeler", "~> 3.0" require 'cuke_modeler' rescue LoadError raise 'Grouping by individual cucumber scenarios requires the `cuke_modeler` modeler gem with requirement `~> 3.0`. Add `gem "cuke_modeler", "~> 3.0"` to your `Gemfile`, run `bundle install` and try again.' end module ParallelTests module Cucumber class Scenarios class << self def all(files, options = {}) # Parse tag expression from given test options and ignore tag pattern. Refer here to understand how new tag expression syntax works - https://github.com/cucumber/cucumber/tree/master/tag-expressions tags = [] words = options[:test_options] || [] words.each_with_index { |w, i| tags << words[i + 1] if ["-t", "--tags"].include?(w) } if ignore = options[:ignore_tag_pattern] tags << "not (#{ignore})" end tags_exp = tags.compact.join(" and ") split_into_scenarios files, tags_exp end private def split_into_scenarios(files, tags = '') # Create the tag expression instance from cucumber tag expressions parser, this is needed to know if the scenario matches with the tags invoked by the request # Create the ScenarioLineLogger which will filter the scenario we want args = [] args << ::Cucumber::TagExpressions::Parser.new.parse(tags) unless tags.empty? scenario_line_logger = ParallelTests::Cucumber::Formatters::ScenarioLineLogger.new(*args) # here we loop on the files map, each file will contain one or more scenario files.each do |path| # Gather up any line numbers attached to the file path path, *test_lines = path.split(/:(?=\d+)/) test_lines.map!(&:to_i) # We create a Gherkin document, this will be used to decode the details of each scenario document = ::CukeModeler::FeatureFile.new(path) feature = document.feature # We make an attempt to parse the gherkin document, this could be failed if the document is not well formatted feature_tags = feature.tags.map(&:name) # We loop on each children of the feature test_models = feature.tests test_models += feature.rules.flat_map(&:tests) if feature.respond_to?(:rules) # cuke_modeler >= 3.2 supports rules test_models.each do |test| # It's a scenario, we add it to the scenario_line_logger scenario_line_logger.visit_feature_element(document.path, test, feature_tags, line_numbers: test_lines) end end scenario_line_logger.scenarios end end end end end parallel_tests-5.4.0/lib/parallel_tests/gherkin/000077500000000000000000000000001504331627400217565ustar00rootroot00000000000000parallel_tests-5.4.0/lib/parallel_tests/gherkin/io.rb000066400000000000000000000014731504331627400227170ustar00rootroot00000000000000# frozen_string_literal: true require 'parallel_tests' module ParallelTests module Gherkin module Io def prepare_io(path_or_io) if path_or_io.respond_to?(:write) path_or_io else # its a path File.open(path_or_io, 'w').close # clean out the file file = File.open(path_or_io, 'a') at_exit do unless file.closed? file.flush file.close end end file end end # do not let multiple processes get in each others way def lock_output if @io.is_a?(File) begin @io.flock File::LOCK_EX yield ensure @io.flock File::LOCK_UN end else yield end end end end end parallel_tests-5.4.0/lib/parallel_tests/gherkin/listener.rb000066400000000000000000000040361504331627400241330ustar00rootroot00000000000000# frozen_string_literal: true module ParallelTests module Gherkin class Listener attr_reader :collect attr_writer :ignore_tag_pattern def initialize @steps = [] @uris = [] @collect = {} @feature, @ignore_tag_pattern = nil reset_counters! end def feature(feature) @feature = feature end def background(*) @background = 1 end def scenario(scenario) @outline = @background = 0 return if should_ignore(scenario) @scenarios += 1 end def scenario_outline(outline) return if should_ignore(outline) @outline = 1 end def step(*) return if @ignoring if @background == 1 @background_steps += 1 elsif @outline > 0 @outline_steps += 1 else @collect[@uri] += 1 end end def uri(path) @uri = path @collect[@uri] = 0 end # # @param [Gherkin::Formatter::Model::Examples] examples # def examples(examples) @collect[@uri] += (@outline_steps * examples.rows.size) unless examples.rows.empty? end def eof(*) @collect[@uri] += (@background_steps * @scenarios) reset_counters! end def reset_counters! @outline = @outline_steps = @background = @background_steps = @scenarios = 0 @ignoring = nil end # ignore lots of other possible callbacks ... def method_missing(*); end # rubocop:disable Style/MissingRespondToMissing private # Return a combination of tags declared on this scenario/outline and the feature it belongs to def all_tags(scenario) (scenario.tags || []) + ((@feature && @feature.tags) || []) end # Set @ignoring if we should ignore this scenario/outline based on its tags def should_ignore(scenario) @ignoring = @ignore_tag_pattern && all_tags(scenario).find { |tag| @ignore_tag_pattern === tag.name } end end end end parallel_tests-5.4.0/lib/parallel_tests/gherkin/runner.rb000066400000000000000000000071651504331627400236250ustar00rootroot00000000000000# frozen_string_literal: true require "parallel_tests/test/runner" module ParallelTests module Gherkin class Runner < ParallelTests::Test::Runner class << self def run_tests(test_files, process_number, num_processes, options) combined_scenarios = test_files if options[:group_by] == :scenarios grouped = test_files.map { |t| t.split(':') }.group_by(&:first) combined_scenarios = grouped.map do |file, files_and_lines| "#{file}:#{files_and_lines.map(&:last).join(':')}" end end options[:env] ||= {} options[:env] = options[:env].merge({ 'AUTOTEST' => '1' }) if $stdout.tty? execute_command(build_command(combined_scenarios, options), process_number, num_processes, options) end def test_file_name @test_file_name || 'feature' end def default_test_folder 'features' end def test_suffix /\.feature$/ end def line_is_result?(line) line =~ /^\d+ (steps?|scenarios?)/ end def build_test_command(file_list, options) [ *executable, *(runtime_logging if File.directory?(File.dirname(runtime_log))), *file_list, *cucumber_opts(options[:test_options]) ] end # cucumber has 2 result lines per test run, that cannot be added # 1 scenario (1 failed) # 1 step (1 failed) def summarize_results(results) sort_order = ['scenario', 'step', 'failed', 'flaky', 'undefined', 'skipped', 'pending', 'passed'] ['scenario', 'step'].map do |group| group_results = results.grep(/^\d+ #{group}/) next if group_results.empty? sums = sum_up_results(group_results) sums = sums.sort_by { |word, _| sort_order.index(word) || 999 } sums.map! do |word, number| plural = "s" if (word == group) && (number != 1) "#{number} #{word}#{plural}" end "#{sums[0]} (#{sums[1..].join(", ")})" end.compact.join("\n") end def cucumber_opts(given) if given&.include?('--profile') || given&.include?('-p') given else [*given, *profile_from_config] end end def profile_from_config # copied from https://github.com/cucumber/cucumber/blob/master/lib/cucumber/cli/profile_loader.rb#L85 config = Dir.glob("{,.config/,config/}#{name}{.yml,.yaml}").first ['--profile', 'parallel'] if config && File.read(config) =~ /^parallel:/ end def tests_in_groups(tests, num_groups, options = {}) @test_file_name = "scenario" if options[:group_by] == :scenarios method = "by_#{options[:group_by]}" if Grouper.respond_to?(method) Grouper.send(method, find_tests(tests, options), num_groups, options) else super end end def runtime_logging ['--format', 'ParallelTests::Gherkin::RuntimeLogger', '--out', runtime_log] end def runtime_log "tmp/parallel_runtime_#{name}.log" end def determine_executable if File.exist?("bin/#{name}") ParallelTests.with_ruby_binary("bin/#{name}") elsif ParallelTests.bundler_enabled? ["bundle", "exec", name] elsif File.file?("script/#{name}") ParallelTests.with_ruby_binary("script/#{name}") else [name.to_s] end end end end end end parallel_tests-5.4.0/lib/parallel_tests/gherkin/runtime_logger.rb000066400000000000000000000013351504331627400253270ustar00rootroot00000000000000# frozen_string_literal: true require 'parallel_tests/gherkin/io' module ParallelTests module Gherkin class RuntimeLogger include Io def initialize(config) @io = prepare_io(config.out_stream) @example_times = Hash.new(0) config.on_event :test_case_started do |_| @start_at = ParallelTests.now.to_f end config.on_event :test_case_finished do |event| @example_times[event.test_case.location.file] += ParallelTests.now.to_f - @start_at end config.on_event :test_run_finished do |_| lock_output do @io.puts(@example_times.map { |file, time| "#{file}:#{time}" }) end end end end end end parallel_tests-5.4.0/lib/parallel_tests/grouper.rb000066400000000000000000000125761504331627400223520ustar00rootroot00000000000000# frozen_string_literal: true module ParallelTests class Grouper class << self def by_steps(tests, num_groups, options) features_with_steps = group_by_features_with_steps(tests, options) in_even_groups_by_size(features_with_steps, num_groups) end def by_scenarios(tests, num_groups, options = {}) scenarios = group_by_scenarios(tests, options) in_even_groups_by_size(scenarios, num_groups) end def in_even_groups_by_size(items, num_groups, options = {}) groups = Array.new(num_groups) { { items: [], size: 0 } } return specify_groups(items, num_groups, options, groups) if options[:specify_groups] # add all files that should run in a single process to one group single_process_patterns = options[:single_process] || [] single_items, items = items.partition do |item, _size| single_process_patterns.any? { |pattern| item =~ pattern } end isolate_count = isolate_count(options) if isolate_count >= num_groups raise 'Number of isolated processes must be >= total number of processes' end if isolate_count >= 1 # add all files that should run in a multiple isolated processes to their own groups group_features_by_size(items_to_group(single_items), groups[0..(isolate_count - 1)]) # group the non-isolated by size group_features_by_size(items_to_group(items), groups[isolate_count..]) else # add all files that should run in a single non-isolated process to first group single_items.each { |item, size| add_to_group(groups.first, item, size) } # group all by size group_features_by_size(items_to_group(items), groups) end groups.map! { |g| g[:items].sort } end private def specified_groups(options) groups = options[:specify_groups] return groups if groups != '-' $stdin.read.chomp end def specify_groups(items, num_groups, options, groups) specify_test_process_groups = specified_groups(options).split('|') if specify_test_process_groups.count > num_groups raise 'Number of processes separated by pipe must be less than or equal to the total number of processes' end all_specified_tests = specify_test_process_groups.map { |group| group.split(',') }.flatten specified_items_found, items = items.partition { |item, _size| all_specified_tests.include?(item) } specified_specs_not_found = all_specified_tests - specified_items_found.map(&:first) if specified_specs_not_found.any? raise "Could not find #{specified_specs_not_found} from --specify-groups in the selected files & folders" end if specify_test_process_groups.count == num_groups && items.flatten.any? raise( <<~ERROR The number of groups in --specify-groups matches the number of groups from -n but there were other specs found in the selected files & folders not specified in --specify-groups. Make sure -n is larger than the number of processes in --specify-groups if there are other specs that need to be run. The specs that aren't run: #{items.map(&:first)} ERROR ) end # First order the specify_groups into the main groups array specify_test_process_groups.each_with_index do |specify_test_process, i| groups[i] = specify_test_process.split(',') end # Return early when processed specify_groups tests exactly match the items passed in return groups if specify_test_process_groups.count == num_groups # Now sort the rest of the items into the main groups array specified_range = specify_test_process_groups.count..-1 remaining_groups = groups[specified_range] group_features_by_size(items_to_group(items), remaining_groups) # Don't sort all the groups, only sort the ones not specified in specify_groups sorted_groups = remaining_groups.map { |g| g[:items].sort } groups[specified_range] = sorted_groups groups end def isolate_count(options) if options[:isolate_count] && options[:isolate_count] > 1 options[:isolate_count] elsif options[:isolate] 1 else 0 end end def largest_first(files) files.sort_by { |_item, size| size }.reverse end def smallest_group(groups) groups.min_by { |g| g[:size] } end def add_to_group(group, item, size) group[:items] << item group[:size] += size end def group_by_features_with_steps(tests, options) require 'parallel_tests/cucumber/features_with_steps' ParallelTests::Cucumber::FeaturesWithSteps.all(tests, options) end def group_by_scenarios(tests, options = {}) require 'parallel_tests/cucumber/scenarios' ParallelTests::Cucumber::Scenarios.all(tests, options) end def group_features_by_size(items, groups_to_fill) items.each do |item, size| size ||= 1 smallest = smallest_group(groups_to_fill) add_to_group(smallest, item, size) end end def items_to_group(items) items.first && items.first.size == 2 ? largest_first(items) : items end end end end parallel_tests-5.4.0/lib/parallel_tests/pids.rb000066400000000000000000000015051504331627400216140ustar00rootroot00000000000000# frozen_string_literal: true require 'json' module ParallelTests class Pids attr_reader :file_path, :mutex def initialize(file_path) @file_path = file_path @mutex = Mutex.new end def add(pid) pids << pid.to_i save end def delete(pid) pids.delete(pid.to_i) save end def count read pids.count end def all read pids end private def pids @pids ||= [] end def clear @pids = [] save end def read sync do contents = File.read(file_path) return if contents.empty? @pids = JSON.parse(contents) end end def save sync { File.write(file_path, pids.to_json) } end def sync(&block) mutex.synchronize(&block) end end end parallel_tests-5.4.0/lib/parallel_tests/railtie.rb000066400000000000000000000002711504331627400223050ustar00rootroot00000000000000# frozen_string_literal: true # rake tasks for Rails 3+ module ParallelTests class Railtie < ::Rails::Railtie rake_tasks do require "parallel_tests/tasks" end end end parallel_tests-5.4.0/lib/parallel_tests/rspec/000077500000000000000000000000001504331627400214435ustar00rootroot00000000000000parallel_tests-5.4.0/lib/parallel_tests/rspec/failures_logger.rb000066400000000000000000000010441504331627400251400ustar00rootroot00000000000000# frozen_string_literal: true require 'parallel_tests/rspec/logger_base' require 'parallel_tests/rspec/runner' class ParallelTests::RSpec::FailuresLogger < ParallelTests::RSpec::LoggerBase RSpec::Core::Formatters.register(self, :dump_summary) def dump_summary(*args) lock_output do notification = args.first unless notification.failed_examples.empty? colorizer = ::RSpec::Core::Formatters::ConsoleCodes output.puts notification.colorized_rerun_commands(colorizer) end end @output.flush end end parallel_tests-5.4.0/lib/parallel_tests/rspec/logger_base.rb000066400000000000000000000017201504331627400242410ustar00rootroot00000000000000# frozen_string_literal: true module ParallelTests module RSpec end end require 'rspec/core/formatters/base_text_formatter' class ParallelTests::RSpec::LoggerBase < RSpec::Core::Formatters::BaseTextFormatter def initialize(*args) super @output ||= args[0] case @output when String # a path ? FileUtils.mkdir_p(File.dirname(@output)) File.open(@output, 'w') {} # overwrite previous results @output = File.open(@output, 'a') when File # close and restart in append mode @output.close @output = File.open(@output.path, 'a') end end # stolen from Rspec def close(*) @output.close if (IO === @output) & (@output != $stdout) end protected # do not let multiple processes get in each others way def lock_output if @output.is_a?(File) begin @output.flock File::LOCK_EX yield ensure @output.flock File::LOCK_UN end else yield end end end parallel_tests-5.4.0/lib/parallel_tests/rspec/runner.rb000066400000000000000000000045261504331627400233100ustar00rootroot00000000000000# frozen_string_literal: true require "parallel_tests/test/runner" module ParallelTests module RSpec class Runner < ParallelTests::Test::Runner class << self def run_tests(test_files, process_number, num_processes, options) execute_command(build_command(test_files, options), process_number, num_processes, options) end def determine_executable if File.exist?("bin/rspec") ParallelTests.with_ruby_binary("bin/rspec") elsif ParallelTests.bundler_enabled? ["bundle", "exec", "rspec"] else ["rspec"] end end def runtime_log "tmp/parallel_runtime_rspec.log" end def default_test_folder "spec" end def test_file_name "spec" end # used to find all _spec.rb files # supports also feature files used by rspec turnip extension def test_suffix /(_spec\.rb|\.feature)$/ end def line_is_result?(line) line =~ /\d+ examples?, \d+ failures?/ end def build_test_command(file_list, options) [*executable, *options[:test_options], *color, *spec_opts, *file_list] end # remove old seed and add new seed # --seed 1234 # --order rand # --order rand:1234 # --order random:1234 def command_with_seed(cmd, seed) clean = remove_command_arguments(cmd, '--seed', '--order') [*clean, '--seed', seed] end # Summarize results from threads and colorize results based on failure and pending counts. # def summarize_results(results) text = super return text unless $stdout.tty? sums = sum_up_results(results) color = if sums['failure'] > 0 31 # red elsif sums['pending'] > 0 33 # yellow else 32 # green end "\e[#{color}m#{text}\e[0m" end private def color ['--color', '--tty'] if $stdout.tty? end def spec_opts options_file = ['.rspec_parallel', 'spec/parallel_spec.opts', 'spec/spec.opts'].detect { |f| File.file?(f) } ["-O", options_file] if options_file end end end end end parallel_tests-5.4.0/lib/parallel_tests/rspec/runtime_logger.rb000066400000000000000000000024571504331627400250220ustar00rootroot00000000000000# frozen_string_literal: true require 'parallel_tests' require 'parallel_tests/rspec/logger_base' class ParallelTests::RSpec::RuntimeLogger < ParallelTests::RSpec::LoggerBase def initialize(*args) super @example_times = Hash.new(0) @group_nesting = 0 end RSpec::Core::Formatters.register(self, :example_group_started, :example_group_finished, :start_dump) def example_group_started(example_group) @time = ParallelTests.now if @group_nesting == 0 @group_nesting += 1 super end def example_group_finished(notification) @group_nesting -= 1 if @group_nesting == 0 @example_times[notification.group.file_path] += ParallelTests.now - @time end super if defined?(super) end def seed(*); end def dump_summary(*); end def dump_failures(*); end def dump_failure(*); end def dump_pending(*); end def start_dump(*) return unless ENV['TEST_ENV_NUMBER'] # only record when running in parallel lock_output do # Order the output from slowest to fastest @example_times = @example_times.sort_by(&:last).reverse @example_times.each do |file, time| relative_path = file.sub(%r{^#{Regexp.escape Dir.pwd}/}, '').sub(%r{^\./}, "") @output.puts "#{relative_path}:#{[time, 0].max}" end end @output.flush end end parallel_tests-5.4.0/lib/parallel_tests/rspec/summary_logger.rb000066400000000000000000000004461504331627400250300ustar00rootroot00000000000000# frozen_string_literal: true require 'parallel_tests/rspec/failures_logger' class ParallelTests::RSpec::SummaryLogger < ParallelTests::RSpec::LoggerBase RSpec::Core::Formatters.register(self, :dump_failures) def dump_failures(*args) lock_output { super } @output.flush end end parallel_tests-5.4.0/lib/parallel_tests/rspec/verbose_logger.rb000066400000000000000000000026721504331627400250030ustar00rootroot00000000000000# frozen_string_literal: true require 'rspec/core/formatters/base_text_formatter' require 'parallel_tests/rspec/runner' class ParallelTests::RSpec::VerboseLogger < RSpec::Core::Formatters::BaseTextFormatter RSpec::Core::Formatters.register( self, :example_group_started, :example_group_finished, :example_started, :example_passed, :example_pending, :example_failed ) def initialize(output) super @line = [] end def example_group_started(notification) @line.push(notification.group.description) end def example_group_finished(_notification) @line.pop end def example_started(notification) @line.push(notification.example.description) output_formatted_line('STARTED', :yellow) end def example_passed(_passed) output_formatted_line('PASSED', :success) @line.pop end def example_pending(_pending) output_formatted_line('PENDING', :pending) @line.pop end def example_failed(_failure) output_formatted_line('FAILED', :failure) @line.pop end private def output_formatted_line(status, console_code) prefix = ["[#{Process.pid}]"] if ENV.include?('TEST_ENV_NUMBER') test_env_number = ENV['TEST_ENV_NUMBER'] == '' ? 1 : Integer(ENV['TEST_ENV_NUMBER']) prefix << "[#{test_env_number}]" end prefix << RSpec::Core::Formatters::ConsoleCodes.wrap("[#{status}]", console_code) output.puts [*prefix, *@line].join(' ') end end parallel_tests-5.4.0/lib/parallel_tests/spinach/000077500000000000000000000000001504331627400217545ustar00rootroot00000000000000parallel_tests-5.4.0/lib/parallel_tests/spinach/runner.rb000066400000000000000000000006261504331627400236160ustar00rootroot00000000000000# frozen_string_literal: true require "parallel_tests/gherkin/runner" module ParallelTests module Spinach class Runner < ParallelTests::Gherkin::Runner class << self def name 'spinach' end def default_test_folder 'features' end def runtime_logging # Not Yet Supported [] end end end end end parallel_tests-5.4.0/lib/parallel_tests/tasks.rb000066400000000000000000000252561504331627400220130ustar00rootroot00000000000000# frozen_string_literal: true require 'rake' require 'shellwords' module ParallelTests module Tasks class << self def rails_env ENV['PARALLEL_RAILS_ENV'] || 'test' end def load_lib $LOAD_PATH << File.expand_path('..', __dir__) require "parallel_tests" end def purge_before_load if ActiveRecord.version > Gem::Version.new('4.2.0') Rake::Task.task_defined?('db:purge') ? 'db:purge' : 'app:db:purge' end end def run_in_parallel(cmd, options = {}) load_lib # Using the relative path to find the binary allow to run a specific version of it executable = File.expand_path('../../bin/parallel_test', __dir__) command = ParallelTests.with_ruby_binary(executable) command += ['--exec', Shellwords.join(cmd)] command += ['-n', options[:count]] unless options[:count].to_s.empty? command << '--non-parallel' if options[:non_parallel] abort unless system(*command) end # this is a crazy-complex solution for a very simple problem: # removing certain lines from the output without changing the exit-status # normally I'd not do this, but it has been lots of fun and a great learning experience :) # # - sed does not support | without -r # - grep changes 0 exitstatus to 1 if nothing matches # - sed changes 1 exitstatus to 0 # - pipefail makes pipe fail with exitstatus of first failed command # - pipefail is not supported in (zsh) # - defining a new rake task like silence_schema would force users to load parallel_tests in test env # - simple system "set -o pipefail" returns nil even though set -o pipefail exists with 0 def suppress_output(command, ignore_regex) activate_pipefail = "set -o pipefail" remove_ignored_lines = %{(grep -v #{Shellwords.escape(ignore_regex)} || true)} # remove nil values (ex: #purge_before_load returns nil) command.compact! if system('/bin/bash', '-c', "#{activate_pipefail} 2>/dev/null") shell_command = "#{activate_pipefail} && (#{Shellwords.shelljoin(command)}) | #{remove_ignored_lines}" ['/bin/bash', '-c', shell_command] else command end end def suppress_schema_load_output(command) ParallelTests::Tasks.suppress_output(command, "^ ->\\|^-- ") end def check_for_pending_migrations ["db:abort_if_pending_migrations", "app:db:abort_if_pending_migrations"].each do |abort_migrations| if Rake::Task.task_defined?(abort_migrations) Rake::Task[abort_migrations].invoke break end end end # parallel:spec[:count, :pattern, :options, :pass_through] def parse_args(args) # order as given by user args = [args[:count], args[:pattern], args[:options], args[:pass_through]] # count given or empty ? # parallel:spec[2,models,options] # parallel:spec[,models,options] count = args.shift if args.first.to_s =~ /^\d*$/ num_processes = (count.to_s.empty? ? nil : Integer(count)) pattern = args.shift options = args.shift pass_through = args.shift [num_processes, pattern, options, pass_through] end def schema_format_based_on_rails_version if active_record_7_or_greater? ActiveRecord.schema_format else ActiveRecord::Base.schema_format end end def schema_type_based_on_rails_version if active_record_61_or_greater? || schema_format_based_on_rails_version == :ruby "schema" else "structure" end end def build_run_command(type, args) count, pattern, options, pass_through = ParallelTests::Tasks.parse_args(args) test_framework = { 'spec' => 'rspec', 'test' => 'test', 'features' => 'cucumber', 'features-spinach' => 'spinach' }.fetch(type) type = 'features' if test_framework == 'spinach' # Using the relative path to find the binary allow to run a specific version of it executable = File.expand_path('../../bin/parallel_test', __dir__) executable = ParallelTests.with_ruby_binary(executable) command = [*executable, type, '--type', test_framework] command += ['-n', count.to_s] if count command += ['--pattern', pattern] if pattern command += ['--test-options', options] if options command += Shellwords.shellsplit pass_through if pass_through command end def configured_databases return [] unless defined?(ActiveRecord) && active_record_61_or_greater? @@configured_databases ||= ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml end def for_each_database(&block) # Use nil to represent all databases block&.call(nil) # skip if not rails or old rails version return if !defined?(ActiveRecord::Tasks::DatabaseTasks) || !ActiveRecord::Tasks::DatabaseTasks.respond_to?(:for_each) ActiveRecord::Tasks::DatabaseTasks.for_each(configured_databases) do |name| block&.call(name) end end private def active_record_7_or_greater? ActiveRecord.version >= Gem::Version.new('7.0') end def active_record_61_or_greater? ActiveRecord.version >= Gem::Version.new('6.1.0') end end end end namespace :parallel do desc "Setup test databases via db:setup --> parallel:setup[num_cpus]" task :setup, :count do |_, args| command = [$0, "db:setup", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"] ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args) end ParallelTests::Tasks.for_each_database do |name| task_name = 'create' task_name += ":#{name}" if name desc "Create test#{" #{name}" if name} database via db:#{task_name} --> parallel:#{task_name}[num_cpus]" task task_name.to_sym, :count do |_, args| ParallelTests::Tasks.run_in_parallel( [$0, "db:#{task_name}", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"], args ) end end ParallelTests::Tasks.for_each_database do |name| task_name = 'drop' task_name += ":#{name}" if name desc "Drop test#{" #{name}" if name} database via db:#{task_name} --> parallel:#{task_name}[num_cpus]" task task_name.to_sym, :count do |_, args| ParallelTests::Tasks.run_in_parallel( [ $0, "db:#{task_name}", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}", "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" ], args ) end end desc "Update test databases by dumping and loading --> parallel:prepare[num_cpus]" task(:prepare, [:count]) do |_, args| ParallelTests::Tasks.check_for_pending_migrations if defined?(ActiveRecord) && [:ruby, :sql].include?(ParallelTests::Tasks.schema_format_based_on_rails_version) # fast: dump once, load in parallel type = ParallelTests::Tasks.schema_type_based_on_rails_version Rake::Task["db:#{type}:dump"].invoke # remove database connection to prevent "database is being accessed by other users" ActiveRecord::Base.remove_connection if ActiveRecord::Base.configurations.any? Rake::Task["parallel:load_#{type}"].invoke(args[:count]) else # slow: dump and load in in serial args = args.to_hash.merge(non_parallel: true) # normal merge returns nil task_name = Rake::Task.task_defined?('db:test:prepare') ? 'db:test:prepare' : 'app:db:test:prepare' ParallelTests::Tasks.run_in_parallel([$0, task_name], args) next end end # when dumping/resetting takes too long ParallelTests::Tasks.for_each_database do |name| task_name = 'migrate' task_name += ":#{name}" if name desc "Update test#{" #{name}" if name} database via db:#{task_name} --> parallel:#{task_name}[num_cpus]" task task_name.to_sym, :count do |_, args| ParallelTests::Tasks.run_in_parallel( [$0, "db:#{task_name}", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"], args ) end end desc "Rollback test databases via db:rollback --> parallel:rollback[num_cpus]" task :rollback, :count do |_, args| ParallelTests::Tasks.run_in_parallel( [$0, "db:rollback", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"], args ) end # just load the schema (good for integration server <-> no development db) ParallelTests::Tasks.for_each_database do |name| rails_task = 'db:schema:load' rails_task += ":#{name}" if name task_name = 'load_schema' task_name += ":#{name}" if name desc "Load dumped schema for test#{" #{name}" if name} database via #{rails_task} --> parallel:#{task_name}[num_cpus]" task task_name.to_sym, :count do |_, args| command = [ $0, ParallelTests::Tasks.purge_before_load, rails_task, "RAILS_ENV=#{ParallelTests::Tasks.rails_env}", "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" ] ParallelTests::Tasks.run_in_parallel(ParallelTests::Tasks.suppress_schema_load_output(command), args) end end # load the structure from the structure.sql file # (faster for rails < 6.1, deprecated after and only configured by `ActiveRecord::Base.schema_format`) desc "Load structure for test databases via db:schema:load --> parallel:load_structure[num_cpus]" task :load_structure, :count do |_, args| ParallelTests::Tasks.run_in_parallel( [ $0, ParallelTests::Tasks.purge_before_load, "db:structure:load", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}", "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" ], args ) end desc "Load the seed data from db/seeds.rb via db:seed --> parallel:seed[num_cpus]" task :seed, :count do |_, args| ParallelTests::Tasks.run_in_parallel( [ $0, "db:seed", "RAILS_ENV=#{ParallelTests::Tasks.rails_env}" ], args ) end desc "Launch given rake command in parallel" task :rake, :command, :count do |_, args| ParallelTests::Tasks.run_in_parallel( [$0, args.command, "RAILS_ENV=#{ParallelTests::Tasks.rails_env}"], args ) end ['test', 'spec', 'features', 'features-spinach'].each do |type| desc "Run #{type} in parallel with parallel:#{type}[num_cpus]" task type, [:count, :pattern, :options, :pass_through] do |_t, args| ParallelTests::Tasks.check_for_pending_migrations ParallelTests::Tasks.load_lib command = ParallelTests::Tasks.build_run_command(type, args) abort unless system(*command) # allow to chain tasks e.g. rake parallel:spec parallel:features end end end parallel_tests-5.4.0/lib/parallel_tests/test/000077500000000000000000000000001504331627400213065ustar00rootroot00000000000000parallel_tests-5.4.0/lib/parallel_tests/test/runner.rb000066400000000000000000000244651504331627400231570ustar00rootroot00000000000000# frozen_string_literal: true require 'shellwords' require 'parallel_tests' module ParallelTests module Test class Runner RuntimeLogTooSmallError = Class.new(StandardError) class << self # --- usually overwritten by other runners def runtime_log 'tmp/parallel_runtime_test.log' end def test_suffix /_(test|spec).rb$/ end def default_test_folder "test" end def test_file_name "test" end def run_tests(test_files, process_number, num_processes, options) require_list = test_files.map { |file| file.gsub(" ", "\\ ") }.join(" ") execute_command(build_command(require_list, options), process_number, num_processes, options) end # ignores other commands runner noise def line_is_result?(line) line =~ /\d+ failure(?!:)/ end # --- usually used by other runners # finds all tests and partitions them into groups def tests_in_groups(tests, num_groups, options = {}) tests = tests_with_size(tests, options) Grouper.in_even_groups_by_size(tests, num_groups, options) end def tests_with_size(tests, options) tests = find_tests(tests, options) case options[:group_by] when :found tests.map! { |t| [t, 1] } when :filesize sort_by_filesize(tests) when :runtime sort_by_runtime( tests, runtimes(tests, options), options.merge(allowed_missing: (options[:allowed_missing_percent] || 50) / 100.0) ) when nil # use recorded test runtime if we got enough data runtimes = begin runtimes(tests, options) rescue StandardError [] end if runtimes.size * 1.5 > tests.size puts "Using recorded test runtime" unless options[:quiet] sort_by_runtime(tests, runtimes) else sort_by_filesize(tests) end else raise ArgumentError, "Unsupported option #{options[:group_by]}" end tests end def execute_command(cmd, process_number, num_processes, options) number = test_env_number(process_number, options).to_s env = (options[:env] || {}).merge( "TEST_ENV_NUMBER" => number, "PARALLEL_TEST_GROUPS" => num_processes.to_s, "PARALLEL_PID_FILE" => ParallelTests.pid_file_path ) cmd = ["nice", *cmd] if options[:nice] # being able to run with for example `-output foo-$TEST_ENV_NUMBER` worked originally and is convenient cmd = cmd.map { |c| c.gsub("$TEST_ENV_NUMBER", number).gsub("${TEST_ENV_NUMBER}", number) } print_command(cmd, env) if report_process_command?(options) && !options[:serialize_stdout] execute_command_and_capture_output(env, cmd, options) end def print_command(command, env) env_str = ['TEST_ENV_NUMBER', 'PARALLEL_TEST_GROUPS'].map { |e| "#{e}=#{env[e]}" }.join(' ') puts [env_str, Shellwords.shelljoin(command)].compact.join(' ') end def execute_command_and_capture_output(env, cmd, options) popen_options = {} # do not add `pgroup: true`, it will break `binding.irb` inside the test popen_options[:err] = [:child, :out] if options[:combine_stderr] pid = nil output = IO.popen(env, cmd, popen_options) do |io| pid = io.pid ParallelTests.pids.add(pid) capture_output(io, env, options) end ParallelTests.pids.delete(pid) if pid exitstatus = $?.exitstatus seed = output[/seed (\d+)/, 1] output = "#{Shellwords.shelljoin(cmd)}\n#{output}" if report_process_command?(options) && options[:serialize_stdout] { env: env, stdout: output, exit_status: exitstatus, command: cmd, seed: seed } end def find_results(test_output) test_output.lines.map do |line| line.chomp! line.gsub!(/\e\[\d+m/, '') # remove color coding next unless line_is_result?(line) line end.compact end def test_env_number(process_number, options = {}) if process_number == 0 && !options[:first_is_1] '' else process_number + 1 end end def summarize_results(results) sums = sum_up_results(results) sums.sort.map { |word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ') end # remove old seed and add new seed def command_with_seed(cmd, seed) clean = remove_command_arguments(cmd, '--seed') [*clean, '--seed', seed] end protected def executable if (executable = ENV['PARALLEL_TESTS_EXECUTABLE']) Shellwords.shellsplit(executable) else determine_executable end end def determine_executable ["ruby"] end def build_command(file_list, options) if options[:execute_args] options[:execute_args] + file_list else build_test_command(file_list, options) end end # load all test files, to be overwritten by other runners def build_test_command(file_list, options) [ *executable, '-Itest', # adding ./test directory to the load path for compatibility to common setups '-e', "%w[#{file_list}].each { |f| require %{./\#{f}} }", # using %w to keep things readable '--', *options[:test_options] ] end def sum_up_results(results) results = results.join(' ').gsub(/s\b/, '') # combine and singularize results counts = results.scan(/(\d+) (\w+)/) counts.each_with_object(Hash.new(0)) do |(number, word), sum| sum[word] += number.to_i end end # read output of the process and print it in chunks def capture_output(out, env, options = {}) result = +"" begin loop do read = out.readpartial(1000000) # read whatever chunk we can get if Encoding.default_internal read = read.force_encoding(Encoding.default_internal) end result << read unless options[:serialize_stdout] message = read message = "[TEST GROUP #{env['TEST_ENV_NUMBER']}] #{message}" if options[:prefix_output_with_test_env_number] $stdout.print message $stdout.flush end end rescue EOFError nil end result end def sort_by_runtime(tests, runtimes, options = {}) allowed_missing = options[:allowed_missing] || 1.0 allowed_missing = tests.size * allowed_missing # set know runtime for each test tests.sort! tests.map! do |test| allowed_missing -= 1 unless time = runtimes[test] if allowed_missing < 0 log = options[:runtime_log] || runtime_log raise RuntimeLogTooSmallError, "Runtime log file '#{log}' does not contain sufficient data to sort #{tests.size} test files, please update or remove it." end [test, time] end puts "Runtime found for #{tests.count(&:last)} of #{tests.size} tests" if options[:verbose] set_unknown_runtime tests, options end def runtimes(tests, options) log = options[:runtime_log] || runtime_log lines = File.read(log).split("\n") lines.each_with_object({}) do |line, times| test, _, time = line.rpartition(':') next unless test && time times[test] = time.to_f if tests.include?(test) end end def sort_by_filesize(tests) tests.sort! tests.map! { |test| [test, File.stat(test).size] } end def find_tests(tests, options = {}) suffix_pattern = options[:suffix] || test_suffix include_pattern = options[:pattern] || // exclude_pattern = options[:exclude_pattern] allow_duplicates = options[:allow_duplicates] files = (tests || []).flat_map do |file_or_folder| if File.directory?(file_or_folder) files = files_in_folder(file_or_folder, options) files = files.grep(suffix_pattern).grep(include_pattern) files -= files.grep(exclude_pattern) if exclude_pattern files else file_or_folder end end allow_duplicates ? files : files.uniq end def files_in_folder(folder, options = {}) pattern = if options[:symlinks] == false # not nil or true "**/*" else # follow one symlink and direct children # http://stackoverflow.com/questions/357754/can-i-traverse-symlinked-directories-in-ruby-with-a-glob "**{,/*/**}/*" end Dir[File.join(folder, pattern)].uniq.sort end def remove_command_arguments(command, *args) remove_next = false command.select do |arg| if remove_next remove_next = false false elsif args.include?(arg) remove_next = true false else true end end end private # fill gaps with unknown-runtime if given, average otherwise # NOTE: an optimization could be doing runtime by average runtime per file size, but would need file checks def set_unknown_runtime(tests, options) known, unknown = tests.partition(&:last) return if unknown.empty? unknown_runtime = options[:unknown_runtime] || (known.empty? ? 1 : known.map!(&:last).sum / known.size) # average unknown.each { |set| set[1] = unknown_runtime } end def report_process_command?(options) options[:verbose] || options[:verbose_process_command] end end end end end parallel_tests-5.4.0/lib/parallel_tests/test/runtime_logger.rb000066400000000000000000000047641504331627400246700ustar00rootroot00000000000000# frozen_string_literal: true require 'parallel_tests' require 'parallel_tests/test/runner' module ParallelTests module Test class RuntimeLogger @@prepared = false class << self def log_test_run(test) prepare result = nil time = ParallelTests.delta { result = yield } log(test, time) result end def unique_log with_locked_log do |logfile| separator = "\n" groups = logfile.read.split(separator).map { |line| line.split(":") }.group_by(&:first) lines = groups.map do |file, times| time = "%.2f" % times.map(&:last).map(&:to_f).sum "#{file}:#{time}" end logfile.rewind logfile.write(lines.join(separator) + separator) logfile.truncate(logfile.pos) end end private def with_locked_log File.open(logfile, File::RDWR | File::CREAT) do |logfile| logfile.flock(File::LOCK_EX) yield logfile end end # ensure folder exists + clean out previous log # this will happen in multiple processes, but should be roughly at the same time # so there should be no log message lost def prepare return if @@prepared @@prepared = true FileUtils.mkdir_p(File.dirname(logfile)) File.write(logfile, '') end def log(test, time) return unless message = message(test, time) with_locked_log do |logfile| logfile.seek(0, IO::SEEK_END) logfile.puts message end end def message(test, delta) return unless method = test.public_instance_methods(true).detect { |m| m =~ /^test_/ } filename = test.instance_method(method).source_location.first.sub("#{Dir.pwd}/", "") "#{filename}:#{delta}" end def logfile ParallelTests::Test::Runner.runtime_log end end end end end if defined?(Minitest::Runnable) # Minitest 5 class << Minitest::Runnable prepend( Module.new do def run(*) ParallelTests::Test::RuntimeLogger.log_test_run(self) do super end end end ) end class << Minitest prepend( Module.new do def run(*args) result = super ParallelTests::Test::RuntimeLogger.unique_log result end end ) end end parallel_tests-5.4.0/lib/parallel_tests/version.rb000066400000000000000000000001131504331627400223340ustar00rootroot00000000000000# frozen_string_literal: true module ParallelTests VERSION = '5.4.0' end parallel_tests-5.4.0/parallel_tests.gemspec000066400000000000000000000020221504331627400211220ustar00rootroot00000000000000# frozen_string_literal: true name = "parallel_tests" require_relative "lib/#{name}/version" Gem::Specification.new name, ParallelTests::VERSION do |s| s.summary = "Run Test::Unit / RSpec / Cucumber / Spinach in parallel" s.authors = ["Michael Grosser"] s.email = "michael@grosser.it" s.homepage = "https://github.com/grosser/#{name}" s.metadata = { "bug_tracker_uri" => "https://github.com/grosser/#{name}/issues", "changelog_uri" => "https://github.com/grosser/#{name}/blob/v#{s.version}/CHANGELOG.md", "documentation_uri" => "https://github.com/grosser/#{name}/blob/v#{s.version}/Readme.md", "source_code_uri" => "https://github.com/grosser/#{name}/tree/v#{s.version}", "wiki_uri" => "https://github.com/grosser/#{name}/wiki", "rubygems_mfa_required" => "true" } s.files = Dir["{lib,bin}/**/*"] + ["Readme.md"] s.license = "MIT" s.executables = ["parallel_spinach", "parallel_cucumber", "parallel_rspec", "parallel_test"] s.add_dependency "parallel" s.required_ruby_version = '>= 3.1.0' end parallel_tests-5.4.0/spec/000077500000000000000000000000001504331627400154755ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/000077500000000000000000000000001504331627400173465ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/000077500000000000000000000000001504331627400206315ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/.gitattributes000066400000000000000000000003661504331627400235310ustar00rootroot00000000000000# See https://git-scm.com/docs/gitattributes for more about git attribute files. # Mark the database schema as having been generated. db/schema.rb linguist-generated # Mark any vendored files as having been vendored. vendor/* linguist-vendored parallel_tests-5.4.0/spec/fixtures/rails72/.gitignore000066400000000000000000000014121504331627400226170ustar00rootroot00000000000000# See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-* # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore pidfiles, but keep the directory. /tmp/pids/* !/tmp/pids/ !/tmp/pids/.keep # Ignore uploaded files in development. /storage/* !/storage/.keep /tmp/storage/* !/tmp/storage/ !/tmp/storage/.keep /public/assets # Ignore master key for decrypting credentials and more. /config/master.key parallel_tests-5.4.0/spec/fixtures/rails72/Gemfile000066400000000000000000000010321504331627400221200ustar00rootroot00000000000000# frozen_string_literal: true source "https://rubygems.org" rails_version = '~> 7.2.0' gem 'actioncable', rails_version gem 'actionmailer', rails_version gem 'actionpack', rails_version gem 'actionview', rails_version gem 'activejob', rails_version gem 'activemodel', rails_version gem 'activerecord', rails_version gem 'activesupport', rails_version gem 'railties', rails_version gem 'sqlite3', '~> 1.7.3' # last before 2.0 which has weird install errors gem 'tzinfo-data' gem 'parallel_tests', path: "../../../", group: :development parallel_tests-5.4.0/spec/fixtures/rails72/Gemfile.lock000066400000000000000000000100471504331627400230550ustar00rootroot00000000000000PATH remote: ../../.. specs: parallel_tests (5.4.0) parallel GEM remote: https://rubygems.org/ specs: actioncable (7.2.2.1) actionpack (= 7.2.2.1) activesupport (= 7.2.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) actionmailer (7.2.2.1) actionpack (= 7.2.2.1) actionview (= 7.2.2.1) activejob (= 7.2.2.1) activesupport (= 7.2.2.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) actionpack (7.2.2.1) actionview (= 7.2.2.1) activesupport (= 7.2.2.1) nokogiri (>= 1.8.5) racc rack (>= 2.2.4, < 3.2) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) actionview (7.2.2.1) activesupport (= 7.2.2.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) activejob (7.2.2.1) activesupport (= 7.2.2.1) globalid (>= 0.3.6) activemodel (7.2.2.1) activesupport (= 7.2.2.1) activerecord (7.2.2.1) activemodel (= 7.2.2.1) activesupport (= 7.2.2.1) timeout (>= 0.4.0) activesupport (7.2.2.1) base64 benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) base64 (0.2.0) benchmark (0.4.0) bigdecimal (3.1.9) builder (3.3.0) concurrent-ruby (1.3.5) connection_pool (2.5.0) crass (1.0.6) date (3.4.1) drb (2.2.1) erubi (1.13.1) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.7) concurrent-ruby (~> 1.0) io-console (0.8.0) irb (1.15.1) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) logger (1.6.6) loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) mini_mime (>= 0.1.1) net-imap net-pop net-smtp mini_mime (1.1.5) mini_portile2 (2.8.8) minitest (5.25.4) net-imap (0.5.6) date net-protocol net-pop (0.1.2) net-protocol net-protocol (0.2.2) timeout net-smtp (0.5.1) net-protocol nio4r (2.7.4) nokogiri (1.18.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) nokogiri (1.18.3-arm64-darwin) racc (~> 1.4) parallel (1.26.3) pp (0.6.2) prettyprint prettyprint (0.2.0) psych (5.2.3) date stringio racc (1.8.1) rack (3.1.10) rack-session (2.1.0) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) rackup (2.2.1) rack (>= 3) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) railties (7.2.2.1) actionpack (= 7.2.2.1) activesupport (= 7.2.2.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rake (13.2.1) rdoc (6.12.0) psych (>= 4.0.0) reline (0.6.0) io-console (~> 0.5) securerandom (0.4.1) sqlite3 (1.7.3) mini_portile2 (~> 2.8.0) stringio (3.1.5) thor (1.3.2) timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) tzinfo-data (1.2025.1) tzinfo (>= 1.0.0) useragent (0.16.11) websocket-driver (0.7.7) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) zeitwerk (2.7.2) PLATFORMS arm64-darwin-23 ruby DEPENDENCIES actioncable (~> 7.2.0) actionmailer (~> 7.2.0) actionpack (~> 7.2.0) actionview (~> 7.2.0) activejob (~> 7.2.0) activemodel (~> 7.2.0) activerecord (~> 7.2.0) activesupport (~> 7.2.0) parallel_tests! railties (~> 7.2.0) sqlite3 (~> 1.7.3) tzinfo-data BUNDLED WITH 2.6.2 parallel_tests-5.4.0/spec/fixtures/rails72/README.md000066400000000000000000000005661504331627400221170ustar00rootroot00000000000000# README This README would normally document whatever steps are necessary to get the application up and running. Things you may want to cover: * Ruby version * System dependencies * Configuration * Database creation * Database initialization * How to run the test suite * Services (job queues, cache servers, search engines, etc.) * Deployment instructions * ... parallel_tests-5.4.0/spec/fixtures/rails72/Rakefile000066400000000000000000000003431504331627400222760ustar00rootroot00000000000000# Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative "config/application" Rails.application.load_tasks parallel_tests-5.4.0/spec/fixtures/rails72/app/000077500000000000000000000000001504331627400214115ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/assets/000077500000000000000000000000001504331627400227135ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/assets/config/000077500000000000000000000000001504331627400241605ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/assets/config/manifest.js000066400000000000000000000002171504331627400263240ustar00rootroot00000000000000//= link_tree ../images //= link_directory ../stylesheets .css //= link_tree ../../javascript .js //= link_tree ../../../vendor/javascript .js parallel_tests-5.4.0/spec/fixtures/rails72/app/assets/images/000077500000000000000000000000001504331627400241605ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/assets/images/.keep000066400000000000000000000000001504331627400250730ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/assets/stylesheets/000077500000000000000000000000001504331627400252675ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/assets/stylesheets/application.css000066400000000000000000000013211504331627400303010ustar00rootroot00000000000000/* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ parallel_tests-5.4.0/spec/fixtures/rails72/app/channels/000077500000000000000000000000001504331627400232045ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/channels/application_cable/000077500000000000000000000000001504331627400266355ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/channels/application_cable/channel.rb000066400000000000000000000001171504331627400305710ustar00rootroot00000000000000module ApplicationCable class Channel < ActionCable::Channel::Base end end parallel_tests-5.4.0/spec/fixtures/rails72/app/channels/application_cable/connection.rb000066400000000000000000000001251504331627400313170ustar00rootroot00000000000000module ApplicationCable class Connection < ActionCable::Connection::Base end end parallel_tests-5.4.0/spec/fixtures/rails72/app/controllers/000077500000000000000000000000001504331627400237575ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/controllers/application_controller.rb000066400000000000000000000000711504331627400310500ustar00rootroot00000000000000class ApplicationController < ActionController::Base end parallel_tests-5.4.0/spec/fixtures/rails72/app/controllers/concerns/000077500000000000000000000000001504331627400255715ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/controllers/concerns/.keep000066400000000000000000000000001504331627400265040ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/helpers/000077500000000000000000000000001504331627400230535ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/helpers/application_helper.rb000066400000000000000000000000351504331627400272400ustar00rootroot00000000000000module ApplicationHelper end parallel_tests-5.4.0/spec/fixtures/rails72/app/javascript/000077500000000000000000000000001504331627400235575ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/javascript/application.js000066400000000000000000000002351504331627400264200ustar00rootroot00000000000000// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "@hotwired/turbo-rails" import "controllers" parallel_tests-5.4.0/spec/fixtures/rails72/app/javascript/controllers/000077500000000000000000000000001504331627400261255ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/javascript/controllers/application.js000066400000000000000000000003321504331627400307640ustar00rootroot00000000000000import { Application } from "@hotwired/stimulus" const application = Application.start() // Configure Stimulus development experience application.debug = false window.Stimulus = application export { application } parallel_tests-5.4.0/spec/fixtures/rails72/app/javascript/controllers/hello_controller.js000066400000000000000000000002351504331627400320310ustar00rootroot00000000000000import { Controller } from "@hotwired/stimulus" export default class extends Controller { connect() { this.element.textContent = "Hello World!" } } parallel_tests-5.4.0/spec/fixtures/rails72/app/javascript/controllers/index.js000066400000000000000000000011101504331627400275630ustar00rootroot00000000000000// Import and register all your controllers from the importmap under controllers/* import { application } from "controllers/application" // Eager load all controllers defined in the import map under controllers/**/*_controller import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application) // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" // lazyLoadControllersFrom("controllers", application) parallel_tests-5.4.0/spec/fixtures/rails72/app/jobs/000077500000000000000000000000001504331627400223465ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/jobs/application_job.rb000066400000000000000000000004151504331627400260300ustar00rootroot00000000000000class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end parallel_tests-5.4.0/spec/fixtures/rails72/app/mailers/000077500000000000000000000000001504331627400230455ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/mailers/application_mailer.rb000066400000000000000000000001461504331627400272270ustar00rootroot00000000000000class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" end parallel_tests-5.4.0/spec/fixtures/rails72/app/models/000077500000000000000000000000001504331627400226745ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/models/application_record.rb000066400000000000000000000001121504331627400270540ustar00rootroot00000000000000class ApplicationRecord < ActiveRecord::Base primary_abstract_class end parallel_tests-5.4.0/spec/fixtures/rails72/app/models/concerns/000077500000000000000000000000001504331627400245065ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/models/concerns/.keep000066400000000000000000000000001504331627400254210ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/models/user.rb000066400000000000000000000000431504331627400241740ustar00rootroot00000000000000class User < ApplicationRecord end parallel_tests-5.4.0/spec/fixtures/rails72/app/views/000077500000000000000000000000001504331627400225465ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/views/layouts/000077500000000000000000000000001504331627400242465ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/app/views/layouts/application.html.erb000066400000000000000000000005351504331627400302110ustar00rootroot00000000000000 Rails70 <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> <%= yield %> parallel_tests-5.4.0/spec/fixtures/rails72/app/views/layouts/mailer.html.erb000066400000000000000000000003451504331627400271560ustar00rootroot00000000000000 <%= yield %> parallel_tests-5.4.0/spec/fixtures/rails72/app/views/layouts/mailer.text.erb000066400000000000000000000000151504331627400271700ustar00rootroot00000000000000<%= yield %> parallel_tests-5.4.0/spec/fixtures/rails72/bin/000077500000000000000000000000001504331627400214015ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/bin/bundle000077500000000000000000000056171504331627400226110ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'bundle' is installed as part of a gem, and # this file is here to facilitate running it. # require "rubygems" m = Module.new do module_function def invoked_as_script? File.expand_path($0) == File.expand_path(__FILE__) end def env_var_version ENV["BUNDLER_VERSION"] end def cli_arg_version return unless invoked_as_script? # don't want to hijack other binstubs return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` bundler_version = nil update_index = nil ARGV.each_with_index do |a, i| if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN bundler_version = a end next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ bundler_version = Regexp.last_match(1) update_index = i end bundler_version end def gemfile gemfile = ENV["BUNDLE_GEMFILE"] return gemfile if gemfile && !gemfile.empty? File.expand_path('../Gemfile', __dir__) end def lockfile lockfile = case File.basename(gemfile) when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) else "#{gemfile}.lock" end File.expand_path(lockfile) end def lockfile_version return unless File.file?(lockfile) lockfile_contents = File.read(lockfile) return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ Regexp.last_match(1) end def bundler_version @bundler_version ||= env_var_version || cli_arg_version || lockfile_version end def bundler_requirement return "#{Gem::Requirement.default}.a" unless bundler_version bundler_gem_version = Gem::Version.new(bundler_version) requirement = bundler_gem_version.approximate_recommendation return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") requirement += ".a" if bundler_gem_version.prerelease? requirement end def load_bundler! ENV["BUNDLE_GEMFILE"] ||= gemfile activate_bundler end def activate_bundler gem_error = activation_error_handling do gem "bundler", bundler_requirement end return if gem_error.nil? require_error = activation_error_handling do require "bundler/version" end return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" exit 42 end def activation_error_handling yield nil rescue StandardError, LoadError => e e end end m.load_bundler! if m.invoked_as_script? load Gem.bin_path("bundler", "bundle") end parallel_tests-5.4.0/spec/fixtures/rails72/bin/importmap000077500000000000000000000001331504331627400233340ustar00rootroot00000000000000#!/usr/bin/env ruby require_relative "../config/application" require "importmap/commands" parallel_tests-5.4.0/spec/fixtures/rails72/bin/rails000077500000000000000000000002151504331627400224370ustar00rootroot00000000000000#!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" parallel_tests-5.4.0/spec/fixtures/rails72/bin/rake000077500000000000000000000001321504331627400222450ustar00rootroot00000000000000#!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run parallel_tests-5.4.0/spec/fixtures/rails72/bin/setup000077500000000000000000000017621504331627400224750ustar00rootroot00000000000000#!/usr/bin/env ruby require "fileutils" # path to your application root. APP_ROOT = File.expand_path("..", __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. puts "== Installing dependencies ==" system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" system! "bin/rails restart" end parallel_tests-5.4.0/spec/fixtures/rails72/config.ru000066400000000000000000000002401504331627400224420ustar00rootroot00000000000000# This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application Rails.application.load_server parallel_tests-5.4.0/spec/fixtures/rails72/config/000077500000000000000000000000001504331627400220765ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/config/application.rb000066400000000000000000000021331504331627400247250ustar00rootroot00000000000000require_relative "boot" require "rails" # Pick the frameworks you want: require "active_model/railtie" require "active_job/railtie" require "active_record/railtie" # require "active_storage/engine" require "action_controller/railtie" require "action_mailer/railtie" # require "action_mailbox/engine" # require "action_text/engine" require "action_view/railtie" require "action_cable/engine" # require "sprockets/railtie" require "rails/test_unit/railtie" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Rails70 class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") end end parallel_tests-5.4.0/spec/fixtures/rails72/config/boot.rb000066400000000000000000000002001504331627400233560ustar00rootroot00000000000000ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. parallel_tests-5.4.0/spec/fixtures/rails72/config/cable.yml000066400000000000000000000002741504331627400236720ustar00rootroot00000000000000development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: rails70_production parallel_tests-5.4.0/spec/fixtures/rails72/config/credentials.yml.enc000066400000000000000000000007201504331627400256610ustar00rootroot00000000000000Hf5WtG5Ycg/SndPHUS0iSmW45dzUSvrchVGgjvSrvDCEcaa8D3tsTh/VFmTP8Gv75jVJplktDXAQxQC9D3BHrMAn8f0vjgG0iqpdnqWSmextXZPYePg6UGCfCWQ+VTFH/SY+epnO7Aa9VER0Ln5CVFRAURrxUNxqbXYIPQElgAdmGnI2LGJtoH/yyOq8cklCCt4bT5jzjiTMhEaA/o6kqLA2gN94oYoHGqsQZTYvo4Jv2S+gWiPOlbGEQbgOtEtnsy4akPpzjOxcGWGqP9cUDZtmO0fIxPI4OjXmlKmdzGa+UWvmWiku8hEu/rdhHU0kn8jqGh5Yoz7jgHY/XQyrRWSqNln+2rS3KwRcJ2dOgx8Nh7vV+zIgb0bQX0ZRGH5AdFdOoVdZaPFjSZIi0YWleXeuGvtfiTZuVpGc--EizbcaQddFxMXHC2--oZbQNGBHn4CVXLXLazOMdw==parallel_tests-5.4.0/spec/fixtures/rails72/config/database.yml000066400000000000000000000012111504331627400243600ustar00rootroot00000000000000# SQLite. Versions 3.8.0 and up are supported. # gem install sqlite3 # # Ensure the SQLite 3 gem is defined in your Gemfile # gem "sqlite3" # default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default database: db/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: db/test<%= ENV['TEST_ENV_NUMBER'] %>.sqlite3 production: <<: *default database: db/production.sqlite3 parallel_tests-5.4.0/spec/fixtures/rails72/config/environment.rb000066400000000000000000000002001504331627400247570ustar00rootroot00000000000000# Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! parallel_tests-5.4.0/spec/fixtures/rails72/config/environments/000077500000000000000000000000001504331627400246255ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/config/environments/development.rb000066400000000000000000000045571504331627400275070ustar00rootroot00000000000000require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable server timing config.server_timing = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). # config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Suppress logger output for asset requests. # config.assets.quiet = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true end parallel_tests-5.4.0/spec/fixtures/rails72/config/environments/production.rb000066400000000000000000000074671504331627400273560ustar00rootroot00000000000000require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. # config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). # config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). config.log_level = :info # Prepend all log lines with the following tags. config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "rails70_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Don't log any deprecations. config.active_support.report_deprecations = false # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. # require "syslog/logger" # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false end parallel_tests-5.4.0/spec/fixtures/rails72/config/environments/test.rb000066400000000000000000000045251504331627400261370ustar00rootroot00000000000000require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Turn false under Spring and add config.action_view.cache_template_loading = true. config.cache_classes = true # Eager loading loads your whole application. When running a single test locally, # this probably isn't necessary. It's a good idea to do in a continuous integration # system, or in some way before deploying your code. config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. # config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true end parallel_tests-5.4.0/spec/fixtures/rails72/config/importmap.rb000066400000000000000000000005311504331627400244320ustar00rootroot00000000000000# Pin npm packages by running ./bin/importmap pin "application", preload: true pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true pin_all_from "app/javascript/controllers", under: "controllers" parallel_tests-5.4.0/spec/fixtures/rails72/config/initializers/000077500000000000000000000000001504331627400246045ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/config/initializers/assets.rb000066400000000000000000000007701504331627400264370ustar00rootroot00000000000000# Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. # Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) parallel_tests-5.4.0/spec/fixtures/rails72/config/initializers/content_security_policy.rb000066400000000000000000000021641504331627400321140ustar00rootroot00000000000000# Be sure to restart your server when you modify this file. # Define an application-wide content security policy # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap and inline scripts # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src) # # # Report CSP violations to a specified URI. See: # # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # # config.content_security_policy_report_only = true # end parallel_tests-5.4.0/spec/fixtures/rails72/config/initializers/filter_parameter_logging.rb000066400000000000000000000006141504331627400321650ustar00rootroot00000000000000# Be sure to restart your server when you modify this file. # Configure parameters to be filtered from the log file. Use this to limit dissemination of # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # notations and behaviors. Rails.application.config.filter_parameters += [ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] parallel_tests-5.4.0/spec/fixtures/rails72/config/initializers/inflections.rb000066400000000000000000000012111504331627400274410ustar00rootroot00000000000000# Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end parallel_tests-5.4.0/spec/fixtures/rails72/config/initializers/permissions_policy.rb000066400000000000000000000006001504331627400310570ustar00rootroot00000000000000# Define an application-wide HTTP permissions policy. For further # information see https://developers.google.com/web/updates/2018/06/feature-policy # # Rails.application.config.permissions_policy do |f| # f.camera :none # f.gyroscope :none # f.microphone :none # f.usb :none # f.fullscreen :self # f.payment :self, "https://secure.example.com" # end parallel_tests-5.4.0/spec/fixtures/rails72/config/locales/000077500000000000000000000000001504331627400235205ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/config/locales/en.yml000066400000000000000000000015211504331627400246440ustar00rootroot00000000000000# Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t "hello" # # In views, this is aliased to just `t`: # # <%= t("hello") %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # "true": "foo" # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" parallel_tests-5.4.0/spec/fixtures/rails72/config/puma.rb000066400000000000000000000033721504331627400233720ustar00rootroot00000000000000# Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # max_threads_count = ENV.fetch("RAILS_MAX_THREADS", 5) min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } threads min_threads_count, max_threads_count # Specifies the `worker_timeout` threshold that Puma will use to wait before # terminating a worker in development environments. # worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch("PORT", 3000) # Specifies the `environment` that Puma will run in. # environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the `pidfile` that Puma will use. pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # # workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. # # preload_app! # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart parallel_tests-5.4.0/spec/fixtures/rails72/config/routes.rb000066400000000000000000000003041504331627400237410ustar00rootroot00000000000000Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Defines the root path route ("/") # root "articles#index" end parallel_tests-5.4.0/spec/fixtures/rails72/config/storage.yml000066400000000000000000000022001504331627400242570ustar00rootroot00000000000000test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket-<%= Rails.env %> # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name-<%= Rails.env %> # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] parallel_tests-5.4.0/spec/fixtures/rails72/db/000077500000000000000000000000001504331627400212165ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/db/migrate/000077500000000000000000000000001504331627400226465ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/db/migrate/20220322153446_create_users.rb000066400000000000000000000002301504331627400273150ustar00rootroot00000000000000class CreateUsers < ActiveRecord::Migration[7.0] def change create_table :users do |t| t.string :name t.timestamps end end end parallel_tests-5.4.0/spec/fixtures/rails72/db/schema.rb000066400000000000000000000016451504331627400230110ustar00rootroot00000000000000# This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[7.2].define(version: 2022_03_22_153446) do create_table "users", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end parallel_tests-5.4.0/spec/fixtures/rails72/db/seeds.rb000066400000000000000000000005661504331627400226550ustar00rootroot00000000000000# This file should contain all the record creation needed to seed the database with its default values. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). # # Examples: # # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) # Character.create(name: "Luke", movie: movies.first) parallel_tests-5.4.0/spec/fixtures/rails72/lib/000077500000000000000000000000001504331627400213775ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/lib/assets/000077500000000000000000000000001504331627400227015ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/lib/assets/.keep000066400000000000000000000000001504331627400236140ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/lib/tasks/000077500000000000000000000000001504331627400225245ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/lib/tasks/.keep000066400000000000000000000000001504331627400234370ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/public/000077500000000000000000000000001504331627400221075ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/public/404.html000066400000000000000000000032721504331627400233100ustar00rootroot00000000000000 The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

You may have mistyped the address or the page may have moved.

If you are the application owner check the logs for more information.

parallel_tests-5.4.0/spec/fixtures/rails72/public/422.html000066400000000000000000000032511504331627400233050ustar00rootroot00000000000000 The change you wanted was rejected (422)

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

If you are the application owner check the logs for more information.

parallel_tests-5.4.0/spec/fixtures/rails72/public/500.html000066400000000000000000000031431504331627400233020ustar00rootroot00000000000000 We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

If you are the application owner check the logs for more information.

parallel_tests-5.4.0/spec/fixtures/rails72/public/apple-touch-icon-precomposed.png000066400000000000000000000000001504331627400302700ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/public/apple-touch-icon.png000066400000000000000000000000001504331627400257520ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/public/favicon.ico000066400000000000000000000000001504331627400242160ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/public/robots.txt000066400000000000000000000001431504331627400241560ustar00rootroot00000000000000# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file parallel_tests-5.4.0/spec/fixtures/rails72/storage/000077500000000000000000000000001504331627400222755ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/storage/.keep000066400000000000000000000000001504331627400232100ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/000077500000000000000000000000001504331627400216105ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/application_system_test_case.rb000066400000000000000000000002351504331627400300760ustar00rootroot00000000000000require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] end parallel_tests-5.4.0/spec/fixtures/rails72/test/channels/000077500000000000000000000000001504331627400234035ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/channels/application_cable/000077500000000000000000000000001504331627400270345ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/channels/application_cable/connection_test.rb000066400000000000000000000003701504331627400325570ustar00rootroot00000000000000require "test_helper" class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase # test "connects with cookies" do # cookies.signed[:user_id] = 42 # # connect # # assert_equal connection.user_id, "42" # end end parallel_tests-5.4.0/spec/fixtures/rails72/test/controllers/000077500000000000000000000000001504331627400241565ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/controllers/.keep000066400000000000000000000000001504331627400250710ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/fixtures/000077500000000000000000000000001504331627400234615ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/fixtures/files/000077500000000000000000000000001504331627400245635ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/fixtures/files/.keep000066400000000000000000000000001504331627400254760ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/fixtures/users.yml000066400000000000000000000005421504331627400253460ustar00rootroot00000000000000# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html # This model initially had no columns defined. If you add columns to the # model remove the "{}" from the fixture names and add the columns immediately # below each fixture, per the syntax in the comments below # one: {} # column: value # two: {} # column: value parallel_tests-5.4.0/spec/fixtures/rails72/test/helpers/000077500000000000000000000000001504331627400232525ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/helpers/.keep000066400000000000000000000000001504331627400241650ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/integration/000077500000000000000000000000001504331627400241335ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/integration/.keep000066400000000000000000000000001504331627400250460ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/mailers/000077500000000000000000000000001504331627400232445ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/mailers/.keep000066400000000000000000000000001504331627400241570ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/models/000077500000000000000000000000001504331627400230735ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/models/.keep000066400000000000000000000000001504331627400240060ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/models/user_test.rb000066400000000000000000000001661504331627400254400ustar00rootroot00000000000000require "test_helper" class UserTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end parallel_tests-5.4.0/spec/fixtures/rails72/test/system/000077500000000000000000000000001504331627400231345ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/system/.keep000066400000000000000000000000001504331627400240470ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/test/test_helper.rb000066400000000000000000000006021504331627400244510ustar00rootroot00000000000000ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" class ActiveSupport::TestCase # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... end parallel_tests-5.4.0/spec/fixtures/rails72/vendor/000077500000000000000000000000001504331627400221265ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/vendor/.keep000066400000000000000000000000001504331627400230410ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/vendor/javascript/000077500000000000000000000000001504331627400242745ustar00rootroot00000000000000parallel_tests-5.4.0/spec/fixtures/rails72/vendor/javascript/.keep000066400000000000000000000000001504331627400252070ustar00rootroot00000000000000parallel_tests-5.4.0/spec/integration_spec.rb000066400000000000000000000756631504331627400214000ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe 'CLI' do before do FileUtils.remove_dir(folder, true) end after do FileUtils.remove_dir(folder, true) end def folder "/tmp/parallel_tests_tests" end def write(file, content) path = "#{folder}/#{file}" FileUtils.mkpath File.dirname(path) File.write(path, content) path end def read(file) File.read "#{folder}/#{file}" end def bin_folder File.expand_path('../bin', __dir__) end def executable(options = {}) ["ruby", "#{bin_folder}/parallel_#{options[:type] || 'test'}"] end def run_tests(test_folder, options = {}) FileUtils.mkpath folder command = [*executable(options), *test_folder] command += ["-n", (options[:processes] || 2).to_s] unless options[:processes] == false command += options[:add] if options[:add] result = '' Dir.chdir(folder) do env = options[:export] || {} IO.popen(env, command, err: [:child, :out]) do |io| yield(io) if block_given? result = io.read end end raise "FAILED #{command}\n#{result}" if $?.success? == !!options[:fail] result end def self.it_runs_the_default_folder_if_it_exists(type, test_folder) it "runs the default folder if it exists" do full_path_to_test_folder = File.join(folder, test_folder) FileUtils.mkpath full_path_to_test_folder results = run_tests(nil, fail: false, type: type) expect(results).to_not include("Pass files or folders to run") FileUtils.remove_dir(full_path_to_test_folder, true) results = run_tests(nil, fail: true, type: type) expect(results).to include("Pass files or folders to run") end end let(:printed_commands) { /specs? per process\nTEST_ENV_NUMBER=(\d+)? PARALLEL_TEST_GROUPS=\d+ bundle exec rspec/ } let(:printed_rerun) { /run the group again:\n\nTEST_ENV_NUMBER=(\d+)? PARALLEL_TEST_GROUPS=\d+ bundle exec rspec/ } context "running tests sequentially" do it "exits with 0 when each run is successful" do run_tests "spec", type: 'rspec', fail: 0 end it "exits with 1 when a test run fails" do write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){ expect(true).to be false }}' run_tests "spec", type: 'rspec', fail: 1 end it "exits with 1 even when the test run exits with a different status" do write 'spec/xxx2_spec.rb', <<~SPEC RSpec.configure { |c| c.failure_exit_code = 99 } describe("it"){it("should"){ expect(true).to be false }} SPEC run_tests "spec", type: 'rspec', fail: 1 end it "exits with the highest exit status" do write 'spec/xxx2_spec.rb', <<~SPEC RSpec.configure { |c| c.failure_exit_code = 99 } describe("it"){it("should"){ expect(true).to be false }} SPEC run_tests "spec", type: 'rspec', add: ["--highest-exit-status"], fail: 99 end end it "runs tests in parallel" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}' write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}' # set processes to false so we verify empty groups are discarded by default result = run_tests "spec", type: 'rspec', processes: 4 # test ran and gave their puts expect(result).to include('TEST1') expect(result).to include('TEST2') # all results present expect(result).to include_exactly_times('1 example, 0 failure', 2) # 2 results expect(result).to include_exactly_times('2 examples, 0 failures', 1) # 1 summary expect(result).to include_exactly_times(/Finished in \d+(\.\d+)? seconds/, 2) expect(result).to include_exactly_times(/Took \d+ seconds/, 1) # parallel summary # verify empty groups are discarded. if retained then it'd say 4 processes for 2 specs expect(result).to include '2 processes for 2 specs, ~ 1 spec per process' end context "running test in parallel" do it "exits with 0 when each run is successful" do run_tests "spec", type: 'rspec', processes: 4, fail: 0 end it "exits with 1 when a test run fails" do write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){ expect(true).to be false }}' run_tests "spec", type: 'rspec', processes: 4, fail: 1 end it "exits with 1 even when the test run exits with a different status" do write 'spec/xxx2_spec.rb', <<~SPEC RSpec.configure { |c| c.failure_exit_code = 99 } describe("it"){it("should"){ expect(true).to be false }} SPEC run_tests "spec", type: 'rspec', processes: 4, fail: 1 end it "exits with the highest exit status" do write 'spec/xxx2_spec.rb', <<~SPEC RSpec.configure { |c| c.failure_exit_code = 99 } describe("it"){it("should"){ expect(true).to be false }} SPEC run_tests "spec", type: 'rspec', processes: 4, add: ["--highest-exit-status"], fail: 99 end end # Uses `Process.kill` under the hood, which on Windows doesn't work as expected. It kills all process group instead of just one process. describe "--fail-fast", unless: Gem.win_platform? do def run_tests(test_option: nil) # group-by + order for stable execution ... doc and verbose to ease debugging test_options = "--format doc --order defined" test_options = "#{test_options} #{test_option}" if test_option super( "spec", fail: true, type: 'rspec', processes: 2, add: ["--group-by", "found", "--verbose", "--fail-fast", "--test-options", test_options] ) end before do write 'spec/xxx1_spec.rb', 'describe("T1"){it("E1"){puts "YE" + "S"; sleep 0.5; expect(1).to eq(2)}}' # group 1 executed write 'spec/xxx2_spec.rb', 'describe("T2"){it("E2"){sleep 1; puts "OK"}}' # group 2 executed write 'spec/xxx3_spec.rb', 'describe("T3"){it("E3"){puts "NO3"}}' # group 1 skipped write 'spec/xxx4_spec.rb', 'describe("T4"){it("E4"){puts "NO4"}}' # group 2 skipped write 'spec/xxx5_spec.rb', 'describe("T5"){it("E5"){puts "NO5"}}' # group 1 skipped write 'spec/xxx6_spec.rb', 'describe("T6"){it("E6"){puts "NO6"}}' # group 2 skipped end it "can fail fast on a single test" do result = run_tests(test_option: "--fail-fast") expect(result).to include_exactly_times("YES", 1) expect(result).to include_exactly_times("OK", 1) # is allowed to finish but no new test is started after expect(result).to_not include("NO") expect(result).to include_exactly_times('1 example, 1 failure', 1) # rspec group 1 expect(result).to include_exactly_times('1 example, 0 failure', 1) # rspec group 2 expect(result).to include_exactly_times('2 examples, 1 failure', 1) # parallel_rspec summary expect(result).to include '2 processes for 6 specs, ~ 3 specs per process' end it "can fail fast on a single group" do result = run_tests expect(result).to include_exactly_times("YES", 1) expect(result).to include_exactly_times("OK", 1) # is allowed to finish but no new test is started after expect(result).to include_exactly_times("NO", 2) expect(result).to include_exactly_times('3 examples, 1 failure', 1) # rspec group 1 expect(result).to include_exactly_times('1 example, 0 failure', 1) # rspec group 2 expect(result).to include_exactly_times('4 examples, 1 failure', 1) # parallel_rspec summary expect(result).to include '2 processes for 6 specs, ~ 3 specs per process' end end it "runs tests which outputs accented characters" do write "spec/xxx_spec.rb", "#encoding: utf-8\ndescribe('it'){it('should'){puts 'Byłem tu'}}" result = run_tests "spec", type: 'rspec' # test ran and gave their puts expect(result).to include('Byłem tu') end it "respects default encoding when reading child stdout" do write 'test/xxx_test.rb', <<-EOF require 'test/unit' class XTest < Test::Unit::TestCase def test_unicode raise '¯\\_(ツ)_/¯' end end EOF # Need to tell Ruby to default to utf-8 to simulate environments where # this is set. (Otherwise, it defaults to nil and the undefined conversion # issue doesn't come up.) result = run_tests('test', fail: true, export: { 'RUBYOPT' => 'Eutf-8:utf-8' }) expect(result).to include('¯\_(ツ)_/¯') end it "does not run any tests if there are none" do write 'spec/xxx_spec.rb', '1' result = run_tests "spec", type: 'rspec' expect(result).to include('No examples found') expect(result).to include('Took') end it "shows command and rerun with --verbose-command" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}' write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){expect(1).to eq(2)}}' result = run_tests ["spec", "--verbose-command"], type: 'rspec', fail: true expect(result).to match printed_commands expect(result).to match printed_rerun expect(result).to include "bundle exec rspec spec/xxx_spec.rb" expect(result).to include "bundle exec rspec spec/xxx2_spec.rb" end it "shows only rerun with --verbose-rerun-command" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){expect(1).to eq(2)}}' result = run_tests ["spec", "--verbose-rerun-command"], type: 'rspec', fail: true expect(result).to match printed_rerun expect(result).to_not match printed_commands end it "shows only process with --verbose-process-command" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){expect(1).to eq(2)}}' result = run_tests ["spec", "--verbose-process-command"], type: 'rspec', fail: true expect(result).to_not match printed_rerun expect(result).to match printed_commands end it "fails when tests fail" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}' write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){expect(1).to eq(2)}}' result = run_tests "spec", fail: true, type: 'rspec' expect(result).to include_exactly_times('1 example, 1 failure', 1) expect(result).to include_exactly_times('1 example, 0 failure', 1) expect(result).to include_exactly_times('2 examples, 1 failure', 1) end it "can serialize stdout" do write 'spec/xxx_spec.rb', '5.times{describe("it"){it("should"){sleep 0.01; puts "TEST1"}}}' write 'spec/xxx2_spec.rb', 'sleep 0.01; 5.times{describe("it"){it("should"){sleep 0.01; puts "TEST2"}}}' result = run_tests "spec", type: 'rspec', add: ["--serialize-stdout"] expect(result).not_to match(/TEST1.*TEST2.*TEST1/m) expect(result).not_to match(/TEST2.*TEST1.*TEST2/m) end it "can show simulated output when serializing stdout" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){sleep 0.5; puts "TEST1"}}' write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){sleep 1; puts "TEST2"}}' result = run_tests "spec", type: 'rspec', add: ["--serialize-stdout"], export: { 'PARALLEL_TEST_HEARTBEAT_INTERVAL' => '0.01' } expect(result).to match(/\.{4}.*TEST1.*\.{4}.*TEST2/m) end it "can show simulated output preceded by command when serializing stdout with verbose option" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){sleep 1.5; puts "TEST1"}}' result = run_tests ["spec", "--verbose"], type: 'rspec', add: ["--serialize-stdout"], export: { 'PARALLEL_TEST_HEARTBEAT_INTERVAL' => '0.02' } expect(result).to match(/\.{5}.*\nbundle exec rspec spec\/xxx_spec\.rb\n.*^TEST1/m) end it "can serialize stdout and stderr" do write 'spec/xxx_spec.rb', '5.times{describe("it"){it("should"){sleep 0.01; $stderr.puts "errTEST1"; puts "TEST1"}}}' write 'spec/xxx2_spec.rb', 'sleep 0.01; 5.times{describe("it"){it("should"){sleep 0.01; $stderr.puts "errTEST2"; puts "TEST2"}}}' result = run_tests ["spec"], type: 'rspec', add: ["--serialize-stdout", "--combine-stderr"] expect(result).not_to match(/TEST1.*TEST2.*TEST1/m) expect(result).not_to match(/TEST2.*TEST1.*TEST2/m) end context "with given commands" do it "can exec given commands with ENV['TEST_ENV_NUMBER']" do result = run_tests ['-e', 'ruby -e "print ENV[:TEST_ENV_NUMBER.to_s].to_i"'], processes: 4 expect(result.gsub('"', '').chars.sort).to eq(['0', '2', '3', '4']) end it "can exec given commands with $TEST_ENV_NUMBER" do result = run_tests ['-e', 'echo foo-$TEST_ENV_NUMBER'], processes: 4 expect(result.split(/\n+/).sort).to eq(['foo-', 'foo-2', 'foo-3', 'foo-4']) end it "can exec given command non-parallel" do result = run_tests( ['-e', 'ruby -e "sleep(rand(10)/100.0); puts ENV[:TEST_ENV_NUMBER.to_s].inspect"'], processes: 4, add: ['--non-parallel'] ) expect(result.split(/\n+/)).to eq(['""', '"2"', '"3"', '"4"']) end it "can exec given command with a restricted set of groups" do result = run_tests( ['-e', 'ruby -e "print ENV[:TEST_ENV_NUMBER.to_s].to_i"'], process: 4, add: ['--only-group', '1,3'] ) expect(result.gsub('"', '').chars.sort).to eq(['0', '3']) end it "can serialize stdout" do result = run_tests( ['-e', 'ruby -e "5.times{sleep 0.01;puts ENV[:TEST_ENV_NUMBER.to_s].to_i;STDOUT.flush}"'], processes: 2, add: ['--serialize-stdout'] ) expect(result).not_to match(/0.*2.*0/m) expect(result).not_to match(/2.*0.*2/m) end it "runs command in parallel with files as arguments" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}' write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}' result = run_tests "spec", type: 'rspec', add: ["--exec-args", "echo"] expect(result).to include_exactly_times('spec/xxx_spec.rb', 1) expect(result).to include_exactly_times('spec/xxx2_spec.rb', 1) end it "runs two commands in parallel with files as arguments" do write 'spec/xxx_spec.rb', 'p ARGV; describe("it"){it("should"){puts "TEST1"}}' write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){puts "TEST2"}}' # need to `--` so sh uses them as arguments that then go into $@ result = run_tests "spec", type: 'rspec', add: ["--exec-args", "sh -c \"echo 'hello world' && rspec $@\" --"] expect(result).to include_exactly_times('hello world', 2) expect(result).to include_exactly_times('TEST1', 1) expect(result).to include_exactly_times('TEST2', 1) end it "exists with success if all sub-processes returned success" do expect(system(*executable, '-e', 'cat /dev/null', '-n', '4')).to eq(true) end it "exists with failure if any sub-processes returned failure" do expect(system(*executable, '-e', 'test -e xxxx', '-n', '4')).to eq(false) end end ['rspec', 'cucumber', 'spinach'].each do |type| it "runs through parallel_#{type}" do test_version = IO.popen([*executable, '-v'], &:read) expect($?.success?).to be(true) type_version = IO.popen(['ruby', "#{bin_folder}/parallel_#{type}", '-v'], &:read) expect($?.success?).to be(true) expect(type_version).to eq(test_version) end end it "runs with --group-by found" do # it only tests that it does not blow up, as it did before fixing... write "spec/x1_spec.rb", "puts 'TEST111'" run_tests ["spec"], type: 'rspec', add: ['--group-by', 'found'] end it "runs in parallel" do 2.times do |i| write "spec/xxx#{i}_spec.rb", 'STDOUT.sync = true; describe("it") { it("should"){ puts "START"; sleep 1; puts "END" } }' end result = run_tests(["spec"], processes: 2, type: 'rspec') expect(result.scan(/START|END/)).to eq(["START", "START", "END", "END"]) end it "disables spring so correct database is used" do write "spec/xxx_spec.rb", 'puts "SPRING: #{ENV["DISABLE_SPRING"]}"' result = run_tests(["spec"], processes: 2, type: 'rspec') expect(result).to include "SPRING: 1" end it "can enable spring" do write "spec/xxx_spec.rb", 'puts "SPRING: #{ENV["DISABLE_SPRING"]}"' result = run_tests(["spec"], processes: 2, type: 'rspec', export: { "DISABLE_SPRING" => "0" }) expect(result).to include "SPRING: 0" end it "runs with files that have spaces" do write "test/xxx _test.rb", 'puts "TEST_SUCCESS"' result = run_tests(["test"], processes: 2, type: 'test') expect(result).to include "TEST_SUCCESS" end it "uses relative paths for easy copying" do write "test/xxx_test.rb", 'puts "Test output: YES"' result = run_tests(["test"], processes: 2, type: 'test', add: ['--verbose']) expect(result).to include "Test output: YES" expect(result).to include "\\[test/xxx_test.rb\\]" expect(result).not_to include Dir.pwd end it "can run with given files" do write "spec/x1_spec.rb", "puts 'TEST111'" write "spec/x2_spec.rb", "puts 'TEST222'" write "spec/x3_spec.rb", "puts 'TEST333'" result = run_tests ["spec/x1_spec.rb", "spec/x3_spec.rb"], type: 'rspec' expect(result).to include('TEST111') expect(result).to include('TEST333') expect(result).not_to include('TEST222') end it "can run with test-options" do write "spec/x1_spec.rb", "111" write "spec/x2_spec.rb", "111" result = run_tests ["spec"], add: ["--test-options", "--version"], processes: 2, type: 'rspec' expect(result).to match(/\d+\.\d+\.\d+.*\d+\.\d+\.\d+/m) # prints version twice end it "runs with PARALLEL_TEST_PROCESSORS processes" do skip if RUBY_PLATFORM == "java" # execution expired issue on JRuby processes = 5 processes.times do |i| write "spec/x#{i}_spec.rb", "puts %{ENV-\#{ENV['TEST_ENV_NUMBER']}-}" end result = run_tests( ["spec"], export: { "PARALLEL_TEST_PROCESSORS" => processes.to_s }, processes: processes, type: 'rspec' ) expect(result.scan(/ENV-.?-/)).to match_array(["ENV--", "ENV-2-", "ENV-3-", "ENV-4-", "ENV-5-"]) end it "filters test by given pattern and relative paths" do write "spec/x_spec.rb", "puts 'TESTXXX'" write "spec/y_spec.rb", "puts 'TESTYYY'" write "spec/z_spec.rb", "puts 'TESTZZZ'" result = run_tests ["spec"], add: ["-p", "^spec/(x|z)"], type: "rspec" expect(result).to include('TESTXXX') expect(result).not_to include('TESTYYY') expect(result).to include('TESTZZZ') end it "excludes test by given pattern and relative paths" do write "spec/x_spec.rb", "puts 'TESTXXX'" write "spec/acceptance/y_spec.rb", "puts 'TESTYYY'" write "spec/integration/z_spec.rb", "puts 'TESTZZZ'" result = run_tests ["spec"], add: ["--exclude-pattern", "spec/(integration|acceptance)"], type: "rspec" expect(result).to include('TESTXXX') expect(result).not_to include('TESTYYY') expect(result).not_to include('TESTZZZ') end it "can wait_for_other_processes_to_finish" do skip if RUBY_PLATFORM == "java" # just too slow ... write "test/a_test.rb", "require 'parallel_tests'; sleep 0.5 ; ParallelTests.wait_for_other_processes_to_finish; puts 'OutputA'" write "test/b_test.rb", "sleep 1; puts 'OutputB'" write "test/c_test.rb", "sleep 1.5; puts 'OutputC'" write "test/d_test.rb", "sleep 2; puts 'OutputD'" actual = run_tests(["test"], processes: 4).scan(/Output[ABCD]/) actual_sorted = [*actual[0..2].sort, actual[3]] expect(actual_sorted).to match(["OutputB", "OutputC", "OutputD", "OutputA"]) end it "can run only a single group" do skip if RUBY_PLATFORM == "java" # just too slow ... write "test/long_test.rb", "puts 'this is a long test'" write "test/short_test.rb", "puts 'short test'" group_1_result = run_tests(["test"], processes: 2, add: ['--only-group', '1']) expect(group_1_result).to include("this is a long test") expect(group_1_result).not_to include("short test") group_2_result = run_tests(["test"], processes: 2, add: ['--only-group', '2']) expect(group_2_result).not_to include("this is a long test") expect(group_2_result).to include("short test") end it "shows nice --help" do result = run_tests ["--help"] expect( result[/(.*)How many processes/, 1].size ).to( eq(result[/( +)found /, 1].size), "Multiline option description must align with regular option description" ) end it "can run with uncommon file names" do skip if RUBY_PLATFORM == "java" # just too slow ... write "test/long ( stuff ) _test.rb", "puts 'hey'" expect(run_tests(["test"], processes: 2)).to include("hey") end context "RSpec" do it_runs_the_default_folder_if_it_exists "rspec", "spec" it "captures seed with random failures with --verbose" do write 'spec/xxx_spec.rb', 'describe("it"){it("should"){puts "TEST1"}}' write 'spec/xxx2_spec.rb', 'describe("it"){it("should"){1.should == 2}}' result = run_tests ["spec", "--verbose"], add: ["--test-options", "--seed 1234"], fail: true, type: 'rspec' expect(result).to include("Randomized with seed 1234") expect(result).to include("bundle exec rspec --seed 1234 spec/xxx2_spec.rb") end end context "Test::Unit" do it "runs" do write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end" result = run_tests ["test"] expect(result).to include('1 test') end it "passes test options" do write "test/x1_test.rb", "require 'test/unit'; class XTest < Test::Unit::TestCase; def test_xxx; end; end" result = run_tests(["test"], add: ['--test-options', '-v']) expect(result).to include('test_xxx') # verbose output of every test end it_runs_the_default_folder_if_it_exists "test", "test" end context "Cucumber" do before do write "features/steps/a.rb", " Given('I print TEST_ENV_NUMBER'){ puts \"YOUR TEST ENV IS \#{ENV['TEST_ENV_NUMBER']}!\" } And('I sleep a bit'){ sleep 0.5 } And('I pass'){ true } And('I fail'){ fail } " end it "runs tests which outputs accented characters" do write "features/good1.feature", "Feature: xxx\n Scenario: xxx\n Given I print accented characters" write "features/steps/a.rb", "#encoding: utf-8\nGiven('I print accented characters'){ puts \"I tu też\" }" result = run_tests ["features"], type: "cucumber", add: ['--pattern', 'good'] expect(result).to include('I tu też') end it "passes TEST_ENV_NUMBER when running with pattern (issue #86)" do write "features/good1.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER" write "features/good2.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER" write "features/b.feature", "Feature: xxx\n Scenario: xxx\n Given I FAIL" write "features/steps/a.rb", "Given('I print TEST_ENV_NUMBER'){ puts \"YOUR TEST ENV IS \#{ENV['TEST_ENV_NUMBER']}!\" }" result = run_tests ["features"], type: "cucumber", add: ['--pattern', 'good'] expect(result).to include('YOUR TEST ENV IS 2!') expect(result).to include('YOUR TEST ENV IS !') expect(result).not_to include('I FAIL') end it "writes a runtime log" do skip "TODO find out why this fails" if RUBY_PLATFORM == "java" log = "tmp/parallel_runtime_cucumber.log" write(log, "x") 2.times do |i| # needs sleep so that runtime loggers dont overwrite each other initially write "features/good#{i}.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER\n And I sleep a bit" end run_tests ["features"], type: "cucumber" expect(read(log).gsub(/\.\d+/, '').split("\n")).to match_array(["features/good0.feature:0", "features/good1.feature:0"]) end it "runs each feature once when there are more processes then features (issue #89)" do 2.times do |i| write "features/good#{i}.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER" end result = run_tests ["features"], type: "cucumber", add: ['-n', '3'] expect(result.scan(/YOUR TEST ENV IS \d?!/).sort).to eq(["YOUR TEST ENV IS !", "YOUR TEST ENV IS 2!"]) end it_runs_the_default_folder_if_it_exists "cucumber", "features" it "collates failing scenarios" do write "features/pass.feature", "Feature: xxx\n Scenario: xxx\n Given I pass" write "features/fail1.feature", "Feature: xxx\n Scenario: xxx\n Given I fail" write "features/fail2.feature", "Feature: xxx\n Scenario: xxx\n Given I fail" results = run_tests ["features"], processes: 3, type: "cucumber", fail: true failing_scenarios = if Gem.win_platform? && RUBY_VERSION < "3.3.0" ["cucumber features/fail1.feature:2 # Scenario: xxx", "cucumber features/fail2.feature:2 # Scenario: xxx"] else ["cucumber features/fail2.feature:2 # Scenario: xxx", "cucumber features/fail1.feature:2 # Scenario: xxx"] end results.gsub!(/.*WARNING.*\n/, "") expect(results).to include <<-EOF.gsub(' ', '') Failing Scenarios: #{failing_scenarios[0]} #{failing_scenarios[1]} 3 scenarios (2 failed, 1 passed) 3 steps (2 failed, 1 passed) EOF end it "groups by scenario" do write "features/long.feature", <<-EOS Feature: xxx Scenario: xxx Given I print TEST_ENV_NUMBER Scenario: xxx Given I print TEST_ENV_NUMBER Scenario Outline: xxx Given I print TEST_ENV_NUMBER Examples: | num | | one | | two | EOS result = run_tests ["features"], type: "cucumber", add: ["--group-by", "scenarios"] expect(result).to include("2 processes for 4 scenarios") end it "groups by step" do write "features/good1.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER" write "features/good2.feature", "Feature: xxx\n Scenario: xxx\n Given I print TEST_ENV_NUMBER" result = run_tests ["features"], type: "cucumber", add: ['--group-by', 'steps'] expect(result).to include("2 processes for 2 features") end it "captures seed with random failures with --verbose" do write "features/good1.feature", "Feature: xxx\n Scenario: xxx\n Given I fail" result = run_tests ["features", "--verbose"], type: "cucumber", add: ["--test-options", "--order random:1234"], fail: true expect(result).to include("Randomized with seed 1234") expect(result).to match(%r{bundle exec cucumber "?features/good1.feature"? --order random:1234}) end end context "Spinach" do before do write "features/steps/a.rb", <<-RUBY.strip_heredoc class A < Spinach::FeatureSteps Given 'I print TEST_ENV_NUMBER' do puts "YOUR TEST ENV IS \#{ENV['TEST_ENV_NUMBER']}!" end And 'I sleep a bit' do sleep 0.2 end end RUBY end it "runs tests which outputs accented characters" do write "features/good1.feature", "Feature: a\n Scenario: xxx\n Given I print accented characters" write "features/steps/a.rb", "#encoding: utf-8\nclass A < Spinach::FeatureSteps\nGiven 'I print accented characters' do\n puts \"I tu też\" \n end\nend" result = run_tests ["features"], type: "spinach", add: ['features/good1.feature'] # , :add => '--pattern good' expect(result).to include('I tu też') end it "passes TEST_ENV_NUMBER when running with pattern (issue #86)" do write "features/good1.feature", "Feature: a\n Scenario: xxx\n Given I print TEST_ENV_NUMBER" write "features/good2.feature", "Feature: a\n Scenario: xxx\n Given I print TEST_ENV_NUMBER" write "features/b.feature", "Feature: b\n Scenario: xxx\n Given I FAIL" # Expect this not to be run result = run_tests ["features"], type: "spinach", add: ['--pattern', 'good'] expect(result).to include('YOUR TEST ENV IS 2!') expect(result).to include('YOUR TEST ENV IS !') expect(result).not_to include('I FAIL') end it "writes a runtime log" do skip 'not yet implemented -- custom runtime logging' log = "tmp/parallel_runtime_spinach.log" write(log, "x") 2.times do |i| # needs sleep so that runtime loggers dont overwrite each other initially write "features/good#{i}.feature", "Feature: A\n Scenario: xxx\n Given I print TEST_ENV_NUMBER\n And I sleep a bit" end run_tests ["features"], type: "spinach" expect(read(log).gsub(/\.\d+/, '').split("\n")).to match_array(["features/good0.feature:0", "features/good1.feature:0"]) end it "runs each feature once when there are more processes then features (issue #89)" do 2.times do |i| write "features/good#{i}.feature", "Feature: A\n Scenario: xxx\n Given I print TEST_ENV_NUMBER\n" end result = run_tests ["features"], type: "spinach", add: ['-n', '3'] expect(result.scan(/YOUR TEST ENV IS \d?!/).sort).to eq(["YOUR TEST ENV IS !", "YOUR TEST ENV IS 2!"]) end it_runs_the_default_folder_if_it_exists "spinach", "features" end describe "graceful shutdown" do # Process.kill on Windows doesn't work as expected. It kills all process group instead of just one process. it "passes on int signal to child processes", unless: Gem.win_platform? do timeout = 2 write( "spec/test_spec.rb", "sleep #{timeout}; describe { specify { p 'Should not get here' }; specify { p 'Should not get here either'} }" ) pid = nil Thread.new { sleep timeout - 0.5; Process.kill("INT", pid) } result = run_tests(["spec"], processes: 2, type: 'rspec', fail: true) { |io| pid = io.pid } expect(result).to include("RSpec is shutting down") expect(result).to_not include("Should not get here") expect(result).to_not include("Should not get here either") end # Process.kill on Windows doesn't work as expected. It kills all process group instead of just one process. it "exits immediately if another int signal is received", unless: Gem.win_platform? do timeout = 2 write "spec/test_spec.rb", "describe { specify { sleep #{timeout}; p 'Should not get here'} }" pid = nil Thread.new { sleep timeout - 0.5; Process.kill("INT", pid) } Thread.new { sleep timeout - 0.3; Process.kill("INT", pid) } result = run_tests(["spec"], processes: 2, type: 'rspec', fail: false) { |io| pid = io.pid } expect(result).to_not include("Should not get here") end end describe "--test-file-limit" do let(:test_count) { 3 } before do test_count.times do |i| write "spec/x#{i}_spec.rb", "puts %(TEST-\#{ENV['TEST_ENV_NUMBER']}-\#{Process.pid})" end end it "runs in batches" do result = run_tests ["spec"], type: 'rspec', add: ['--test-file-limit', '1', '--first-is-1', '-n', '2'] expect(result.scan(/TEST-\d/).sort).to eq(["TEST-1", "TEST-1", "TEST-2"]) pids = result.scan(/TEST-\d-(\d+)/).flatten.uniq expect(pids.size).to eq test_count # did not run 2 tests in the same process end it "does not run in batches when above limit" do result = run_tests ["spec"], type: 'rspec', add: ['--test-file-limit', '2', '--first-is-1', '-n', '2'] expect(result.scan(/TEST-\d/).sort).to eq(["TEST-1", "TEST-1", "TEST-2"]) pids = result.scan(/TEST-\d-(\d+)/).flatten.uniq expect(pids.size).to eq 2 end end end parallel_tests-5.4.0/spec/parallel_tests/000077500000000000000000000000001504331627400205135ustar00rootroot00000000000000parallel_tests-5.4.0/spec/parallel_tests/cli_spec.rb000066400000000000000000000425761504331627400226370ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "parallel_tests/cli" require "parallel_tests/rspec/runner" describe ParallelTests::CLI do subject { ParallelTests::CLI.new } describe "#parse_options" do let(:defaults) { { files: ["test"] } } def call(*args) subject.send(:parse_options!, *args) end it "does not fail without file" do expect(subject).not_to receive(:abort) call(["-n3", "-t", "rspec"]) end it "cleanups file paths" do expect(call(["./test"])).to eq(defaults) end it "parses execute" do expect(call(["--exec", "echo"])).to eq(execute: ["echo"]) end it "parses execute arguments" do expect(call(["test", "--exec-args", "echo"])).to eq(defaults.merge(execute_args: ["echo"])) end it "parses excludes pattern" do expect(call(["test", "--exclude-pattern", "spec/"])).to eq(defaults.merge(exclude_pattern: /spec\//)) end it "parses regular count" do expect(call(["test", "-n3"])).to eq(defaults.merge(count: 3)) end it "parses count 0 as non-parallel" do expect(call(["test", "-n0"])).to eq(defaults.merge(non_parallel: true)) end it "parses non-parallel as non-parallel" do expect(call(["test", "--non-parallel"])).to eq(defaults.merge(non_parallel: true)) end it "finds the correct type when multiple are given" do call(["test", "--type", "test", "-t", "rspec"]) expect(subject.instance_variable_get(:@runner)).to eq(ParallelTests::RSpec::Runner) end it "parses nice as nice" do expect(call(["test", "--nice"])).to eq(defaults.merge(nice: true)) end it "parses --verbose" do expect(call(["test", "--verbose"])).to eq(defaults.merge(verbose: true)) end it "parses --verbose-command" do expect(call(['test', '--verbose-command'])).to eq( defaults.merge(verbose_process_command: true, verbose_rerun_command: true) ) end it "parses --verbose-process-command" do expect(call(['test', '--verbose-process-command'])).to eq( defaults.merge(verbose_process_command: true) ) end it "parses --verbose-rerun-command" do expect(call(['test', '--verbose-rerun-command'])).to eq( defaults.merge(verbose_rerun_command: true) ) end it "parses --failure-exit-code" do expect(call(["test", "--failure-exit-code", "42"])).to eq(defaults.merge(failure_exit_code: 42)) end it "parses --quiet" do expect(call(["test", "--quiet"])).to eq(defaults.merge(quiet: true)) end it "fails if both --verbose and --quiet are present" do expect { call(["test", "--verbose", "--quiet"]) }.to raise_error(RuntimeError) end it "parses --suffix" do expect(call(["test", "--suffix", "_(test|spec).rb$"])).to eq(defaults.merge(suffix: /_(test|spec).rb$/)) end it "parses --first-is-1" do expect(call(["test", "--first-is-1"])) .to eq(defaults.merge(first_is_1: true)) end it "parses allow-duplicates" do expect(call(["test", "--allow-duplicates"])).to eq(defaults.merge(allow_duplicates: true)) end context "parse only-group" do it "group_by should be set to filesize" do expect(call(["test", "--only-group", '1'])).to eq(defaults.merge(only_group: [1], group_by: :filesize)) end it "allows runtime" do expect(call(["test", "--only-group", '1', '--group-by', 'runtime'])).to eq(defaults.merge(only_group: [1], group_by: :runtime)) end it "raise error when group_by isn't filesize" do expect do call(["test", "--only-group", '1', '--group-by', 'steps']) end.to raise_error(RuntimeError) end it "with multiple groups" do expect(call(["test", "--only-group", '4,5'])).to eq(defaults.merge(only_group: [4, 5], group_by: :filesize)) end it "with a single group" do expect(call(["test", "--only-group", '4'])).to eq(defaults.merge(only_group: [4], group_by: :filesize)) end end context "single and isolate" do it "single_process should be an array of patterns" do expect(call(["test", "--single", '1'])).to eq(defaults.merge(single_process: [/1/])) end it "single_process should be an array of patterns" do expect(call(["test", "--single", '1', "--single", '2'])).to eq(defaults.merge(single_process: [/1/, /2/])) end it "isolate should set isolate_count defaults" do expect(call(["test", "--single", '1', "--isolate"])).to eq(defaults.merge(single_process: [/1/], isolate: true)) end it "isolate_n should set isolate_count and turn on isolate" do expect(call(["test", "-n", "3", "--single", '1', "--isolate-n", "2"])).to eq( defaults.merge(count: 3, single_process: [/1/], isolate_count: 2) ) end end context "specify groups" do it "groups can be just one string" do expect(call(["test", "--specify-groups", 'test'])).to eq(defaults.merge(specify_groups: 'test')) end it "groups can be a string separated by commas and pipes" do expect(call(["test", "--specify-groups", 'test1,test2|test3'])).to eq(defaults.merge(specify_groups: 'test1,test2|test3')) end end context "when the -- option separator is used" do it "interprets arguments as files/directories" do expect(call(['--', 'test'])).to eq(files: ['test']) expect(call(['--', './test'])).to eq(files: ['test']) expect(call(['--', 'test', 'test2'])).to eq(files: ['test', 'test2']) expect(call(['--', '--foo', 'test'])).to eq(files: ['--foo', 'test']) expect(call(['--', 'test', '--foo', 'test2'])).to eq(files: ['test', '--foo', 'test2']) end it "correctly handles arguments with spaces" do expect(call(['--', 'file name with space'])).to eq(files: ['file name with space']) end context "when the -o options has also been given" do it "merges the options together" do expect(call(['-o', "'-f'", '--', 'test', '--foo', 'test2'])).to eq(files: ['test', '--foo', 'test2'], test_options: ['-f']) end end context "when a second -- option separator is used" do it "interprets the first set as test_options" do expect(call(['--', '-r', 'foo', '--', 'test'])).to eq(files: ['test'], test_options: ['-r', 'foo']) expect(call(['--', '-r', 'foo', '--', 'test', 'test2'])).to eq(files: ['test', 'test2'], test_options: ['-r', 'foo']) expect(call(['--', '-r', 'foo', '-o', 'out.log', '--', 'test', 'test2'])).to eq(files: ['test', 'test2'], test_options: ['-r', 'foo', '-o', 'out.log']) end context "when existing test_options have previously been given" do it "appends the new options" do expect(call(['-o', '-f', '--', '-r', 'foo.rb', '--', 'test'])).to eq(files: ['test'], test_options: ['-f', '-r', 'foo.rb']) end it "correctly handles argument values with spaces" do argv = ["-o 'path with spaces1'", '--', '--out', 'path with spaces2', '--', 'foo'] expected_test_options = ['path with spaces1', '--out', 'path with spaces2'] expect(call(argv)).to eq(files: ['foo'], test_options: expected_test_options) end end end end end describe "#load_runner" do it "requires and loads default runner" do expect(subject).to receive(:require).with("parallel_tests/test/runner") expect(subject.send(:load_runner, "test")).to eq(ParallelTests::Test::Runner) end it "requires and loads rspec runner" do expect(subject).to receive(:require).with("parallel_tests/rspec/runner") expect(subject.send(:load_runner, "rspec")).to eq(ParallelTests::RSpec::Runner) end it "requires and loads runner with underscores" do expect(subject).to receive(:require).with("parallel_tests/my_test_runner/runner") expect(subject.send(:load_runner, "my_test_runner")).to eq(ParallelTests::MyTestRunner::Runner) end it "fails to load unfindable runner" do expect do expect(subject.send(:load_runner, "foo")).to eq(ParallelTests::RSpec::Runner) end.to raise_error(LoadError) end end describe ".report_failure_rerun_command" do let(:single_failed_command) { [{ exit_status: 1, command: ['foo'], seed: nil, output: 'blah' }] } it "prints nothing if there are no failures" do expect($stdout).not_to receive(:puts) subject.send( :report_failure_rerun_commmand, [ { exit_status: 0, command: 'foo', seed: nil, output: 'blah' } ], { verbose: true } ) end def self.it_prints_nothing_about_rerun_commands(options) it 'prints nothing about rerun commands' do expect do subject.send(:report_failure_rerun_commmand, single_failed_command, options) end.to_not output(/Use the following command to run the group again/).to_stdout end end describe "failure" do before do subject.instance_variable_set(:@runner, ParallelTests::Test::Runner) end context 'without options' do it_prints_nothing_about_rerun_commands({}) end context 'with verbose disabled' do it_prints_nothing_about_rerun_commands(verbose: false) end context "with verbose rerun command" do it "prints command if there is a failure" do expect do subject.send(:report_failure_rerun_commmand, single_failed_command, verbose_rerun_command: true) end.to output("\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\nTEST_ENV_NUMBER= PARALLEL_TEST_GROUPS= foo\n").to_stdout end end context 'with verbose' do it "prints a message and the command if there is a failure" do expect do subject.send(:report_failure_rerun_commmand, single_failed_command, verbose: true) end.to output("\n\nTests have failed for a parallel_test group. Use the following command to run the group again:\n\nTEST_ENV_NUMBER= PARALLEL_TEST_GROUPS= foo\n").to_stdout end it "prints multiple commands if there are multiple failures" do expect do subject.send( :report_failure_rerun_commmand, [ { exit_status: 1, command: ['foo'], seed: nil, output: 'blah' }, { exit_status: 1, command: ['bar'], seed: nil, output: 'blah' }, { exit_status: 1, command: ['baz'], seed: nil, output: 'blah' } ], { verbose: true } ) end.to output(/\sfoo\n.+?\sbar\n.+?\sbaz/).to_stdout end it "only includes failures" do expect do subject.send( :report_failure_rerun_commmand, [ { exit_status: 1, command: ['foo', '--color'], seed: nil, output: 'blah' }, { exit_status: 0, command: ['bar'], seed: nil, output: 'blah' }, { exit_status: 1, command: ['baz'], seed: nil, output: 'blah' } ], { verbose: true } ) end.to output(/\sfoo --color\n.+?\sbaz/).to_stdout end it "prints the command with the seed added by the runner" do command = ['rspec', '--color', 'spec/foo_spec.rb'] seed = 555 expect(ParallelTests::Test::Runner).to receive(:command_with_seed).with(command, seed) .and_return(['my', 'seeded', 'command', 'result', '--seed', seed]) single_failed_command[0].merge!(seed: seed, command: command) expect do subject.send(:report_failure_rerun_commmand, single_failed_command, verbose: true) end.to output(/my seeded command result --seed 555/).to_stdout end end end end describe "#final_fail_message" do before do subject.instance_variable_set(:@runner, ParallelTests::Test::Runner) end it 'returns a plain fail message if colors are nor supported' do expect(subject).to receive(:use_colors?).and_return(false) expect(subject.send(:final_fail_message)).to eq("Tests Failed") end it 'returns a colorized fail message if colors are supported' do expect(subject).to receive(:use_colors?).and_return(true) expect(subject.send(:final_fail_message)).to eq("\e[31mTests Failed\e[0m") end end describe "#run_tests_in_parallel" do context "specific groups to run" do let(:results) { { stdout: "", exit_status: 0 } } let(:common_options) do { files: ["test"], group_by: :filesize, first_is_1: false } end before do allow(subject).to receive(:puts) expect(subject).to receive(:load_runner).with("my_test_runner").and_return(ParallelTests::MyTestRunner::Runner) allow(ParallelTests::MyTestRunner::Runner).to receive(:test_file_name).and_return("test") expect(ParallelTests::MyTestRunner::Runner).to receive(:tests_in_groups).and_return( [ ['aaa', 'bbb'], ['ccc', 'ddd'], ['eee', 'fff'] ] ) expect(subject).to receive(:report_results).and_return(nil) end it "calls run_tests once when one group specified" do expect(subject).to receive(:run_tests).once.and_return(results) subject.run(['test', '-n', '3', '--only-group', '1', '-t', 'my_test_runner']) end it "calls run_tests twice when two groups are specified" do expect(subject).to receive(:run_tests).twice.and_return(results) subject.run(['test', '-n', '3', '--only-group', '1,2', '-t', 'my_test_runner']) end it "run only one group specified" do options = common_options.merge(count: 3, only_group: [2]) expect(subject).to receive(:run_tests).once.with(['ccc', 'ddd'], 0, 1, options).and_return(results) subject.run(['test', '-n', '3', '--only-group', '2', '-t', 'my_test_runner']) end it "run last group when passing a group that is not filled" do count = 3 options = common_options.merge(count: count, only_group: [count]) expect(subject).to receive(:run_tests).once.with(['eee', 'fff'], 0, 1, options).and_return(results) subject.run(['test', '-n', count.to_s, '--only-group', count.to_s, '-t', 'my_test_runner']) end it "run twice with multiple groups" do skip "fails on jruby" if RUBY_PLATFORM == "java" options = common_options.merge(count: 3, only_group: [2, 3]) expect(subject).to receive(:run_tests).once.with(['ccc', 'ddd'], 0, 1, options).and_return(results) expect(subject).to receive(:run_tests).once.with(['eee', 'fff'], 1, 1, options).and_return(results) subject.run(['test', '-n', '3', '--only-group', '2,3', '-t', 'my_test_runner']) end end context 'when --allow-duplicates' do let(:results) { { stdout: "", exit_status: 0 } } let(:processes) { 2 } let(:common_options) do { files: ['test'], allow_duplicates: true, first_is_1: false } end before do allow(subject).to receive(:puts) expect(subject).to receive(:load_runner).with("my_test_runner").and_return(ParallelTests::MyTestRunner::Runner) allow(ParallelTests::MyTestRunner::Runner).to receive(:test_file_name).and_return("test") expect(subject).to receive(:report_results).and_return(nil) end before do expect(ParallelTests::MyTestRunner::Runner).to receive(:tests_in_groups).and_return( [ ['foo'], ['foo'], ['bar'] ] ) end it "calls run_tests with --only-group" do options = common_options.merge(count: processes, only_group: [2, 3], group_by: :filesize) expect(subject).to receive(:run_tests).once.with(['foo'], 0, 1, options).and_return(results) expect(subject).to receive(:run_tests).once.with(['bar'], 1, 1, options).and_return(results) subject.run(['test', '-n', processes.to_s, '--allow-duplicates', '--only-group', '2,3', '-t', 'my_test_runner']) end it "calls run_tests with --first-is-1" do options = common_options.merge(count: processes, first_is_1: true) expect(subject).to receive(:run_tests).once.with(['foo'], 0, processes, options).and_return(results) expect(subject).to receive(:run_tests).once.with(['foo'], 1, processes, options).and_return(results) expect(subject).to receive(:run_tests).once.with(['bar'], 2, processes, options).and_return(results) subject.run(['test', '-n', processes.to_s, '--first-is-1', '--allow-duplicates', '-t', 'my_test_runner']) end end end describe "#display_duration" do def call(*args) subject.send(:detailed_duration, *args) end it "displays for durations near one minute" do expect(call(59)).to eq(nil) expect(call(60)).to eq(" (1:00)") expect(call(61)).to eq(" (1:01)") end it "displays for durations near one hour" do expect(call(3599)).to eq(" (59:59)") expect(call(3600)).to eq(" (1:00:00)") expect(call(3601)).to eq(" (1:00:01)") end it "displays the correct string for miscellaneous durations" do expect(call(9296)).to eq(" (2:34:56)") expect(call(45296)).to eq(" (12:34:56)") expect(call(2756601)).to eq(" (765:43:21)") # hours into three digits? Buy more CI hardware... expect(call(0)).to eq(nil) end end end module ParallelTests module MyTestRunner class Runner end end end parallel_tests-5.4.0/spec/parallel_tests/cucumber/000077500000000000000000000000001504331627400223205ustar00rootroot00000000000000parallel_tests-5.4.0/spec/parallel_tests/cucumber/failure_logger_spec.rb000066400000000000000000000023071504331627400266470ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' require 'parallel_tests/gherkin/io' require 'parallel_tests/cucumber/failures_logger' require 'cucumber/configuration' describe ParallelTests::Cucumber::FailuresLogger do let(:parallel_cucumber_failures) { StringIO.new } let(:config) { Cucumber::Configuration.new(out_stream: parallel_cucumber_failures) } let(:logger1) { ParallelTests::Cucumber::FailuresLogger.new(config) } let(:logger2) { ParallelTests::Cucumber::FailuresLogger.new(config) } let(:logger3) { ParallelTests::Cucumber::FailuresLogger.new(config) } it "should produce a list of failing scenarios" do feature1 = double('feature', file: "feature/one.feature") feature2 = double('feature', file: "feature/two.feature") logger1.instance_variable_set("@failures", { feature1.file => [1, 3] }) logger2.instance_variable_set("@failures", { feature2.file => [2, 4] }) logger3.instance_variable_set("@failures", {}) config.event_bus.broadcast(Cucumber::Events::TestRunFinished.new) parallel_cucumber_failures.rewind expect(parallel_cucumber_failures.read).to eq 'feature/one.feature:1 feature/one.feature:3 feature/two.feature:2 feature/two.feature:4 ' end end parallel_tests-5.4.0/spec/parallel_tests/cucumber/runner_spec.rb000066400000000000000000000052031504331627400251700ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "parallel_tests/gherkin/runner_behaviour" require "parallel_tests/cucumber/runner" describe ParallelTests::Cucumber::Runner do test_tests_in_groups(ParallelTests::Cucumber::Runner, ".feature") it_should_behave_like 'gherkin runners' do let(:runner_name) { 'cucumber' } let(:runner_class) { ParallelTests::Cucumber::Runner } describe :summarize_results do def call(*args) runner_class.summarize_results(*args) end it "collates failing scenarios" do results = [ "Failing Scenarios:", "cucumber features/failure:1", "cucumber features/failure:2", "Failing Scenarios:", "cucumber features/failure:3", "cucumber features/failure:4", "Failing Scenarios:", "cucumber features/failure:5", "cucumber features/failure:6" ] output = call(results) output.gsub!(/.*WARNING.*\n/, "") expect(output).to eq(<<~TXT) Failing Scenarios: cucumber features/failure:1 cucumber features/failure:2 cucumber features/failure:3 cucumber features/failure:4 cucumber features/failure:5 cucumber features/failure:6 TXT end it "collates flaky scenarios separately" do results = [ "Failing Scenarios:", "cucumber features/failure:1", "cucumber features/failure:2", "Flaky Scenarios:", "cucumber features/failure:3", "cucumber features/failure:4", "Failing Scenarios:", "cucumber features/failure:5", "cucumber features/failure:6", "Flaky Scenarios:", "cucumber features/failure:7", "cucumber features/failure:8" ] expect(call(results)).to eq("Failing Scenarios:\ncucumber features/failure:1\ncucumber features/failure:2\ncucumber features/failure:5\ncucumber features/failure:6\n\nFlaky Scenarios:\ncucumber features/failure:3\ncucumber features/failure:4\ncucumber features/failure:7\ncucumber features/failure:8\n\n") end end end describe ".command_with_seed" do def call(*args) ParallelTests::Cucumber::Runner.command_with_seed(['cucumber', *args], 555) end it "adds the randomized seed" do expect(call).to eq(["cucumber", "--order", "random:555"]) end it "does not duplicate existing random command" do expect(call("--order", "random", "good1.feature")).to eq(["cucumber", "good1.feature", "--order", "random:555"]) end it "does not duplicate existing random command with seed" do expect(call("--order", "random:123", "good1.feature")).to eq(["cucumber", "good1.feature", "--order", "random:555"]) end end end parallel_tests-5.4.0/spec/parallel_tests/cucumber/scenarios_spec.rb000066400000000000000000000214721504331627400256530ustar00rootroot00000000000000# frozen_string_literal: true require 'tempfile' require 'parallel_tests/cucumber/scenarios' describe ParallelTests::Cucumber::Scenarios do let(:feature_file) do Tempfile.new('grouper.feature').tap do |feature| feature.write <<-EOS Feature: Grouping by scenario Scenario: First Given I do nothing Scenario: Second Given I don't do anything Scenario Outline: Third Given I don't do anything Examples: | param | | value 1 | | value 2 | EOS feature.rewind end end context 'by default' do it 'returns all the scenarios' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path]) expect(scenarios).to eq [ "#{feature_file.path}:3", "#{feature_file.path}:6", "#{feature_file.path}:13", "#{feature_file.path}:14" ] end end context 'with line numbers' do it 'only returns scenarios that match the provided lines' do scenarios = ParallelTests::Cucumber::Scenarios.all(["#{feature_file.path}:6:14"]) expect(scenarios).to eq ["#{feature_file.path}:6", "#{feature_file.path}:14"] end end context 'with tags' do let(:feature_file) do Tempfile.new('grouper.feature').tap do |feature| feature.write <<-EOS @colours Feature: Grouping by scenario @black Scenario: Black Given I am black @white Scenario: White Given I am blue @black @white Scenario: Gray Given I am Gray @red Scenario Outline: Red Give I am @blue Examples: | colour | | magenta | | fuchsia | @green Examples: | colour | | yellow | @blue @green Examples: | colour | | white | EOS feature.rewind end end it 'Single Feature Tag: colours' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@colours"]) expect(scenarios.length).to eq 7 end it 'Single Scenario Tag: white' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@white"]) expect(scenarios.length).to eq 2 end it 'Multiple Scenario Tags 1: black && white' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@black and @white"]) expect(scenarios.length).to eq 1 end it 'Multiple Scenario Tags 2: black || white scenarios' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@black or @white"]) expect(scenarios.length).to eq 3 end it 'Scenario Outline Tag: red' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@red"]) expect(scenarios.length).to eq 4 end it 'Example Tag: blue' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@blue"]) expect(scenarios.length).to eq 3 end it 'Multiple Example Tags 1: blue && green' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@blue and @green"]) expect(scenarios.length).to eq 1 end it 'Multiple Example Tags 2: blue || green' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@blue or @green"]) expect(scenarios.length).to eq 4 end it 'Single Negative Feature Tag: !colours' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "not @colours"]) expect(scenarios.length).to eq 0 end it 'Single Negative Scenario Tag: !black' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "not @black"]) expect(scenarios.length).to eq 5 end it 'Multiple Negative Scenario Tags And: !(black && white)' do scenarios = ParallelTests::Cucumber::Scenarios.all( [feature_file.path], test_options: ["-t", "not (@black and @white)"] ) expect(scenarios.length).to eq 6 end it 'Multiple Negative Scenario Tags Or: !(black || red)' do scenarios = ParallelTests::Cucumber::Scenarios.all( [feature_file.path], test_options: ["-t", "not (@black or @red)"] ) expect(scenarios.length).to eq 1 end it 'Negative Scenario Outline Tag: !red' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "not @red"]) expect(scenarios.length).to eq 3 end it 'Negative Example Tag: !blue' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "not @blue"]) expect(scenarios.length).to eq 4 end it 'Multiple Negative Example Tags 1: !blue && !green' do scenarios = ParallelTests::Cucumber::Scenarios.all( [feature_file.path], test_options: ["-t", "not @blue and not @green"] ) expect(scenarios.length).to eq 3 end it 'Multiple Negative Example Tags 2: !blue || !green) ' do scenarios = ParallelTests::Cucumber::Scenarios.all( [feature_file.path], test_options: ["-t", "not @blue or not @green"] ) expect(scenarios.length).to eq 6 end it 'Scenario and Example Mixed Tags: black || green' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], test_options: ["-t", "@black or @green"]) expect(scenarios.length).to eq 4 end it 'Positive and Negative Mixed Tags: red && !blue' do scenarios = ParallelTests::Cucumber::Scenarios.all( [feature_file.path], test_options: ["-t", "@red and not @blue"] ) expect(scenarios.length).to eq 1 end it 'Multiple Positive and Negative Mixed Tags: (white && black) || (red && !blue)' do scenarios = ParallelTests::Cucumber::Scenarios.all( [feature_file.path], test_options: ["--tags", "(not @white and @black) or (@red and not @green)"] ) expect(scenarios.length).to eq 3 end it 'Ignore Tag Pattern Feature: colours' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], ignore_tag_pattern: "@colours") expect(scenarios.length).to eq 0 end it 'Ignore Tag Pattern Scenario: black' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], ignore_tag_pattern: "@black") expect(scenarios.length).to eq 5 end it 'Ignore Tag Pattern Scenario Outline: red' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], ignore_tag_pattern: "@red") expect(scenarios.length).to eq 3 end it 'Ignore Tag Pattern Example: green' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], ignore_tag_pattern: "@green") expect(scenarios.length).to eq 5 end it 'Ignore Tag Pattern Multiple Tags: black || red' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path], ignore_tag_pattern: "@black or @red") expect(scenarios.length).to eq 1 end it 'Scenario Mixed tags: black && !blue with Ignore Tag Pattern Multiple Tags: red || white' do scenarios = ParallelTests::Cucumber::Scenarios.all( [feature_file.path], test_options: ["-t", "@black and not @blue"], ignore_tag_pattern: "@red or @white" ) expect(scenarios.length).to eq 1 end end context 'with Rules' do # cuke_modeler >=3.2 let(:feature_file) do Tempfile.new('grouper.feature').tap do |feature| feature.write <<-EOS Feature: Grouping by scenario Scenario: First Given I do nothing Scenario Outline: Second Given I don't do anything Examples: | param | | value 1 | | value 2 | Rule: Scenario: Third Given I don't do anything Rule: Scenario Outline: Fourth Given I don't do anything Examples: | param | | value 1 | | value 2 | EOS feature.rewind end end it 'returns all the scenarios' do scenarios = ParallelTests::Cucumber::Scenarios.all([feature_file.path]) expect(scenarios).to match_array [ "#{feature_file.path}:3", "#{feature_file.path}:10", "#{feature_file.path}:11", "#{feature_file.path}:14", "#{feature_file.path}:22", "#{feature_file.path}:23" ] end end end parallel_tests-5.4.0/spec/parallel_tests/gherkin/000077500000000000000000000000001504331627400221425ustar00rootroot00000000000000parallel_tests-5.4.0/spec/parallel_tests/gherkin/listener_spec.rb000066400000000000000000000066161504331627400253370ustar00rootroot00000000000000# frozen_string_literal: true require 'parallel_tests/gherkin/listener' describe ParallelTests::Gherkin::Listener do describe :collect do before(:each) do @listener = ParallelTests::Gherkin::Listener.new @listener.uri("feature_file") end it "returns steps count" do 3.times { @listener.step(nil) } expect(@listener.collect).to eq({ "feature_file" => 3 }) end it "counts background steps separately" do @listener.background("background") 5.times { @listener.step(nil) } expect(@listener.collect).to eq({ "feature_file" => 0 }) @listener.scenario("scenario") 2.times { @listener.step(nil) } expect(@listener.collect).to eq({ "feature_file" => 2 }) @listener.scenario("scenario") expect(@listener.collect).to eq({ "feature_file" => 2 }) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 12 }) end it "counts scenario outlines steps separately" do @listener.scenario_outline("outline") 5.times { @listener.step(nil) } @listener.examples(double('examples', rows: Array.new(3))) expect(@listener.collect).to eq({ "feature_file" => 15 }) @listener.scenario("scenario") 2.times { @listener.step(nil) } expect(@listener.collect).to eq({ "feature_file" => 17 }) @listener.scenario("scenario") expect(@listener.collect).to eq({ "feature_file" => 17 }) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 17 }) end it 'counts scenarios that should not be ignored' do @listener.ignore_tag_pattern = nil @listener.scenario(double('scenario', tags: [double('tag', name: '@WIP')])) @listener.step(nil) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 1 }) @listener.ignore_tag_pattern = /@something_other_than_WIP/ @listener.scenario(double('scenario', tags: [double('tag', name: '@WIP')])) @listener.step(nil) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 2 }) end it 'does not count scenarios that should be ignored' do @listener.ignore_tag_pattern = /@WIP/ @listener.scenario(double('scenario', tags: [double('tag', name: '@WIP')])) @listener.step(nil) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 0 }) end it 'counts outlines that should not be ignored' do @listener.ignore_tag_pattern = nil @listener.scenario_outline(double('scenario', tags: [double('tag', name: '@WIP')])) @listener.step(nil) @listener.examples(double('examples', rows: Array.new(3))) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 3 }) @listener.ignore_tag_pattern = /@something_other_than_WIP/ @listener.scenario_outline(double('scenario', tags: [double('tag', name: '@WIP')])) @listener.step(nil) @listener.examples(double('examples', rows: Array.new(3))) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 6 }) end it 'does not count outlines that should be ignored' do @listener.ignore_tag_pattern = /@WIP/ @listener.scenario_outline(double('scenario', tags: [double('tag', name: '@WIP')])) @listener.step(nil) @listener.examples(double('examples', rows: Array.new(3))) @listener.eof expect(@listener.collect).to eq({ "feature_file" => 0 }) end end end parallel_tests-5.4.0/spec/parallel_tests/gherkin/runner_behaviour.rb000066400000000000000000000204051504331627400260450ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "parallel_tests/gherkin/runner" shared_examples_for 'gherkin runners' do describe :run_tests do before do allow(ParallelTests).to receive(:bundler_enabled?).and_return false allow(File).to receive(:file?).with('.bundle/environment.rb').and_return false allow(File).to receive(:file?).with("script/#{runner_name}").and_return true end def call(*args) runner_class.run_tests(*args) end it "allows to override runner executable via PARALLEL_TESTS_EXECUTABLE" do ENV['PARALLEL_TESTS_EXECUTABLE'] = 'script/custom_rspec' should_run_with ["script/custom_rspec"] call(['xxx'], 1, 22, {}) end it "permits setting env options" do expect(ParallelTests::Test::Runner).to receive(:execute_command) do |_, _, _, options| expect(options[:env]["TEST"]).to eq("ME") end call(['xxx'], 1, 22, { env: { 'TEST' => 'ME' } }) end it "runs bundle exec {runner_name} when on bundler 0.9" do allow(ParallelTests).to receive(:bundler_enabled?).and_return true should_run_with ["bundle", "exec", runner_name] call(['xxx'], 1, 22, {}) end it "runs script/{runner_name} when script/{runner_name} is found" do should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}") call(['xxx'], 1, 22, {}) end it "runs {runner_name} by default" do allow(File).to receive(:file?).with("script/#{runner_name}").and_return false should_run_with [runner_name] call(['xxx'], 1, 22, {}) end it "uses bin/{runner_name} when present" do allow(File).to receive(:exist?).with("bin/#{runner_name}").and_return true should_run_with ParallelTests.with_ruby_binary("bin/#{runner_name}") call(['xxx'], 1, 22, {}) end it "uses options passed in" do should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}"), "-p", "default" call(['xxx'], 1, 22, test_options: ['-p', 'default']) end it "sanitizes dangerous file runner_names" do should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}"), "xx x" call(['xx x'], 1, 22, {}) end context "with parallel profile in config/{runner_name}.yml" do before do file_contents = 'parallel: -f progress' allow(Dir).to receive(:glob).and_return ["config/#{runner_name}.yml"] allow(File).to receive(:read).with("config/#{runner_name}.yml").and_return file_contents end it "uses parallel profile" do should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}"), "xxx", "foo", "bar", "--profile", "parallel" call(['xxx'], 1, 22, test_options: ['foo', 'bar']) end it "uses given profile via --profile" do should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}"), "--profile", "foo" call(['xxx'], 1, 22, test_options: ['--profile', 'foo']) end it "uses given profile via -p" do should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}"), "-p", "foo" call(['xxx'], 1, 22, test_options: ['-p', 'foo']) end end it "does not use parallel profile if config/{runner_name}.yml does not contain it" do file_contents = 'blob: -f progress' should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}"), "foo", "bar" expect(Dir).to receive(:glob).and_return ["config/#{runner_name}.yml"] expect(File).to receive(:read).with("config/#{runner_name}.yml").and_return file_contents call(['xxx'], 1, 22, test_options: ['foo', 'bar']) end it "does not use the parallel profile if config/{runner_name}.yml does not exist" do should_run_with ParallelTests.with_ruby_binary("script/#{runner_name}") # TODO: this test looks useless... expect(Dir).to receive(:glob).and_return [] call(['xxx'], 1, 22, {}) end end describe :line_is_result? do it "should match lines with only one scenario" do line = "1 scenario (1 failed)" expect(runner_class.line_is_result?(line)).to be_truthy end it "should match lines with multiple scenarios" do line = "2 scenarios (1 failed, 1 passed)" expect(runner_class.line_is_result?(line)).to be_truthy end it "should match lines with only one step" do line = "1 step (1 failed)" expect(runner_class.line_is_result?(line)).to be_truthy end it "should match lines with multiple steps" do line = "5 steps (1 failed, 4 passed)" expect(runner_class.line_is_result?(line)).to be_truthy end it "should not match other lines" do line = ' And I should have "2" emails # features/step_definitions/user_steps.rb:25' expect(runner_class.line_is_result?(line)).to be_falsey end end describe :find_results do it "finds multiple results in test output" do output = <<~EOF And I should not see "/en/" # features/step_definitions/webrat_steps.rb:87 7 scenarios (3 failed, 4 passed) 33 steps (3 failed, 2 skipped, 28 passed) /apps/rs/features/signup.feature:2 Given I am on "/" # features/step_definitions/common_steps.rb:12 When I click "register" # features/step_definitions/common_steps.rb:6 And I should have "2" emails # features/step_definitions/user_steps.rb:25 4 scenarios (4 passed) 40 steps (40 passed) And I should not see "foo" # features/step_definitions/webrat_steps.rb:87 1 scenario (1 passed) 1 step (1 passed) EOF expect(runner_class.find_results(output)).to eq( [ "7 scenarios (3 failed, 4 passed)", "33 steps (3 failed, 2 skipped, 28 passed)", "4 scenarios (4 passed)", "40 steps (40 passed)", "1 scenario (1 passed)", "1 step (1 passed)" ] ) end end describe :summarize_results do def call(*args) runner_class.summarize_results(*args) end it "sums up results for scenarios and steps separately from each other" do results = [ "7 scenarios (2 failed, 1 flaky, 4 passed)", "33 steps (3 failed, 2 skipped, 28 passed)", "4 scenarios (4 passed)", "40 steps (40 passed)", "1 scenario (1 passed)", "1 step (1 passed)" ] expect(call(results)).to eq("12 scenarios (2 failed, 1 flaky, 9 passed)\n74 steps (3 failed, 2 skipped, 69 passed)") end it "adds same results with plurals" do results = [ "1 scenario (1 passed)", "2 steps (2 passed)", "2 scenarios (2 passed)", "7 steps (7 passed)" ] expect(call(results)).to eq("3 scenarios (3 passed)\n9 steps (9 passed)") end it "adds non-similar results" do results = [ "1 scenario (1 passed)", "1 step (1 passed)", "2 scenarios (1 failed, 1 pending)", "2 steps (1 failed, 1 pending)" ] expect(call(results)).to eq("3 scenarios (1 failed, 1 pending, 1 passed)\n3 steps (1 failed, 1 pending, 1 passed)") end it "does not pluralize 1" do expect(call(["1 scenario (1 passed)", "1 step (1 passed)"])).to eq("1 scenario (1 passed)\n1 step (1 passed)") end end describe 'grouping by scenarios for cucumber' do def call(*args) runner_class.send(:run_tests, *args) end it 'groups cucumber invocation by feature files to achieve correct cucumber hook behaviour' do test_files = ['features/a.rb:23', 'features/a.rb:44', 'features/b.rb:12'] expect(ParallelTests::Test::Runner).to receive(:execute_command) do |a, _b, _c, _d| argv = a.last(2) expect(argv).to eq(["features/a.rb:23:44", "features/b.rb:12"]) end call(test_files, 1, 2, { group_by: :scenarios }) end end describe ".find_tests" do def call(*args) ParallelTests::Gherkin::Runner.send(:find_tests, *args) end it "doesn't find backup files with the same name as test files" do with_files(['a/x.feature', 'a/x.feature.bak']) do |root| expect(call(["#{root}/"])).to eq( [ "#{root}/a/x.feature" ] ) end end end end parallel_tests-5.4.0/spec/parallel_tests/grouper_spec.rb000066400000000000000000000106561504331627400235450ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' require 'parallel_tests/grouper' require 'parallel_tests/cucumber/scenarios' require 'tmpdir' describe ParallelTests::Grouper do describe '.by_steps' do def write(file, content) File.write(file, content) end it "sorts features by steps" do tmpdir = nil result = Dir.mktmpdir do |dir| tmpdir = dir write("#{dir}/a.feature", "Feature: xxx\n Scenario: xxx\n Given something") write( "#{dir}/b.feature", "Feature: xxx\n Scenario: xxx\n Given something\n Scenario: yyy\n Given something" ) write("#{dir}/c.feature", "Feature: xxx\n Scenario: xxx\n Given something") ParallelTests::Grouper.by_steps(["#{dir}/a.feature", "#{dir}/b.feature", "#{dir}/c.feature"], 2, {}) end # testing inside mktmpdir is always green expect(result).to match_array( [ ["#{tmpdir}/a.feature", "#{tmpdir}/c.feature"], ["#{tmpdir}/b.feature"] ] ) end end describe '.in_even_groups_by_size' do let(:files_with_size) { { "1" => 1, "2" => 2, "3" => 3, "4" => 4, "5" => 5 } } def call(num_groups, options = {}) ParallelTests::Grouper.in_even_groups_by_size(files_with_size, num_groups, options) end it "groups 1 by 1 for same groups as size" do expect(call(5)).to eq([["5"], ["4"], ["3"], ["2"], ["1"]]) end it "groups into even groups" do expect(call(2)).to eq([["1", "2", "5"], ["3", "4"]]) end it "groups into a single group" do expect(call(1)).to eq([["1", "2", "3", "4", "5"]]) end it "adds empty groups if there are more groups than feature files" do expect(call(6)).to eq([["5"], ["4"], ["3"], ["2"], ["1"], []]) end it "groups single items into first group" do expect(call(2, single_process: [/1|2|3|4/])).to eq([["1", "2", "3", "4"], ["5"]]) end it "groups single items into specified isolation groups" do expect(call(3, single_process: [/1|2|3|4/], isolate_count: 2)).to eq([["1", "4"], ["2", "3"], ["5"]]) end it "groups single items with others if there are too few" do expect(call(2, single_process: [/1/])).to eq([["1", "3", "4"], ["2", "5"]]) end it "groups must abort when isolate_count is out of bounds" do expect do call(3, single_process: [/1/], isolate_count: 3) end.to raise_error( "Number of isolated processes must be >= total number of processes" ) end context 'specify_groups' do it "groups with one spec" do expect(call(3, specify_groups: '1')).to eq([["1"], ["2", "5"], ["3", "4"]]) end it "groups with multiple specs in one process" do expect(call(3, specify_groups: '3,1')).to eq([["3", "1"], ["5"], ["2", "4"]]) end it "groups with multiple specs and multiple processes" do expect(call(3, specify_groups: '1,2|4')).to eq([["1", "2"], ["4"], ["3", "5"]]) end it "aborts when number of specs is higher than number of processes" do expect do call(3, specify_groups: '1|2|3|4') end.to raise_error( "Number of processes separated by pipe must be less than or equal to the total number of processes" ) end it "aborts when spec passed in doesn't match existing specs" do expect do call(3, specify_groups: '1|2|6') end.to raise_error( "Could not find [\"6\"] from --specify-groups in the selected files & folders" ) end it "aborts when number of specs is equal to number of processes and not all specs are used" do expect do call(3, specify_groups: '1|2|3') end.to raise_error(/The specs that aren't run:\n\["4", "5"\]/) end it "does not abort when the every single spec is specified" do expect(call(3, specify_groups: '1,2|3,4|5')).to eq([["1", "2"], ["3", "4"], ["5"]]) end it "can read from stdin" do allow($stdin).to receive(:read).and_return("3,1\n") expect(call(3, specify_groups: '-')).to eq([["3", "1"], ["5"], ["2", "4"]]) end end end describe '.by_scenarios' do let(:feature_file) { double 'file' } it 'splits a feature into individual scenarios' do expect(ParallelTests::Cucumber::Scenarios).to receive(:all).and_return({ 'feature_file:3' => 1 }) ParallelTests::Grouper.by_scenarios([feature_file], 1) end end end parallel_tests-5.4.0/spec/parallel_tests/pids_spec.rb000066400000000000000000000013021504331627400230050ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' RSpec.describe ParallelTests::Pids do let(:file_path) { Tempfile.new('pidfile').path } subject { described_class.new(file_path) } before do subject.send(:clear) subject.add(123) subject.add(456) end describe '#add' do specify do subject.add(789) expect(subject.all).to eq([123, 456, 789]) end end describe '#delete' do specify do subject.add(101) subject.delete(123) expect(subject.all).to eq([456, 101]) end end describe '#count' do specify { expect(subject.count).to eq(2) } end describe '#all' do specify { expect(subject.all).to eq([123, 456]) } end end parallel_tests-5.4.0/spec/parallel_tests/rspec/000077500000000000000000000000001504331627400216275ustar00rootroot00000000000000parallel_tests-5.4.0/spec/parallel_tests/rspec/failures_logger_spec.rb000066400000000000000000000006311504331627400263370ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ParallelTests::RSpec::FailuresLogger do let(:output) { OutputLogger.new([]) } let(:logger) { ParallelTests::RSpec::FailuresLogger.new(output) } it "prints failures" do logger.dump_summary(double(failed_examples: [1], colorized_rerun_commands: "HEYHO")) expect(output.output).to eq( [ "HEYHO\n" ] ) end end parallel_tests-5.4.0/spec/parallel_tests/rspec/logger_base_spec.rb000066400000000000000000000014571504331627400254460ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ParallelTests::RSpec::LoggerBase do before do @temp_file = Tempfile.open('xxx') @logger = ParallelTests::RSpec::LoggerBase.new(@temp_file) end after do @temp_file.close end describe 'on tests finished' do it 'should respond to close' do expect(@logger).to respond_to(:close) end it 'should close output' do expect(@temp_file).to receive(:close) @logger.close end it 'should not close stdout' do @logger = ParallelTests::RSpec::LoggerBase.new($stdout) expect($stdout).not_to receive(:close) @logger.close end it 'should not close IO instance' do io = double(IO) @logger = ParallelTests::RSpec::LoggerBase.new(io) @logger.close end end end parallel_tests-5.4.0/spec/parallel_tests/rspec/runner_spec.rb000066400000000000000000000154561504331627400245120ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "parallel_tests/rspec/runner" describe ParallelTests::RSpec::Runner do test_tests_in_groups(ParallelTests::RSpec::Runner, '_spec.rb') describe '.run_tests' do before do allow(File).to receive(:file?).with('spec/spec.opts').and_return false allow(File).to receive(:file?).with('spec/parallel_spec.opts').and_return false allow(File).to receive(:file?).with('.rspec_parallel').and_return false allow(ParallelTests).to receive(:bundler_enabled?).and_return false end def call(*args) ParallelTests::RSpec::Runner.run_tests(*args) end it "runs command using nice when specified" do ParallelTests.with_pid_file do expect(ParallelTests::Test::Runner).to receive(:execute_command_and_capture_output) do |_a, b, _c| expect(b.first(2)).to eq(["nice", "rspec"]) end call('xxx', 1, 22, nice: true) end end it "runs with color when called from cmdline" do should_run_with ["rspec"], "--tty" expect($stdout).to receive(:tty?).and_return true call('xxx', 1, 22, {}) end it "runs without color when not called from cmdline" do should_not_run_with('--tty') expect($stdout).to receive(:tty?).and_return false call('xxx', 1, 22, {}) end it "uses bin/rspec when present" do allow(File).to receive(:exist?).with('bin/rspec').and_return true should_run_with ParallelTests.with_ruby_binary("bin/rspec") call('xxx', 1, 22, {}) end it "uses no -O when no opts where found" do allow(File).to receive(:file?).with('spec/spec.opts').and_return false should_not_run_with 'spec/spec.opts' call('xxx', 1, 22, {}) end it "uses -O spec/parallel_spec.opts with rspec2" do skip if RUBY_PLATFORM == "java" # FIXME: not sure why, but fails on travis expect(File).to receive(:file?).with('spec/parallel_spec.opts').and_return true allow(ParallelTests).to receive(:bundler_enabled?).and_return true should_run_with ["bundle", "exec", "rspec"], "-O", "spec/parallel_spec.opts", "xxx" call('xxx', 1, 22, {}) end it "uses options passed in" do should_run_with ["rspec"], "-f", "n" call('xxx', 1, 22, test_options: ['-f', 'n']) end it "returns the output" do expect(ParallelTests::RSpec::Runner).to receive(:execute_command).and_return x: 1 expect(call('xxx', 1, 22, {})).to eq({ x: 1 }) end end describe '.find_results' do def call(*args) ParallelTests::RSpec::Runner.find_results(*args) end it "finds multiple results in spec output" do output = <<-OUT.gsub(/^ /, '') ....F... .. failute fsddsfsd ... ff.**.. 0 examples, 0 failures, 0 pending ff.**.. 1 example, 1 failure, 1 pending OUT expect(call(output)).to eq(['0 examples, 0 failures, 0 pending', '1 example, 1 failure, 1 pending']) end it "does not mistakenly count 'pending' failures as real failures" do output = <<-OUT.gsub(/^ /, '') ..... Pending: (Failures listed here are expected and do not affect your suite's status) 1) Foo Got 1 failure and 1 other error: 1.1) Failure/Error: Bar Baz 1.2) Failure/Error: Bar Baz 1 examples, 0 failures, 1 pending OUT expect(call(output)).to eq(['1 examples, 0 failures, 1 pending']) end end describe ".find_tests" do def call(*args) ParallelTests::RSpec::Runner.send(:find_tests, *args) end it "finds turnip feature files" do with_files(['a/test.feature']) do |root| expect(call(["#{root}/"])).to eq(["#{root}/a/test.feature"]) end end it "doesn't find backup files with the same name as test files" do with_files(['a/x_spec.rb', 'a/x_spec.rb.bak']) do |root| expect(call(["#{root}/"])).to eq(["#{root}/a/x_spec.rb"]) end end end describe '.summarize_results' do context 'not on TTY device' do before { allow($stdout).to receive(:tty?).and_return false } it 'is not colourized' do results = ParallelTests::RSpec::Runner.send(:summarize_results, ['1 example, 0 failures, 0 pendings']) expect(results).to eq('1 example, 0 failures, 0 pendings') end end context 'on TTY device' do before { allow($stdout).to receive(:tty?).and_return true } subject(:colorized_results) { ParallelTests::RSpec::Runner.send(:summarize_results, [result_string]) } context 'when there are no pending or failed tests' do let(:result_string) { '1 example, 0 failures, 0 pendings' } it 'is green' do expect(colorized_results).to eq("\e[32m#{result_string}\e[0m") # 32 is green end end context 'when there is a pending test and no failed tests' do let(:result_string) { '1 example, 0 failures, 1 pending' } it 'is yellow' do expect(colorized_results).to eq("\e[33m#{result_string}\e[0m") # 33 is yellow end end context 'when there is a pending test and a failed test' do let(:result_string) { '1 example, 1 failure, 1 pending' } it 'is red' do expect(colorized_results).to eq("\e[31m#{result_string}\e[0m") # 31 is red end end context 'when there is no pending tests and a failed test' do let(:result_string) { '1 example, 1 failure, 0 pendings' } it 'is red' do expect(colorized_results).to eq("\e[31m#{result_string}\e[0m") # 31 is red end end end end describe ".command_with_seed" do def call(*args) base = ["ruby", "-Ilib:test", "test/minitest/test_minitest_unit.rb"] result = ParallelTests::RSpec::Runner.command_with_seed([*base, *args], "555") result[base.length..] end it "adds the randomized seed" do expect(call).to eq(["--seed", "555"]) end it "does not duplicate seed" do expect(call("--seed", "123")).to eq(["--seed", "555"]) end it "does not duplicate strange seeds" do expect(call("--seed", "123asdasd")).to eq(["--seed", "555"]) end it "does not match non seeds" do expect(call("--seedling", "123")).to eq(["--seedling", "123", "--seed", "555"]) end it "does not duplicate random" do expect(call("--order", "random")).to eq(["--seed", "555"]) end it "does not duplicate rand" do expect(call("--order", "rand")).to eq(["--seed", "555"]) end it "does not duplicate rand with seed" do expect(call("--order", "rand:123")).to eq(["--seed", "555"]) end it "does not duplicate random with seed" do expect(call("--order", "random:123")).to eq(["--seed", "555"]) end end end parallel_tests-5.4.0/spec/parallel_tests/rspec/runtime_logger_spec.rb000066400000000000000000000067211504331627400262160ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ParallelTests::RSpec::RuntimeLogger do before do # pretend we run in parallel or the logger will log nothing ENV['TEST_ENV_NUMBER'] = '' @clean_output = %r{^spec/foo.rb:[-.e\d]+$}m end def log_for_a_file(_options = {}) Tempfile.open('xxx') do |temp| temp.close f = File.open(temp.path, 'w') logger = if block_given? yield(f) else ParallelTests::RSpec::RuntimeLogger.new(f) end example = double(file_path: "#{Dir.pwd}/spec/foo.rb") example = double(group: example) logger.example_group_started example logger.example_group_finished example logger.start_dump # f.close return File.read(f.path) end end it "logs runtime with relative paths" do expect(log_for_a_file).to match(@clean_output) end it "does not log if we do not run in parallel" do ENV.delete 'TEST_ENV_NUMBER' expect(log_for_a_file).to eq("") end it "appends to a given file" do result = log_for_a_file do |f| f.write 'FooBar' ParallelTests::RSpec::RuntimeLogger.new(f) end expect(result).to include('FooBar') expect(result).to include('foo.rb') end it "overwrites a given path" do result = log_for_a_file do |f| f.write 'FooBar' ParallelTests::RSpec::RuntimeLogger.new(f.path) end expect(result).not_to include('FooBar') expect(result).to include('foo.rb') end context "integration" do around do |example| Dir.mktmpdir do |dir| Dir.chdir(dir, &example) end end def write(file, content) FileUtils.mkdir_p(File.dirname(file)) File.write(file, content) end it "logs shared examples into the running files" do write "spec/spec_helper.rb", <<-RUBY shared_examples "foo" do it "is slow" do sleep 0.5 end end RUBY ["a", "b"].each do |letter| write "spec/#{letter}_spec.rb", <<-RUBY require 'spec_helper' describe 'xxx' do it_behaves_like "foo" end RUBY end system( { 'TEST_ENV_NUMBER' => '1' }, "rspec", "spec", "-I", Bundler.root.join("lib").to_s, "--format", "ParallelTests::RSpec::RuntimeLogger", "--out", "runtime.log" ) || raise("nope") result = File.read("runtime.log") expect(result).to match(%r{^spec/a_spec.rb:0.5}) expect(result).to match(%r{^spec/b_spec.rb:0.5}) expect(result).not_to include "spec_helper" end it "logs multiple describe blocks" do write "spec/a_spec.rb", <<-RUBY describe "xxx" do it "is slow" do sleep 0.5 end end describe "yyy" do it "is slow" do sleep 0.5 end describe "yep" do it "is slow" do sleep 0.5 end end end RUBY write "spec/slower_spec.rb", <<-RUBY describe "xxx" do it "is slow" do sleep 3 end end RUBY system( { 'TEST_ENV_NUMBER' => '1' }, "rspec", "spec", "-I", Bundler.root.join("lib").to_s, "--format", "ParallelTests::RSpec::RuntimeLogger", "--out", "runtime.log" ) || raise("nope") result = File.read("runtime.log") expect(result).to start_with("spec/slower_spec.rb:3.0") expect(result).to include "spec/a_spec.rb:1.5" end end end parallel_tests-5.4.0/spec/parallel_tests/rspec/summary_logger_spec.rb000066400000000000000000000006551504331627400262300ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ParallelTests::RSpec::SummaryLogger do let(:output) { OutputLogger.new([]) } let(:logger) { ParallelTests::RSpec::SummaryLogger.new(output) } it "prints failing examples" do logger.dump_failures(double(failure_notifications: [1], fully_formatted_failed_examples: "HEYHO")) expect(output.output).to eq( [ "HEYHO\n" ] ) end end parallel_tests-5.4.0/spec/parallel_tests/rspec/verbose_logger_spec.rb000066400000000000000000000024111504331627400261700ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ParallelTests::RSpec::VerboseLogger do def run(command) result = IO.popen(command, err: [:child, :out], &:read) raise "FAILED: #{result}" unless $?.success? result end it 'outputs verbose information' do repo_root = Dir.pwd use_temporary_directory do # setup simple structure FileUtils.mkdir "spec" File.write "spec/foo_spec.rb", <<-RUBY describe "Foo" do it "foo" do sleep 0.5 expect(true).to be(true) end end RUBY File.write "spec/bar_spec.rb", <<-RUBY describe "Bar" do it "bar" do sleep 0.25111 expect(true).to be(true) end end RUBY result = run [ "ruby", "#{repo_root}/bin/parallel_rspec", "-n", "2", "--", "--format", "ParallelTests::RSpec::VerboseLogger", "--" ] expect(result).to match(/^\[\d+\] \[(1|2)\] \[STARTED\] Foo foo$/) expect(result).to match(/^\[\d+\] \[(1|2)\] \[PASSED\] Foo foo$/) expect(result).to match(/^\[\d+\] \[(1|2)\] \[STARTED\] Bar bar$/) expect(result).to match(/^\[\d+\] \[(1|2)\] \[PASSED\] Bar bar$/) end end end parallel_tests-5.4.0/spec/parallel_tests/spinach/000077500000000000000000000000001504331627400221405ustar00rootroot00000000000000parallel_tests-5.4.0/spec/parallel_tests/spinach/runner_spec.rb000066400000000000000000000006231504331627400250110ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "parallel_tests/gherkin/runner_behaviour" require "parallel_tests/spinach/runner" describe ParallelTests::Spinach::Runner do test_tests_in_groups(ParallelTests::Spinach::Runner, ".feature") it_should_behave_like 'gherkin runners' do let(:runner_name) { 'spinach' } let(:runner_class) { ParallelTests::Spinach::Runner } end end parallel_tests-5.4.0/spec/parallel_tests/tasks_spec.rb000066400000000000000000000215311504331627400232010ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' require 'parallel_tests/tasks' require 'rspec/support/spec/shell_out' describe ParallelTests::Tasks do describe ".parse_args" do it "should return the count" do args = { count: 2 } expect(ParallelTests::Tasks.parse_args(args)).to eq([2, nil, nil, nil]) end it "should default to the prefix" do args = { count: "models" } expect(ParallelTests::Tasks.parse_args(args)).to eq([nil, "models", nil, nil]) end it "should return the count and pattern" do args = { count: 2, pattern: "models" } expect(ParallelTests::Tasks.parse_args(args)).to eq([2, "models", nil, nil]) end it "should return the count, pattern, and options" do args = { count: 2, pattern: "plain", options: "-p default" } expect(ParallelTests::Tasks.parse_args(args)).to eq([2, "plain", "-p default", nil]) end it "should return the count, pattern, and options" do args = { count: 2, pattern: "plain", options: "-p default --group-by steps" } expect(ParallelTests::Tasks.parse_args(args)).to eq([2, "plain", "-p default --group-by steps", nil]) end it "should return the count, pattern, test options, and pass-through options" do args = { count: 2, pattern: "plain", options: "-p default --group-by steps", pass_through: "--runtime-log /path/to/log" } expect(ParallelTests::Tasks.parse_args(args)).to eq( [2, "plain", "-p default --group-by steps", "--runtime-log /path/to/log"] ) end end describe ".rails_env" do include RSpec::Support::ShellOut it "is test when nothing was set" do expect(ParallelTests::Tasks.rails_env).to eq("test") end it "ignores RAILS_ENV since that is often set when rake is executed" do with_env "RAILS_ENV" => "bar" do expect(ParallelTests::Tasks.rails_env).to eq("test") end end it "uses PARALLEL_RAILS_ENV" do with_env "PARALLEL_RAILS_ENV" => "bar" do expect(ParallelTests::Tasks.rails_env).to eq("bar") end end end describe ".run_in_parallel" do let(:full_path) { File.expand_path('../../bin/parallel_test', __dir__) } it "has the executable" do expect(File.file?(full_path)).to eq(true) expect(File.executable?(full_path)).to eq(true) unless Gem.win_platform? end it "runs command in parallel" do expect(ParallelTests::Tasks).to receive(:system) .with(*ParallelTests.with_ruby_binary(full_path), '--exec', 'echo') .and_return true ParallelTests::Tasks.run_in_parallel(["echo"]) end it "runs command with :count option" do expect(ParallelTests::Tasks).to receive(:system) .with(*ParallelTests.with_ruby_binary(full_path), '--exec', 'echo', '-n', 123) .and_return true ParallelTests::Tasks.run_in_parallel(["echo"], count: 123) end it "runs without -n with blank :count option" do expect(ParallelTests::Tasks).to receive(:system) .with(*ParallelTests.with_ruby_binary(full_path), '--exec', 'echo') .and_return true ParallelTests::Tasks.run_in_parallel(["echo"], count: "") end it "runs command with :non_parallel option" do expect(ParallelTests::Tasks).to receive(:system) .with(*ParallelTests.with_ruby_binary(full_path), '--exec', 'echo', '--non-parallel') .and_return true ParallelTests::Tasks.run_in_parallel(["echo"], non_parallel: true) end it "runs aborts if the command fails" do expect(ParallelTests::Tasks).to receive(:system).and_return false expect(ParallelTests::Tasks).to receive(:abort).and_return false ParallelTests::Tasks.run_in_parallel(["echo"]) end end describe ".suppress_output", unless: Gem.win_platform? do def call(command, grep) # Explicitly run as a parameter to /bin/bash to simulate how # the command will be run by parallel_test --exec # This also tests shell escaping of single quotes shell_command = [ '/bin/bash', '-c', Shellwords.shelljoin(ParallelTests::Tasks.suppress_output(command, grep)) ] result = IO.popen(shell_command, &:read) [result, $?.success?] end context "with pipefail supported" do before :all do unless system("/bin/bash", "-c", "set -o pipefail 2>/dev/null") skip "pipefail is not supported on your system" end end it "should hide offending lines" do expect(call(["echo", "123"], "123")).to eq(["", true]) end it "should not hide other lines" do expect(call(["echo", "124"], "123")).to eq(["124\n", true]) end it "should fail if command fails and the pattern matches" do expect(call(['/bin/bash', '-c', 'echo 123 && false'], "123")).to eq(["", false]) end it "should fail if command fails and the pattern fails" do expect(call(['/bin/bash', '-c', 'echo 124 && false'], "123")).to eq(["124\n", false]) end end context "without pipefail supported" do before do expect(ParallelTests::Tasks).to receive(:system).with( '/bin/bash', '-c', 'set -o pipefail 2>/dev/null' ).and_return false end it "should not filter and succeed" do expect(call(["echo", "123"], "123")).to eq(["123\n", true]) end it "should not filter and fail" do expect(call(['/bin/bash', '-c', 'echo 123 && false'], "123")).to eq(["123\n", false]) end end end describe ".suppress_schema_load_output" do before do allow(ParallelTests::Tasks).to receive(:suppress_output) end it 'should call suppress output with command' do ParallelTests::Tasks.suppress_schema_load_output('command') expect(ParallelTests::Tasks).to have_received(:suppress_output).with('command', "^ ->\\|^-- ") end end describe ".check_for_pending_migrations" do after do Rake.application.instance_variable_get('@tasks').delete("db:abort_if_pending_migrations") Rake.application.instance_variable_get('@tasks').delete("app:db:abort_if_pending_migrations") end it "should do nothing if pending migrations is no defined" do ParallelTests::Tasks.check_for_pending_migrations end it "should run pending migrations is task is defined" do foo = 1 Rake::Task.define_task("db:abort_if_pending_migrations") do foo = 2 end ParallelTests::Tasks.check_for_pending_migrations expect(foo).to eq(2) end it "should run pending migrations is app task is defined" do foo = 1 Rake::Task.define_task("app:db:abort_if_pending_migrations") do foo = 2 end ParallelTests::Tasks.check_for_pending_migrations expect(foo).to eq(2) end it "should not execute the task twice" do foo = 1 Rake::Task.define_task("db:abort_if_pending_migrations") do foo += 1 end ParallelTests::Tasks.check_for_pending_migrations ParallelTests::Tasks.check_for_pending_migrations expect(foo).to eq(2) end end describe ".purge_before_load" do context 'ActiveRecord < 4.2.0' do before do stub_const('ActiveRecord', double(version: Gem::Version.new('3.2.1'))) end it "should return nil for ActiveRecord < 4.2.0" do expect(ParallelTests::Tasks.purge_before_load).to eq nil end end context 'ActiveRecord > 4.2.0' do before do stub_const('ActiveRecord', double(version: Gem::Version.new('4.2.8'))) end it "should return db:purge when defined" do allow(Rake::Task).to receive(:task_defined?).with('db:purge') { true } expect(ParallelTests::Tasks.purge_before_load).to eq 'db:purge' end it "should return app:db:purge when db:purge is not defined" do allow(Rake::Task).to receive(:task_defined?).with('db:purge') { false } expect(ParallelTests::Tasks.purge_before_load).to eq 'app:db:purge' end end end describe ".build_run_command" do it "builds simple command" do command = ParallelTests::Tasks.build_run_command("test", {}) command.shift 2 if command.include?("--") # windows prefixes ruby executable expect(command).to eq [ "#{Dir.pwd}/bin/parallel_test", "test", "--type", "test" ] end it "fails on unknown" do expect { ParallelTests::Tasks.build_run_command("foo", {}) }.to raise_error(KeyError) end it "builds with all arguments" do command = ParallelTests::Tasks.build_run_command( "test", count: 1, pattern: "foo", options: "bar", pass_through: "baz baz" ) command.shift 2 if command.include?("--") # windows prefixes ruby executable expect(command).to eq [ "#{Dir.pwd}/bin/parallel_test", "test", "--type", "test", "-n", "1", "--pattern", "foo", "--test-options", "bar", "baz", "baz" ] end end end parallel_tests-5.4.0/spec/parallel_tests/test/000077500000000000000000000000001504331627400214725ustar00rootroot00000000000000parallel_tests-5.4.0/spec/parallel_tests/test/runner_spec.rb000066400000000000000000000525211504331627400243470ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" require "parallel_tests/test/runner" describe ParallelTests::Test::Runner do test_tests_in_groups(ParallelTests::Test::Runner, '_test.rb') test_tests_in_groups(ParallelTests::Test::Runner, '_spec.rb') describe ".run_tests" do def call(*args) ParallelTests::Test::Runner.run_tests(*args) end it "allows to override runner executable via PARALLEL_TESTS_EXECUTABLE" do ENV['PARALLEL_TESTS_EXECUTABLE'] = 'script/custom_rspec' expect(ParallelTests::Test::Runner).to receive(:execute_command) do |a, _, _, _d| expect(a).to include("script/custom_rspec") end call(['xxx'], 1, 22, {}) ENV['PARALLEL_TESTS_EXECUTABLE'] = 'ruby -Icustom_option script/custom_rspec' expect(ParallelTests::Test::Runner).to receive(:execute_command) do |a, _, _, _d| expect(a).to include("ruby", "-Icustom_option", "script/custom_rspec") end call(['xxx'], 1, 22, {}) end it "uses options" do expect(ParallelTests::Test::Runner).to receive(:execute_command) do |a, _, _, _d| expect(a).to eq(["ruby", "-Itest", "-e", "%w[xxx].each { |f| require %{./\#{f}} }", "--", "-v"]) end call(['xxx'], 1, 22, test_options: '-v') end it "returns the output" do expect(ParallelTests::Test::Runner).to receive(:execute_command).and_return(x: 1) expect(call(['xxx'], 1, 22, {})).to eq({ x: 1 }) end end describe ".test_in_groups" do def call(*args) ParallelTests::Test::Runner.tests_in_groups(*args) end it "raises when passed invalid group" do expect { call([], 1, group_by: :sdjhfdfdjs) }.to raise_error(ArgumentError) end it "uses given when passed found" do result = (Gem.win_platform? && RUBY_VERSION < "3.3.0" ? [["a", "b"], ["c"]] : [["a", "c"], ["b"]]) expect(call(["a", "b", "c"], 2, group_by: :found)).to eq(result) end context "when passed no group" do it "sort by file size" do expect(File).to receive(:stat).with("a").and_return 1 expect(File).to receive(:stat).with("b").and_return 1 expect(File).to receive(:stat).with("c").and_return 3 call(["a", "b", "c"], 2) end it "sorts by runtime when runtime is available" do expect(ParallelTests::Test::Runner).to receive(:puts).with("Using recorded test runtime") expect(ParallelTests::Test::Runner).to receive(:runtimes).and_return("a" => 1, "b" => 1, "c" => 3) expect(call(["a", "b", "c"], 2)).to eq([["c"], ["a", "b"]]) end it "sorts by filesize when there are no files" do expect(ParallelTests::Test::Runner).to receive(:puts).never expect(ParallelTests::Test::Runner).to receive(:runtimes).and_return({}) expect(call([], 2)).to eq([[], []]) end it "sorts by filesize when runtime is too little" do expect(ParallelTests::Test::Runner).not_to receive(:puts) expect(ParallelTests::Test::Runner).to receive(:runtimes).and_return(["a:1"]) expect(File).to receive(:stat).with("a").and_return 1 expect(File).to receive(:stat).with("b").and_return 1 expect(File).to receive(:stat).with("c").and_return 3 call(["a", "b", "c"], 2) end end context "when passed runtime" do around { |test| Dir.mktmpdir { |dir| Dir.chdir(dir, &test) } } before do ["aaa", "bbb", "ccc", "ddd"].each { |f| File.write(f, f) } FileUtils.mkdir("tmp") end it "fails when there is no log" do expect { call(["aaa"], 3, group_by: :runtime) }.to raise_error(Errno::ENOENT) end it "fails when there is too little log" do File.write("tmp/parallel_runtime_test.log", "xxx:123\nyyy:123\naaa:123") expect { call(["aaa", "bbb", "ccc"], 3, group_by: :runtime) }.to raise_error(ParallelTests::Test::Runner::RuntimeLogTooSmallError) end it "groups a lot of missing files when allow-missing is high" do File.write("tmp/parallel_runtime_test.log", "xxx:123\nyyy:123\naaa:123") call(["aaa", "bbb", "ccc"], 3, group_by: :runtime, allowed_missing_percent: 80) end it "groups when there is enough log" do File.write("tmp/parallel_runtime_test.log", "xxx:123\nbbb:123\naaa:123") call(["aaa", "bbb", "ccc"], 3, group_by: :runtime) end it "groups when test name contains colons" do File.write("tmp/parallel_runtime_test.log", "ccc[1:2:3]:1\nbbb[1:2:3]:2\naaa[1:2:3]:3") expect( call( ["aaa[1:2:3]", "bbb[1:2:3]", "ccc[1:2:3]"], 2, group_by: :runtime ) ).to match_array([["aaa[1:2:3]"], ["bbb[1:2:3]", "ccc[1:2:3]"]]) end it "groups when not even statistic" do File.write("tmp/parallel_runtime_test.log", "aaa:1\nbbb:1\nccc:8") expect(call(["aaa", "bbb", "ccc"], 2, group_by: :runtime)).to match_array([["aaa", "bbb"], ["ccc"]]) end it "groups with average for missing" do File.write("tmp/parallel_runtime_test.log", "xxx:123\nbbb:10\nccc:1") expect(call(["aaa", "bbb", "ccc", "ddd"], 2, group_by: :runtime)).to eq([["bbb", "ccc"], ["aaa", "ddd"]]) end it "groups with unknown-runtime for missing" do File.write("tmp/parallel_runtime_test.log", "xxx:123\nbbb:10\nccc:1") expect( call(["aaa", "bbb", "ccc", "ddd"], 2, group_by: :runtime, unknown_runtime: 0.0) ).to eq([["bbb"], ["aaa", "ccc", "ddd"]]) end it "groups by single_process pattern and then via size" do expect(ParallelTests::Test::Runner).to receive(:runtimes) .and_return({ "aaa" => 5, "bbb" => 2, "ccc" => 1, "ddd" => 1 }) result = call(["aaa", "aaa2", "bbb", "ccc", "ddd"], 3, single_process: [/^a.a/], group_by: :runtime) expect(result).to eq([["aaa", "aaa2"], ["bbb"], ["ccc", "ddd"]]) end it "groups by size and adds isolated separately" do skip if RUBY_PLATFORM == "java" expect(ParallelTests::Test::Runner).to receive(:runtimes) .and_return({ "aaa" => 0, "bbb" => 3, "ccc" => 1, "ddd" => 2 }) result = call( ["aaa", "bbb", "ccc", "ddd", "eee"], 3, isolate: true, single_process: [/^aaa/], group_by: :runtime ) isolated, *groups = result expect(isolated).to eq(["aaa"]) actual = groups.to_set(&:to_set) # both eee and ccs are the same size, so either can be in either group valid_combinations = [ [["bbb", "eee"], ["ccc", "ddd"]].to_set(&:to_set), [["bbb", "ccc"], ["eee", "ddd"]].to_set(&:to_set) ] expect(valid_combinations).to include(actual) end it "groups by size and use specified number of isolation groups" do skip if RUBY_PLATFORM == "java" expect(ParallelTests::Test::Runner).to receive(:runtimes) .and_return({ "aaa1" => 1, "aaa2" => 3, "aaa3" => 2, "bbb" => 3, "ccc" => 1, "ddd" => 2 }) result = call( ["aaa1", "aaa2", "aaa3", "bbb", "ccc", "ddd", "eee"], 4, isolate_count: 2, single_process: [/^aaa/], group_by: :runtime ) isolated_1, isolated_2, *groups = result expect(isolated_1).to eq(["aaa2"]) expect(isolated_2).to eq(["aaa1", "aaa3"]) actual = groups.to_set(&:to_set) # both eee and ccs are the same size, so either can be in either group valid_combinations = [ [["bbb", "eee"], ["ccc", "ddd"]].to_set(&:to_set), [["bbb", "ccc"], ["eee", "ddd"]].to_set(&:to_set) ] expect(valid_combinations).to include(actual) end it 'groups by size and uses specified groups of specs in specific order in specific processes' do skip if RUBY_PLATFORM == "java" expect(ParallelTests::Test::Runner).to receive(:runtimes) .and_return({ "aaa1" => 1, "aaa2" => 1, "aaa3" => 2, "bbb" => 3, "ccc" => 1, "ddd" => 2, "eee" => 1 }) result = call( ["aaa1", "aaa2", "aaa3", "bbb", "ccc", "ddd", "eee"], 4, specify_groups: 'aaa2,aaa1|bbb', group_by: :runtime ) specify_groups_1, specify_groups_2, *groups = result expect(specify_groups_1).to eq(["aaa2", "aaa1"]) expect(specify_groups_2).to eq(["bbb"]) actual = groups.to_set(&:to_set) # both eee and ccs are the same size, so either can be in either group valid_combinations = [ [["aaa3", "ccc"], ["ddd", "eee"]].to_set(&:to_set), [["aaa3", "eee"], ["ddd", "ccc"]].to_set(&:to_set) ] expect(valid_combinations).to include(actual) end end end describe ".find_results" do def call(*args) ParallelTests::Test::Runner.find_results(*args) end it "finds multiple results in test output" do output = <<~OUT Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.4/lib/rake/rake_test_loader Started .............. Finished in 0.145069 seconds. 10 tests, 20 assertions, 0 failures, 0 errors Loaded suite /opt/ruby-enterprise/lib/ruby/gems/1.8/gems/rake-0.8.4/lib/rake/rake_test_loader Started .............. Finished in 0.145069 seconds. 14 tests, 20 assertions, 0 failures, 0 errors OUT expect(call(output)).to eq( [ '10 tests, 20 assertions, 0 failures, 0 errors', '14 tests, 20 assertions, 0 failures, 0 errors' ] ) end it "ignores color-codes" do output = <<~EOF 10 tests, 20 assertions, 0 \e[31mfailures, 0 errors EOF expect(call(output)).to eq(['10 tests, 20 assertions, 0 failures, 0 errors']) end it "splits lines with Windows line separators" do output = "10 tests, 20 assertions, 0 failures, 0 errors\r\n15 tests, 25 assertions, 0 failures, 0 errors" expect(call(output)).to eq( [ "10 tests, 20 assertions, 0 failures, 0 errors", "15 tests, 25 assertions, 0 failures, 0 errors" ] ) end end describe ".find_tests" do def call(*args) ParallelTests::Test::Runner.send(:find_tests, *args) end it "finds test in folders with appended /" do with_files(['b/a_test.rb']) do |root| expect(call(["#{root}/"]).sort).to eq(["#{root}/b/a_test.rb"]) end end it "finds test files nested in symlinked folders" do with_files(['a/a_test.rb', 'b/b_test.rb']) do |root| File.symlink("#{root}/a", "#{root}/b/link") expect(call(["#{root}/b"]).sort).to eq( [ "#{root}/b/b_test.rb", "#{root}/b/link/a_test.rb" ] ) end end it "finds test files but ignores those in symlinked folders" do skip if RUBY_PLATFORM == "java" || Gem.win_platform? with_files(['a/a_test.rb', 'b/b_test.rb']) do |root| File.symlink("#{root}/a", "#{root}/b/link") expect(call(["#{root}/b"], symlinks: false).sort).to eq(["#{root}/b/b_test.rb"]) end end it "finds test files nested in different folders" do with_files(['a/a_test.rb', 'b/b_test.rb', 'c/c_test.rb']) do |root| expect(call(["#{root}/a", "#{root}/b"]).sort).to eq( [ "#{root}/a/a_test.rb", "#{root}/b/b_test.rb" ] ) end end it "only finds tests in folders" do with_files(['a/a_test.rb', 'a/test.rb', 'a/test_helper.rb']) do |root| expect(call(["#{root}/a"]).sort).to eq( [ "#{root}/a/a_test.rb" ] ) end end it "finds tests in nested folders" do with_files(['a/b/c/d/a_test.rb']) do |root| expect(call(["#{root}/a"]).sort).to eq( [ "#{root}/a/b/c/d/a_test.rb" ] ) end end it "does not expand paths" do with_files(['a/x_test.rb']) do |root| Dir.chdir root do expect(call(['a']).sort).to eq( [ "a/x_test.rb" ] ) end end end it "finds test files in folders by pattern" do with_files(['a/x_test.rb', 'a/y_test.rb', 'a/z_test.rb']) do |root| Dir.chdir root do expect(call(["a"], pattern: %r{^a/(y|z)_test}).sort).to eq( [ "a/y_test.rb", "a/z_test.rb" ] ) end end end it "finds test files in folders using suffix and overriding built in suffix" do with_files(['a/x_test.rb', 'a/y_test.rb', 'a/z_other.rb', 'a/x_different.rb']) do |root| Dir.chdir root do expect(call(["a"], suffix: /_(test|other)\.rb$/).sort).to eq( [ "a/x_test.rb", "a/y_test.rb", "a/z_other.rb" ] ) end end end it "doesn't find backup files with the same name as test files" do with_files(['a/x_test.rb', 'a/x_test.rb.bak']) do |root| expect(call(["#{root}/"])).to eq( [ "#{root}/a/x_test.rb" ] ) end end it "finds minispec files" do with_files(['a/x_spec.rb']) do |root| expect(call(["#{root}/"])).to eq( [ "#{root}/a/x_spec.rb" ] ) end end it "finds nothing if I pass nothing" do expect(call(nil)).to eq([]) end it "finds nothing if I pass nothing (empty array)" do expect(call([])).to eq([]) end it "keeps invalid files" do expect(call(['baz'])).to eq(['baz']) end it "discards duplicates" do expect(call(['baz', 'baz'])).to eq(['baz']) end it "keeps duplicates when allow_duplicates" do expect(call(['baz', 'baz'], allow_duplicates: true)).to eq(['baz', 'baz']) end end describe ".summarize_results" do def call(*args) ParallelTests::Test::Runner.summarize_results(*args) end it "adds results" do expect(call(['1 foo 3 bar', '2 foo 5 bar'])).to eq('8 bars, 3 foos') end it "adds results with braces" do expect(call(['1 foo(s) 3 bar(s)', '2 foo 5 bar'])).to eq('8 bars, 3 foos') end it "adds same results with plurals" do expect(call(['1 foo 3 bar', '2 foos 5 bar'])).to eq('8 bars, 3 foos') end it "adds non-similar results" do expect(call(['1 xxx 2 yyy', '1 xxx 2 zzz'])).to eq('2 xxxs, 2 yyys, 2 zzzs') end it "does not pluralize 1" do expect(call(['1 xxx 2 yyy'])).to eq('1 xxx, 2 yyys') end end describe ".execute_command" do def call(*args) ParallelTests::Test::Runner.execute_command(*args) end let(:new_line_char) { Gem.win_platform? ? "\r\n" : "\n" } def capture_output $stdout = StringIO.new $stderr = StringIO.new yield [$stdout.string, $stderr.string] ensure $stdout = STDOUT $stderr = STDERR end def run_with_file(content) ParallelTests.with_pid_file do capture_output do Tempfile.open("xxx") do |f| f.write(content) f.flush yield f.path end end end end it "sets process number to 2 for 1" do run_with_file("puts ENV['TEST_ENV_NUMBER']") do |path| result = call(["ruby", path], 1, 4, {}) expect(result[:stdout].chomp).to eq '2' expect(result[:exit_status]).to eq 0 end end it "sets process number to '' for 0" do run_with_file("puts ENV['TEST_ENV_NUMBER'].inspect") do |path| result = call(["ruby", path], 0, 4, {}) expect(result[:stdout].chomp).to eq '""' expect(result[:exit_status]).to eq 0 end end it "sets process number to 1 for 0 if requested" do run_with_file("puts ENV['TEST_ENV_NUMBER']") do |path| result = call(["ruby", path], 0, 4, first_is_1: true) expect(result[:stdout].chomp).to eq '1' expect(result[:exit_status]).to eq 0 end end it 'sets PARALLEL_TEST_GROUPS so child processes know that they are being run under parallel_tests' do run_with_file("puts ENV['PARALLEL_TEST_GROUPS']") do |path| result = call(["ruby", path], 1, 4, {}) expect(result[:stdout].chomp).to eq('4') expect(result[:exit_status]).to eq(0) end end it "skips reads from stdin" do skip "hangs on normal ruby, works on jruby" unless RUBY_PLATFORM == "java" run_with_file("$stdin.read; puts 123") do |path| result = call(["ruby", path], 1, 2, {}) expect(result).to include( { stdout: "123\n", exit_status: 0 } ) end end it "waits for process to finish" do run_with_file("sleep 0.5; puts 123; sleep 0.5; puts 345") do |path| result = call(["ruby", path], 1, 4, {}) expect(result[:stdout].lines.map(&:chomp)).to eq ['123', '345'] expect(result[:exit_status]).to eq 0 end end it "prints output while running" do skip "too slow" if RUBY_PLATFORM == " java" run_with_file("$stdout.sync = true; puts 123; sleep 0.1; print 345; sleep 0.1; puts 567") do |path| received = +"" allow($stdout).to receive(:print) do |x| received << x.strip end result = call(["ruby", path], 1, 4, {}) expect(received).to eq("123345567") expect(result[:stdout].lines.map(&:chomp)).to eq ['123', '345567'] expect(result[:exit_status]).to eq 0 end end it "works with synced stdout" do run_with_file("$stdout.sync = true; puts 123; sleep 0.1; puts 345") do |path| result = call(["ruby", path], 1, 4, {}) expect(result[:stdout].lines.map(&:chomp)).to eq ['123', '345'] expect(result[:exit_status]).to eq 0 end end it "does not print to stdout with :serialize_stdout" do run_with_file("puts 123") do |path| expect($stdout).not_to receive(:print) result = call(["ruby", path], 1, 4, serialize_stdout: true) expect(result[:stdout].chomp).to eq '123' expect(result[:exit_status]).to eq 0 end end it "adds test env number to stdout with :prefix_output_with_test_env_number" do run_with_file("puts 123") do |path| expect($stdout).to receive(:print).with("[TEST GROUP 2] 123#{new_line_char}") result = call(["ruby", path], 1, 4, prefix_output_with_test_env_number: true) expect(result[:stdout].chomp).to eq '123' expect(result[:exit_status]).to eq 0 end end it "does not add test env number to stdout without :prefix_output_with_test_env_number" do run_with_file("puts 123") do |path| expect($stdout).to receive(:print).with("123#{new_line_char}") result = call(["ruby", path], 1, 4, prefix_output_with_test_env_number: false) expect(result[:stdout].chomp).to eq '123' expect(result[:exit_status]).to eq 0 end end it "returns correct exit status" do run_with_file("puts 123; exit 5") do |path| result = call(["ruby", path], 1, 4, {}) expect(result[:stdout].chomp).to eq '123' expect(result[:exit_status]).to eq 5 end end it "prints each stream to the correct stream" do skip "open3" _out, err = run_with_file("puts 123 ; $stderr.puts 345 ; exit 5") do |path| result = call(["ruby", path], 1, 4, {}) expect(result).to include( stdout: "123\n", exit_status: 5 ) end expect(err).to eq("345\n") end it "uses a lower priority process when the nice option is used", unless: Gem.win_platform? do priority_cmd = "puts Process.getpriority(Process::PRIO_PROCESS, 0)" priority_without_nice = run_with_file(priority_cmd) { |cmd| call(["ruby", cmd], 1, 4, {}) }.first.to_i priority_with_nice = run_with_file(priority_cmd) { |cmd| call(["ruby", cmd], 1, 4, nice: true) }.first.to_i expect(priority_without_nice).to be < priority_with_nice end it "returns command used" do run_with_file("puts 123; exit 5") do |path| result = call(["ruby", path], 1, 4, {}) expect(result).to include(command: ["ruby", path]) end end it "allows using TEST_ENV_NUMBER in arguments" do run_with_file("puts ARGV") do |path| result = call(["ruby", path, "$TEST_ENV_NUMBER"], 1, 4, {}) expect(result[:stdout].chomp).to eq '2' expect(result[:exit_status]).to eq 0 end end describe "rspec seed" do it "includes seed when provided" do run_with_file("puts 'Run options: --seed 555'") do |path| result = call(["ruby", path], 1, 4, {}) expect(result).to include(seed: "555") end end it "seed is nil when not provided" do run_with_file("puts 555") do |path| result = call(["ruby", path], 1, 4, {}) expect(result).to include(seed: nil) end end end end describe ".command_with_seed" do def call(*args) base = ["ruby", "-Ilib:test", "test/minitest/test_minitest_unit.rb"] result = ParallelTests::Test::Runner.command_with_seed([*base, *args], "555") result[base.length..] end it "adds the randomized seed" do expect(call).to eq(["--seed", "555"]) end it "does not duplicate seed" do expect(call("--seed", "123")).to eq(["--seed", "555"]) end it "does not duplicate strange seeds" do expect(call("--seed", "123asdasd")).to eq(["--seed", "555"]) end it "does not match non seeds" do expect(call("--seedling", "123")).to eq(["--seedling", "123", "--seed", "555"]) end end end parallel_tests-5.4.0/spec/parallel_tests/test/runtime_logger_spec.rb000066400000000000000000000024761504331627400260640ustar00rootroot00000000000000# frozen_string_literal: true require 'spec_helper' describe ParallelTests::Test::RuntimeLogger do def run(command) result = IO.popen(command, err: [:child, :out], &:read) raise "FAILED: #{result}" unless $?.success? end def run_tests(repo_root_dir) run ["ruby", "#{repo_root_dir}/bin/parallel_test", "test", "-n", "2"] end it "writes a correct log on minitest-5" do skip if RUBY_PLATFORM == "java" # just too slow ... repo_root = Dir.pwd use_temporary_directory do # setup simple structure FileUtils.mkdir "test" 2.times do |i| File.write("test/#{i}_test.rb", <<-RUBY) require 'minitest/autorun' require 'parallel_tests/test/runtime_logger' class Foo#{i} < Minitest::Test def test_foo sleep 0.5 assert true end end class Bar#{i} < Minitest::Test def test_foo sleep 0.25111 assert true end end RUBY end run_tests(repo_root) # log looking good ? lines = File.read("tmp/parallel_runtime_test.log").split("\n").sort.map { |x| x.sub(/\d$/, "") } expect(lines).to eq( [ "test/0_test.rb:0.7", "test/1_test.rb:0.7" ] ) end end end parallel_tests-5.4.0/spec/parallel_tests_spec.rb000066400000000000000000000151161504331627400220560ustar00rootroot00000000000000# frozen_string_literal: true require "spec_helper" describe ParallelTests do describe ".determine_number_of_processes" do before do allow(Parallel).to receive(:processor_count).and_return 20 end def call(count) ParallelTests.determine_number_of_processes(count) end it "uses the given count if set" do expect(call('5')).to eq(5) end it "uses the processor count from Parallel" do expect(call(nil)).to eq(20) end it "uses the processor count from ENV before Parallel" do ENV['PARALLEL_TEST_PROCESSORS'] = '22' expect(call(nil)).to eq(22) end it "does not use blank count" do expect(call(' ')).to eq(20) end it "does not use blank env" do ENV['PARALLEL_TEST_PROCESSORS'] = ' ' expect(call(nil)).to eq(20) end end describe ".determine_multiple" do let(:default_multiple) { 1.0 } before do allow(Parallel).to receive(:multiple).and_return(default_multiple) end after do ENV.delete('PARALLEL_TEST_MULTIPLY_PROCESSES') end def call(multiple) ParallelTests.determine_multiple(multiple) end it "uses the given multiple if set" do expect(call('.5')).to eq(0.5) end it "uses the processor multiple from Parallel" do expect(call(nil)).to eq(default_multiple) end it "uses the processor multiple from ENV before Parallel" do ENV['PARALLEL_TEST_MULTIPLY_PROCESSES'] = '0.75' expect(call(nil)).to eq(0.75) end it "does not use blank multiple" do expect(call(' ')).to eq(default_multiple) end it "does not use blank env" do ENV['PARALLEL_TEST_MULTIPLY_PROCESSES'] = ' ' expect(call(nil)).to eq(default_multiple) end end describe ".bundler_enabled?" do before do allow(Object).to receive(:const_defined?).with(:Bundler).and_return false end it "is false" do use_temporary_directory do expect(ParallelTests.send(:bundler_enabled?)).to eq(false) end end it "is true when there is a constant called Bundler" do use_temporary_directory do allow(Object).to receive(:const_defined?).with(:Bundler).and_return true expect(ParallelTests.send(:bundler_enabled?)).to eq(true) end end it "is true when there is a Gemfile" do use_temporary_directory do FileUtils.touch("Gemfile") expect(ParallelTests.send(:bundler_enabled?)).to eq(true) end end it "is true when there is a Gemfile in the parent directory" do use_temporary_directory do FileUtils.mkdir "nested" Dir.chdir "nested" do FileUtils.touch(File.join("..", "Gemfile")) expect(ParallelTests.send(:bundler_enabled?)).to eq(true) end end end end describe ".wait_for_other_processes_to_finish" do around do |example| ParallelTests.with_pid_file do example.run end end def with_running_processes(count, wait = 0.2) count.times { |x| ParallelTests.pids.add(x) } sleep 0.1 yield ensure sleep wait # make sure the threads have finished end it "does not wait if not run in parallel" do expect(ParallelTests).not_to receive(:sleep) ParallelTests.wait_for_other_processes_to_finish end it "stops if only itself is running" do ParallelTests.pids.add(123) expect(ParallelTests).not_to receive(:sleep) with_running_processes(1) do ParallelTests.wait_for_other_processes_to_finish end end it "waits for other processes to finish" do skip if RUBY_PLATFORM == "java" ENV["TEST_ENV_NUMBER"] = "2" counter = 0 allow(ParallelTests).to receive(:sleep) do sleep 0.1 ParallelTests.pids.delete(1) if counter > 3 counter += 1 end with_running_processes(2, 0.6) do ParallelTests.wait_for_other_processes_to_finish end expect(counter).to be >= 2 end end describe ".number_of_running_processes" do around do |example| ParallelTests.with_pid_file do example.run end end it "is 0 for nothing" do expect(ParallelTests.number_of_running_processes).to eq(0) end it "is 2 when 2 are running" do wait = 0.2 2.times { |_x| ParallelTests.pids.add(123) } sleep wait / 2 expect(ParallelTests.number_of_running_processes).to eq(2) sleep wait end end describe ".first_process?" do it "is first if no env is set" do expect(ParallelTests.first_process?).to eq(true) end it "is first if env is set to blank" do ENV["TEST_ENV_NUMBER"] = "" expect(ParallelTests.first_process?).to eq(true) end it "is first if env is set to 1" do ENV["TEST_ENV_NUMBER"] = "1" expect(ParallelTests.first_process?).to eq(true) end it "is not first if env is set to something else" do ENV["TEST_ENV_NUMBER"] = "2" expect(ParallelTests.first_process?).to eq(false) end end describe ".last_process?" do it "is last if no envs are set" do expect(ParallelTests.last_process?).to eq(true) end it "is last if envs are set to blank" do ENV["TEST_ENV_NUMBER"] = "" ENV["PARALLEL_TEST_GROUPS"] = "" expect(ParallelTests.last_process?).to eq(true) end it "is last if TEST_ENV_NUMBER is set to PARALLEL_TEST_GROUPS" do ENV["TEST_ENV_NUMBER"] = "4" ENV["PARALLEL_TEST_GROUPS"] = "4" expect(ParallelTests.last_process?).to eq(true) end it "is not last if TEST_ENV_NUMBER is set to else" do ENV["TEST_ENV_NUMBER"] = "2" ENV["PARALLEL_TEST_GROUPS"] = "4" expect(ParallelTests.first_process?).to eq(false) end end describe ".stop_all_processes" do # Process.kill on Windows doesn't work as expected. It kills all process group instead of just one process. it 'kills the running child process', unless: Gem.win_platform? do ParallelTests.with_pid_file do Thread.new do ParallelTests::Test::Runner.execute_command(['sleep', '3'], 1, 1, {}) end sleep(0.2) expect(ParallelTests.pids.count).to eq(1) ParallelTests.stop_all_processes sleep(0.2) expect(ParallelTests.pids.count).to eq(0) end end it "doesn't fail if the pid has already been killed", unless: Gem.win_platform? do ParallelTests.with_pid_file do ParallelTests.pids.add(1234) expect { ParallelTests.stop_all_processes }.not_to raise_error end end end it "has a version" do expect(ParallelTests::VERSION).to match(/^\d+\.\d+\.\d+/) end end parallel_tests-5.4.0/spec/rails_spec.rb000066400000000000000000000031641504331627400201520ustar00rootroot00000000000000# frozen_string_literal: true require 'fileutils' require 'spec_helper' describe 'rails' do let(:test_timeout) { 800 } # this can take very long on fresh bundle ... def run(command, options = {}) result = IO.popen(options.fetch(:environment, {}), command, err: [:child, :out], &:read) raise "FAILED #{command}\n#{result}" if $?.success? == !!options[:fail] result end Dir["spec/fixtures/rails*"].each do |folder| rails = File.basename(folder) it "can create and run #{rails}" do skip 'ruby 3.1 is not supported by rails 72' if RUBY_VERSION < "3.2.0" && rails == "rails72" skip 'rails fixtures are not set up for java' if RUBY_PLATFORM == "java" Dir.chdir("spec/fixtures/#{rails}") do Bundler.with_unbundled_env do ENV.delete "RUBYLIB" run ["bundle", "config", "--local", "path", "vendor/bundle"] run ["bundle", "config", "--local", "frozen", "true"] run ["bundle", "install"] FileUtils.rm_f(Dir['db/*.sqlite3']) run ["bundle", "exec", "rake", "db:setup", "parallel:create"] # Also test the case where the DBs need to be dropped run ["bundle", "exec", "rake", "parallel:drop", "parallel:create"] run ["bundle", "exec", "rake", "parallel:setup"] run ["bundle", "exec", "rake", "parallel:prepare"] run ["bundle", "exec", "rails", "runner", "User.create"], environment: { 'RAILS_ENV' => 'test' } # pollute the db out = run ["bundle", "exec", "rake", "parallel:prepare", "parallel:test"] expect(out).to match(/ 2 (tests|runs)/) end end end end end parallel_tests-5.4.0/spec/spec_helper.rb000066400000000000000000000153021504331627400203140ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler/setup' require 'tempfile' require 'tmpdir' require 'timeout' require 'parallel_tests' require 'parallel_tests/test/runtime_logger' require 'parallel_tests/rspec/runtime_logger' require 'parallel_tests/rspec/summary_logger' require 'parallel_tests/rspec/verbose_logger' String.class_eval do def strip_heredoc gsub(/^#{self[/^\s*/]}/, '') end end OutputLogger = Struct.new(:output) do attr_reader :flock, :flush def puts(s = nil) output << "#{s}\n" end def print(s = nil) output << s.to_s end end module SpecHelper def mocked_process StringIO.new end def size_of(group) group.map { |test| File.stat(test).size }.inject(:+) end def use_temporary_directory(&block) Dir.mktmpdir { |dir| Dir.chdir(dir, &block) } end def with_files(files) Dir.mktmpdir do |root| files.each do |file| parent = "#{root}/#{File.dirname(file)}" FileUtils.mkpath(parent) FileUtils.touch(File.join(root, file)) end yield root end end def should_run_with(command, *args) expect(ParallelTests::Test::Runner).to receive(:execute_command) do |a, _b, _c, _d| expect(a.first(command.length)).to eq(command) args.each do |arg| expect(a).to include(arg) end end end def should_not_run_with(arg) expect(ParallelTests::Test::Runner).to receive(:execute_command) do |a, _b, _c, _d| expect(a).to_not include(arg) end end end module SharedExamples def test_tests_in_groups(klass, suffix) describe ".tests_in_groups" do let(:log) { klass.runtime_log } let(:test_root) { "temp" } around { |test| use_temporary_directory(&test) } before do FileUtils.mkdir test_root @files = [0, 1, 2, 3, 4, 5, 6, 7].map { |i| "#{test_root}/x#{i}#{suffix}" } @files.each { |file| File.write(file, 'x' * 100) } FileUtils.mkdir_p File.dirname(log) end def setup_runtime_log # rubocop:disable Lint/NestedMethodDefinition File.open(log, 'w') do |f| @files[1..].each { |file| f.puts "#{file}:#{@files.index(file)}" } f.puts "#{@files[0]}:10" end end it "groups when given an array of files" do list_of_files = Dir["#{test_root}/**/*#{suffix}"] result = list_of_files.dup klass.send(:sort_by_filesize, result) expect(result).to match_array(list_of_files.map { |file| [file, File.stat(file).size] }) end it "finds all tests" do found = klass.tests_in_groups([test_root], 1) all = [Dir["#{test_root}/**/*#{suffix}"]] expect(found.flatten - all.flatten).to eq([]) end it "partitions them into groups by equal size" do groups = klass.tests_in_groups([test_root], 2) expect(groups.map { |g| size_of(g) }).to eq([400, 400]) end it 'should partition correctly with a group size of 4' do groups = klass.tests_in_groups([test_root], 4) expect(groups.map { |g| size_of(g) }).to eq([200, 200, 200, 200]) end it 'should partition correctly with an uneven group size' do groups = klass.tests_in_groups([test_root], 3) expect(groups.map { |g| size_of(g) }).to match_array([300, 300, 200]) end it "partitions by runtime when runtime-data is available" do allow(klass).to receive(:puts) setup_runtime_log groups = klass.tests_in_groups([test_root], 2) expect(groups.size).to eq(2) # 10 + 1 + 3 + 5 = 19 expect(groups[0]).to eq([@files[0], @files[1], @files[3], @files[5]]) # 2 + 4 + 6 + 7 = 19 expect(groups[1]).to eq([@files[2], @files[4], @files[6], @files[7]]) end it 'partitions from custom runtime-data location' do allow(klass).to receive(:puts) allow_any_instance_of(klass).to receive(:runtime_log).and_return('tmp/custom_runtime.log') setup_runtime_log groups = klass.tests_in_groups([test_root], 2, runtime_log: log) expect(groups.size).to eq(2) # 10 + 1 + 3 + 5 = 19 expect(groups[0]).to eq([@files[0], @files[1], @files[3], @files[5]]) # 2 + 4 + 6 + 7 = 19 expect(groups[1]).to eq([@files[2], @files[4], @files[6], @files[7]]) end it "alpha-sorts partitions when runtime-data is available" do allow(klass).to receive(:puts) setup_runtime_log groups = klass.tests_in_groups([test_root], 2) expect(groups.size).to eq(2) expect(groups[0]).to eq(groups[0].sort) expect(groups[1]).to eq(groups[1].sort) end it "partitions by round-robin when not sorting" do files = ["file1.rb", "file2.rb", "file3.rb", "file4.rb"] expect(klass).to receive(:find_tests).and_return(files) groups = klass.tests_in_groups(files, 2, group_by: :found).sort expect(groups[0]).to eq(["file1.rb", "file3.rb"]) expect(groups[1]).to eq(["file2.rb", "file4.rb"]) end it "alpha-sorts partitions when not sorting by runtime" do files = ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm'] expect(klass).to receive(:find_tests).and_return(files) groups = klass.tests_in_groups(files, 2, group_by: :found).sort expect(groups[0]).to eq(groups[0].sort) expect(groups[1]).to eq(groups[1].sort) end end end end RSpec::Matchers.define :include_exactly_times do |expected, times| match do |actual| actual.scan(expected).size == times end failure_message do |actual| action = (expected.is_a?(String) ? "to contain '#{expected}'" : "to match /#{expected}/") outcome = (expected.is_a?(String) ? "appears" : "matches") <<~FAILURE expected the following string: """" #{actual} """" #{action} #{times} time(s), but it #{outcome} #{actual.scan(expected).size} time(s) FAILURE end end TestTookTooLong = Class.new(Timeout::Error) RSpec.configure do |config| config.filter_run focus: true config.run_all_when_everything_filtered = true config.include SpecHelper config.extend SharedExamples config.raise_errors_for_deprecations! # sometimes stuff hangs -> do not hang everything # NOTE: the timeout error can sometimes swallow errors, comment it out if you run into trouble config.include( Module.new do def test_timeout 30 end end ) config.around do |example| Timeout.timeout(test_timeout, TestTookTooLong, &example) end config.after do ENV.delete "PARALLEL_TEST_GROUPS" ENV.delete "PARALLEL_TEST_PROCESSORS" ENV.delete "PARALLEL_TESTS_EXECUTABLE" ENV.delete "TEST_ENV_NUMBER" ENV.delete "RAILS_ENV" end end