Design Notes

These are some notes on the underlying design of GemInstaller, and the current state of development:

  • GemInstaller was developed from nothing but a concept, using Behavior-Driven Development and Rspec.
  • GemInstaller uses “Dependency Injection”, an architecture which has many benefits, including testability and enabling loose coupling and high cohesion. I originally started with Needle, a Ruby Dependency Injection framework, but switched to a simple home-grown approach in order to not have a dependency on the Needle gem. Read more about Dependency Injection here:
  • A lot of effort has gone into supporting isolated functional testing. Every run of a functional spec or the spec suite creates a new “Test Gem Home” sandbox installation of RubyGems. This creates a new GEM_HOME on the fly for each run. This allows me to test actual gem installation without being connected to a network, and avoid mucking with (or having false failures due to) my actual local RubyGems gem repository. Unfortunately, the Ruby load path still uses the executables from the system installation of RubyGems. I plan on fixing that too (which will allow me to test multiple RubyGems versions), but that seems to be a much trickier task than just having a different GEM_HOME.
  • Spec/Test Philosophy:
    • GemInstaller’s specs are grouped into distinct categories. This and other testing approaches I use are heavily influenced by this fine article at GroboUtils: Naming Your Junit Tests
    • Unit vs. Functional: Many classes have two identically-named spec files associated with them, under unit and functional.
      • Unit Specs: The Unit specs for the most part test only a single class in isolation, usually using mock objects, so they run very fast. Many of them are vestiges of my initial BDD approach when I started from nothing. I incur a little bit of overhead cost to maintain them as the app evolves, but I don’t mind that as much as some people :). They also come in very handy when I want to BDD some new behavior and don’t want to have the high “Test Gem Home” fixture creation overhead that the functional specs have.
      • Functional Specs: These also have a one-to-one relationship with classes for the most part – geminstaller_spec is an exception. Most of these test how groups of classes interact together at various levels. Most of them use the “Test Gem Home” fixture approach. This is effective, but adds several seconds to the startup of each run. There is also overlap between some of them, especially at high levels of the API, which adds some maintenance overhead, but is worth it to me since it helps catch integration bugs.
      • Smoke Tests: There are some tests under /spec/smoketests which are not part of the main spec suite. These are really coarse grained scripts. They hit the live gem repository, and install/uninstall actual gems, and exercise my sample rails app which uses GemInstaller. They are used as a check to ensure that my “Test Gem Home” fixture approach is not masking any real-life bugs.
  • I’m a proponent of high code coverage. I check periodically to ensure I maintain close to 100% code coverage, not counting platform- and version-specific code blocks (and I’ll get around to those some day). Also, the sudo recursive invocation stuff still needs some tests, but that’s a bit tricky to automate.
  • The tests are “harder” on Windows (but run fine on mac/linux). The app should work fine, but testing is pretty tricky. I have all tests working against the latest rubygems (1.0.1). Here’s what does and doesn’t work with tests against latest rubygems on windows:
    * install_smoketest.rb and autogem_smoketest.rb work
    * ruby test_suites/test_all.rb works
    * rake default test task does not work (this seems to be a problem with hoe on windows)
    * Sometimes test suite still hangs after a while, this is probably some orphaned EmbeddedGemServer process somewhere. A reboot should fix it – this is windows, after all!
  • One of my motivations for creating GemInstaller was as an exercise to help me learn Ruby better. If you see me doing anything obviously dumb or inefficient in the code or design, please let me know. Some of them I already know about, but haven’t fixed, most I’m probably not aware of :)

Maintaining forward and backward compatibility with multiple RubyGems versions

  • BACKWARD COMPATIBILITY UPDATE: As of July 2009, I had to give up on being able to keep the test suite running against all old versions of RubyGems back to 0.8.11, and I’ve had to drop test support for some of the old versions. The world moves on, and changes in Rspec, Rake, Hoe, Ruby, and Rubygems itself make it hard to make the tests themselves run under old versions of Rubygems. GemInstaller ITSELF should still run against these versions, I just can’t run the automated tests for them.
  • FORWARD COMPATIBILITY UPDATE: Between my move to git (no more automagically-updating svn:externals, just lame manual git submodules), and RubyGems’ refusal to allow incrementing of the RubyGems trunk version ([refusal](, [explanation](, maintaining automated tests and version checks against the latest RubyGems trunk presented some challenges. However, I have it working again, in an ugly Rube-Goldbergesque kind of way. More details later…
  • I’ve put a lot of effort into ensuring that GemInstaller works with older versions of RubyGems, and run automated tests against several recent versions on Continuous Integration.
  • This wasn’t as hard as it seems. The hardest part was figuring out what was different between versions. Once you understand that, you can add switch statements to perform different logic or tests depending on the version (which is easy if you use rubygems built in Version Requirement support – see GemInstaller::RubygemsVersionChecker).
  • To run against different RubyGems versions, I have a RUBYGEMS_VERSION environment variable which causes the correct version or RubyGems in spec/fixture/rubygems_dist to be put on the load path and used for the test run.
  • RubyGems 0.9.5 was a major upgrade, the preview release for 1.0. It introduced support for platforms, auto-install of missing dependencies, and many other things that GemInstaller already did.
  • Here are the main differences in GemInstaller when running on RubyGems >=0.9.5:
    • Don’t use missing dependency finder (auto-install of missing dependencies is now built in)
    • Don’t use valid platform selector, use —platform option (auto-selection of platform is now built in)
    • GemInteractionHandler should throw error on any interactive prompt (since dependencies and platforms are now handled automatically by default)
    • The prefer_binary_platform config property no longer applies, and has no effect.

Release Process

Yeah, it’s too manual. Notes for improvement below.

  • Add section and entries for new release to History.txt
  • Add same history info to history section on homepage.
  • Update version in geminstaller.rb (if it was not done immediately after last publish, which it should have been)
  • rake update_manifest
  • Run tests:
    • CI should be green for latest version and as many old versions as possible
    • install_smoketest.rb and autogem_smoketest.rb should pass for all RubyGems versions on mac/linux (if they are in a working state)
    • test_all, install_smoketest.rb and autogem_smoketest.rb should pass for latest RubyGems version on windows (if they are in a working state)
  • Make sure everything is checked in
  • Make a tag in git (git tag x.y.z; git push origin x.y.z)
  • rake clean package
  • Upload gem to (gem push pkg/geminstaller-x.y.z.gem)
  • rake publish_website
  • Bump the version in geminstaller.rb to the expected next release version.

Here’s the improvements I need to make to the release process:

  • Avoid duplication of history file to display on home page
  • Make CI automatically build and tag a gem/package with next version + revision: x.y.z.
  • Rake task to automatically tag and release latest build from CI and tag

Rubygems Upgrade Process

Process to test against new rubygems release.

svn export spec/fixture/rubygems_dist/rubygems-X.Y.Z
  • add spec/fixture/rubygems_dist/rubytems-X.Y.Z
  • checkin
  • Add project for that release on CI