├── .github └── workflows │ ├── dynamic-security.yml │ └── main.yml ├── .gitignore ├── CONTRIBUTING.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── SECURITY.md ├── appraisal.gemspec ├── bin ├── bundle └── rspec ├── exe └── appraisal ├── lib ├── appraisal.rb └── appraisal │ ├── appraisal.rb │ ├── appraisal_file.rb │ ├── bundler_dsl.rb │ ├── cli.rb │ ├── command.rb │ ├── conditional.rb │ ├── customize.rb │ ├── dependency.rb │ ├── dependency_list.rb │ ├── errors.rb │ ├── gemfile.rb │ ├── gemspec.rb │ ├── git.rb │ ├── group.rb │ ├── path.rb │ ├── platform.rb │ ├── source.rb │ ├── task.rb │ ├── utils.rb │ └── version.rb └── spec ├── acceptance ├── appraisals_file_bundler_dsl_compatibility_spec.rb ├── bundle_with_custom_path_spec.rb ├── bundle_without_spec.rb ├── cli │ ├── clean_spec.rb │ ├── generate_spec.rb │ ├── help_spec.rb │ ├── install_spec.rb │ ├── list_spec.rb │ ├── run_spec.rb │ ├── update_spec.rb │ ├── version_spec.rb │ └── with_no_arguments_spec.rb ├── gemfile_dsl_compatibility_spec.rb └── gemspec_spec.rb ├── appraisal ├── appraisal_file_spec.rb ├── appraisal_spec.rb ├── customize_spec.rb ├── dependency_list_spec.rb ├── gemfile_spec.rb └── utils_spec.rb ├── spec_helper.rb └── support ├── acceptance_test_helpers.rb ├── dependency_helpers.rb └── stream_helpers.rb /.github/workflows/dynamic-security.yml: -------------------------------------------------------------------------------- 1 | name: update-security 2 | 3 | on: 4 | push: 5 | paths: 6 | - SECURITY.md 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-security: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | 4 | on: 5 | push: 6 | branches: 7 | - '**' 8 | pull_request: 9 | branches: 10 | - '**' 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | ruby: 20 | - '3.3' 21 | - '3.2' 22 | - '3.1' 23 | - '3.0' 24 | - 'head' 25 | - jruby 26 | - jruby-head 27 | - truffleruby 28 | - truffleruby-head 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | 33 | - name: Setup Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{ matrix.ruby }} 37 | 38 | - name: Update bundler 39 | env: 40 | RUBY_VERSION: ${{ matrix.ruby }} 41 | run: | 42 | case ${RUBY_VERSION} in 43 | truffleruby|truffleruby-head) 44 | gem install bundler -v 2.5.18 45 | ;; 46 | 47 | *) 48 | gem update --system 49 | ;; 50 | esac 51 | 52 | bundle install 53 | 54 | - name: Run tests 55 | run: bin/rspec 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | tmp 3 | pkg 4 | .swo 5 | *~ 6 | *.gem 7 | Gemfile.lock 8 | .bundle 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests from everyone. By participating in this project, you agree 4 | to abide by the thoughtbot [code of conduct]. 5 | 6 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 7 | 8 | 1. Fork the repo. 9 | 10 | 2. Run the tests. We only take pull requests with passing tests, and it's great 11 | to know that you have a clean slate: `bundle && rake` 12 | 13 | 3. Add a test for your change. Only refactoring and documentation changes 14 | require no new tests. If you are adding functionality or fixing a bug, we need 15 | a test! 16 | 17 | 4. Make the test pass. 18 | 19 | 5. Push to your fork and submit a pull request. 20 | 21 | At this point you're waiting on us. We like to at least comment on, if not 22 | accept, pull requests within three business days (and, typically, one business 23 | day). We may suggest some changes or improvements or alternatives. 24 | 25 | Some things that will increase the chance that your pull request is accepted, 26 | taken straight from the Ruby on Rails guide: 27 | 28 | * Use Rails idioms and helpers 29 | * Include tests that fail without your code, and pass with it 30 | * Update the documentation, the surrounding one, examples elsewhere, guides, 31 | whatever is affected by your contribution 32 | 33 | Syntax: 34 | 35 | * Two spaces, no tabs. 36 | * No trailing whitespace. Blank lines should not have any space. 37 | * Prefer &&/|| over and/or. 38 | * MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 39 | * a = b and not a=b. 40 | * Follow the conventions you see used in the source already. 41 | 42 | And in case we didn't emphasize it enough: we love tests! 43 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | # This here to make sure appraisal works with Rails 3.0.0. 6 | gem "thor", "~> 0.14.0" 7 | 8 | group :development, :test do 9 | gem "activesupport", ">= 3.2.21" 10 | gem "rspec", "~> 3.0" 11 | end 12 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2013 thoughtbot, inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Appraisal 2 | ========= 3 | 4 | Find out what your Ruby gems are worth. 5 | 6 | Synopsis 7 | -------- 8 | 9 | Appraisal integrates with bundler and rake to test your library against 10 | different versions of dependencies in repeatable scenarios called "appraisals." 11 | Appraisal is designed to make it easy to check for regressions in your library 12 | without interfering with day-to-day development using Bundler. 13 | 14 | Installation 15 | ------------ 16 | 17 | In your package's `.gemspec`: 18 | 19 | s.add_development_dependency "appraisal" 20 | 21 | Note that gems must be bundled in the global namespace. Bundling gems to a 22 | local location or vendoring plugins is not supported. If you do not want to 23 | pollute the global namespace, one alternative is 24 | [RVM's Gemsets](http://rvm.io/gemsets). 25 | 26 | Setup 27 | ----- 28 | 29 | Setting up appraisal requires an `Appraisals` file (similar to a `Gemfile`) in 30 | your project root, named "Appraisals" (note the case), and some slight changes 31 | to your project's `Rakefile`. 32 | 33 | An `Appraisals` file consists of several appraisal definitions. An appraisal 34 | definition is simply a list of gem dependencies. For example, to test with a 35 | few versions of Rails: 36 | 37 | appraise "rails-3" do 38 | gem "rails", "3.2.14" 39 | end 40 | 41 | appraise "rails-4" do 42 | gem "rails", "4.0.0" 43 | end 44 | 45 | The dependencies in your `Appraisals` file are combined with dependencies in 46 | your `Gemfile`, so you don't need to repeat anything that's the same for each 47 | appraisal. If something is specified in both the Gemfile and an appraisal, the 48 | version from the appraisal takes precedence. 49 | 50 | Usage 51 | ----- 52 | 53 | Once you've configured the appraisals you want to use, you need to install the 54 | dependencies for each appraisal: 55 | 56 | $ bundle exec appraisal install 57 | 58 | This will resolve, install, and lock the dependencies for that appraisal using 59 | bundler. Once you have your dependencies set up, you can run any command in a 60 | single appraisal: 61 | 62 | $ bundle exec appraisal rails-3 rake test 63 | 64 | This will run `rake test` using the dependencies configured for Rails 3. You can 65 | also run each appraisal in turn: 66 | 67 | $ bundle exec appraisal rake test 68 | 69 | If you want to use only the dependencies from your Gemfile, just run `rake 70 | test` as normal. This allows you to keep running with the latest versions of 71 | your dependencies in quick test runs, but keep running the tests in older 72 | versions to check for regressions. 73 | 74 | In the case that you want to run all the appraisals by default when you run 75 | `rake`, you can override your default Rake task by put this into your Rakefile: 76 | 77 | if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"] 78 | task :default => :appraisal 79 | end 80 | 81 | (Appraisal sets `APPRAISAL_INITIALIZED` environment variable when it runs your 82 | process. We put a check here to ensure that `appraisal rake` command should run 83 | your real default task, which usually is your `test` task.) 84 | 85 | Note that this may conflict with your CI setup if you decide to split the test 86 | into multiple processes by Appraisal and you are using `rake` to run tests by 87 | default. 88 | 89 | ### Commands 90 | 91 | ```bash 92 | appraisal clean # Remove all generated gemfiles and lockfiles from gemfiles folder 93 | appraisal generate # Generate a gemfile for each appraisal 94 | appraisal help [COMMAND] # Describe available commands or one specific command 95 | appraisal install # Resolve and install dependencies for each appraisal 96 | appraisal list # List the names of the defined appraisals 97 | appraisal update [LIST_OF_GEMS] # Remove all generated gemfiles and lockfiles, resolve, and install dependencies again 98 | appraisal version # Display the version and exit 99 | ``` 100 | 101 | Under the hood 102 | -------------- 103 | 104 | Running `appraisal install` generates a Gemfile for each appraisal by combining 105 | your root Gemfile with the specific requirements for each appraisal. These are 106 | stored in the `gemfiles` directory, and should be added to version control to 107 | ensure that the same versions are always used. 108 | 109 | When you prefix a command with `appraisal`, the command is run with the 110 | appropriate Gemfile for that appraisal, ensuring the correct dependencies 111 | are used. 112 | 113 | Removing Gems using Appraisal 114 | ------- 115 | 116 | It is common while managing multiple Gemfiles for dependencies to become deprecated and no 117 | longer necessary, meaning they need to be removed from the Gemfile for a specific `appraisal`. 118 | To do this, use the `remove_gem` declaration within the necessary `appraise` block in your 119 | `Appraisals` file. 120 | 121 | ### Example Usage 122 | 123 | **Gemfile** 124 | ```ruby 125 | gem 'rails', '~> 4.2' 126 | 127 | group :test do 128 | gem 'rspec', '~> 4.0' 129 | gem 'test_after_commit' 130 | end 131 | ``` 132 | 133 | **Appraisals** 134 | ```ruby 135 | appraise 'rails-5' do 136 | gem 'rails', '~> 5.2' 137 | 138 | group :test do 139 | remove_gem 'test_after_commit' 140 | end 141 | end 142 | ``` 143 | 144 | Using the `Appraisals` file defined above, this is what the resulting `Gemfile` will look like: 145 | ```ruby 146 | gem 'rails', '~> 5.2' 147 | 148 | group :test do 149 | gem 'rspec', '~> 4.0' 150 | end 151 | ``` 152 | 153 | Customization 154 | ------------- 155 | 156 | It is possible to customize the generated Gemfiles by adding a `customize_gemfiles` block to 157 | your `Appraisals` file. The block must contain a hash of key/value pairs. Currently supported 158 | customizations include: 159 | - heading: a string that by default adds "# This file was generated by Appraisal" to the top of each Gemfile, (the string will be commented for you) 160 | - single_quotes: a boolean that controls if strings are single quoted in each Gemfile, defaults to false 161 | 162 | You can also provide variables for substitution in the heading, based on each appraisal. Currently supported variables: 163 | - `%{appraisal}`: Becomes the name of each appraisal, e.g. `rails-3` 164 | - `%{gemfile}`: Becomes the filename of each gemfile, e.g. `rails-3.gemfile` 165 | - `%{gemfile_path}`: Becomes the full path of each gemfile, e.g. `/path/to/project/gemfiles/rails-3.gemfile` 166 | - `%{lockfile}`: Becomes the filename of each lockfile, e.g. `rails-3.gemfile.lock` 167 | - `%{lockfile_path}`: Becomes the full path of each lockfile, e.g. `/path/to/project/gemfiles/rails-3.gemfile.lock` 168 | - `%{relative_gemfile_path}`: Becomes the relative path of each gemfile, e.g. `gemfiles/rails-3.gemfile` 169 | - `%{relative_lockfile_path}`: Becomes the relative path of each lockfile, e.g. `gemfiles/rails-3.gemfile.lock` 170 | 171 | ### Example Usage 172 | 173 | **Appraisals** 174 | ```ruby 175 | customize_gemfiles do 176 | { 177 | single_quotes: true, 178 | heading: <<~HEADING 179 | frozen_string_literal: true 180 | 181 | `%{gemfile}` has been generated by Appraisal, do NOT modify it or `%{lockfile}` directly! 182 | Make the changes to the "%{appraisal}" block in `Appraisals` instead. See the conventions at https://example.com/ 183 | HEADING 184 | } 185 | end 186 | 187 | appraise "rails-3" do 188 | gem "rails", "3.2.14" 189 | end 190 | ``` 191 | 192 | Using the `Appraisals` file defined above, this is what the resulting `Gemfile` will look like: 193 | ```ruby 194 | # frozen_string_literal: true 195 | 196 | # `rails-3.gemfile` has been generated by Appraisal, do NOT modify it or `rails-3.gemfile.lock` directly! 197 | # Make the changes to the "rails-3" block in `Appraisals` instead. See the conventions at https://example.com/ 198 | 199 | gem 'rails', '3.2.14' 200 | ``` 201 | 202 | Version Control 203 | --------------- 204 | 205 | When using Appraisal, we recommend you check in the Gemfiles that Appraisal 206 | generates within the gemfiles directory, but exclude the lockfiles there 207 | (`*.gemfile.lock`.) The Gemfiles are useful when running your tests against a 208 | continuous integration server. 209 | 210 | Circle CI Integration 211 | --------------------- 212 | 213 | In Circle CI you can override the default testing behaviour to customize your 214 | testing. Using this feature you can configure appraisal to execute your tests. 215 | 216 | In order to this you can put the following configuration in your circle.yml file: 217 | 218 | ```yml 219 | dependencies: 220 | post: 221 | - bundle exec appraisal install 222 | test: 223 | pre: 224 | - bundle exec appraisal rake db:create 225 | - bundle exec appraisal rake db:migrate 226 | override: 227 | - bundle exec appraisal rspec 228 | ``` 229 | 230 | Notice that we are running an rspec suite. You can customize your testing 231 | command in the `override` section and use your favourite one. 232 | 233 | Credits 234 | ------- 235 | 236 | ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg) 237 | 238 | Appraisal is maintained and funded by [thoughtbot, inc][thoughtbot] 239 | 240 | Thank you to all [the contributors][contributors] 241 | 242 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 243 | 244 | [thoughtbot]: http://thoughtbot.com/community 245 | [contributors]: https://github.com/thoughtbot/appraisal/contributors 246 | 247 | License 248 | ------- 249 | 250 | Appraisal is Copyright © 2010-2013 Joe Ferris and thoughtbot, inc. It is free 251 | software, and may be redistributed under the terms specified in the MIT-LICENSE 252 | file. 253 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "bundler/gem_tasks" 5 | require "rspec/core/rake_task" 6 | 7 | RSpec::Core::RakeTask.new do |t| 8 | t.pattern = "spec/**/*_spec.rb" 9 | t.ruby_opts = %w[-w] 10 | t.verbose = false 11 | end 12 | 13 | desc "Default: run the rspec examples" 14 | task default: [:spec] 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # Security Policy 3 | 4 | ## Supported Versions 5 | 6 | Only the the latest version of this project is supported at a given time. If 7 | you find a security issue with an older version, please try updating to the 8 | latest version first. 9 | 10 | If for some reason you can't update to the latest version, please let us know 11 | your reasons so that we can have a better understanding of your situation. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | For security inquiries or vulnerability reports, visit 16 | . 17 | 18 | If you have any suggestions to improve this policy, visit . 19 | 20 | 21 | -------------------------------------------------------------------------------- /appraisal.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/appraisal/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "appraisal" 7 | s.version = Appraisal::VERSION.dup 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Joe Ferris", "Prem Sichanugrist"] 10 | s.email = ["jferris@thoughtbot.com", "prem@thoughtbot.com"] 11 | s.homepage = "http://github.com/thoughtbot/appraisal" 12 | s.summary = "Find out what your Ruby gems are worth" 13 | s.description = 'Appraisal integrates with bundler and rake to test your library against different versions of dependencies in repeatable scenarios called "appraisals."' 14 | s.license = "MIT" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) } 19 | s.bindir = "exe" 20 | 21 | s.required_ruby_version = ">= 2.3.0" 22 | 23 | s.add_dependency("rake") 24 | s.add_dependency("bundler") 25 | s.add_dependency("thor", ">= 0.14.0") 26 | end 27 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../Gemfile", __dir__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || 66 | cli_arg_version || 67 | bundler_requirement_for(lockfile_version) 68 | end 69 | 70 | def bundler_requirement_for(version) 71 | return "#{Gem::Requirement.default}.a" unless version 72 | 73 | bundler_gem_version = Gem::Version.new(version) 74 | 75 | bundler_gem_version.approximate_recommendation 76 | end 77 | 78 | def load_bundler! 79 | ENV["BUNDLE_GEMFILE"] ||= gemfile 80 | 81 | activate_bundler 82 | end 83 | 84 | def activate_bundler 85 | gem_error = activation_error_handling do 86 | gem "bundler", bundler_requirement 87 | end 88 | return if gem_error.nil? 89 | require_error = activation_error_handling do 90 | require "bundler/version" 91 | end 92 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 93 | 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}'`" 94 | exit 42 95 | end 96 | 97 | def activation_error_handling 98 | yield 99 | nil 100 | rescue StandardError, LoadError => e 101 | e 102 | end 103 | end 104 | 105 | m.load_bundler! 106 | 107 | if m.invoked_as_script? 108 | load Gem.bin_path("bundler", "bundle") 109 | end 110 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rspec-core", "rspec") 28 | -------------------------------------------------------------------------------- /exe/appraisal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "rubygems" 5 | require "bundler/setup" 6 | require "appraisal" 7 | require "appraisal/cli" 8 | 9 | begin 10 | Appraisal::CLI.start 11 | rescue Appraisal::AppraisalsNotFound => e 12 | puts e.message 13 | exit 127 14 | end 15 | -------------------------------------------------------------------------------- /lib/appraisal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/version" 4 | require "appraisal/task" 5 | 6 | Appraisal::Task.new 7 | -------------------------------------------------------------------------------- /lib/appraisal/appraisal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/gemfile" 4 | require "appraisal/command" 5 | require "appraisal/customize" 6 | require "appraisal/utils" 7 | require "fileutils" 8 | require "pathname" 9 | 10 | module Appraisal 11 | # Represents one appraisal and its dependencies 12 | class Appraisal 13 | DEFAULT_INSTALL_OPTIONS = { "jobs" => 1 }.freeze 14 | 15 | attr_reader :name, :gemfile 16 | 17 | def initialize(name, source_gemfile) 18 | @name = name 19 | @gemfile = source_gemfile.dup 20 | end 21 | 22 | def gem(*args) 23 | gemfile.gem(*args) 24 | end 25 | 26 | def remove_gem(*args) 27 | gemfile.remove_gem(*args) 28 | end 29 | 30 | def source(*args, &block) 31 | gemfile.source(*args, &block) 32 | end 33 | 34 | def ruby(*args) 35 | gemfile.ruby(*args) 36 | end 37 | 38 | def git(*args, &block) 39 | gemfile.git(*args, &block) 40 | end 41 | 42 | def path(*args, &block) 43 | gemfile.path(*args, &block) 44 | end 45 | 46 | def group(*args, &block) 47 | gemfile.group(*args, &block) 48 | end 49 | 50 | def install_if(*args, &block) 51 | gemfile.install_if(*args, &block) 52 | end 53 | 54 | def platforms(*args, &block) 55 | gemfile.platforms(*args, &block) 56 | end 57 | 58 | def gemspec(options = {}) 59 | gemfile.gemspec(options) 60 | end 61 | 62 | def git_source(*args, &block) 63 | gemfile.git_source(*args, &block) 64 | end 65 | 66 | def write_gemfile 67 | File.open(gemfile_path, "w") do |file| 68 | signature = 69 | Customize.heading(self) || "This file was generated by Appraisal" 70 | file.puts([comment_lines(signature), quoted_gemfile].join("\n\n")) 71 | end 72 | end 73 | 74 | def install(options = {}) 75 | commands = [install_command(options).join(" ")] 76 | 77 | if options["without"].nil? || options["without"].empty? 78 | commands.unshift(check_command.join(" ")) 79 | end 80 | 81 | command = commands.join(" || ") 82 | 83 | if Bundler.settings[:path] 84 | env = { "BUNDLE_DISABLE_SHARED_GEMS" => "1" } 85 | Command.new(command, env: env).run 86 | else 87 | Command.new(command).run 88 | end 89 | end 90 | 91 | def update(gems = []) 92 | Command.new(update_command(gems), gemfile: gemfile_path).run 93 | end 94 | 95 | def gemfile_path 96 | unless gemfile_root.exist? 97 | gemfile_root.mkdir 98 | end 99 | 100 | gemfile_root.join(gemfile_name).to_s 101 | end 102 | 103 | def relative_gemfile_path 104 | File.join("gemfiles", gemfile_name) 105 | end 106 | 107 | def relativize 108 | current_directory = Pathname.new(Dir.pwd) 109 | relative_path = current_directory.relative_path_from(gemfile_root).cleanpath 110 | lockfile_content = File.read(lockfile_path) 111 | 112 | File.open(lockfile_path, "w") do |file| 113 | file.write lockfile_content.gsub( 114 | / #{current_directory}/, 115 | " #{relative_path}" 116 | ) 117 | end 118 | end 119 | 120 | private 121 | 122 | def check_command 123 | gemfile_option = "--gemfile='#{gemfile_path}'" 124 | ["bundle", "check", gemfile_option] 125 | end 126 | 127 | def install_command(options = {}) 128 | gemfile_option = "--gemfile='#{gemfile_path}'" 129 | ["bundle", "install", gemfile_option, bundle_options(options)].compact 130 | end 131 | 132 | def update_command(gems) 133 | ["bundle", "update", *gems].compact 134 | end 135 | 136 | def gemfile_root 137 | project_root + "gemfiles" 138 | end 139 | 140 | def project_root 141 | Pathname.new(Dir.pwd) 142 | end 143 | 144 | def gemfile_name 145 | "#{clean_name}.gemfile" 146 | end 147 | 148 | def lockfile_path 149 | "#{gemfile_path}.lock" 150 | end 151 | 152 | def clean_name 153 | name.gsub(/[^\w\.]/, "_") 154 | end 155 | 156 | def bundle_options(options) 157 | full_options = DEFAULT_INSTALL_OPTIONS.dup.merge(options) 158 | options_strings = [] 159 | jobs = full_options.delete("jobs") 160 | if jobs > 1 161 | if Utils.support_parallel_installation? 162 | options_strings << "--jobs=#{jobs}" 163 | else 164 | warn "Your current version of Bundler does not support parallel installation. Please " + 165 | "upgrade Bundler to version >= 1.4.0, or invoke `appraisal` without `--jobs` option." 166 | end 167 | end 168 | 169 | path = full_options.delete("path") 170 | if path 171 | relative_path = project_root.join(options["path"]) 172 | options_strings << "--path #{relative_path}" 173 | end 174 | 175 | full_options.each do |flag, val| 176 | options_strings << "--#{flag} #{val}" 177 | end 178 | 179 | options_strings.join(" ") if options_strings != [] 180 | end 181 | 182 | def comment_lines(heading) 183 | heading.lines.map do |line| 184 | if line.lstrip.empty? 185 | line 186 | else 187 | "# #{line}" 188 | end 189 | end.join 190 | end 191 | 192 | def quoted_gemfile 193 | return gemfile.to_s unless Customize.single_quotes 194 | 195 | gemfile.to_s.tr('"', "'") 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/appraisal/appraisal_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/appraisal" 4 | require "appraisal/customize" 5 | require "appraisal/errors" 6 | require "appraisal/gemfile" 7 | 8 | module Appraisal 9 | # Loads and parses Appraisals file 10 | class AppraisalFile 11 | attr_reader :appraisals, :gemfile 12 | 13 | def self.each(&block) 14 | new.each(&block) 15 | end 16 | 17 | def initialize 18 | @appraisals = [] 19 | @gemfile = Gemfile.new 20 | @gemfile.load(ENV["BUNDLE_GEMFILE"] || "Gemfile") 21 | 22 | if File.exist? path 23 | run IO.read(path) 24 | else 25 | raise AppraisalsNotFound 26 | end 27 | end 28 | 29 | def each(&block) 30 | appraisals.each(&block) 31 | end 32 | 33 | def appraise(name, &block) 34 | appraisal = Appraisal.new(name, gemfile) 35 | appraisal.instance_eval(&block) 36 | @appraisals << appraisal 37 | end 38 | 39 | def customize_gemfiles(&_block) 40 | Customize.new(**yield) 41 | end 42 | 43 | private 44 | 45 | def run(definitions) 46 | instance_eval(definitions, __FILE__, __LINE__) 47 | end 48 | 49 | def path 50 | "Appraisals" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/appraisal/bundler_dsl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/dependency_list" 4 | 5 | module Appraisal 6 | class BundlerDSL 7 | attr_reader :dependencies 8 | 9 | PARTS = %w[source ruby_version gits paths dependencies groups 10 | platforms source_blocks install_if gemspec] 11 | 12 | def initialize 13 | @sources = [] 14 | @ruby_version = nil 15 | @dependencies = DependencyList.new 16 | @gemspecs = [] 17 | @groups = {} 18 | @platforms = {} 19 | @gits = {} 20 | @paths = {} 21 | @source_blocks = {} 22 | @git_sources = {} 23 | @install_if = {} 24 | end 25 | 26 | def run(&block) 27 | instance_exec(&block) 28 | end 29 | 30 | def gem(name, *requirements) 31 | @dependencies.add(name, substitute_git_source(requirements)) 32 | end 33 | 34 | def remove_gem(name) 35 | @dependencies.remove(name) 36 | end 37 | 38 | def group(*names, &block) 39 | @groups[names] ||= 40 | Group.new(names).tap { |g| g.git_sources = @git_sources.dup } 41 | @groups[names].run(&block) 42 | end 43 | 44 | def install_if(condition, &block) 45 | @install_if[condition] ||= 46 | Conditional.new(condition).tap { |g| g.git_sources = @git_sources.dup } 47 | @install_if[condition].run(&block) 48 | end 49 | 50 | def platforms(*names, &block) 51 | @platforms[names] ||= 52 | Platform.new(names).tap { |g| g.git_sources = @git_sources.dup } 53 | @platforms[names].run(&block) 54 | end 55 | 56 | alias_method :platform, :platforms 57 | 58 | def source(source, &block) 59 | if block_given? 60 | @source_blocks[source] ||= 61 | Source.new(source).tap { |g| g.git_sources = @git_sources.dup } 62 | @source_blocks[source].run(&block) 63 | else 64 | @sources << source 65 | end 66 | end 67 | 68 | def ruby(ruby_version) 69 | @ruby_version = ruby_version 70 | end 71 | 72 | def git(source, options = {}, &block) 73 | @gits[source] ||= 74 | Git.new(source, options).tap { |g| g.git_sources = @git_sources.dup } 75 | @gits[source].run(&block) 76 | end 77 | 78 | def path(source, options = {}, &block) 79 | @paths[source] ||= 80 | Path.new(source, options).tap { |g| g.git_sources = @git_sources.dup } 81 | @paths[source].run(&block) 82 | end 83 | 84 | def to_s 85 | Utils.join_parts(PARTS.map { |part| send("#{part}_entry") }) 86 | end 87 | 88 | def for_dup 89 | Utils.join_parts(PARTS.map { |part| send("#{part}_entry_for_dup") }) 90 | end 91 | 92 | def gemspec(options = {}) 93 | @gemspecs << Gemspec.new(options) 94 | end 95 | 96 | def git_source(source, &block) 97 | @git_sources[source] = block 98 | end 99 | 100 | protected 101 | 102 | attr_writer :git_sources 103 | 104 | private 105 | 106 | def source_entry 107 | @sources.uniq.map { |source| "source #{source.inspect}" }.join("\n") 108 | end 109 | 110 | alias_method :source_entry_for_dup, :source_entry 111 | 112 | def ruby_version_entry 113 | return unless @ruby_version 114 | 115 | case @ruby_version 116 | when String then "ruby #{@ruby_version.inspect}" 117 | else "ruby(#{@ruby_version.inspect})" 118 | end 119 | end 120 | 121 | alias_method :ruby_version_entry_for_dup, :ruby_version_entry 122 | 123 | def gemspec_entry 124 | @gemspecs.map(&:to_s).join("\n") 125 | end 126 | 127 | def gemspec_entry_for_dup 128 | @gemspecs.map(&:for_dup).join("\n") 129 | end 130 | 131 | def dependencies_entry 132 | @dependencies.to_s 133 | end 134 | 135 | def dependencies_entry_for_dup 136 | @dependencies.for_dup 137 | end 138 | 139 | %i[gits paths platforms groups source_blocks install_if].each do |method_name| 140 | class_eval <<-METHODS, __FILE__, __LINE__ 141 | private 142 | 143 | def #{method_name}_entry 144 | @#{method_name}.values.map(&:to_s).join("\n\n") 145 | end 146 | 147 | def #{method_name}_entry_for_dup 148 | @#{method_name}.values.map(&:for_dup).join("\n\n") 149 | end 150 | METHODS 151 | end 152 | 153 | def indent(string) 154 | string.strip.gsub(/^(.+)$/, ' \1') 155 | end 156 | 157 | def substitute_git_source(requirements) 158 | requirements.each do |requirement| 159 | if requirement.is_a?(Hash) 160 | (requirement.keys & @git_sources.keys).each do |matching_source| 161 | value = requirement.delete(matching_source) 162 | requirement[:git] = @git_sources[matching_source].call(value) 163 | end 164 | end 165 | end 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/appraisal/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "thor" 4 | require "fileutils" 5 | 6 | module Appraisal 7 | class CLI < Thor 8 | default_task :install 9 | map ["-v", "--version"] => "version" 10 | 11 | class << self 12 | # Override help command to print out usage 13 | def help(shell, subcommand = false) 14 | shell.say strip_heredoc(<<-HELP) 15 | Appraisal: Find out what your Ruby gems are worth. 16 | 17 | Usage: 18 | appraisal [APPRAISAL_NAME] EXTERNAL_COMMAND 19 | 20 | If APPRAISAL_NAME is given, only run that EXTERNAL_COMMAND against the given 21 | appraisal, otherwise it runs the EXTERNAL_COMMAND against all appraisals. 22 | HELP 23 | 24 | if File.exist?("Appraisals") 25 | shell.say 26 | shell.say "Available Appraisal(s):" 27 | 28 | AppraisalFile.each do |appraisal| 29 | shell.say " - #{appraisal.name}" 30 | end 31 | end 32 | 33 | shell.say 34 | 35 | super 36 | end 37 | 38 | def exit_on_failure? 39 | true 40 | end 41 | 42 | private 43 | 44 | def strip_heredoc(string) 45 | indent = string.scan(/^[ \t]*(?=\S)/).min.size || 0 46 | string.gsub(/^[ \t]{#{indent}}/, "") 47 | end 48 | end 49 | 50 | desc "install", "Resolve and install dependencies for each appraisal" 51 | method_option "jobs", aliases: "j", type: :numeric, default: 1, 52 | banner: "SIZE", 53 | desc: "Install gems in parallel using the given number of workers." 54 | method_option "retry", type: :numeric, default: 1, 55 | desc: "Retry network and git requests that have failed" 56 | method_option "without", banner: "GROUP_NAMES", 57 | desc: "A space-separated list of groups referencing gems to skip " + 58 | "during installation. Bundler will remember this option." 59 | method_option "full-index", type: :boolean, 60 | desc: "Run bundle install with the " \ 61 | "full-index argument." 62 | method_option "path", type: :string, 63 | desc: "Install gems in the specified directory. " \ 64 | "Bundler will remember this option." 65 | 66 | def install 67 | invoke :generate, [], {} 68 | 69 | AppraisalFile.each do |appraisal| 70 | appraisal.install(options) 71 | appraisal.relativize 72 | end 73 | end 74 | 75 | desc "generate", "Generate a gemfile for each appraisal" 76 | def generate 77 | AppraisalFile.each do |appraisal| 78 | appraisal.write_gemfile 79 | end 80 | end 81 | 82 | desc "clean", "Remove all generated gemfiles and lockfiles from gemfiles folder" 83 | def clean 84 | FileUtils.rm_f Dir["gemfiles/*.{gemfile,gemfile.lock}"] 85 | end 86 | 87 | desc "update [LIST_OF_GEMS]", "Remove all generated gemfiles and lockfiles, resolve, and install dependencies again" 88 | def update(*gems) 89 | invoke :generate, [] 90 | 91 | AppraisalFile.each do |appraisal| 92 | appraisal.update(gems) 93 | end 94 | end 95 | 96 | desc "list", "List the names of the defined appraisals" 97 | def list 98 | AppraisalFile.new.appraisals.each { |appraisal| puts appraisal.name } 99 | end 100 | 101 | desc "version", "Display the version and exit" 102 | def version 103 | puts "Appraisal #{VERSION}" 104 | end 105 | 106 | private 107 | 108 | def method_missing(name, *args, &block) 109 | matching_appraisal = AppraisalFile.new.appraisals.detect do |appraisal| 110 | appraisal.name == name.to_s 111 | end 112 | 113 | if matching_appraisal 114 | Command.new(args, gemfile: matching_appraisal.gemfile_path).run 115 | else 116 | AppraisalFile.each do |appraisal| 117 | Command.new(ARGV, gemfile: appraisal.gemfile_path).run 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/appraisal/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "shellwords" 4 | 5 | module Appraisal 6 | # Executes commands with a clean environment 7 | class Command 8 | attr_reader :command, :env, :gemfile 9 | 10 | def initialize(command, options = {}) 11 | @gemfile = options[:gemfile] 12 | @env = options.fetch(:env, {}) 13 | @command = command_starting_with_bundle(command) 14 | end 15 | 16 | def run 17 | run_env = test_environment.merge(env) 18 | 19 | Bundler.with_original_env do 20 | ensure_bundler_is_available 21 | announce 22 | 23 | ENV["BUNDLE_GEMFILE"] = gemfile 24 | ENV["APPRAISAL_INITIALIZED"] = "1" 25 | run_env.each_pair do |key, value| 26 | ENV[key] = value 27 | end 28 | 29 | unless Kernel.system(command_as_string) 30 | exit(1) 31 | end 32 | end 33 | end 34 | 35 | private 36 | 37 | def ensure_bundler_is_available 38 | version = Utils.bundler_version 39 | unless system %(gem list --silent -i bundler -v #{version}) 40 | puts ">> Reinstall Bundler into #{ENV["GEM_HOME"]}" 41 | 42 | unless system "gem install bundler --version #{version}" 43 | puts 44 | puts <<-ERROR.strip.gsub(/\s+/, " ") 45 | Bundler installation failed. 46 | Please try running: 47 | `GEM_HOME="#{ENV["GEM_HOME"]}" gem install bundler --version #{version}` 48 | manually. 49 | ERROR 50 | exit(1) 51 | end 52 | end 53 | end 54 | 55 | def announce 56 | if gemfile 57 | puts ">> BUNDLE_GEMFILE=#{gemfile} #{command_as_string}" 58 | else 59 | puts ">> #{command_as_string}" 60 | end 61 | end 62 | 63 | def command_starts_with_bundle?(original_command) 64 | if original_command.is_a?(Array) 65 | original_command.first =~ /^bundle/ 66 | else 67 | original_command =~ /^bundle/ 68 | end 69 | end 70 | 71 | def command_starting_with_bundle(original_command) 72 | if command_starts_with_bundle?(original_command) 73 | original_command 74 | else 75 | %w[bundle exec] + original_command 76 | end 77 | end 78 | 79 | def command_as_string 80 | if command.is_a?(Array) 81 | Shellwords.join(command) 82 | else 83 | command 84 | end 85 | end 86 | 87 | def test_environment 88 | return {} unless ENV["APPRAISAL_UNDER_TEST"] == "1" 89 | 90 | { 91 | "GEM_HOME" => ENV["GEM_HOME"], 92 | "GEM_PATH" => "", 93 | } 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/appraisal/conditional.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/bundler_dsl" 4 | require "appraisal/utils" 5 | 6 | module Appraisal 7 | class Conditional < BundlerDSL 8 | def initialize(condition) 9 | super() 10 | @condition = condition 11 | end 12 | 13 | def to_s 14 | "install_if #{@condition} do\n#{indent(super)}\nend" 15 | end 16 | 17 | # :nodoc: 18 | def for_dup 19 | return unless @condition.is_a?(String) 20 | 21 | "install_if #{@condition} do\n#{indent(super)}\nend" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/appraisal/customize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appraisal 4 | class Customize 5 | def initialize(heading: nil, single_quotes: false) 6 | @@heading = heading&.chomp 7 | @@single_quotes = single_quotes 8 | end 9 | 10 | def self.heading(gemfile) 11 | @@heading ||= nil 12 | customize(@@heading, gemfile) 13 | end 14 | 15 | def self.single_quotes 16 | @@single_quotes ||= false 17 | end 18 | 19 | def self.customize(heading, gemfile) 20 | return nil unless heading 21 | 22 | format( 23 | heading.to_s, 24 | appraisal: gemfile.send("clean_name"), 25 | gemfile: gemfile.send("gemfile_name"), 26 | gemfile_path: gemfile.gemfile_path, 27 | lockfile: "#{gemfile.send('gemfile_name')}.lock", 28 | lockfile_path: gemfile.send("lockfile_path"), 29 | relative_gemfile_path: gemfile.relative_gemfile_path, 30 | relative_lockfile_path: "#{gemfile.relative_gemfile_path}.lock" 31 | ) 32 | end 33 | 34 | private_class_method :customize 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/appraisal/dependency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/utils" 4 | 5 | module Appraisal 6 | # Dependency on a gem and optional version requirements 7 | class Dependency 8 | attr_accessor :requirements 9 | attr_reader :name 10 | 11 | def initialize(name, requirements) 12 | @name = name 13 | @requirements = requirements 14 | end 15 | 16 | def to_s 17 | formatted_output Utils.format_arguments(path_prefixed_requirements) 18 | end 19 | 20 | # :nodoc: 21 | def for_dup 22 | formatted_output Utils.format_arguments(requirements) 23 | end 24 | 25 | private 26 | 27 | def path_prefixed_requirements 28 | requirements.map do |requirement| 29 | if requirement.is_a?(Hash) 30 | if requirement[:path] 31 | requirement[:path] = Utils.prefix_path(requirement[:path]) 32 | end 33 | 34 | if requirement[:git] 35 | requirement[:git] = Utils.prefix_path(requirement[:git]) 36 | end 37 | end 38 | 39 | requirement 40 | end 41 | end 42 | 43 | def formatted_output(output_requirements) 44 | [gem_name, output_requirements].compact.join(", ") 45 | end 46 | 47 | def gem_name 48 | %(gem "#{name}") 49 | end 50 | 51 | def no_requirements? 52 | requirements.nil? || requirements.empty? 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/appraisal/dependency_list.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/dependency" 4 | require "set" 5 | 6 | module Appraisal 7 | class DependencyList 8 | def initialize 9 | @dependencies = {} 10 | @removed_dependencies = Set.new 11 | end 12 | 13 | def add(name, requirements) 14 | unless @removed_dependencies.include?(name) 15 | @dependencies[name] = Dependency.new(name, requirements) 16 | end 17 | end 18 | 19 | def remove(name) 20 | if @removed_dependencies.add?(name) 21 | @dependencies.delete(name) 22 | end 23 | end 24 | 25 | def to_s 26 | @dependencies.values.map(&:to_s).join("\n") 27 | end 28 | 29 | # :nodoc: 30 | def for_dup 31 | @dependencies.values.map(&:for_dup).join("\n") 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/appraisal/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appraisal 4 | # Raises when Appraisal is unable to locate Appraisals file in the current directory. 5 | class AppraisalsNotFound < StandardError 6 | def message 7 | "Unable to locate 'Appraisals' file in the current directory." 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/appraisal/gemfile.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/bundler_dsl" 4 | 5 | module Appraisal 6 | autoload :Gemspec, "appraisal/gemspec" 7 | autoload :Git, "appraisal/git" 8 | autoload :Group, "appraisal/group" 9 | autoload :Path, "appraisal/path" 10 | autoload :Platform, "appraisal/platform" 11 | autoload :Source, "appraisal/source" 12 | autoload :Conditional, "appraisal/conditional" 13 | 14 | # Load bundler Gemfiles and merge dependencies 15 | class Gemfile < BundlerDSL 16 | def load(path) 17 | run(IO.read(path), path) if File.exist?(path) 18 | end 19 | 20 | def run(definitions, path, line = 1) 21 | instance_eval(definitions, path, line) if definitions 22 | end 23 | 24 | def dup 25 | Gemfile.new.tap do |gemfile| 26 | gemfile.git_sources = @git_sources 27 | gemfile.run(for_dup, __FILE__, __LINE__) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/appraisal/gemspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/utils" 4 | 5 | module Appraisal 6 | class Gemspec 7 | attr_reader :options 8 | 9 | def initialize(options = {}) 10 | @options = options 11 | @options[:path] ||= "." 12 | end 13 | 14 | def to_s 15 | "gemspec #{Utils.format_string(exported_options)}" 16 | end 17 | 18 | # :nodoc: 19 | def for_dup 20 | "gemspec #{Utils.format_string(@options)}" 21 | end 22 | 23 | private 24 | 25 | def exported_options 26 | @options.merge( 27 | path: Utils.prefix_path(@options[:path]) 28 | ) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/appraisal/git.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/bundler_dsl" 4 | require "appraisal/utils" 5 | 6 | module Appraisal 7 | class Git < BundlerDSL 8 | def initialize(source, options = {}) 9 | super() 10 | @source = source 11 | @options = options 12 | end 13 | 14 | def to_s 15 | if @options.empty? 16 | "git #{Utils.prefix_path(@source).inspect} do\n#{indent(super)}\nend" 17 | else 18 | "git #{Utils.prefix_path(@source).inspect}, #{Utils.format_string(@options)} do\n" + 19 | "#{indent(super)}\nend" 20 | end 21 | end 22 | 23 | # :nodoc: 24 | def for_dup 25 | if @options.empty? 26 | "git #{@source.inspect} do\n#{indent(super)}\nend" 27 | else 28 | "git #{@source.inspect}, #{Utils.format_string(@options)} do\n" + 29 | "#{indent(super)}\nend" 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/appraisal/group.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/bundler_dsl" 4 | require "appraisal/utils" 5 | 6 | module Appraisal 7 | class Group < BundlerDSL 8 | def initialize(group_names) 9 | super() 10 | @group_names = group_names 11 | end 12 | 13 | def to_s 14 | formatted_output indent(super) 15 | end 16 | 17 | # :nodoc: 18 | def for_dup 19 | formatted_output indent(super) 20 | end 21 | 22 | private 23 | 24 | def formatted_output(output_dependencies) 25 | <<~OUTPUT.strip 26 | group #{Utils.format_arguments(@group_names)} do 27 | #{output_dependencies} 28 | end 29 | OUTPUT 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/appraisal/path.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/bundler_dsl" 4 | require "appraisal/utils" 5 | 6 | module Appraisal 7 | class Path < BundlerDSL 8 | def initialize(source, options = {}) 9 | super() 10 | @source = source 11 | @options = options 12 | end 13 | 14 | def to_s 15 | if @options.empty? 16 | "path #{Utils.prefix_path(@source).inspect} do\n#{indent(super)}\nend" 17 | else 18 | "path #{Utils.prefix_path(@source).inspect}, #{Utils.format_string(@options)} do\n" + 19 | "#{indent(super)}\nend" 20 | end 21 | end 22 | 23 | # :nodoc: 24 | def for_dup 25 | if @options.empty? 26 | "path #{@source.inspect} do\n#{indent(super)}\nend" 27 | else 28 | "path #{@source.inspect}, #{Utils.format_string(@options)} do\n" + 29 | "#{indent(super)}\nend" 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/appraisal/platform.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/bundler_dsl" 4 | require "appraisal/utils" 5 | 6 | module Appraisal 7 | class Platform < BundlerDSL 8 | def initialize(platform_names) 9 | super() 10 | @platform_names = platform_names 11 | end 12 | 13 | def to_s 14 | formatted_output indent(super) 15 | end 16 | 17 | # :nodoc: 18 | def for_dup 19 | formatted_output indent(super) 20 | end 21 | 22 | private 23 | 24 | def formatted_output(output_dependencies) 25 | <<~OUTPUT.strip 26 | platforms #{Utils.format_arguments(@platform_names)} do 27 | #{output_dependencies} 28 | end 29 | OUTPUT 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/appraisal/source.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/bundler_dsl" 4 | require "appraisal/utils" 5 | 6 | module Appraisal 7 | class Source < BundlerDSL 8 | def initialize(source) 9 | super() 10 | @source = source 11 | end 12 | 13 | def to_s 14 | formatted_output indent(super) 15 | end 16 | 17 | # :nodoc: 18 | def for_dup 19 | formatted_output indent(super) 20 | end 21 | 22 | private 23 | 24 | def formatted_output(output_dependencies) 25 | <<~OUTPUT.strip 26 | source #{@source.inspect} do 27 | #{output_dependencies} 28 | end 29 | OUTPUT 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/appraisal/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "appraisal/appraisal_file" 4 | require "rake/tasklib" 5 | 6 | module Appraisal 7 | # Defines tasks for installing appraisal dependencies and running other tasks 8 | # for a given appraisal. 9 | class Task < Rake::TaskLib 10 | def initialize 11 | namespace :appraisal do 12 | desc "DEPRECATED: Generate a Gemfile for each appraisal" 13 | task :gemfiles do 14 | warn "`rake appraisal:gemfile` task is deprecated and will be removed soon. " + 15 | "Please use `appraisal generate`." 16 | exec "bundle exec appraisal generate" 17 | end 18 | 19 | desc "DEPRECATED: Resolve and install dependencies for each appraisal" 20 | task :install do 21 | warn "`rake appraisal:install` task is deprecated and will be removed soon. " + 22 | "Please use `appraisal install`." 23 | exec "bundle exec appraisal install" 24 | end 25 | 26 | desc "DEPRECATED: Remove all generated gemfiles from gemfiles/ folder" 27 | task :cleanup do 28 | warn "`rake appraisal:cleanup` task is deprecated and will be removed soon. " + 29 | "Please use `appraisal clean`." 30 | exec "bundle exec appraisal clean" 31 | end 32 | 33 | begin 34 | AppraisalFile.each do |appraisal| 35 | desc "DEPRECATED: Run the given task for appraisal #{appraisal.name}" 36 | task appraisal.name do 37 | ARGV.shift 38 | warn "`rake appraisal:#{appraisal.name}` task is deprecated and will be removed soon. " + 39 | "Please use `appraisal #{appraisal.name} rake #{ARGV.join(' ')}`." 40 | exec "bundle exec appraisal #{appraisal.name} rake #{ARGV.join(' ')}" 41 | end 42 | end 43 | rescue AppraisalsNotFound 44 | end 45 | 46 | task :all do 47 | ARGV.shift 48 | exec "bundle exec appraisal rake #{ARGV.join(' ')}" 49 | end 50 | end 51 | 52 | desc "Run the given task for all appraisals" 53 | task appraisal: "appraisal:all" 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/appraisal/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appraisal 4 | # Contains methods for various operations 5 | module Utils 6 | def self.support_parallel_installation? 7 | Gem::Version.create(Bundler::VERSION) >= Gem::Version.create("1.4.0.pre.1") 8 | end 9 | 10 | def self.format_string(object, enclosing_object = false) 11 | case object 12 | when Hash 13 | items = object.map do |key, value| 14 | format_hash_value(key, value) 15 | end 16 | 17 | if enclosing_object 18 | "{ #{items.join(', ')} }" 19 | else 20 | items.join(", ") 21 | end 22 | else 23 | object.inspect 24 | end 25 | end 26 | 27 | def self.format_hash_value(key, value) 28 | key = format_string(key, true) 29 | value = format_string(value, true) 30 | 31 | if key.start_with?(":") 32 | "#{key.sub(/^:/, "")}: #{value}" 33 | else 34 | "#{key} => #{value}" 35 | end 36 | end 37 | 38 | def self.format_arguments(arguments) 39 | unless arguments.empty? 40 | arguments.map { |object| format_string(object, false) }.join(", ") 41 | end 42 | end 43 | 44 | def self.join_parts(parts) 45 | parts.reject(&:nil?).reject(&:empty?).join("\n\n").strip 46 | end 47 | 48 | def self.prefix_path(path) 49 | if path !~ /^(?:\/|\S:)/ && path !~ /^\S+:\/\// && path !~ /^\S+@\S+:/ 50 | cleaned_path = path.gsub(/(^|\/)\.(?:\/|$)/, "\\1") 51 | File.join("..", cleaned_path) 52 | else 53 | path 54 | end 55 | end 56 | 57 | def self.bundler_version 58 | Gem::Specification.detect { |spec| spec.name == "bundler" }.version.to_s 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/appraisal/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Appraisal 4 | VERSION = "3.0.0.rc1" 5 | end 6 | -------------------------------------------------------------------------------- /spec/acceptance/appraisals_file_bundler_dsl_compatibility_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Appraisals file Bundler DSL compatibility" do 6 | it "supports all Bundler DSL in Appraisals file" do 7 | build_gems %w[bagel orange_juice milk waffle coffee ham 8 | sausage pancake rotten_egg mayonnaise] 9 | build_git_gems %w[egg croissant pain_au_chocolat omelette] 10 | 11 | build_gemfile <<-GEMFILE 12 | source 'https://rubygems.org' 13 | git_source(:custom_git_source) { |repo| "../build/\#{repo}" } 14 | ruby RUBY_VERSION 15 | 16 | gem 'bagel' 17 | gem "croissant", :custom_git_source => "croissant" 18 | 19 | git '../build/egg' do 20 | gem 'egg' 21 | end 22 | 23 | path '../build/waffle' do 24 | gem 'waffle' 25 | end 26 | 27 | group :breakfast do 28 | gem 'orange_juice' 29 | gem "omelette", :custom_git_source => "omelette" 30 | gem 'rotten_egg' 31 | end 32 | 33 | platforms :ruby, :jruby do 34 | gem 'milk' 35 | 36 | group :lunch do 37 | gem "coffee" 38 | end 39 | end 40 | 41 | source "https://other-rubygems.org" do 42 | gem "sausage" 43 | end 44 | 45 | install_if '"-> { true }"' do 46 | gem 'mayonnaise' 47 | end 48 | 49 | gem 'appraisal', :path => #{PROJECT_ROOT.inspect} 50 | GEMFILE 51 | 52 | build_appraisal_file <<-APPRAISALS 53 | appraise 'breakfast' do 54 | source 'http://some-other-source.com' 55 | ruby "2.3.0" 56 | 57 | gem 'bread' 58 | gem "pain_au_chocolat", :custom_git_source => "pain_au_chocolat" 59 | 60 | git '../build/egg' do 61 | gem 'porched_egg' 62 | end 63 | 64 | path '../build/waffle' do 65 | gem 'chocolate_waffle' 66 | end 67 | 68 | group :breakfast do 69 | remove_gem 'rotten_egg' 70 | gem 'bacon' 71 | 72 | platforms :rbx do 73 | gem "ham" 74 | end 75 | end 76 | 77 | platforms :ruby, :jruby do 78 | gem 'yoghurt' 79 | end 80 | 81 | source "https://other-rubygems.org" do 82 | gem "pancake" 83 | end 84 | 85 | install_if "-> { true }" do 86 | gem 'ketchup' 87 | end 88 | 89 | gemspec 90 | gemspec :path => "sitepress" 91 | end 92 | APPRAISALS 93 | 94 | run "bundle install --local" 95 | run "appraisal generate" 96 | 97 | expect(content_of("gemfiles/breakfast.gemfile")).to eq <<-GEMFILE.strip_heredoc 98 | # This file was generated by Appraisal 99 | 100 | source "https://rubygems.org" 101 | source "http://some-other-source.com" 102 | 103 | ruby "2.3.0" 104 | 105 | git "../../build/egg" do 106 | gem "egg" 107 | gem "porched_egg" 108 | end 109 | 110 | path "../../build/waffle" do 111 | gem "waffle" 112 | gem "chocolate_waffle" 113 | end 114 | 115 | gem "bagel" 116 | gem "croissant", :git => "../../build/croissant" 117 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 118 | gem "bread" 119 | gem "pain_au_chocolat", :git => "../../build/pain_au_chocolat" 120 | 121 | group :breakfast do 122 | gem "orange_juice" 123 | gem "omelette", :git => "../../build/omelette" 124 | gem "bacon" 125 | 126 | platforms :rbx do 127 | gem "ham" 128 | end 129 | end 130 | 131 | platforms :ruby, :jruby do 132 | gem "milk" 133 | gem "yoghurt" 134 | 135 | group :lunch do 136 | gem "coffee" 137 | end 138 | end 139 | 140 | source "https://other-rubygems.org" do 141 | gem "sausage" 142 | gem "pancake" 143 | end 144 | 145 | install_if -> { true } do 146 | gem "mayonnaise" 147 | gem "ketchup" 148 | end 149 | 150 | gemspec :path => "../" 151 | gemspec :path => "../sitepress" 152 | GEMFILE 153 | end 154 | 155 | it 'supports ruby file: ".ruby-version" DSL' do 156 | build_gemfile <<-GEMFILE 157 | source 'https://rubygems.org' 158 | 159 | ruby RUBY_VERSION 160 | 161 | gem 'appraisal', :path => #{PROJECT_ROOT.inspect} 162 | GEMFILE 163 | 164 | build_appraisal_file <<-APPRAISALS 165 | appraise 'ruby-version' do 166 | ruby file: ".ruby-version" 167 | end 168 | APPRAISALS 169 | 170 | run "bundle install --local" 171 | run "appraisal generate" 172 | 173 | expect(content_of("gemfiles/ruby_version.gemfile")).to eq <<-GEMFILE.strip_heredoc 174 | # This file was generated by Appraisal 175 | 176 | source "https://rubygems.org" 177 | 178 | ruby({:file=>".ruby-version"}) 179 | 180 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 181 | GEMFILE 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /spec/acceptance/bundle_with_custom_path_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Bundle with custom path" do 6 | let(:gem_name) { "rack" } 7 | let(:path) { "vendor/bundle" } 8 | 9 | shared_examples :gemfile_dependencies_are_satisfied do 10 | it "installs gems in the --path directory" do 11 | build_gemfile <<-GEMFILE 12 | source "https://rubygems.org" 13 | 14 | gem 'appraisal', :path => #{PROJECT_ROOT.inspect} 15 | GEMFILE 16 | 17 | build_appraisal_file <<-APPRAISALS 18 | appraise "#{gem_name}" do 19 | gem '#{gem_name}' 20 | end 21 | APPRAISALS 22 | 23 | run "bundle config set --local path #{path}" 24 | run "bundle install" 25 | run "bundle exec appraisal install" 26 | 27 | installed_gem = Dir.glob("tmp/stage/#{path}/#{Gem.ruby_engine}/*/gems/*") 28 | .map { |path| path.split("/").last } 29 | .select { |gem| gem.include?(gem_name) } 30 | expect(installed_gem).not_to be_empty 31 | 32 | bundle_output = run "bundle check" 33 | expect(bundle_output).to include("The Gemfile's dependencies are satisfied") 34 | 35 | appraisal_output = run "bundle exec appraisal install" 36 | expect(appraisal_output).to include("The Gemfile's dependencies are satisfied") 37 | 38 | run "bundle config unset --local path" 39 | end 40 | end 41 | 42 | include_examples :gemfile_dependencies_are_satisfied 43 | 44 | context "when already installed in vendor/another" do 45 | before do 46 | build_gemfile <<-GEMFILE 47 | source "https://rubygems.org" 48 | 49 | gem '#{gem_name}' 50 | GEMFILE 51 | 52 | run "bundle config set --local path vendor/another" 53 | run "bundle install" 54 | run "bundle config unset --local path" 55 | end 56 | 57 | include_examples :gemfile_dependencies_are_satisfied 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/acceptance/bundle_without_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Bundler without flag" do 6 | it "passes --without flag to Bundler on install" do 7 | build_gems %w[pancake orange_juice waffle coffee sausage soda] 8 | 9 | build_gemfile <<-GEMFILE 10 | source "https://rubygems.org" 11 | 12 | gem "pancake" 13 | gem "rake", "~> 10.5", :platform => :ruby_18 14 | 15 | group :drinks do 16 | gem "orange_juice" 17 | end 18 | 19 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 20 | GEMFILE 21 | 22 | build_appraisal_file <<-APPRAISALS 23 | appraise "breakfast" do 24 | gem "waffle" 25 | 26 | group :drinks do 27 | gem "coffee" 28 | end 29 | end 30 | 31 | appraise "lunch" do 32 | gem "sausage" 33 | 34 | group :drinks do 35 | gem "soda" 36 | end 37 | end 38 | APPRAISALS 39 | 40 | run "bundle install --local" 41 | run "bundle config set --local without 'drinks'" 42 | output = run "appraisal install" 43 | 44 | expect(output).to include("Bundle complete") 45 | expect(output).not_to include("orange_juice") 46 | expect(output).not_to include("coffee") 47 | expect(output).not_to include("soda") 48 | 49 | output = run "appraisal install" 50 | 51 | expect(output).to include("The Gemfile's dependencies are satisfied") 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/acceptance/cli/clean_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI", "appraisal clean" do 6 | it "remove all gemfiles from gemfiles directory" do 7 | build_appraisal_file <<-APPRAISAL 8 | appraise '1.0.0' do 9 | gem 'dummy', '1.0.0' 10 | end 11 | APPRAISAL 12 | 13 | run "appraisal install" 14 | write_file "gemfiles/non_related_file", "" 15 | 16 | run "appraisal clean" 17 | 18 | expect(file("gemfiles/1.0.0.gemfile")).not_to be_exists 19 | expect(file("gemfiles/1.0.0.gemfile.lock")).not_to be_exists 20 | expect(file("gemfiles/non_related_file")).to be_exists 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/acceptance/cli/generate_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI", "appraisal generate" do 6 | it "generates the gemfiles" do 7 | build_gemfile <<-GEMFILE 8 | source "https://rubygems.org" 9 | 10 | gem "appraisal", :path => "#{PROJECT_ROOT}" 11 | GEMFILE 12 | 13 | build_appraisal_file <<-APPRAISAL 14 | appraise '1.0.0' do 15 | gem 'dummy', '1.0.0' 16 | end 17 | 18 | appraise '1.1.0' do 19 | gem 'dummy', '1.1.0' 20 | end 21 | APPRAISAL 22 | 23 | run "appraisal generate" 24 | 25 | expect(file("gemfiles/1.0.0.gemfile")).to be_exists 26 | expect(file("gemfiles/1.1.0.gemfile")).to be_exists 27 | expect(content_of("gemfiles/1.0.0.gemfile")).to eq <<-GEMFILE.strip_heredoc 28 | # This file was generated by Appraisal 29 | 30 | source "https://rubygems.org" 31 | 32 | gem "appraisal", :path => "#{PROJECT_ROOT}" 33 | gem "dummy", "1.0.0" 34 | GEMFILE 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/acceptance/cli/help_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI", "appraisal help" do 6 | it "prints usage along with commands, and list of appraisals" do 7 | build_appraisal_file <<-APPRAISAL 8 | appraise '1.0.0' do 9 | gem 'dummy', '1.0.0' 10 | end 11 | APPRAISAL 12 | 13 | output = run "appraisal help" 14 | 15 | expect(output).to include "Usage:" 16 | expect(output).to include "appraisal [APPRAISAL_NAME] EXTERNAL_COMMAND" 17 | expect(output).to include "1.0.0" 18 | end 19 | 20 | it "prints out usage even Appraisals file does not exist" do 21 | output = run "appraisal help" 22 | 23 | expect(output).to include "Usage:" 24 | expect(output).not_to include "Unable to locate" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/acceptance/cli/install_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI", "appraisal install" do 6 | it "raises error when there is no Appraisals file" do 7 | output = run "appraisal install 2>&1", false 8 | 9 | expect(output).to include "Unable to locate 'Appraisals' file" 10 | end 11 | 12 | it "installs the dependencies" do 13 | build_appraisal_file <<-APPRAISAL 14 | appraise '1.0.0' do 15 | gem 'dummy', '1.0.0' 16 | end 17 | 18 | appraise '1.1.0' do 19 | gem 'dummy', '1.1.0' 20 | end 21 | APPRAISAL 22 | 23 | run "appraisal install" 24 | 25 | expect(file("gemfiles/1.0.0.gemfile.lock")).to be_exists 26 | expect(file("gemfiles/1.1.0.gemfile.lock")).to be_exists 27 | end 28 | 29 | it "relativize directory in gemfile.lock" do 30 | build_gemspec 31 | add_gemspec_to_gemfile 32 | 33 | build_appraisal_file <<-APPRAISAL 34 | appraise '1.0.0' do 35 | gem 'dummy', '1.0.0' 36 | end 37 | APPRAISAL 38 | 39 | run "appraisal install" 40 | 41 | expect(content_of("gemfiles/1.0.0.gemfile.lock")).not_to include(current_directory) 42 | end 43 | 44 | it "does not relativize directory of uris in gemfile.lock" do 45 | build_gemspec 46 | add_gemspec_to_gemfile 47 | 48 | build_git_gem("uri_dummy") 49 | uri_dummy_path = "#{current_directory}/uri_dummy" 50 | FileUtils.symlink(File.absolute_path("tmp/build/uri_dummy"), uri_dummy_path) 51 | 52 | build_appraisal_file <<-APPRAISAL 53 | appraise '1.0.0' do 54 | gem 'uri_dummy', git: 'file://#{uri_dummy_path}' 55 | end 56 | APPRAISAL 57 | 58 | run "appraisal install" 59 | 60 | expect(content_of("gemfiles/1.0.0.gemfile.lock")).to include("file://#{uri_dummy_path}") 61 | end 62 | 63 | context "with job size", parallel: true do 64 | before do 65 | build_appraisal_file <<-APPRAISAL 66 | appraise '1.0.0' do 67 | gem 'dummy', '1.0.0' 68 | end 69 | APPRAISAL 70 | end 71 | 72 | it "accepts --jobs option to set job size" do 73 | output = run "appraisal install --jobs=2" 74 | 75 | expect(output).to include("bundle install --gemfile='#{file('gemfiles/1.0.0.gemfile')}' --jobs=2") 76 | end 77 | 78 | it "ignores --jobs option if the job size is less than or equal to 1" do 79 | output = run "appraisal install --jobs=0" 80 | 81 | expect(output).to include("bundle install --gemfile='#{file('gemfiles/1.0.0.gemfile')}'") 82 | expect(output).not_to include("bundle install --gemfile='#{file('gemfiles/1.0.0.gemfile')}' --jobs=0") 83 | expect(output).not_to include("bundle install --gemfile='#{file('gemfiles/1.0.0.gemfile')}' --jobs=1") 84 | end 85 | end 86 | 87 | context "with full-index", :parallel do 88 | before do 89 | build_appraisal_file <<-APPRAISAL 90 | appraise '1.0.0' do 91 | gem 'dummy', '1.0.0' 92 | end 93 | APPRAISAL 94 | end 95 | 96 | it "accepts --full-index option to pull the full RubyGems index" do 97 | output = run("appraisal install --full-index") 98 | 99 | expect(output).to include("bundle install --gemfile='#{file('gemfiles/1.0.0.gemfile')}' --retry 1 --full-index true") 100 | end 101 | end 102 | 103 | context "with path", :parallel do 104 | before do 105 | build_appraisal_file <<-APPRAISAL 106 | appraise '1.0.0' do 107 | gem 'dummy', '1.0.0' 108 | end 109 | APPRAISAL 110 | end 111 | 112 | it "accepts --path option to specify the location to install gems into" do 113 | output = run("appraisal install --path vendor/appraisal") 114 | 115 | expect(output).to include("bundle install --gemfile='#{file('gemfiles/1.0.0.gemfile')}' --path #{file('vendor/appraisal')} --retry 1") 116 | end 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /spec/acceptance/cli/list_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI", "appraisal list" do 6 | it "prints list of appraisals" do 7 | build_appraisal_file <<-APPRAISAL 8 | appraise '1.0.0' do 9 | gem 'dummy', '1.0.0' 10 | end 11 | appraise '2.0.0' do 12 | gem 'dummy', '1.0.0' 13 | end 14 | appraise '1.1.0' do 15 | gem 'dummy', '1.0.0' 16 | end 17 | APPRAISAL 18 | 19 | output = run "appraisal list" 20 | 21 | expect(output).to eq("1.0.0\n2.0.0\n1.1.0\n") 22 | end 23 | 24 | it "prints nothing if there are no appraisals in the file" do 25 | build_appraisal_file "" 26 | output = run "appraisal list" 27 | 28 | expect(output.length).to eq(0) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/acceptance/cli/run_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI appraisal (with arguments)" do 6 | before do 7 | build_appraisal_file <<-APPRAISAL 8 | appraise '1.0.0' do 9 | gem 'dummy', '1.0.0' 10 | end 11 | 12 | appraise '1.1.0' do 13 | gem 'dummy', '1.1.0' 14 | end 15 | APPRAISAL 16 | 17 | run "appraisal install" 18 | write_file "test.rb", 'puts "Running: #{$dummy_version}"' 19 | write_file "test with spaces.rb", 'puts "Running: #{$dummy_version}"' 20 | end 21 | 22 | it "sets APPRAISAL_INITIALIZED environment variable" do 23 | write_file "test.rb", <<-TEST_FILE.strip_heredoc 24 | if ENV['APPRAISAL_INITIALIZED'] 25 | puts "Appraisal initialized!" 26 | end 27 | TEST_FILE 28 | 29 | output = run "appraisal 1.0.0 ruby -rbundler/setup -rdummy test.rb" 30 | expect(output).to include "Appraisal initialized!" 31 | end 32 | 33 | context "with appraisal name" do 34 | it "runs the given command against a correct versions of dependency" do 35 | output = run "appraisal 1.0.0 ruby -rbundler/setup -rdummy test.rb" 36 | 37 | expect(output).to include "Running: 1.0.0" 38 | expect(output).not_to include "Running: 1.1.0" 39 | end 40 | end 41 | 42 | context "without appraisal name" do 43 | it "runs the given command against all versions of dependency" do 44 | output = run "appraisal ruby -rbundler/setup -rdummy test.rb" 45 | 46 | expect(output).to include "Running: 1.0.0" 47 | expect(output).to include "Running: 1.1.0" 48 | end 49 | end 50 | 51 | context "when one of the arguments contains spaces" do 52 | it "preserves those spaces" do 53 | command = 'appraisal 1.0.0 ruby -rbundler/setup -rdummy "test with spaces.rb"' 54 | output = run(command) 55 | expect(output).to include "Running: 1.0.0" 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/acceptance/cli/update_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI", "appraisal update" do 6 | before do 7 | build_gem "dummy2", "1.0.0" 8 | 9 | build_appraisal_file <<-APPRAISAL 10 | appraise 'dummy' do 11 | gem 'dummy', '~> 1.0.0' 12 | gem 'dummy2', '~> 1.0.0' 13 | end 14 | APPRAISAL 15 | 16 | run "appraisal install" 17 | build_gem "dummy", "1.0.1" 18 | build_gem "dummy2", "1.0.1" 19 | end 20 | 21 | after do 22 | in_test_directory do 23 | `gem uninstall dummy -v 1.0.1` 24 | `gem uninstall dummy2 -a` 25 | end 26 | end 27 | 28 | context "with no arguments" do 29 | it "updates all the gems" do 30 | output = run "appraisal update" 31 | 32 | expect(output).to include("gemfiles/dummy.gemfile bundle update") 33 | expect(content_of("gemfiles/dummy.gemfile.lock")).to include "dummy (1.0.1)" 34 | expect(content_of("gemfiles/dummy.gemfile.lock")).to include "dummy2 (1.0.1)" 35 | end 36 | end 37 | 38 | context "with a list of gems" do 39 | it "only updates specified gems" do 40 | run "appraisal update dummy" 41 | 42 | expect(content_of("gemfiles/dummy.gemfile.lock")).to include "dummy (1.0.1)" 43 | expect(content_of("gemfiles/dummy.gemfile.lock")).to include "dummy2 (1.0.0)" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/acceptance/cli/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI", "appraisal version" do 6 | context "with version subcommand" do 7 | it "prints out version string" do 8 | output = run "appraisal version" 9 | 10 | expect(output).to include("Appraisal #{Appraisal::VERSION}") 11 | end 12 | end 13 | 14 | context "with -v flag" do 15 | it "prints out version string" do 16 | output = run "appraisal -v" 17 | 18 | expect(output).to include("Appraisal #{Appraisal::VERSION}") 19 | end 20 | end 21 | 22 | context "with --version flag" do 23 | it "prints out version string" do 24 | output = run "appraisal --version" 25 | 26 | expect(output).to include("Appraisal #{Appraisal::VERSION}") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/acceptance/cli/with_no_arguments_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "CLI appraisal (with no arguments)" do 6 | it "runs install command" do 7 | build_appraisal_file <<-APPRAISAL 8 | appraise '1.0.0' do 9 | gem 'dummy', '1.0.0' 10 | end 11 | APPRAISAL 12 | 13 | run "appraisal" 14 | 15 | expect(file("gemfiles/1.0.0.gemfile")).to be_exists 16 | expect(file("gemfiles/1.0.0.gemfile.lock")).to be_exists 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/acceptance/gemfile_dsl_compatibility_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Gemfile DSL compatibility" do 6 | it "supports all Bundler DSL in Gemfile" do 7 | build_gems %w[bacon orange_juice waffle] 8 | build_git_gem "egg" 9 | build_gemspec 10 | 11 | build_gemfile <<-GEMFILE 12 | source "https://rubygems.org" 13 | ruby RUBY_VERSION 14 | 15 | git "../build/egg" do 16 | gem "egg" 17 | end 18 | 19 | path "../build/orange_juice" do 20 | gem "orange_juice" 21 | end 22 | 23 | group :breakfast do 24 | gem "bacon" 25 | end 26 | 27 | platforms :ruby, :jruby do 28 | gem "waffle" 29 | end 30 | 31 | gem 'appraisal', :path => #{PROJECT_ROOT.inspect} 32 | 33 | gemspec 34 | GEMFILE 35 | 36 | build_appraisal_file <<-APPRAISALS 37 | appraise "japanese" do 38 | gem "rice" 39 | gem "miso_soup" 40 | end 41 | 42 | appraise "english" do 43 | gem "bread" 44 | end 45 | APPRAISALS 46 | 47 | run "bundle install --local" 48 | run "appraisal generate" 49 | 50 | expect(content_of("gemfiles/japanese.gemfile")).to eq <<-GEMFILE.strip_heredoc 51 | # This file was generated by Appraisal 52 | 53 | source "https://rubygems.org" 54 | 55 | ruby "#{RUBY_VERSION}" 56 | 57 | git "../../build/egg" do 58 | gem "egg" 59 | end 60 | 61 | path "../../build/orange_juice" do 62 | gem "orange_juice" 63 | end 64 | 65 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 66 | gem "rice" 67 | gem "miso_soup" 68 | 69 | group :breakfast do 70 | gem "bacon" 71 | end 72 | 73 | platforms :ruby, :jruby do 74 | gem "waffle" 75 | end 76 | 77 | gemspec :path => "../" 78 | GEMFILE 79 | 80 | expect(content_of("gemfiles/english.gemfile")).to eq <<-GEMFILE.strip_heredoc 81 | # This file was generated by Appraisal 82 | 83 | source "https://rubygems.org" 84 | 85 | ruby "#{RUBY_VERSION}" 86 | 87 | git "../../build/egg" do 88 | gem "egg" 89 | end 90 | 91 | path "../../build/orange_juice" do 92 | gem "orange_juice" 93 | end 94 | 95 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 96 | gem "bread" 97 | 98 | group :breakfast do 99 | gem "bacon" 100 | end 101 | 102 | platforms :ruby, :jruby do 103 | gem "waffle" 104 | end 105 | 106 | gemspec :path => "../" 107 | GEMFILE 108 | end 109 | 110 | it "merges gem requirements" do 111 | build_gem "bacon", "1.0.0" 112 | build_gem "bacon", "1.1.0" 113 | build_gem "bacon", "1.2.0" 114 | 115 | build_gemfile <<-GEMFILE 116 | source "https://rubygems.org" 117 | 118 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 119 | gem "bacon", "1.2.0" 120 | GEMFILE 121 | 122 | build_appraisal_file <<-APPRAISALS 123 | appraise "1.0.0" do 124 | gem "bacon", "1.0.0" 125 | end 126 | 127 | appraise "1.1.0" do 128 | gem "bacon", "1.1.0" 129 | end 130 | 131 | appraise "1.2.0" do 132 | gem "bacon", "1.2.0" 133 | end 134 | APPRAISALS 135 | 136 | run "bundle install --local" 137 | run "appraisal generate" 138 | 139 | expect(content_of("gemfiles/1.0.0.gemfile")).to include('gem "bacon", "1.0.0"') 140 | expect(content_of("gemfiles/1.1.0.gemfile")).to include('gem "bacon", "1.1.0"') 141 | expect(content_of("gemfiles/1.2.0.gemfile")).to include('gem "bacon", "1.2.0"') 142 | end 143 | 144 | it "supports gemspec in the group block" do 145 | build_gem "bacon", "1.0.0" 146 | build_gemspec 147 | 148 | build_gemfile <<-GEMFILE 149 | source "https://rubygems.org" 150 | 151 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 152 | 153 | group :plugin do 154 | gemspec 155 | end 156 | GEMFILE 157 | 158 | build_appraisal_file <<-APPRAISALS 159 | appraise "1.0.0" do 160 | gem "bacon", "1.0.0" 161 | end 162 | APPRAISALS 163 | 164 | run "bundle install --local" 165 | run "appraisal generate" 166 | 167 | expect(content_of("gemfiles/1.0.0.gemfile")).to eq <<-GEMFILE.strip_heredoc 168 | # This file was generated by Appraisal 169 | 170 | source "https://rubygems.org" 171 | 172 | gem "appraisal", :path => #{PROJECT_ROOT.inspect} 173 | gem "bacon", "1.0.0" 174 | 175 | group :plugin do 176 | gemspec :path => "../" 177 | end 178 | GEMFILE 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/acceptance/gemspec_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Gemspec" do 6 | before do 7 | build_appraisal_file 8 | build_rakefile 9 | end 10 | 11 | it "supports gemspec syntax with default options" do 12 | build_gemspec 13 | 14 | write_file "Gemfile", <<-GEMFILE 15 | source "https://rubygems.org" 16 | 17 | gem 'appraisal', :path => #{PROJECT_ROOT.inspect} 18 | 19 | gemspec 20 | GEMFILE 21 | 22 | run "bundle install --local" 23 | run "appraisal install" 24 | output = run "appraisal rake version" 25 | 26 | expect(output).to include "Loaded 1.1.0" 27 | end 28 | 29 | it "supports gemspec syntax with path option" do 30 | build_gemspec "specdir" 31 | 32 | write_file "Gemfile", <<-GEMFILE 33 | source "https://rubygems.org" 34 | 35 | gem 'appraisal', :path => #{PROJECT_ROOT.inspect} 36 | 37 | gemspec :path => './specdir' 38 | GEMFILE 39 | 40 | run "bundle install --local" 41 | run "appraisal install" 42 | output = run "appraisal rake version" 43 | 44 | expect(output).to include "Loaded 1.1.0" 45 | end 46 | 47 | def build_appraisal_file 48 | super <<-APPRAISALS 49 | appraise 'stock' do 50 | gem 'rake' 51 | end 52 | APPRAISALS 53 | end 54 | 55 | def build_rakefile 56 | write_file "Rakefile", <<-RAKEFILE 57 | require 'rubygems' 58 | require 'bundler/setup' 59 | require 'appraisal' 60 | 61 | task :version do 62 | require 'dummy' 63 | puts "Loaded \#{$dummy_version}" 64 | end 65 | RAKEFILE 66 | end 67 | 68 | def build_gemspec(path = ".") 69 | Dir.mkdir("tmp/stage/#{path}") rescue nil 70 | 71 | write_file File.join(path, "gemspec_project.gemspec"), <<-GEMSPEC 72 | Gem::Specification.new do |s| 73 | s.name = 'gemspec_project' 74 | s.version = '0.1' 75 | s.summary = 'Awesome Gem!' 76 | s.authors = "Appraisal" 77 | 78 | s.add_development_dependency('dummy', '1.1.0') 79 | end 80 | GEMSPEC 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/appraisal/appraisal_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "appraisal/appraisal_file" 5 | 6 | # Requiring this to make the test pass on Rubinius 2.2.5 7 | # https://github.com/rubinius/rubinius/issues/2934 8 | require "rspec/matchers/composable" 9 | require "rspec/matchers/built_in/raise_error" 10 | 11 | RSpec.describe Appraisal::AppraisalFile do 12 | it "complains when no Appraisals file is found" do 13 | allow(File).to receive(:exist?).with(/Gemfile/).and_return(true) 14 | allow(File).to receive(:exist?).with("Appraisals").and_return(false) 15 | expect { described_class.new }.to raise_error(Appraisal::AppraisalsNotFound) 16 | end 17 | 18 | describe "#customize_gemfiles" do 19 | before(:each) do 20 | allow(File).to receive(:exist?).with(anything).and_return(true) 21 | allow(IO).to receive(:read).with(anything).and_return("") 22 | end 23 | 24 | context "when no arguments are given" do 25 | subject { described_class.new.customize_gemfiles } 26 | 27 | it "raises an error" do 28 | expect { subject }.to raise_error(LocalJumpError) 29 | end 30 | end 31 | 32 | context "when a block is given" do 33 | context "when the block returns a hash with :heading key" do 34 | subject do 35 | described_class.new.customize_gemfiles do 36 | { heading: "foo" } 37 | end 38 | end 39 | 40 | it "sets the heading" do 41 | pending("test is broken: wrong number of arguments (given 0, expected 1)") 42 | 43 | expect { subject }.to change { Appraisal::Customize.heading }.to("foo") 44 | end 45 | end 46 | 47 | context "when the block returns a hash with :single_quotes key" do 48 | subject do 49 | described_class.new.customize_gemfiles do 50 | { single_quotes: true } 51 | end 52 | end 53 | 54 | it "sets the single_quotes" do 55 | expect { subject }.to change { Appraisal::Customize.single_quotes }.to(true) 56 | end 57 | end 58 | 59 | context "when the block returns a hash with :heading and :single_quotes keys" do 60 | subject do 61 | described_class.new.customize_gemfiles do 62 | { heading: "foo", single_quotes: true } 63 | end 64 | end 65 | 66 | it "sets the heading and single_quotes" do 67 | pending("test is broken: wrong number of arguments (given 0, expected 1)") 68 | 69 | subject 70 | expect(Appraisal::Customize.heading).to eq("foo") 71 | expect(Appraisal::Customize.single_quotes).to eq(true) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/appraisal/appraisal_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "appraisal/appraisal" 5 | require "tempfile" 6 | 7 | RSpec.describe Appraisal::Appraisal do 8 | it "converts spaces to underscores in the gemfile path" do 9 | appraisal = Appraisal::Appraisal.new("one two", "Gemfile") 10 | expect(appraisal.gemfile_path).to match(/one_two\.gemfile$/) 11 | end 12 | 13 | it "converts punctuation to underscores in the gemfile path" do 14 | appraisal = Appraisal::Appraisal.new("o&ne!", "Gemfile") 15 | expect(appraisal.gemfile_path).to match(/o_ne_\.gemfile$/) 16 | end 17 | 18 | it "keeps dots in the gemfile path" do 19 | appraisal = Appraisal::Appraisal.new("rails3.0", "Gemfile") 20 | expect(appraisal.gemfile_path).to match(/rails3\.0\.gemfile$/) 21 | end 22 | 23 | it "generates a gemfile with a newline at the end of file" do 24 | output_file = Tempfile.new("gemfile") 25 | appraisal = Appraisal::Appraisal.new("fake", "fake") 26 | allow(appraisal).to receive(:gemfile_path).and_return(output_file.path) 27 | 28 | appraisal.write_gemfile 29 | 30 | expect(output_file.read).to match(/[^\n]*\n\z/m) 31 | end 32 | 33 | context "gemfile customization" do 34 | it "generates a gemfile with a custom heading" do 35 | heading = "This file was generated with a custom heading!" 36 | Appraisal::Customize.new(heading: heading) 37 | output_file = Tempfile.new("gemfile") 38 | appraisal = Appraisal::Appraisal.new("custom", "Gemfile") 39 | allow(appraisal).to receive(:gemfile_path).and_return(output_file.path) 40 | 41 | appraisal.write_gemfile 42 | 43 | expected_output = "# #{heading}" 44 | expect(output_file.read).to start_with(expected_output) 45 | end 46 | 47 | it "generates a gemfile with multiple lines of custom heading" do 48 | heading = <<~HEADING 49 | frozen_string_literal: true\n 50 | This file was generated with a custom heading! 51 | HEADING 52 | Appraisal::Customize.new(heading: heading) 53 | output_file = Tempfile.new("gemfile") 54 | appraisal = Appraisal::Appraisal.new("custom", "Gemfile") 55 | allow(appraisal).to receive(:gemfile_path).and_return(output_file.path) 56 | 57 | appraisal.write_gemfile 58 | 59 | expected_output = <<~HEADING 60 | # frozen_string_literal: true\n 61 | # This file was generated with a custom heading! 62 | HEADING 63 | expect(output_file.read).to start_with(expected_output) 64 | end 65 | 66 | it "generates a gemfile with single quotes rather than doubles" do 67 | Appraisal::Customize.new(single_quotes: true) 68 | output_file = Tempfile.new("gemfile") 69 | appraisal = Appraisal::Appraisal.new("quotes", 'gem "foo"') 70 | allow(appraisal).to receive(:gemfile_path).and_return(output_file.path) 71 | 72 | appraisal.write_gemfile 73 | 74 | expect(output_file.read).to match(/gem 'foo'/) 75 | end 76 | 77 | it "does not customize anything by default" do 78 | Appraisal::Customize.new 79 | output_file = Tempfile.new("gemfile") 80 | appraisal = Appraisal::Appraisal.new("fake", 'gem "foo"') 81 | allow(appraisal).to receive(:gemfile_path).and_return(output_file.path) 82 | 83 | appraisal.write_gemfile 84 | 85 | expected_file = %(# This file was generated by Appraisal\n\ngem "foo"\n) 86 | expect(output_file.read).to eq(expected_file) 87 | end 88 | end 89 | 90 | context "parallel installation" do 91 | include StreamHelpers 92 | 93 | before do 94 | @appraisal = Appraisal::Appraisal.new("fake", "fake") 95 | allow(@appraisal).to receive(:gemfile_path).and_return("/home/test/test directory") 96 | allow(@appraisal).to receive(:project_root).and_return(Pathname.new("/home/test")) 97 | allow(Appraisal::Command).to receive(:new).and_return(double(run: true)) 98 | end 99 | 100 | it "runs single install command on Bundler < 1.4.0" do 101 | stub_const("Bundler::VERSION", "1.3.0") 102 | 103 | warning = capture(:stderr) do 104 | @appraisal.install("jobs" => 42) 105 | end 106 | 107 | expect(Appraisal::Command).to have_received(:new).with("#{bundle_check_command} || #{bundle_single_install_command}") 108 | expect(warning).to include "Please upgrade Bundler" 109 | end 110 | 111 | it "runs parallel install command on Bundler >= 1.4.0" do 112 | stub_const("Bundler::VERSION", "1.4.0") 113 | 114 | @appraisal.install("jobs" => 42) 115 | 116 | expect(Appraisal::Command).to have_received(:new).with("#{bundle_check_command} || #{bundle_parallel_install_command}") 117 | end 118 | 119 | it "runs install command with retries on Bundler" do 120 | @appraisal.install("retry" => 3) 121 | 122 | expect(Appraisal::Command).to have_received(:new).with("#{bundle_check_command} || #{bundle_install_command_with_retries}") 123 | end 124 | 125 | it "runs install command with path on Bundler" do 126 | @appraisal.install("path" => "vendor/appraisal") 127 | 128 | expect(Appraisal::Command).to have_received(:new).with("#{bundle_check_command} || #{bundle_install_command_with_path}") 129 | end 130 | 131 | def bundle_check_command 132 | "bundle check --gemfile='/home/test/test directory'" 133 | end 134 | 135 | def bundle_single_install_command 136 | "bundle install --gemfile='/home/test/test directory'" 137 | end 138 | 139 | def bundle_parallel_install_command 140 | "bundle install --gemfile='/home/test/test directory' --jobs=42" 141 | end 142 | 143 | def bundle_install_command_with_retries 144 | "bundle install --gemfile='/home/test/test directory' --retry 3" 145 | end 146 | 147 | def bundle_install_command_with_path 148 | "bundle install --gemfile='/home/test/test directory' " \ 149 | "--path /home/test/vendor/appraisal" 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /spec/appraisal/customize_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "appraisal/appraisal" 5 | require "appraisal/customize" 6 | 7 | RSpec.describe Appraisal::Customize do 8 | let(:appraisal) { Appraisal::Appraisal.new("test", "Gemfile") } 9 | let(:single_line_heading) { "This file was generated with a custom heading!" } 10 | let(:multi_line_heading) do 11 | <<~HEADING 12 | frozen_string_literal: true 13 | 14 | This file was generated with a custom heading! 15 | HEADING 16 | end 17 | let(:subject) { described_class.new } 18 | let(:single_line_subject) do 19 | described_class.new(heading: single_line_heading) 20 | end 21 | let(:multi_line_single_quotes_subject) do 22 | described_class.new(heading: multi_line_heading, single_quotes: true) 23 | end 24 | 25 | describe ".heading" do 26 | it "returns nil if no heading is set" do 27 | subject 28 | expect(described_class.heading(appraisal)).to eq(nil) 29 | end 30 | 31 | it "returns the heading if set" do 32 | single_line_subject 33 | expect(described_class.heading(appraisal)).to eq(single_line_heading) 34 | end 35 | 36 | it "returns the heading without an trailing newline" do 37 | multi_line_single_quotes_subject 38 | expect(described_class.heading(appraisal)).to eq(multi_line_heading.chomp) 39 | expect(described_class.heading(appraisal)).to_not end_with("\n") 40 | end 41 | end 42 | 43 | describe ".single_quotes" do 44 | it "returns false if not set" do 45 | subject 46 | expect(described_class.single_quotes).to eq(false) 47 | end 48 | 49 | it "returns true if set" do 50 | multi_line_single_quotes_subject 51 | expect(described_class.single_quotes).to eq(true) 52 | end 53 | end 54 | 55 | describe ".customize" do 56 | let(:appraisal_name) { "test" } 57 | let(:gemfile) { "test.gemfile" } 58 | let(:lockfile) { "#{gemfile}.lock" } 59 | let(:gemfile_relative_path) { "gemfiles/#{gemfile}" } 60 | let(:lockfile_relative_path) { "gemfiles/#{lockfile}" } 61 | let(:gemfile_full_path) { "/path/to/project/#{gemfile_relative_path}" } 62 | let(:lockfile_full_path) { "/path/to/project/#{lockfile_relative_path}" } 63 | before do 64 | allow(appraisal).to receive(:gemfile_name).and_return(gemfile) 65 | allow(appraisal).to receive(:gemfile_path).and_return(gemfile_full_path) 66 | allow(appraisal).to receive(:lockfile_path).and_return(lockfile_full_path) 67 | allow(appraisal).to receive(:relative_gemfile_path).and_return(gemfile_relative_path) 68 | end 69 | 70 | it "returns nil if no heading is set" do 71 | subject 72 | expect(described_class.send(:customize, nil, appraisal)).to eq(nil) 73 | end 74 | 75 | it "returns the heading unchanged" do 76 | single_line_subject 77 | expect(described_class.send( 78 | :customize, 79 | single_line_heading, 80 | appraisal 81 | )).to eq(single_line_heading) 82 | end 83 | 84 | it "returns the heading with the appraisal name" do 85 | expect(described_class.send( 86 | :customize, 87 | "Appraisal: %{appraisal}", # rubocop:disable Style/FormatStringToken 88 | appraisal 89 | )).to eq("Appraisal: #{appraisal_name}") 90 | end 91 | 92 | it "returns the heading with the gemfile name" do 93 | expect(described_class.send( 94 | :customize, 95 | "Gemfile: %{gemfile}", # rubocop:disable Style/FormatStringToken 96 | appraisal 97 | )).to eq("Gemfile: #{gemfile}") 98 | end 99 | 100 | it "returns the heading with the gemfile path" do 101 | expect(described_class.send( 102 | :customize, 103 | "Gemfile: %{gemfile_path}", # rubocop:disable Style/FormatStringToken 104 | appraisal 105 | )).to eq("Gemfile: #{gemfile_full_path}") 106 | end 107 | 108 | it "returns the heading with the lockfile name" do 109 | expect(described_class.send( 110 | :customize, 111 | "Lockfile: %{lockfile}", # rubocop:disable Style/FormatStringToken 112 | appraisal 113 | )).to eq("Lockfile: #{lockfile}") 114 | end 115 | 116 | it "returns the heading with the lockfile path" do 117 | expect(described_class.send( 118 | :customize, 119 | "Lockfile: %{lockfile_path}", # rubocop:disable Style/FormatStringToken 120 | appraisal 121 | )).to eq("Lockfile: #{lockfile_full_path}") 122 | end 123 | 124 | it "returns the heading with the relative gemfile path" do 125 | expect(described_class.send( 126 | :customize, 127 | "Gemfile: %{relative_gemfile_path}", # rubocop:disable Style/FormatStringToken 128 | appraisal 129 | )).to eq("Gemfile: #{gemfile_relative_path}") 130 | end 131 | 132 | it "returns the heading with the relative lockfile path" do 133 | expect(described_class.send( 134 | :customize, 135 | "Gemfile: %{relative_lockfile_path}", # rubocop:disable Style/FormatStringToken 136 | appraisal 137 | )).to eq("Gemfile: #{lockfile_relative_path}") 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /spec/appraisal/dependency_list_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "appraisal/dependency_list" 5 | 6 | RSpec.describe Appraisal::DependencyList do 7 | describe "#add" do 8 | let(:dependency_list) { Appraisal::DependencyList.new } 9 | 10 | it "adds dependency to the list" do 11 | dependency_list.add("rails", ["4.1.4"]) 12 | 13 | expect(dependency_list.to_s).to eq %(gem "rails", "4.1.4") 14 | end 15 | 16 | it "retains the order of dependencies" do 17 | dependency_list.add("rails", ["4.1.4"]) 18 | dependency_list.add("bundler", ["1.7.2"]) 19 | 20 | expect(dependency_list.to_s).to eq <<-GEMS.strip_heredoc.strip 21 | gem "rails", "4.1.4" 22 | gem "bundler", "1.7.2" 23 | GEMS 24 | end 25 | 26 | it "overrides dependency with the same name" do 27 | dependency_list.add("rails", ["4.1.0"]) 28 | dependency_list.add("rails", ["4.1.4"]) 29 | 30 | expect(dependency_list.to_s).to eq %(gem "rails", "4.1.4") 31 | end 32 | end 33 | 34 | describe "#remove" do 35 | let(:dependency_list) { Appraisal::DependencyList.new } 36 | 37 | before do 38 | dependency_list.add("rails", ["4.1.4"]) 39 | end 40 | 41 | it "removes the dependency from the list" do 42 | dependency_list.remove("rails") 43 | expect(dependency_list.to_s).to eq("") 44 | end 45 | 46 | it "respects the removal over an addition" do 47 | dependency_list.remove("rails") 48 | dependency_list.add("rails", ["4.1.0"]) 49 | expect(dependency_list.to_s).to eq("") 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/appraisal/gemfile_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "appraisal/gemfile" 5 | require "active_support/core_ext/string/strip" 6 | 7 | RSpec.describe Appraisal::Gemfile do 8 | include StreamHelpers 9 | 10 | it "supports gemfiles without sources" do 11 | gemfile = Appraisal::Gemfile.new 12 | expect(gemfile.to_s.strip).to eq "" 13 | end 14 | 15 | it "supports multiple sources" do 16 | gemfile = Appraisal::Gemfile.new 17 | gemfile.source "one" 18 | gemfile.source "two" 19 | expect(gemfile.to_s.strip).to eq %(source "one"\nsource "two") 20 | end 21 | 22 | it "ignores duplicate sources" do 23 | gemfile = Appraisal::Gemfile.new 24 | gemfile.source "one" 25 | gemfile.source "one" 26 | expect(gemfile.to_s.strip).to eq %(source "one") 27 | end 28 | 29 | it "preserves dependency order" do 30 | gemfile = Appraisal::Gemfile.new 31 | gemfile.gem "one" 32 | gemfile.gem "two" 33 | gemfile.gem "three" 34 | expect(gemfile.to_s).to match(/one.*two.*three/m) 35 | end 36 | 37 | it "supports symbol sources" do 38 | gemfile = Appraisal::Gemfile.new 39 | gemfile.source :one 40 | expect(gemfile.to_s.strip).to eq %(source :one) 41 | end 42 | 43 | it "supports group syntax" do 44 | gemfile = Appraisal::Gemfile.new 45 | 46 | gemfile.group :development, :test do 47 | gem "one" 48 | end 49 | 50 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 51 | group :development, :test do 52 | gem "one" 53 | end 54 | GEMFILE 55 | end 56 | 57 | it "supports nested DSL within group syntax" do 58 | gemfile = Appraisal::Gemfile.new 59 | 60 | gemfile.group :development, :test do 61 | platforms :jruby do 62 | gem "one" 63 | end 64 | git "git://example.com/repo.git" do 65 | gem "two" 66 | end 67 | path ".." do 68 | gem "three" 69 | end 70 | end 71 | 72 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 73 | group :development, :test do 74 | git "git://example.com/repo.git" do 75 | gem "two" 76 | end 77 | 78 | path "../.." do 79 | gem "three" 80 | end 81 | 82 | platforms :jruby do 83 | gem "one" 84 | end 85 | end 86 | GEMFILE 87 | end 88 | 89 | it "supports platform syntax" do 90 | gemfile = Appraisal::Gemfile.new 91 | 92 | gemfile.platform :jruby do 93 | gem "one" 94 | end 95 | 96 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 97 | platforms :jruby do 98 | gem "one" 99 | end 100 | GEMFILE 101 | end 102 | 103 | it "supports nested DSL within platform syntax" do 104 | gemfile = Appraisal::Gemfile.new 105 | 106 | gemfile.platform :jruby do 107 | group :development, :test do 108 | gem "one" 109 | end 110 | git "git://example.com/repo.git" do 111 | gem "two" 112 | end 113 | path ".." do 114 | gem "three" 115 | end 116 | end 117 | 118 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 119 | platforms :jruby do 120 | git "git://example.com/repo.git" do 121 | gem "two" 122 | end 123 | 124 | path "../.." do 125 | gem "three" 126 | end 127 | 128 | group :development, :test do 129 | gem "one" 130 | end 131 | end 132 | GEMFILE 133 | end 134 | 135 | it "supports git syntax" do 136 | gemfile = Appraisal::Gemfile.new 137 | 138 | gemfile.git "git://example.com/repo.git" do 139 | gem "one" 140 | end 141 | 142 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 143 | git "git://example.com/repo.git" do 144 | gem "one" 145 | end 146 | GEMFILE 147 | end 148 | 149 | it "supports nested DSL within git syntax" do 150 | gemfile = Appraisal::Gemfile.new 151 | 152 | gemfile.git "git://example.com/repo.git" do 153 | group :development, :test do 154 | gem "one" 155 | end 156 | platforms :jruby do 157 | gem "two" 158 | end 159 | path ".." do 160 | gem "three" 161 | end 162 | end 163 | 164 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 165 | git "git://example.com/repo.git" do 166 | path "../.." do 167 | gem "three" 168 | end 169 | 170 | group :development, :test do 171 | gem "one" 172 | end 173 | 174 | platforms :jruby do 175 | gem "two" 176 | end 177 | end 178 | GEMFILE 179 | end 180 | 181 | it "supports path syntax" do 182 | gemfile = Appraisal::Gemfile.new 183 | 184 | gemfile.path "../path" do 185 | gem "one" 186 | end 187 | 188 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 189 | path "../../path" do 190 | gem "one" 191 | end 192 | GEMFILE 193 | end 194 | 195 | it "supports nested DSL within path syntax" do 196 | gemfile = Appraisal::Gemfile.new 197 | 198 | gemfile.path "../path" do 199 | group :development, :test do 200 | gem "one" 201 | end 202 | platforms :jruby do 203 | gem "two" 204 | end 205 | git "git://example.com/repo.git" do 206 | gem "three" 207 | end 208 | end 209 | 210 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 211 | path "../../path" do 212 | git "git://example.com/repo.git" do 213 | gem "three" 214 | end 215 | 216 | group :development, :test do 217 | gem "one" 218 | end 219 | 220 | platforms :jruby do 221 | gem "two" 222 | end 223 | end 224 | GEMFILE 225 | end 226 | 227 | context "excess new line" do 228 | context "no contents" do 229 | it "shows empty string" do 230 | gemfile = Appraisal::Gemfile.new 231 | expect(gemfile.to_s).to eq "" 232 | end 233 | end 234 | 235 | context "full contents" do 236 | it "does not show newline at end" do 237 | gemfile = Appraisal::Gemfile.new 238 | gemfile.source "source" 239 | gemfile.gem "gem" 240 | gemfile.gemspec 241 | expect(gemfile.to_s).to match(/[^\n]\z/m) 242 | end 243 | end 244 | 245 | context "no gemspec" do 246 | it "does not show newline at end" do 247 | gemfile = Appraisal::Gemfile.new 248 | gemfile.source "source" 249 | gemfile.gem "gem" 250 | expect(gemfile.to_s).to match(/[^\n]\z/m) 251 | end 252 | end 253 | end 254 | 255 | context "relative path handling" do 256 | before { stub_const("RUBY_VERSION", "2.3.0") } 257 | 258 | context "in :path option" do 259 | it "handles dot path" do 260 | gemfile = Appraisal::Gemfile.new 261 | gemfile.gem "bacon", path: "." 262 | 263 | expect(gemfile.to_s).to eq %(gem "bacon", path: "../") 264 | end 265 | 266 | it "handles relative path" do 267 | gemfile = Appraisal::Gemfile.new 268 | gemfile.gem "bacon", path: "../bacon" 269 | 270 | expect(gemfile.to_s).to eq %(gem "bacon", path: "../../bacon") 271 | end 272 | 273 | it "handles absolute path" do 274 | gemfile = Appraisal::Gemfile.new 275 | gemfile.gem "bacon", path: "/tmp" 276 | 277 | expect(gemfile.to_s).to eq %(gem "bacon", path: "/tmp") 278 | end 279 | end 280 | 281 | context "in :git option" do 282 | it "handles dot git path" do 283 | gemfile = Appraisal::Gemfile.new 284 | gemfile.gem "bacon", git: "." 285 | 286 | expect(gemfile.to_s).to eq %(gem "bacon", git: "../") 287 | end 288 | 289 | it "handles relative git path" do 290 | gemfile = Appraisal::Gemfile.new 291 | gemfile.gem "bacon", git: "../bacon" 292 | 293 | expect(gemfile.to_s).to eq %(gem "bacon", git: "../../bacon") 294 | end 295 | 296 | it "handles absolute git path" do 297 | gemfile = Appraisal::Gemfile.new 298 | gemfile.gem "bacon", git: "/tmp" 299 | 300 | expect(gemfile.to_s).to eq %(gem "bacon", git: "/tmp") 301 | end 302 | 303 | it "handles git uri" do 304 | gemfile = Appraisal::Gemfile.new 305 | gemfile.gem "bacon", git: "git@github.com:bacon/bacon.git" 306 | 307 | expect(gemfile.to_s).to eq %(gem "bacon", git: "git@github.com:bacon/bacon.git") 308 | end 309 | end 310 | 311 | context "in path block" do 312 | it "handles dot path" do 313 | gemfile = Appraisal::Gemfile.new 314 | 315 | gemfile.path "." do 316 | gem "bacon" 317 | end 318 | 319 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 320 | path "../" do 321 | gem "bacon" 322 | end 323 | GEMFILE 324 | end 325 | 326 | it "handles relative path" do 327 | gemfile = Appraisal::Gemfile.new 328 | 329 | gemfile.path "../bacon" do 330 | gem "bacon" 331 | end 332 | 333 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 334 | path "../../bacon" do 335 | gem "bacon" 336 | end 337 | GEMFILE 338 | end 339 | 340 | it "handles absolute path" do 341 | gemfile = Appraisal::Gemfile.new 342 | 343 | gemfile.path "/tmp" do 344 | gem "bacon" 345 | end 346 | 347 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 348 | path "/tmp" do 349 | gem "bacon" 350 | end 351 | GEMFILE 352 | end 353 | end 354 | 355 | context "in git block" do 356 | it "handles dot git path" do 357 | gemfile = Appraisal::Gemfile.new 358 | 359 | gemfile.git "." do 360 | gem "bacon" 361 | end 362 | 363 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 364 | git "../" do 365 | gem "bacon" 366 | end 367 | GEMFILE 368 | end 369 | 370 | it "handles relative git path" do 371 | gemfile = Appraisal::Gemfile.new 372 | 373 | gemfile.git "../bacon" do 374 | gem "bacon" 375 | end 376 | 377 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 378 | git "../../bacon" do 379 | gem "bacon" 380 | end 381 | GEMFILE 382 | end 383 | 384 | it "handles absolute git path" do 385 | gemfile = Appraisal::Gemfile.new 386 | 387 | gemfile.git "/tmp" do 388 | gem "bacon" 389 | end 390 | 391 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 392 | git "/tmp" do 393 | gem "bacon" 394 | end 395 | GEMFILE 396 | end 397 | 398 | it "handles git uri" do 399 | gemfile = Appraisal::Gemfile.new 400 | 401 | gemfile.git "git@github.com:bacon/bacon.git" do 402 | gem "bacon" 403 | end 404 | 405 | expect(gemfile.to_s).to eq <<-GEMFILE.strip_heredoc.strip 406 | git "git@github.com:bacon/bacon.git" do 407 | gem "bacon" 408 | end 409 | GEMFILE 410 | end 411 | end 412 | 413 | context "in gemspec directive" do 414 | it "handles gemspec path" do 415 | gemfile = Appraisal::Gemfile.new 416 | gemfile.gemspec path: "." 417 | 418 | expect(gemfile.to_s).to eq %(gemspec path: "../") 419 | end 420 | end 421 | end 422 | 423 | context "git_source support" do 424 | before { stub_const("RUBY_VERSION", "2.3.0") } 425 | 426 | it "stores git_source declaration and apply it as git option" do 427 | gemfile = Appraisal::Gemfile.new 428 | gemfile.git_source(:custom_source) { |repo| "path/#{repo}" } 429 | gemfile.gem "bacon", custom_source: "bacon_pancake" 430 | 431 | expect(gemfile.to_s).to eq %(gem "bacon", git: "../path/bacon_pancake") 432 | end 433 | end 434 | 435 | it "preserves the Gemfile's __FILE__" do 436 | gemfile = Appraisal::Gemfile.new 437 | Tempfile.open do |tmpfile| 438 | tmpfile.write "__FILE__" 439 | tmpfile.rewind 440 | expect(gemfile.load(tmpfile.path)).to include(File.dirname(tmpfile.path)) 441 | end 442 | end 443 | end 444 | -------------------------------------------------------------------------------- /spec/appraisal/utils_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "appraisal/utils" 5 | 6 | RSpec.describe Appraisal::Utils do 7 | describe ".format_string" do 8 | it "prints out a nice looking hash without brackets with new syntax" do 9 | hash = { foo: "bar" } 10 | expect(Appraisal::Utils.format_string(hash)).to eq('foo: "bar"') 11 | 12 | hash = { "baz" => { ball: "boo" } } 13 | expect(Appraisal::Utils.format_string(hash)).to eq('"baz" => { ball: "boo" }') 14 | end 15 | end 16 | 17 | describe ".format_arguments" do 18 | before { stub_const("RUBY_VERSION", "2.3.0") } 19 | 20 | it "prints out arguments without enclosing square brackets" do 21 | arguments = [:foo, { bar: { baz: "ball" } }] 22 | 23 | expect(Appraisal::Utils.format_arguments(arguments)).to eq(':foo, bar: { baz: "ball" }') 24 | end 25 | 26 | it "returns nil if arguments is empty" do 27 | arguments = [] 28 | 29 | expect(Appraisal::Utils.format_arguments(arguments)).to eq(nil) 30 | end 31 | end 32 | 33 | describe ".prefix_path" do 34 | it "prepends two dots in front of relative path" do 35 | expect(Appraisal::Utils.prefix_path("test")).to eq "../test" 36 | end 37 | 38 | it "replaces single dot with two dots" do 39 | expect(Appraisal::Utils.prefix_path(".")).to eq "../" 40 | end 41 | 42 | it "ignores absolute path" do 43 | expect(Appraisal::Utils.prefix_path("/tmp")).to eq "/tmp" 44 | end 45 | 46 | it "strips out './' from path" do 47 | expect(Appraisal::Utils.prefix_path("./tmp/./appraisal././")).to eq "../tmp/appraisal./" 48 | end 49 | 50 | it "does not prefix Git uri" do 51 | expect(Appraisal::Utils.prefix_path("git@github.com:bacon/bacon.git")).to eq "git@github.com:bacon/bacon.git" 52 | expect(Appraisal::Utils.prefix_path("git://github.com/bacon/bacon.git")).to eq "git://github.com/bacon/bacon.git" 53 | expect(Appraisal::Utils.prefix_path("https://github.com/bacon/bacon.git")).to eq("https://github.com/bacon/bacon.git") 54 | end 55 | end 56 | 57 | describe ".bundler_version" do 58 | it "returns the bundler version" do 59 | bundler = double("Bundler", name: "bundler", version: "a.b.c") 60 | allow(Gem::Specification).to receive(:detect).and_return(bundler) 61 | 62 | version = Appraisal::Utils.bundler_version 63 | 64 | expect(version).to eq "a.b.c" 65 | expect(Gem::Specification).to have_received(:detect) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | require "./spec/support/acceptance_test_helpers" 6 | require "./spec/support/stream_helpers" 7 | 8 | PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), "..")).freeze 9 | TMP_GEM_ROOT = File.join(PROJECT_ROOT, "tmp", "bundler") 10 | TMP_GEM_BUILD = File.join(PROJECT_ROOT, "tmp", "build") 11 | ENV["APPRAISAL_UNDER_TEST"] = "1" 12 | 13 | RSpec.configure do |config| 14 | config.raise_errors_for_deprecations! 15 | 16 | config.define_derived_metadata(file_path: %r{spec\/acceptance\/}) do |metadata| 17 | metadata[:type] = :acceptance 18 | end 19 | 20 | config.include AcceptanceTestHelpers, type: :acceptance 21 | 22 | # disable monkey patching 23 | # see: https://relishapp.com/rspec/rspec-core/v/3-8/docs/configuration/zero-monkey-patching-mode 24 | config.disable_monkey_patching! 25 | 26 | config.before :suite do 27 | FileUtils.rm_rf TMP_GEM_ROOT 28 | FileUtils.rm_rf TMP_GEM_BUILD 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/support/acceptance_test_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec/expectations/expectation_target" 4 | require "active_support/core_ext/string/strip" 5 | require "active_support/core_ext/string/filters" 6 | require "active_support/concern" 7 | require "appraisal/utils" 8 | require "./spec/support/dependency_helpers" 9 | 10 | module AcceptanceTestHelpers 11 | extend ActiveSupport::Concern 12 | include DependencyHelpers 13 | 14 | BUNDLER_ENVIRONMENT_VARIABLES = %w[ 15 | RUBYOPT 16 | BUNDLE_PATH 17 | BUNDLE_BIN_PATH 18 | BUNDLE_GEMFILE 19 | BUNDLER_SETUP 20 | ].freeze 21 | 22 | included do 23 | metadata[:type] = :acceptance 24 | 25 | before parallel: true do 26 | unless Appraisal::Utils.support_parallel_installation? 27 | pending "This Bundler version does not support --jobs flag." 28 | end 29 | end 30 | 31 | before do 32 | cleanup_artifacts 33 | save_environment_variables 34 | unset_bundler_environment_variables 35 | build_default_dummy_gems 36 | ensure_bundler_is_available 37 | add_binstub_path 38 | build_default_gemfile 39 | end 40 | 41 | after do 42 | restore_environment_variables 43 | end 44 | end 45 | 46 | def save_environment_variables 47 | @original_environment_variables = {} 48 | 49 | (BUNDLER_ENVIRONMENT_VARIABLES + %w[PATH]).each do |key| 50 | @original_environment_variables[key] = ENV[key] 51 | end 52 | end 53 | 54 | def unset_bundler_environment_variables 55 | BUNDLER_ENVIRONMENT_VARIABLES.each do |key| 56 | ENV[key] = nil 57 | end 58 | end 59 | 60 | def add_binstub_path 61 | ENV["PATH"] = "bin:#{ENV['PATH']}" 62 | end 63 | 64 | def restore_environment_variables 65 | @original_environment_variables.each_pair do |key, value| 66 | ENV[key] = value 67 | end 68 | end 69 | 70 | def build_appraisal_file(content) 71 | write_file "Appraisals", content.strip_heredoc 72 | end 73 | 74 | def build_gemfile(content) 75 | write_file "Gemfile", content.strip_heredoc 76 | end 77 | 78 | def add_gemspec_to_gemfile 79 | in_test_directory do 80 | File.open("Gemfile", "a") { |file| file.puts "gemspec" } 81 | end 82 | end 83 | 84 | def build_gemspec 85 | write_file "stage.gemspec", <<-GEMSPEC 86 | Gem::Specification.new do |s| 87 | s.name = 'stage' 88 | s.version = '0.1' 89 | s.summary = 'Awesome Gem!' 90 | s.authors = "Appraisal" 91 | end 92 | GEMSPEC 93 | end 94 | 95 | def content_of(path) 96 | file(path).read.tap do |content| 97 | content.gsub!(/(\S+): /, ":\\1 => ") 98 | end 99 | end 100 | 101 | def file(path) 102 | Pathname.new(current_directory) + path 103 | end 104 | 105 | def be_exists 106 | be_exist 107 | end 108 | 109 | private 110 | 111 | def current_directory 112 | File.expand_path("tmp/stage") 113 | end 114 | 115 | def write_file(filename, content) 116 | in_test_directory { File.open(filename, "w") { |file| file.puts content } } 117 | end 118 | 119 | def cleanup_artifacts 120 | FileUtils.rm_rf current_directory 121 | end 122 | 123 | def build_default_dummy_gems 124 | FileUtils.mkdir_p(TMP_GEM_ROOT) 125 | 126 | build_gem "dummy", "1.0.0" 127 | build_gem "dummy", "1.1.0" 128 | end 129 | 130 | def ensure_bundler_is_available 131 | run "bundle -v 2>&1", false 132 | 133 | if $?.exitstatus != 0 134 | puts <<-WARNING.squish.strip_heredoc 135 | Reinstall Bundler to #{TMP_GEM_ROOT} as `BUNDLE_DISABLE_SHARED_GEMS` 136 | is enabled. 137 | WARNING 138 | version = Utils.bundler_version 139 | 140 | run "gem install bundler --version #{version} --install-dir '#{TMP_GEM_ROOT}'" 141 | end 142 | end 143 | 144 | def build_default_gemfile 145 | build_gemfile <<-GEMFILE 146 | source 'https://rubygems.org' 147 | 148 | gem 'appraisal', :path => '#{PROJECT_ROOT}' 149 | GEMFILE 150 | 151 | run "bundle install --local" 152 | run "bundle binstubs --all" 153 | end 154 | 155 | def in_test_directory(&block) 156 | FileUtils.mkdir_p current_directory 157 | Dir.chdir current_directory, &block 158 | end 159 | 160 | def run(command, raise_on_error = true) 161 | in_test_directory do 162 | `#{command}`.tap do |output| 163 | exitstatus = $?.exitstatus 164 | 165 | if ENV["VERBOSE"] 166 | puts output 167 | end 168 | 169 | if raise_on_error && exitstatus != 0 170 | raise RuntimeError, <<-ERROR_MESSAGE.strip_heredoc 171 | Command #{command.inspect} exited with status #{exitstatus}. Output: 172 | #{output.gsub(/^/, ' ')} 173 | ERROR_MESSAGE 174 | end 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /spec/support/dependency_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DependencyHelpers 4 | def build_gem(gem_name, version = "1.0.0") 5 | ENV["GEM_HOME"] = TMP_GEM_ROOT 6 | 7 | unless File.exist? "#{TMP_GEM_ROOT}/gems/#{gem_name}-#{version}" 8 | FileUtils.mkdir_p "#{TMP_GEM_BUILD}/#{gem_name}/lib" 9 | 10 | FileUtils.cd "#{TMP_GEM_BUILD}/#{gem_name}" do 11 | gemspec = "#{gem_name}.gemspec" 12 | lib_file = "lib/#{gem_name}.rb" 13 | 14 | File.open gemspec, "w" do |file| 15 | file.puts <<-GEMSPEC 16 | Gem::Specification.new do |s| 17 | s.name = #{gem_name.inspect} 18 | s.version = #{version.inspect} 19 | s.authors = 'Mr. Smith' 20 | s.summary = 'summary' 21 | s.files = #{lib_file.inspect} 22 | s.license = 'MIT' 23 | s.homepage = 'http://github.com/thoughtbot/#{gem_name}' 24 | s.required_ruby_version = '>= 2.3.0' 25 | end 26 | GEMSPEC 27 | end 28 | 29 | File.open lib_file, "w" do |file| 30 | file.puts "$#{gem_name}_version = '#{version}'" 31 | end 32 | 33 | redirect = ENV["VERBOSE"] ? "" : "2>&1" 34 | 35 | puts "building gem: #{gem_name} #{version}" if ENV["VERBOSE"] 36 | `gem build #{gemspec} #{redirect}` 37 | 38 | puts "installing gem: #{gem_name} #{version}" if ENV["VERBOSE"] 39 | `gem install -lN #{gem_name}-#{version}.gem -v #{version} #{redirect}` 40 | 41 | puts "" if ENV["VERBOSE"] 42 | end 43 | end 44 | end 45 | 46 | def build_gems(gems) 47 | gems.each { |gem| build_gem(gem) } 48 | end 49 | 50 | def build_git_gem(gem_name, version = "1.0.0") 51 | puts "building git gem: #{gem_name} #{version}" if ENV["VERBOSE"] 52 | build_gem gem_name, version 53 | 54 | Dir.chdir "#{TMP_GEM_BUILD}/#{gem_name}" do 55 | `git init . --initial-branch=master` 56 | `git config user.email "appraisal@thoughtbot.com"` 57 | `git config user.name "Appraisal"` 58 | `git add .` 59 | `git commit --all --no-verify --message "initial commit"` 60 | end 61 | 62 | # Cleanup Bundler cache path manually for now 63 | git_cache_path = File.join(ENV["GEM_HOME"], "cache", "bundler", "git") 64 | 65 | Dir[File.join(git_cache_path, "#{gem_name}-*")].each do |path| 66 | puts "deleting: #{path}" if ENV["VERBOSE"] 67 | FileUtils.rm_r(path) 68 | end 69 | end 70 | 71 | def build_git_gems(gems) 72 | gems.each { |gem| build_git_gem(gem) } 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/support/stream_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tempfile" 4 | 5 | module StreamHelpers 6 | def capture(stream) 7 | stream = stream.to_s 8 | captured_stream = Tempfile.new(stream) 9 | stream_io = eval("$#{stream}") 10 | origin_stream = stream_io.dup 11 | stream_io.reopen(captured_stream) 12 | 13 | yield 14 | 15 | stream_io.rewind 16 | return captured_stream.read 17 | ensure 18 | captured_stream.close 19 | captured_stream.unlink 20 | stream_io.reopen(origin_stream) 21 | end 22 | end 23 | --------------------------------------------------------------------------------