require 'rake/clean' require 'tempfile' require 'net/http' require 'launchy' require 'colorize' require 'erb' require 'tempfile' # Build Constants REPO_ROOT = File.dirname(__FILE__) BUILD_DIR = File.join(REPO_ROOT, "build") REPORT_DIR = File.join(REPO_ROOT, "reports") LMS_REPORT_DIR = File.join(REPORT_DIR, "lms") # Packaging constants DEPLOY_DIR = "/opt/wwc" PACKAGE_NAME = "mitx" LINK_PATH = "/opt/wwc/mitx" PKG_VERSION = "0.1" COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10] BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '') BUILD_NUMBER = (ENV["BUILD_NUMBER"] || "dev").chomp() if BRANCH == "master" DEPLOY_NAME = "#{PACKAGE_NAME}-#{BUILD_NUMBER}-#{COMMIT}" else DEPLOY_NAME = "#{PACKAGE_NAME}-#{BRANCH}-#{BUILD_NUMBER}-#{COMMIT}" end PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming" NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-') INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME) # Set up the clean and clobber tasks CLOBBER.include(BUILD_DIR, REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles') CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util") def select_executable(*cmds) cmds.find_all{ |cmd| system("which #{cmd} > /dev/null 2>&1") }[0] || fail("No executables found from #{cmds.join(', ')}") end def django_admin(system, env, command, *args) django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin') return "#{django_admin} #{command} --traceback --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}" end def django_for_jasmine(system, django_reload) if !django_reload reload_arg = '--noreload' end django_pid = fork do exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', "12345", reload_arg).split(' ')) end jasmine_url = 'http://localhost:12345/_jasmine/' up = false start_time = Time.now until up do if Time.now - start_time > 30 abort "Timed out waiting for server to start to run jasmine tests" end begin response = Net::HTTP.get_response(URI(jasmine_url)) puts response.code up = response.code == '200' rescue => e puts e.message ensure puts('Waiting server to start') sleep(0.5) end end begin yield jasmine_url ensure if django_reload Process.kill(:SIGKILL, -Process.getpgid(django_pid)) else Process.kill(:SIGKILL, django_pid) end Process.wait(django_pid) end end def template_jasmine_runner(lib) coffee_files = Dir["#{lib}/**/js/**/*.coffee", "common/static/coffee/src/**/*.coffee"] if !coffee_files.empty? sh("coffee -c #{coffee_files.join(' ')}") end phantom_jasmine_path = File.expand_path("common/test/phantom-jasmine") common_js_root = File.expand_path("common/static/js") common_coffee_root = File.expand_path("common/static/coffee/src") # Get arrays of spec and source files, ordered by how deep they are nested below the library # (and then alphabetically) and expanded from a relative to an absolute path spec_glob = File.join("#{lib}", "**", "spec", "**", "*.js") src_glob = File.join("#{lib}", "**", "src", "**", "*.js") js_specs = Dir[spec_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)} js_source = Dir[src_glob].sort_by {|p| [p.split('/').length, p]} .map {|f| File.expand_path(f)} template = ERB.new(File.read("#{lib}/jasmine_test_runner.html.erb")) template_output = "#{lib}/jasmine_test_runner.html" File.open(template_output, 'w') do |f| f.write(template.result(binding)) end yield File.expand_path(template_output) end def report_dir_path(dir) return File.join(REPORT_DIR, dir.to_s) end task :default => [:test, :pep8, :pylint] directory REPORT_DIR default_options = { :lms => '8000', :cms => '8001', } task :predjango do sh("find . -type f -name '*.pyc' -delete") sh('pip install -q --upgrade --no-deps -r local-requirements.txt') end task :clean_test_files do sh("git clean -fqdx test_root") end [:lms, :cms, :common].each do |system| report_dir = report_dir_path(system) directory report_dir desc "Run pep8 on all #{system} code" task "pep8_#{system}" => report_dir do sh("pep8 --ignore=E501 #{system}/djangoapps #{system}/lib | tee #{report_dir}/pep8.report") end task :pep8 => "pep8_#{system}" desc "Run pylint on all #{system} code" task "pylint_#{system}" => report_dir do Dir["#{system}/djangoapps/*", "#{system}/lib/*"].each do |app| if File.exists? "#{app}/setup.py" pythonpath_prefix = "PYTHONPATH=#{app}" else pythonpath_prefix = "PYTHONPATH=#{File.dirname(app)}" end app = File.basename(app) sh("#{pythonpath_prefix} pylint --rcfile=.pylintrc -f parseable #{app} | tee #{report_dir}/#{app}.pylint.report") end end task :pylint => "pylint_#{system}" end $failed_tests = 0 def run_under_coverage(cmd, root) cmd0, cmd_rest = cmd.split(" ", 2) # We use "python -m coverage" so that the proper python will run the importable coverage # rather than the coverage that OS path finds. cmd = "python -m coverage run --rcfile=#{root}/.coveragerc `which #{cmd0}` #{cmd_rest}" return cmd end def run_tests(system, report_dir, stop_on_failure=true) ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"] cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each) sh(run_under_coverage(cmd, system)) do |ok, res| if !ok and stop_on_failure abort "Test failed!" end $failed_tests += 1 unless ok end end TEST_TASK_DIRS = [] [:lms, :cms].each do |system| report_dir = report_dir_path(system) # Per System tasks desc "Run all django tests on our djangoapps for the #{system}" task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"] # Have a way to run the tests without running collectstatic -- useful when debugging without # messing with static files. task "fasttest_#{system}", [:stop_on_failure] => [report_dir, :predjango] do |t, args| args.with_defaults(:stop_on_failure => 'true') run_tests(system, report_dir, args.stop_on_failure) end TEST_TASK_DIRS << system desc <<-desc Start the #{system} locally with the specified environment (defaults to dev). Other useful environments are devplus (for dev testing with a real local database) desc task system, [:env, :options] => [:predjango] do |t, args| args.with_defaults(:env => 'dev', :options => default_options[system]) sh(django_admin(system, args.env, 'runserver', args.options)) end # Per environment tasks Dir["#{system}/envs/**/*.py"].each do |env_file| env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.') desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors" task "#{system}:check_settings:#{env}" => :predjango do sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}") end desc "Run collectstatic in the specified environment" task "#{system}:collectstatic:#{env}" => :predjango do sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /tmp/collectstatic.out") do |ok, status| if !ok abort "collectstatic failed!" end end end end desc "Open jasmine tests for #{system} in your default browser" task "browse_jasmine_#{system}" do django_for_jasmine(system, true) do |jasmine_url| Launchy.open(jasmine_url) puts "Press ENTER to terminate".red $stdin.gets end end desc "Use phantomjs to run jasmine tests for #{system} from the console" task "phantomjs_jasmine_#{system}" do phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' django_for_jasmine(system, false) do |jasmine_url| sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{jasmine_url}") end end end desc "Reset the relational database used by django. WARNING: this will delete all of your existing users" task :resetdb, [:env] do |t, args| args.with_defaults(:env => 'dev') sh(django_admin(:lms, args.env, 'syncdb')) sh(django_admin(:lms, args.env, 'migrate')) end desc "Update the relational database to the latest migration" task :migrate, [:env] do |t, args| args.with_defaults(:env => 'dev') sh(django_admin(:lms, args.env, 'migrate')) end Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib| task_name = "test_#{lib}" report_dir = report_dir_path(lib) desc "Run tests for common lib #{lib}" task task_name => report_dir do ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml") cmd = "nosetests #{lib} --logging-clear-handlers --with-xunit" sh(run_under_coverage(cmd, lib)) do |ok, res| $failed_tests += 1 unless ok end end TEST_TASK_DIRS << lib desc "Run tests for common lib #{lib} (without coverage)" task "fasttest_#{lib}" do sh("nosetests #{lib}") end desc "Open jasmine tests for #{lib} in your default browser" task "browse_jasmine_#{lib}" do template_jasmine_runner(lib) do |f| sh("python -m webbrowser -t 'file://#{f}'") puts "Press ENTER to terminate".red $stdin.gets end end desc "Use phantomjs to run jasmine tests for #{lib} from the console" task "phantomjs_jasmine_#{lib}" do phantomjs = ENV['PHANTOMJS_PATH'] || 'phantomjs' template_jasmine_runner(lib) do |f| sh("#{phantomjs} common/test/phantom-jasmine/lib/run_jasmine_test.coffee #{f}") end end end task :report_dirs TEST_TASK_DIRS.each do |dir| report_dir = report_dir_path(dir) directory report_dir task :report_dirs => [REPORT_DIR, report_dir] end task :test do TEST_TASK_DIRS.each do |dir| Rake::Task["test_#{dir}"].invoke(false) end if $failed_tests > 0 abort "Tests failed!" end end namespace :coverage do desc "Build the html coverage reports" task :html => :report_dirs do TEST_TASK_DIRS.each do |dir| report_dir = report_dir_path(dir) if !File.file?("#{report_dir}/.coverage") next end sh("coverage html --rcfile=#{dir}/.coveragerc") end end desc "Build the xml coverage reports" task :xml => :report_dirs do TEST_TASK_DIRS.each do |dir| report_dir = report_dir_path(dir) if !File.file?("#{report_dir}/.coverage") next end # Why doesn't the rcfile control the xml output file properly?? sh("coverage xml -o #{report_dir}/coverage.xml --rcfile=#{dir}/.coveragerc") end end end task :runserver => :lms desc "Run django-admin <action> against the specified system and environment" task "django-admin", [:action, :system, :env, :options] => [:predjango] do |t, args| args.with_defaults(:env => 'dev', :system => 'lms', :options => '') sh(django_admin(args.system, args.env, args.action, args.options)) end desc "Set the staff bit for a user" task :set_staff, [:user, :system, :env] do |t, args| args.with_defaults(:env => 'dev', :system => 'lms', :options => '') sh(django_admin(args.system, args.env, 'set_staff', args.user)) end task :package do FileUtils.mkdir_p(BUILD_DIR) Dir.chdir(BUILD_DIR) do afterremove = Tempfile.new('afterremove') afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '') #! /bin/bash set -e set -x # to be a little safer this rm is executed # as the makeitso user if [[ -d "#{INSTALL_DIR_PATH}" ]]; then sudo rm -rf "#{INSTALL_DIR_PATH}" fi AFTERREMOVE afterremove.close() FileUtils.chmod(0755, afterremove.path) args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb", "--after-remove=#{afterremove.path}", "--prefix=#{INSTALL_DIR_PATH}", "--exclude=**/build/**", "--exclude=**/rakefile", "--exclude=**/.git/**", "--exclude=**/*.pyc", "--exclude=**/reports/**", "--exclude=**/test_root/**", "--exclude=**/.coverage/**", "-C", "#{REPO_ROOT}", "--provides=#{PACKAGE_NAME}", "--name=#{NORMALIZED_DEPLOY_NAME}", "--version=#{PKG_VERSION}", "-a", "all", "."] system(*args) || raise("fpm failed to build the .deb") end end task :publish => :package do sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}") end namespace :cms do desc "Clone existing MongoDB based course" task :clone do if ENV['SOURCE_LOC'] and ENV['DEST_LOC'] sh(django_admin(:cms, :dev, :clone, ENV['SOURCE_LOC'], ENV['DEST_LOC'])) else raise "You must pass in a SOURCE_LOC and DEST_LOC parameters" end end end namespace :cms do desc "Delete existing MongoDB based course" task :delete_course do if ENV['LOC'] sh(django_admin(:cms, :dev, :delete_course, ENV['LOC'])) else raise "You must pass in a LOC parameter" end end end namespace :cms do desc "Import course data within the given DATA_DIR variable" task :import do if ENV['DATA_DIR'] and ENV['COURSE_DIR'] sh(django_admin(:cms, :dev, :import, ENV['DATA_DIR'], ENV['COURSE_DIR'])) elsif ENV['DATA_DIR'] sh(django_admin(:cms, :dev, :import, ENV['DATA_DIR'])) else raise "Please specify a DATA_DIR variable that point to your data directory.\n" + "Example: \`rake cms:import DATA_DIR=../data\`" end end end namespace :cms do desc "Import course data within the given DATA_DIR variable" task :xlint do if ENV['DATA_DIR'] and ENV['COURSE_DIR'] sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR'])) elsif ENV['DATA_DIR'] sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'])) else raise "Please specify a DATA_DIR variable that point to your data directory.\n" + "Example: \`rake cms:import DATA_DIR=../data\`" end end end namespace :cms do desc "Export course data to a tar.gz file" task :export do if ENV['COURSE_ID'] and ENV['OUTPUT_PATH'] sh(django_admin(:cms, :dev, :export, ENV['COURSE_ID'], ENV['OUTPUT_PATH'])) else raise "Please specify a COURSE_ID and OUTPUT_PATH.\n" + "Example: \`rake cms:export COURSE_ID=MITx/12345/name OUTPUT_PATH=foo.tar.gz\`" end end end desc "Build a properties file used to trigger autodeploy builds" task :autodeploy_properties do File.open("autodeploy.properties", "w") do |file| file.puts("UPSTREAM_NOOP=false") file.puts("UPSTREAM_BRANCH=#{BRANCH}") file.puts("UPSTREAM_JOB=#{PACKAGE_NAME}") file.puts("UPSTREAM_REVISION=#{COMMIT}") end end desc "Invoke sphinx 'make build' to generate docs." task :builddocs do Dir.chdir('docs') do sh('make html') end end desc "Show docs in browser (mac and ubuntu)." task :showdocs do Dir.chdir('docs/build/html') do if RUBY_PLATFORM.include? 'darwin' # mac os sh('open index.html') elsif RUBY_PLATFORM.include? 'linux' # make more ubuntu specific? sh('sensible-browser index.html') # ubuntu else raise "\nUndefined how to run browser on your machine. Please use 'rake builddocs' and then manually open 'mitx/docs/build/html/index.html." end end end desc "Build docs and show them in browser" task :doc => :builddocs do Rake::Task["showdocs"].invoke end