railsapp_factory 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,118 @@
1
+ require 'logger'
2
+ require 'railsapp_factory/string_inquirer'
3
+
4
+ class RailsappFactory
5
+ module HelperMethods
6
+
7
+ attr_writer :logger
8
+
9
+ attr_accessor :gem_source, :db, :timeout, :logger
10
+
11
+ def override_ENV
12
+ @override_ENV ||= {}
13
+ end
14
+
15
+ def logger
16
+ @logger ||= Logger.new(STDERR)
17
+ end
18
+
19
+ def uri(path = '', query_args = {})
20
+ URI(self.url(path, query_args))
21
+ end
22
+
23
+ def url(path = '', query_args = {})
24
+ "http://127.0.0.1:#{self.port}/" + path.to_s.sub(/^\//, '') + self.class.encode_query(query_args)
25
+ end
26
+
27
+ def env=(value)
28
+ self.override_ENV['RAILS_ENV'] = value
29
+ self.override_ENV['RACK_ENV'] = value
30
+ self.env
31
+ end
32
+
33
+ def env
34
+ rails_env = self.override_ENV['RAILS_ENV'] || self.override_ENV['RACK_ENV'] || 'test'
35
+ @_env = nil unless @_env.to_s == rails_env
36
+ @_env ||= RailsappFactory::StringInquirer.new(rails_env)
37
+ end
38
+
39
+ #def rubies(rails_v = @version)
40
+ # self.class.rubies(rails_v)
41
+ #end
42
+
43
+ def alive?
44
+ if @pid
45
+ begin
46
+ Process.kill(0, @pid)
47
+ self.logger.debug "Process #{@pid} is alive"
48
+ true
49
+ rescue Errno::EPERM
50
+ self.logger.warning "Process #{@pid} has changed uid - we will not be able to signal it to finish"
51
+ true # changed uid
52
+ rescue Errno::ESRCH
53
+ self.logger.debug "Process #{@pid} not found"
54
+ false # NOT running
55
+ rescue Exception => ex
56
+ self.logger.warning "Process #{@pid} in unknown state: #{ex}"
57
+ nil # Unable to determine status
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # get backtrace except for this method
65
+
66
+ def get_backtrace
67
+ raise 'get backtrace'
68
+ rescue => ex
69
+ trace = ex.backtrace
70
+ trace.shift
71
+ trace
72
+ end
73
+
74
+ def see_log(file)
75
+ self.logger.debug? ? '' : " - see #{file}"
76
+ end
77
+
78
+ def append_log(file)
79
+ self.logger.debug? ? '' : " >> #{file} 2>&1"
80
+ end
81
+
82
+ def bundle_command
83
+ "#{self.class.ruby_command_prefix(self.using)} bundle"
84
+ end
85
+
86
+ def ruby_command(bundled = true)
87
+ "#{self.class.ruby_command_prefix(self.using)} #{bundled ? 'bundle exec' : ''} ruby"
88
+ end
89
+
90
+ def find_command(script_name, rails_arg)
91
+ result = Dir.chdir(root) do
92
+ if File.exists?("script/#{script_name}")
93
+ "#{bundle_command} exec script/#{script_name}"
94
+ elsif File.exists?('script/rails')
95
+ "#{bundle_command} exec script/rails #{rails_arg}"
96
+ else
97
+ "#{ruby_command(false)} .bundle/bin/rails #{rails_arg}"
98
+ end
99
+ end
100
+ self.logger.info("find_command(#{script_name.inspect}, #{rails_arg.inspect}) returned #{result.inspect}")
101
+ result
102
+ end
103
+
104
+ def generate_command
105
+ find_command('generate', 'generate')
106
+ end
107
+
108
+ def runner_command
109
+ find_command('runner', 'runner')
110
+ end
111
+
112
+ def server_command
113
+ find_command('server', 'server')
114
+ end
115
+
116
+ end
117
+
118
+ end
@@ -0,0 +1,157 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
6
+ class RailsappFactory
7
+ module RunInAppMethods
8
+
9
+ def using
10
+ @using ||= nil
11
+ end
12
+
13
+ def use(ruby_v)
14
+ if block_given?
15
+ begin
16
+ orig_using = self.using
17
+ @using = ruby_v.to_s
18
+ result = yield
19
+ ensure
20
+ @using = orig_using
21
+ end
22
+ else
23
+ result = @using = ruby_v.to_s
24
+ end
25
+ result
26
+ end
27
+
28
+ def in_app(in_directory = built? ? root : base_dir)
29
+ Dir.chdir(in_directory) do
30
+ setup_env do
31
+ if @timeout > 0
32
+ Timeout.timeout(@timeout) do
33
+ yield
34
+ end
35
+ else
36
+ yield
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # returns value from ruby command run in ruby, serialized via to_json to keep to simple values
43
+ def ruby_eval(expression, serialize_with = :json, evaluate_with = :ruby)
44
+ result = nil
45
+
46
+ evaluate_with = :runner if RUBY_VERSION =~ /^1\.8/ and serialize_with == :json
47
+ script_dir = File.join(base_dir, 'scripts')
48
+ FileUtils.mkdir_p script_dir
49
+ script_file = Tempfile.new(["#{evaluate_with}_", '.rb'], script_dir)
50
+ expression_file = script_file.path
51
+ output_file = "#{expression_file.sub(/\.rb$/, '')}.output"
52
+ script_contents = <<-EOF
53
+ require '#{serialize_with}'; value = begin; def value_of_expression; #{expression}
54
+ end
55
+ { 'value' => value_of_expression }
56
+ rescue Exception => ex
57
+ { 'exception' => ex.class.to_s, 'message' => ex.message, 'backtrace' => ex.backtrace }
58
+ end
59
+ File.open('#{output_file}', 'w') do |_script_output|
60
+ _script_output.puts value.to_#{serialize_with}
61
+ end
62
+ EOF
63
+ script_file.puts script_contents
64
+ script_file.close
65
+ self.logger.debug "#{evaluate_with}_eval running script #{expression_file} #{see_log('eval.log')}"
66
+ command = if evaluate_with == :ruby
67
+ ruby_command(bundled?)
68
+ elsif evaluate_with == :runner
69
+ runner_command
70
+ else
71
+ raise ArgumentError.new('invalid evaluate_with argument')
72
+ end
73
+ system_in_app "sh -xc '#{command} #{expression_file}' #{append_log('eval.log')}"
74
+ self.logger.info("#{evaluate_with}_eval of #{expression} returned exit status of #{$?} - #{expression_file}")
75
+ if File.size?(output_file)
76
+ res = if serialize_with == :json
77
+ JSON.parse(File.read(output_file))
78
+ else
79
+ YAML.load_file(output_file)
80
+ end
81
+ FileUtils.rm_f output_file
82
+ FileUtils.rm_f expression_file
83
+ if res.include? 'value'
84
+ result = res['value']
85
+ elsif res.include? 'exception'
86
+ result = begin
87
+ Object.const_get(res['exception']).new(res['message'] || 'Unknown')
88
+ rescue
89
+ RuntimeError.new("#{res['exception']}: #{res['message']}")
90
+ end
91
+ result.set_backtrace(res['backtrace'] + get_backtrace) if res['backtrace']
92
+ raise result
93
+ end
94
+ else
95
+ result = ArgumentError.new("unknown error, possibly a syntax error, (missing #{output_file}) #{see_log('eval.log')} - #{expression_file} contains:\n#{command}")
96
+ raise result
97
+ end
98
+ result
99
+ end
100
+
101
+ # returns value from expression passed to runner, serialized via to_json to keep to simple values
102
+ def rails_eval(expression, serialize_with = :json)
103
+ ruby_eval(expression, serialize_with, :runner)
104
+ end
105
+
106
+ def shell_eval(*args)
107
+ arg = prepend_ruby_version_command_to_arg(args)
108
+ in_app { IO.popen(arg, 'r').read }
109
+ end
110
+
111
+ def prepend_ruby_version_command_to_arg(args)
112
+ arg = args.count == 1 ? args.first : args
113
+ command_prefix = RailsappFactory.ruby_command_prefix(@using)
114
+ if arg.kind_of?(Array)
115
+ arg = command_prefix.split(' ') + arg
116
+ else
117
+ arg = "#{command_prefix} #{arg}"
118
+ end
119
+ self.logger.info("prepend_ruby_version_command_to_arg returned: #{arg.inspect}")
120
+ arg
121
+ end
122
+
123
+ def system_in_app(*args)
124
+ arg = prepend_ruby_version_command_to_arg(args)
125
+ in_app { Kernel.system(arg) }
126
+ end
127
+
128
+ private
129
+
130
+ def setup_env
131
+ result = nil
132
+ Bundler.with_clean_env do
133
+ self.override_ENV.each do |key, value|
134
+ unless %w{RAILS_ENV RACK_ENV}.include? key
135
+ self.logger.debug "setup_env: setting ENV[#{key.inspect}] = #{value.inspect}"
136
+ ENV[key] = value
137
+ end
138
+ end
139
+ rails_env = self.env.to_s
140
+ ENV['RAILS_ENV'] = ENV['RACK_ENV'] = rails_env
141
+ self.logger.debug "setup_env: setting ENV['RAILS_ENV'] = ENV['RACK_ENV'] = #{rails_env.inspect}"
142
+ ENV['RAILS_LTS'] = if @version =~ /lts/ then
143
+ 'true'
144
+ elsif @version =~ /^2\.3/
145
+ 'false'
146
+ else
147
+ nil
148
+ end
149
+ ENV['GEM_SOURCE'] = @gem_source if ENV['RAILS_LTS']
150
+ self.logger.debug "setup_env: setting ENV['GEM_SOURCE'] = #{@gem_source.inspect}, ENV['RAILS_LTS'] = #{ENV['RAILS_LTS'].inspect}" if ENV['RAILS_LTS']
151
+ result = yield
152
+ end
153
+ result
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,82 @@
1
+ require 'tempfile'
2
+ require 'fileutils'
3
+
4
+ class RailsappFactory
5
+ module ServerMethods
6
+
7
+ attr_reader :pid
8
+ attr_reader :port
9
+
10
+ def stop
11
+ if alive?
12
+ self.logger.info "Stopping server (pid #{pid})"
13
+ Kernel.system "ps -fp #{@pid}" if self.logger.debug?
14
+ Process.kill('INT', @pid) rescue nil
15
+ sleep(1)
16
+ Process.kill('INT', @pid) rescue nil
17
+ 20.times do
18
+ sleep(1)
19
+ break unless alive?
20
+ end
21
+ if alive?
22
+ self.logger.info "Gave up waiting (terminating process #{pid} with extreme prejudice)"
23
+ Process.kill('KILL', @pid) rescue nil
24
+ end
25
+ @pid = nil
26
+ end
27
+ if @server_handle
28
+ self.logger.debug 'Closing pipe to server process'
29
+ Timeout.timeout(@timeout) do
30
+ @server_handle.close
31
+ end
32
+ @server_handle = nil
33
+ end
34
+ self.logger.info 'Server has stopped'
35
+ end
36
+
37
+ def start
38
+ build unless built?
39
+ # find random unassigned port
40
+ server = TCPServer.new('127.0.0.1', 0)
41
+ @port = server.addr[1]
42
+ server.close
43
+ unless self.logger.debug?
44
+ log_dir = File.join(base_dir, 'logs')
45
+ FileUtils.mkdir_p log_dir
46
+ file = Tempfile.new(['server_', '.log'], log_dir)
47
+ @server_logfile = file.path
48
+ file.close
49
+ end
50
+ self.logger.info "Running Rails #{@version} server on port #{port} #{see_log @server_logfile}"
51
+ exec_arg = defined?(JRUBY_VERSION) ? '' : 'exec'
52
+ in_app { @server_handle = IO.popen("#{exec_arg} /bin/sh -xc 'exec #{server_command} -p #{port}' #{append_log @server_logfile}", 'w') }
53
+ @pid = @server_handle.pid
54
+ # Detach process so alive? will detect if process dies (zombies still accept signals)
55
+ Process.detach(@pid)
56
+ serving_requests = false
57
+ t1 = Time.new
58
+ while true
59
+ raise TimeoutError.new("Waiting for server to be available on the port #{see_log @server_logfile}") if t1 + @timeout < Time.new
60
+ raise RailsappFactory::BuildError.new("Error starting server #{see_log @server_logfile}") unless alive?
61
+ sleep(1)
62
+ begin
63
+ response = Net::HTTP.get(self.uri)
64
+ if response
65
+ t2 = Time.new
66
+ self.logger.info 'Server responded to http GET after %3.1f seconds' % (t2 - t1)
67
+ serving_requests = true
68
+ break
69
+ end
70
+ rescue Errno::ECONNREFUSED
71
+ # do nothing
72
+ rescue Exception => ex
73
+ self.logger.debug "Ignoring exception #{ex} whilst waiting for server to start"
74
+ end
75
+ end
76
+ Kernel.system 'ps -f' if defined?(JRUBY_VERSION) #DEBUG
77
+ serving_requests
78
+ end
79
+
80
+ end
81
+
82
+ end
@@ -0,0 +1,26 @@
1
+ class RailsappFactory
2
+ # Wrapping a string in this class gives you a prettier way to test
3
+ # for equality. The value returned by <tt>Rails.env</tt> is wrapped
4
+ # in a StringInquirer object so instead of calling this:
5
+ #
6
+ # Rails.env == 'production'
7
+ #
8
+ # you can call this:
9
+ #
10
+ # Rails.env.production?
11
+ class StringInquirer < String
12
+ private
13
+
14
+ def respond_to_missing?(method_name, include_private = false)
15
+ method_name.to_s[-1, 1] == '?'
16
+ end
17
+
18
+ def method_missing(method_name, *arguments)
19
+ if method_name.to_s[-1, 1] == '?'
20
+ self == method_name.to_s[0..-2]
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,54 @@
1
+ require 'fileutils'
2
+ require 'open-uri'
3
+
4
+ require 'railsapp_factory/build_error'
5
+
6
+ class RailsappFactory
7
+ module TemplateMethods
8
+
9
+ attr_reader :template
10
+
11
+ def process_template
12
+ if self.template
13
+ if self.built?
14
+ if self.version =~ /^2/
15
+ # recheck config/environment.rb in case the template/s add more config.gem lines
16
+ use_template 'templates/use_bundler_with_rails23.rb'
17
+ end
18
+ self.logger.info "Processing template #{@template}"
19
+ unless self.system_in_app "sh -xc '.bundle/bin/rake rails:template LOCATION=#{@template}' #{append_log 'template.log'}"
20
+ raise RailsappFactory::BuildError.new("rake rails:template returned exist status #{$?} #{see_log 'rails_new.log'}")
21
+ end
22
+ clear_template
23
+ else
24
+ # build actions template
25
+ self.build
26
+ end
27
+ end
28
+ end
29
+
30
+ def append_to_template(text, source="append_to_template")
31
+ unless @template
32
+ template_dir = File.join(base_dir, 'templates')
33
+ FileUtils.mkdir_p(template_dir) unless File.directory?(template_dir)
34
+ @template = Tempfile.new('append_', template_dir).path + '.rb'
35
+ end
36
+ open(@template, 'a+') do |f|
37
+ f.puts "\n# #{source}:"
38
+ f.puts text
39
+ end
40
+ end
41
+
42
+ def use_template(template)
43
+ append_to_template(open(template).read, "use_template(#{template}")
44
+ end
45
+
46
+ protected
47
+
48
+ def clear_template
49
+ @template = nil
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,3 @@
1
+ class RailsappFactory
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,50 @@
1
+ require 'bundler'
2
+ require 'logger'
3
+
4
+ require 'railsapp_factory/class_methods'
5
+
6
+ require 'railsapp_factory/build_methods'
7
+ require 'railsapp_factory/helper_methods'
8
+ require 'railsapp_factory/run_in_app_methods'
9
+ require 'railsapp_factory/server_methods'
10
+ require 'railsapp_factory/string_inquirer'
11
+ require 'railsapp_factory/template_methods'
12
+
13
+ require 'railsapp_factory/build_error'
14
+
15
+ require 'railsapp_factory/version'
16
+
17
+ class RailsappFactory
18
+
19
+ extend RailsappFactory::ClassMethods
20
+ include RailsappFactory::BuildMethods
21
+ include RailsappFactory::HelperMethods
22
+ include RailsappFactory::RunInAppMethods
23
+ include RailsappFactory::ServerMethods
24
+ include RailsappFactory::TemplateMethods
25
+
26
+ TMPDIR = File.expand_path('tmp/railsapps')
27
+
28
+ attr_reader :version # version requested, may be specific release, or the first part of a release number
29
+
30
+ def initialize(version = nil, logger = Logger.new(STDERR))
31
+ self.logger = logger
32
+ @version = version
33
+ unless @version
34
+ @version = RailsappFactory.versions(RUBY_VERSION).last || '4.0'
35
+ end
36
+ self.logger.info("RailsappFactory.new(#{version.inspect}) called - version set to #{@version}")
37
+ raise ArgumentError.new("Invalid version (#{@version})") if @version.to_s !~ /^[2-9](\.\d+){1,2}(-lts)?$/
38
+ self.gem_source = 'https://rubygems.org'
39
+ self.db = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3'
40
+ # 5 minutes
41
+ self.timeout = 300
42
+ # clears build vars
43
+ destroy
44
+ # clear template vars
45
+ clear_template
46
+ # use default ruby
47
+ use(nil)
48
+ end
49
+
50
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'railsapp_factory/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'railsapp_factory'
8
+ spec.version = RailsappFactory::VERSION
9
+ spec.authors = ['Ian Heggie']
10
+ spec.email = ['ian@heggie.biz']
11
+ spec.description = %q{Rails application factory to make testing gems against multiple versions easier}
12
+ spec.summary = %q{The prupose of this gem is to make integration testing of gems and libraries against multiple versions of rails easy and avoid having to keep copies of the framework in the gem being tested}
13
+ spec.homepage = ''
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'bundler', '~> 1.3'
22
+ spec.add_dependency 'json_pure'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec', '~> 2.12'
25
+ end
@@ -0,0 +1,121 @@
1
+ require 'rspec'
2
+ require "spec_helper"
3
+ require 'railsapp_factory/class_methods'
4
+
5
+ describe 'RailsappFactory::ClassMethods' do
6
+
7
+ class SubjectClass
8
+ extend RailsappFactory::ClassMethods
9
+ end
10
+
11
+ describe '::versions' do
12
+
13
+ it "should list some rails versions that are compatible with ruby #{RUBY_VERSION}" do
14
+ list = SubjectClass.versions
15
+ list.should be_a_kind_of(Array)
16
+ list.should_not be_empty
17
+ end
18
+
19
+ it 'should return an empty list for unknown ruby versions' do
20
+ list = SubjectClass.versions('1.5.0')
21
+ list.should be_a_kind_of(Array)
22
+ list.should be_empty
23
+ end
24
+
25
+ # taken from http://www.devalot.com/articles/2012/03/ror-compatibility
26
+ {
27
+ '1.something' => [],
28
+ '1.8.6' => %w{2.3},
29
+ '1.8.7' => %w{2.3 2.3-lts 3.0 3.1 3.2},
30
+ '1.9.1' => %w{2.3},
31
+ '1.9.2' => %w{3.0 3.1 3.2},
32
+ '1.9.3' => %w{3.0 3.1 3.2 4.0},
33
+ '2.0.x' => %w{4.0},
34
+ 'unknown' => %w{4.0},
35
+ '' => %w{2.3 2.3-lts 3.0 3.1 3.2 4.0}
36
+ }.each do |ruby_v, expected|
37
+ it "should list rails versions that are compatible with ruby #{ruby_v}" do
38
+ list = SubjectClass.versions(ruby_v)
39
+ list.should be_a_kind_of(Array)
40
+ list.should == expected
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ describe '::rubies' do
47
+
48
+ it 'should list some ruby versions' do
49
+ list = SubjectClass.rubies
50
+ list.should be_a_kind_of(Array)
51
+ list.should_not be_empty
52
+ end
53
+
54
+ #it 'should return an empty list for unknown rails versions' do
55
+ # list = SubjectClass.rubies('1.5.0')
56
+ # list.should be_a_kind_of(Array)
57
+ # list.should be_empty
58
+ #end
59
+
60
+ #SubjectClass.versions(nil).each do |rails_v|
61
+ # it "should only list ruby versions that are compatible with rails #{rails_v}" do
62
+ # SubjectClass.rubies(rails_v).each do |ruby_v|
63
+ # SubjectClass.versions(ruby_v).should include(rails_v)
64
+ # end
65
+ # end
66
+ #end
67
+ end
68
+
69
+ it '::ruby_command_prefix should return a string' do
70
+ res = SubjectClass.ruby_command_prefix
71
+ res.should be_a(String)
72
+ end
73
+
74
+ it '::has_ruby_version_manager? should return a Boolean' do
75
+ res = SubjectClass.has_ruby_version_manager?
76
+ res.should be_a(res ? TrueClass : FalseClass)
77
+ end
78
+
79
+ it '::using_system_ruby? should return a Boolean' do
80
+ res = SubjectClass.using_system_ruby?
81
+ res.should be_a(res ? TrueClass : FalseClass)
82
+ end
83
+
84
+ describe '::ruby_command_prefix' do
85
+ include ::SpecHelper
86
+
87
+ SubjectClass.rubies.each do |ruby_v|
88
+ it "provides a command prefix that will run ruby #{ruby_v}" do
89
+ prefix = SubjectClass.ruby_command_prefix(ruby_v)
90
+ #puts "RailsappFactory.ruby_command_prefix(#{ruby_v}) = '#{prefix}'"
91
+ actual_ruby_v=`#{prefix} ruby -v`
92
+ actual_version_should_match_rubies_version(actual_ruby_v, ruby_v)
93
+ end
94
+ end
95
+
96
+ end
97
+
98
+ describe '::encode_query' do
99
+
100
+ it 'should encode a simple argument' do
101
+ SubjectClass.encode_query(:ian => 23).should == '?ian=23'
102
+ end
103
+
104
+ it 'should encode a nested argument' do
105
+ SubjectClass.encode_query(:author => {:ian => 23}).should == '?author%5Bian%5D=23'
106
+ end
107
+
108
+ it 'should encode a multiple arguments' do
109
+ res = SubjectClass.encode_query(:ian => 23, :john => '45')
110
+ #order not guaranteed
111
+ if res =~ /^.ian/
112
+ res.should == '?ian=23&john=45'
113
+ else
114
+ res.should == '?john=45&ian=23'
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+