Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit cb10715e37874f37d4cf2c39e383476508326e3a @dmajda dmajda committed
Showing with 701 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +4 −0 CHANGELOG
  3. +3 −0 Gemfile
  4. +22 −0 LICENSE
  5. +6 −0 README.md
  6. +8 −0 Rakefile
  7. +1 −0 VERSION
  8. +31 −0 cheetah.gemspec
  9. +222 −0 lib/cheetah.rb
  10. +395 −0 test/cheetah_test.rb
  11. +8 −0 test/test_helper.rb
1 .gitignore
@@ -0,0 +1 @@
+Gemfile.lock
4 CHANGELOG
@@ -0,0 +1,4 @@
+0.1.0 (2012-03-23)
+------------------
+
+* Initial release.
3 Gemfile
@@ -0,0 +1,3 @@
+source "http://rubygems.org"
+
+gemspec
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 SUSE
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
6 README.md
@@ -0,0 +1,6 @@
+Cheetah
+=======
+
+Cheetah is a simple library for executing external commands safely and conveniently. It is meant as a safe replacement of `backticks`, Kernel#system and similar methods, which are often used in unsecure way (they allow shell expansion of commands).
+
+Proper documentation is coming soon.
8 Rakefile
@@ -0,0 +1,8 @@
+require "rake/testtask"
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.test_files = FileList["test/**/*_test.rb"]
+end
+
+task :default => :test
1 VERSION
@@ -0,0 +1 @@
+0.1.0
31 cheetah.gemspec
@@ -0,0 +1,31 @@
+# -*- encoding: utf-8 -*-
+
+require File.expand_path(File.dirname(__FILE__) + "/lib/cheetah")
+
+Gem::Specification.new do |s|
+ s.name = "cheetah"
+ s.version = Cheetah::VERSION
+ s.summary = "Simple library for executing external commands safely and conveniently"
+ s.description = <<-EOT.split("\n").map(&:strip).join(" ")
+ Cheetah is a simple library for executing external commands safely and
+ conveniently. It is meant as a safe replacement of `backticks`,
+ Kernel#system and similar methods, which are often used in unsecure way
+ (they allow shell expansion of commands).
+ EOT
+
+ s.author = "David Majda"
+ s.email = "dmajda@suse.de"
+ s.homepage = "https://github.com/openSUSE/cheetah"
+ s.license = "MIT"
+
+ s.files = [
+ "CHANGELOG",
+ "LICENSE",
+ "README.md"
+ "VERSION",
+ "lib/cheetah.rb"
+ ]
+
+ s.add_development_dependency "shoulda-context"
+ s.add_development_dependency "mocha"
+end
222 lib/cheetah.rb
@@ -0,0 +1,222 @@
+# Contains methods for executing external commands safely and conveniently.
+module Cheetah
+ VERSION = File.read(File.dirname(__FILE__) + "/../VERSION").strip
+
+ # Exception raised when a command execution fails.
+ class ExecutionFailed < StandardError
+ attr_reader :command, :args, :status, :stdout, :stderr
+
+ def initialize(command, args, status, stdout, stderr, message = nil)
+ super(message)
+ @command = command
+ @args = args
+ @status = status
+ @stdout = stdout
+ @stderr = stderr
+ end
+ end
+
+ # Runs an external command, optionally capturing its output. Meant as a safe
+ # replacement of `backticks`, Kernel#system and similar methods, which are
+ # often used in unsecure way. (They allow shell expansion of commands, which
+ # often means their arguments need proper escaping. The problem is that people
+ # forget to do it or do it badly, causing serious security issues.)
+ #
+ # Examples:
+ #
+ # # Run a command, grab its output and handle failures.
+ # files = nil
+ # begin
+ # files = Cheetah.run("ls", "-la", :capture => :stdout)
+ # rescue Cheetah::ExecutionFailed => e
+ # puts "Command #{e.command} failed with status #{e.status}."
+ # end
+ #
+ # # Log the executed command, it's status, input and both outputs into
+ # # user-supplied logger.
+ # Cheetah.run("qemu-kvm", "foo.raw", :logger => my_logger)
+ #
+ # The first parameter specifies the command to run, the remaining parameters
+ # specify its arguments. It is also possible to specify both the command and
+ # arguments in the first parameter using an array. If the last parameter is a
+ # hash, it specifies options.
+ #
+ # For security reasons, the command never goes through shell expansion even if
+ # only one parameter is specified (i.e. the method does do not adhere to the
+ # convention used by other Ruby methods for launching external commands, e.g.
+ # Kernel#system).
+ #
+ # If the command execution succeeds, the returned value depends on the
+ # value of the :capture option (see below). If it fails (the command is not
+ # executed for some reason or returns a non-zero exit status), the method
+ # raises a ExecutionFailed exception with detailed information about the
+ # failure.
+ #
+ # Options:
+ #
+ # :capture - configures which output(s) the method captures and returns, the
+ # valid values are:
+ #
+ # - nil - no output is captured and returned
+ # (the default)
+ # - :stdout - standard output is captured and
+ # returned as a string
+ # - :stderr - error output is captured and returned
+ # as a string
+ # - [:stdout, :stderr] - both outputs are captured and returned
+ # as a two-element array of strings
+ #
+ # :stdin - if specified, it is a string sent to command's standard input
+ #
+ # :logger - if specified, the method will log the command, its status, input
+ # and both outputs to passed logger at the "debug" level
+ #
+ def self.run(command, *args)
+ options = args.last.is_a?(Hash) ? args.pop : {}
+
+ capture = options[:capture]
+ stdin = options[:stdin] || ""
+ logger = options[:logger]
+
+ if command.is_a?(Array)
+ args = command[1..-1]
+ command = command.first
@jreidinger The openSUSE Project member

You can replace it with one command -
args = command.splice! 1..-1

@dmajda The openSUSE Project member
dmajda added a note

IMO the original version is slightly more readable because it doesn't combine an assignment with a side effect, so i think it should stay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ end
+
+ pass_stdin = !stdin.empty?
+ pipe_stdin_read, pipe_stdin_write = pass_stdin ? IO.pipe : [nil, nil]
+
+ capture_stdout = [:stdout, [:stdout, :stderr]].include?(capture) || logger
+ pipe_stdout_read, pipe_stdout_write = capture_stdout ? IO.pipe : [nil, nil]
+
+ capture_stderr = [:stderr, [:stdout, :stderr]].include?(capture) || logger
+ pipe_stderr_read, pipe_stderr_write = capture_stderr ? IO.pipe : [nil, nil]
+
+ if logger
+ logger.debug "Executing command #{command.inspect} with #{describe_args(args)}."
+ logger.debug "Standard input: " + (stdin.empty? ? "(none)" : stdin)
+ end
+
+ pid = fork do
+ begin
+ if pass_stdin
+ pipe_stdin_write.close
+ STDIN.reopen(pipe_stdin_read)
+ pipe_stdin_read.close
+ else
+ STDIN.reopen("/dev/null", "r")
+ end
+
+ if capture_stdout
+ pipe_stdout_read.close
+ STDOUT.reopen(pipe_stdout_write)
+ pipe_stdout_write.close
+ else
+ STDOUT.reopen("/dev/null", "w")
+ end
+
+ if capture_stderr
+ pipe_stderr_read.close
+ STDERR.reopen(pipe_stderr_write)
+ pipe_stderr_write.close
+ else
+ STDERR.reopen("/dev/null", "w")
+ end
+
+ # All file descriptors from 3 above should be closed here, but since I
+ # don't know about any way how to detect the maximum file descriptor
+ # number portably in Ruby, I didn't implement it. Patches welcome.
+
+ exec([command, command], *args)
+ rescue SystemCallError => e
+ exit!(127)
+ end
+ end
+
+ [pipe_stdin_read, pipe_stdout_write, pipe_stderr_write].each { |p| p.close if p }
+
+ # We write the command's input and read its output using a select loop. Why?
+ # Because otherwise we could end up with a deadlock.
+ #
+ # Imagine if we first read the whole standard output and then the whole
+ # error output, but the executed command would write lot of data but only to
+ # the error output. Sooner or later it would fill the buffer and block,
+ # while we would be blocked on reading the standard output -- classic
+ # deadlock.
+ #
+ # Similar issues can happen with standard input vs. one of the outputs.
+ if pass_stdin || capture_stdout || capture_stderr
+ stdout = ""
+ stderr = ""
+
+ loop do
+ pipes_readable = [pipe_stdout_read, pipe_stderr_read].compact.select { |p| !p.closed? }
+ pipes_writable = [pipe_stdin_write].compact.select { |p| !p.closed? }
+
+ break if pipes_readable.empty? && pipes_writable.empty?
+
+ ios_read, ios_write, ios_error = select(pipes_readable, pipes_writable,
+ pipes_readable + pipes_writable)
+
+ if !ios_error.empty?
+ raise IOError, "Error when communicating with executed program."
+ end
+
+ if ios_read.include?(pipe_stdout_read)
+ begin
+ stdout += pipe_stdout_read.readpartial(4096)
+ rescue EOFError
+ pipe_stdout_read.close
+ end
+ end
+
+ if ios_read.include?(pipe_stderr_read)
+ begin
+ stderr += pipe_stderr_read.readpartial(4096)
+ rescue EOFError
+ pipe_stderr_read.close
+ end
+ end
+
+ if ios_write.include?(pipe_stdin_write)
+ n = pipe_stdin_write.syswrite(stdin)
+ stdin = stdin[n..-1]
+ pipe_stdin_write.close if stdin.empty?
+ end
+ end
+ end
+
+ pid, status = Process.wait2(pid)
+ begin
+ if !status.success?
+ raise ExecutionFailed.new(command, args, status,
+ capture_stdout ? stdout : nil,
+ capture_stderr ? stderr : nil,
+ "Execution of command #{command.inspect} " +
+ "with #{describe_args(args)} " +
+ "failed with status #{status.exitstatus}.")
+ end
+ ensure
+ if logger
+ logger.debug "Status: #{status.exitstatus}"
+ logger.debug "Standard output: " + (stdout.empty? ? "(none)" : stdout)
+ logger.debug "Error output: " + (stderr.empty? ? "(none)" : stderr)
+ end
+ end
+
+ case capture
+ when nil
+ nil
+ when :stdout
+ stdout
+ when :stderr
+ stderr
+ when [:stdout, :stderr]
+ [stdout, stderr]
+ end
+ end
+
+ def self.describe_args(args)
+ args.empty? ? "no arguments" : "arguments #{args.map(&:inspect).join(", ")}"
+ end
+end
395 test/cheetah_test.rb
@@ -0,0 +1,395 @@
+require File.expand_path(File.dirname(__FILE__) + "/test_helper")
+
+class CheetahTest < Test::Unit::TestCase
+ context "run" do
+ # Fundamental question: To mock or not to mock the actual system interface?
+ #
+ # I decided not to mock so we can be sure Cheetah#run really works. Also the
+ # mocking would be quite complex (if possible at all) given the
+ # forking/piping/selecting in the code.
+ #
+ # Of course, this decision makes this unit test an intgration test by strict
+ # definitions.
+
+ setup do
+ @tmp_dir = "/tmp/cheetah_test_#{Process.pid}"
+ FileUtils.mkdir(@tmp_dir)
+ end
+
+ teardown do
+ FileUtils.rm_rf(@tmp_dir)
+ end
+
+ context "running commands" do
+ should "run a command without arguments" do
+ command = create_command("touch #@tmp_dir/touched")
+
+ Cheetah.run command
+
+ assert File.exists?("#@tmp_dir/touched")
+ end
+
+ should "run a command with arguments" do
+ command = create_command("echo -n \"$@\" >> #@tmp_dir/args")
+
+ Cheetah.run command, "foo", "bar", "baz"
+
+ assert_equal "foo bar baz", File.read("#@tmp_dir/args")
+ end
+
+ should "run a command without arguments using one array param" do
+ command = create_command("touch #@tmp_dir/touched")
+
+ Cheetah.run [command]
+
+ assert File.exists?("#@tmp_dir/touched")
+ end
+
+ should "run a command with arguments using one array param" do
+ command = create_command("echo -n \"$@\" >> #@tmp_dir/args")
+
+ Cheetah.run [command, "foo", "bar", "baz"]
+
+ assert_equal "foo bar baz", File.read("#@tmp_dir/args")
+ end
+
+ should "not mind weird characters in the command" do
+ command = create_command("touch #@tmp_dir/touched", :name => "we ! ir $d")
+
+ Cheetah.run command
+
+ assert File.exists?("#@tmp_dir/touched")
+ end
+
+ should "not mind weird characters in the arguments" do
+ command = create_command("echo -n \"$@\" >> #@tmp_dir/args")
+
+ Cheetah.run command, "we ! ir $d", "we ! ir $d", "we ! ir $d"
+
+ assert_equal "we ! ir $d we ! ir $d we ! ir $d", File.read("#@tmp_dir/args")
+ end
+
+ should "not pass the command to the shell" do
+ command = create_command("touch #@tmp_dir/touched", :name => "foo < bar > baz | qux")
+
+ Cheetah.run command
+
+ assert File.exists?("#@tmp_dir/touched")
+ end
+ end
+
+ context "error handling" do
+ context "basics" do
+ should "raise an exception when the command is not found" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ Cheetah.run "unknown", "foo", "bar", "baz"
+ end
+
+ assert_equal "unknown", e.command
+ assert_equal ["foo", "bar", "baz"], e.args
+ assert_equal 127, e.status.exitstatus
+ end
+
+ should "raise an exception when the command returns non-zero status" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ Cheetah.run "false", "foo", "bar", "baz"
+ end
+
+ assert_equal "false", e.command
+ assert_equal ["foo", "bar", "baz"], e.args
+ assert_equal 1, e.status.exitstatus
+ end
+ end
+
+ context "error message" do
+ should "raise an exception with a correct message for a command without arguments" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ Cheetah.run "false"
+ end
+
+ assert_equal(
+ "Execution of command \"false\" with no arguments failed with status 1.",
+ e.message
+ )
+ end
+
+ should "raise an exception with a correct message for a command with arguments" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ Cheetah.run "false", "foo", "bar", "baz"
+ end
+
+ assert_equal(
+ "Execution of command \"false\" with arguments \"foo\", \"bar\", \"baz\" failed with status 1.",
+ e.message
+ )
+ end
+ end
+
+ context "capturing" do
+ should "raise an exception with both stdout and stderr not set with no :capture option" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ Cheetah.run "false"
+ end
+
+ assert_equal nil, e.stdout
+ assert_equal nil, e.stderr
+ end
+
+ should "raise an exception with both stdout and stderr not set with :capture => nil" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ Cheetah.run "false", :capture => nil
+ end
+
+ assert_equal nil, e.stdout
+ assert_equal nil, e.stderr
+ end
+
+ should "raise an exception with only stdout set with :capture => :stdout" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ command = create_command("echo -n ''; exit 1")
+ Cheetah.run command, :capture => :stdout
+ end
+
+ assert_equal "", e.stdout
+ assert_equal nil, e.stderr
+
+ e = assert_raise Cheetah::ExecutionFailed do
+ command = create_command("echo -n output; exit 1")
+ Cheetah.run command, :capture => :stdout
+ end
+
+ assert_equal "output", e.stdout
+ assert_equal nil, e.stderr
+ end
+
+ should "raise an exception with only stderr set with :capture => :stderr" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ command = create_command("echo -n '' 1>&2; exit 1")
+ Cheetah.run command, :capture => :stderr
+ end
+
+ assert_equal nil, e.stdout
+ assert_equal "", e.stderr
+
+ e = assert_raise Cheetah::ExecutionFailed do
+ command = create_command("echo -n error 1>&2; exit 1")
+ Cheetah.run command, :capture => :stderr
+ end
+
+ assert_equal nil, e.stdout
+ assert_equal "error", e.stderr
+ end
+
+ should "raise an exception with both stdout and stderr set with :capture => [:stdout, :stderr]" do
+ e = assert_raise Cheetah::ExecutionFailed do
+ command = create_command(<<-EOT)
+ echo -n ''
+ echo -n '' 1>&2
+ exit 1
+ EOT
+ Cheetah.run command, :capture => [:stdout, :stderr]
+ end
+
+ assert_equal "", e.stdout
+ assert_equal "", e.stderr
+
+ e = assert_raise Cheetah::ExecutionFailed do
+ command = create_command(<<-EOT)
+ echo -n output
+ echo -n error 1>&2
+ exit 1
+ EOT
+ Cheetah.run command, :capture => [:stdout, :stderr]
+ end
+
+ assert_equal "output", e.stdout
+ assert_equal "error", e.stderr
+ end
+ end
+ end
+
+ context "capturing" do
+ should "not use standard output of the parent process with no :capture option" do
+ # We just open a random file to get a file descriptor into which we can
+ # save our stdout.
+ saved_stdout = File.open("/dev/null", "w")
+ saved_stdout.reopen(STDOUT)
+
+ reader, writer = IO.pipe
+
+ STDOUT.reopen(writer)
+ begin
+ Cheetah.run("echo", "-n", "output")
+ ensure
+ STDOUT.reopen(saved_stdout)
+ writer.close
+ end
+
+ assert_equal "", reader.read
+ reader.close
+ end
+
+ should "not use error output of the parent process with no :capture option" do
+ # We just open a random file to get a file descriptor into which we can
+ # save our stderr.
+ saved_stderr = File.open("/dev/null", "w")
+ saved_stderr.reopen(STDERR)
+
+ reader, writer = IO.pipe
+
+ STDERR.reopen(writer)
+ begin
+ command = create_command("echo -n error 1>&2")
+ Cheetah.run command
+ ensure
+ STDERR.reopen(saved_stderr)
+ writer.close
+ end
+
+ assert_equal "", reader.read
+ reader.close
+ end
+
+ should "return nil with no :capture option" do
+ assert_equal nil, Cheetah.run("true")
+ end
+
+ should "return nil with :capture => nil" do
+ assert_equal nil, Cheetah.run("true", :capture => nil)
+ end
+
+ should "return the standard output with :capture => :stdout" do
+ assert_equal "", Cheetah.run("echo", "-n", "", :capture => :stdout)
+ assert_equal "output", Cheetah.run("echo", "-n", "output", :capture => :stdout)
+ end
+
+ should "return the error output with :capture => :stderr" do
+ command = create_command("echo -n '' 1>&2")
+ assert_equal "", Cheetah.run(command, :capture => :stderr)
+
+ command = create_command("echo -n error 1>&2")
+ assert_equal "error", Cheetah.run(command, :capture => :stderr)
+ end
+
+ should "return both outputs with :capture => [:stdout, :stderr]" do
+ command = create_command(<<-EOT)
+ echo -n ''
+ echo -n '' 1>&2
+ EOT
+ assert_equal ["", ""], Cheetah.run(command, :capture => [:stdout, :stderr])
+
+ command = create_command(<<-EOT)
+ echo -n output
+ echo -n error 1>&2
+ EOT
+ assert_equal ["output", "error"], Cheetah.run(command, :capture => [:stdout, :stderr])
+ end
+ end
+
+ context "logging" do
+ should "log a successful execution of a command without arguments" do
+ logger = mock
+ logger.expects(:debug).with("Executing command \"true\" with no arguments.")
+ logger.expects(:debug).with("Standard input: (none)")
+ logger.expects(:debug).with("Status: 0")
+ logger.expects(:debug).with("Standard output: (none)")
+ logger.expects(:debug).with("Error output: (none)")
+
+ Cheetah.run "true", :logger => logger
+ end
+
+ should "log a successful execution of a command with arguments" do
+ logger = mock
+ logger.expects(:debug).with("Executing command \"true\" with arguments \"foo\", \"bar\", \"baz\".")
+ logger.expects(:debug).with("Standard input: (none)")
+ logger.expects(:debug).with("Status: 0")
+ logger.expects(:debug).with("Standard output: (none)")
+ logger.expects(:debug).with("Error output: (none)")
+
+ Cheetah.run "true", "foo", "bar", "baz", :logger => logger
+ end
+
+ should "log a successful execution of a command doing I/O" do
+ logger = mock
+ logger.expects(:debug).with("Executing command \"#@tmp_dir/command\" with no arguments.")
+ logger.expects(:debug).with("Standard input: (none)")
+ logger.expects(:debug).with("Status: 0")
+ logger.expects(:debug).with("Standard output: (none)")
+ logger.expects(:debug).with("Error output: (none)")
+
+ command = create_command(<<-EOT)
+ echo -n ''
+ echo -n '' 1>&2
+ EOT
+ Cheetah.run command, :stdin => "", :logger => logger
+
+ logger = mock
+ logger.expects(:debug).with("Executing command \"#@tmp_dir/command\" with no arguments.")
+ logger.expects(:debug).with("Standard input: blah")
+ logger.expects(:debug).with("Status: 0")
+ logger.expects(:debug).with("Standard output: output")
+ logger.expects(:debug).with("Error output: error")
+
+ command = create_command(<<-EOT)
+ echo -n output
+ echo -n error 1>&2
+ EOT
+ Cheetah.run command, :stdin => "blah", :logger => logger
+ end
+
+ should "log an unsuccessful execution of a command" do
+ logger = mock
+ logger.expects(:debug).with("Executing command \"false\" with no arguments.")
+ logger.expects(:debug).with("Standard input: (none)")
+ logger.expects(:debug).with("Status: 1")
+ logger.expects(:debug).with("Standard output: (none)")
+ logger.expects(:debug).with("Error output: (none)")
+
+ begin
+ Cheetah.run "false", :logger => logger
+ rescue Cheetah::ExecutionFailed
+ # Eat it.
+ end
+ end
+ end
+
+ context "input" do
+ should "not use standard input of the parent process with no :stdin option" do
+ # We just open a random file to get a file descriptor into which we can
+ # save our stdin.
+ saved_stdin = File.open("/dev/null", "r")
+ saved_stdin.reopen(STDIN)
+
+ reader, writer = IO.pipe
+
+ writer.write "blah"
+ writer.close
+
+ STDIN.reopen(reader)
+ begin
+ assert_equal "", Cheetah.run("cat", :capture => :stdout)
+ ensure
+ STDIN.reopen(saved_stdin)
+ reader.close
+ end
+ end
+
+ should "pass :stdin option value to standard input" do
+ assert_equal "", Cheetah.run("cat", :stdin => "", :capture => :stdout)
+ assert_equal "blah", Cheetah.run("cat", :stdin => "blah", :capture => :stdout)
+ end
+ end
+ end
+
+ def create_command(source, options = {})
+ command = "#@tmp_dir/" + (options[:name] || "command")
+
+ File.open(command, "w") do |f|
+ f.puts "#!/bin/sh"
+ f.puts source
+ end
+ FileUtils.chmod(0777, command)
+
+ command
+ end
+end
8 test/test_helper.rb
@@ -0,0 +1,8 @@
+require "test/unit"
+require "fileutils"
+
+require "rubygems"
+require "shoulda-context"
+require "mocha"
+
+require File.expand_path(File.dirname(__FILE__) + "/../lib/cheetah")

0 comments on commit cb10715

Please sign in to comment.
Something went wrong with that request. Please try again.