rakefile 16.1 KB
Newer Older
1 2
require 'rake/clean'
require 'tempfile'
3 4 5
require 'net/http'
require 'launchy'
require 'colorize'
6 7
require 'erb'
require 'tempfile'
8

9
# Build Constants
10 11
REPO_ROOT = File.dirname(__FILE__)
BUILD_DIR = File.join(REPO_ROOT, "build")
12
REPORT_DIR = File.join(REPO_ROOT, "reports")
13
LMS_REPORT_DIR = File.join(REPORT_DIR, "lms")
14 15

# Packaging constants
16
DEPLOY_DIR = "/opt/wwc"
17
PACKAGE_NAME = "mitx"
18
LINK_PATH = "/opt/wwc/mitx"
19
PKG_VERSION = "0.1"
20
COMMIT = (ENV["GIT_COMMIT"] || `git rev-parse HEAD`).chomp()[0, 10]
21
BRANCH = (ENV["GIT_BRANCH"] || `git symbolic-ref -q HEAD`).chomp().gsub('refs/heads/', '').gsub('origin/', '')
22 23 24
BUILD_NUMBER = (ENV["BUILD_NUMBER"] || "dev").chomp()

if BRANCH == "master"
25
    DEPLOY_NAME = "#{PACKAGE_NAME}-#{BUILD_NUMBER}-#{COMMIT}"
26
else
27
    DEPLOY_NAME = "#{PACKAGE_NAME}-#{BRANCH}-#{BUILD_NUMBER}-#{COMMIT}"
28 29 30
end
PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming"

31 32
NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
33
# Set up the clean and clobber tasks
34
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles')
35
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
36

37 38 39 40
def select_executable(*cmds)
    cmds.find_all{ |cmd| system("which #{cmd} > /dev/null 2>&1") }[0] || fail("No executables found from #{cmds.join(', ')}")
end

41 42
def django_admin(system, env, command, *args)
    django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin')
43
    return "#{django_admin} #{command} --traceback --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}"
44
end
45

46 47 48 49 50
def django_for_jasmine(system, django_reload)
    if !django_reload
        reload_arg = '--noreload'
    end

51
    django_pid = fork do
52
        exec(*django_admin(system, 'jasmine', 'runserver', '-v', '0', "12345", reload_arg).split(' '))
53 54 55
    end
    jasmine_url = 'http://localhost:12345/_jasmine/'
    up = false
56
    start_time = Time.now
57
    until up do
58 59 60
        if Time.now - start_time > 30
            abort "Timed out waiting for server to start to run jasmine tests"
        end
61 62 63 64 65 66 67 68 69 70 71 72 73 74
        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
75 76 77 78 79
        if django_reload
            Process.kill(:SIGKILL, -Process.getpgid(django_pid))
        else
            Process.kill(:SIGKILL, django_pid)
        end
80 81 82
        Process.wait(django_pid)
    end
end
83

84 85 86 87 88 89 90 91
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")
92 93 94 95 96 97 98 99

    # 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)}

100 101 102 103 104 105 106 107 108
    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


109 110 111 112
def report_dir_path(dir)
    return File.join(REPORT_DIR, dir.to_s)
end

113
task :default => [:test, :pep8, :pylint]
114 115

directory REPORT_DIR
Calen Pennington committed
116

117 118 119 120 121
default_options = {
    :lms => '8000',
    :cms => '8001',
}

122
task :predjango do
cahrens committed
123
    sh("find . -type f -name '*.pyc' -delete")
124
    sh('pip install -q --upgrade --no-deps -r local-requirements.txt')
125 126
end

127
task :clean_test_files do
128
    sh("git clean -fqdx test_root")
129 130
end

131
[:lms, :cms, :common].each do |system|
132
    report_dir = report_dir_path(system)
133 134 135 136
    directory report_dir

    desc "Run pep8 on all #{system} code"
    task "pep8_#{system}" => report_dir do
137
        sh("pep8 #{system} | tee #{report_dir}/pep8.report")
138 139 140 141 142 143
    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|
144 145 146 147 148
            if File.exists? "#{app}/setup.py"
                pythonpath_prefix = "PYTHONPATH=#{app}"
            else
                pythonpath_prefix = "PYTHONPATH=#{File.dirname(app)}"
            end
149
            app = File.basename(app)
150 151 152 153 154
            if app =~ /.py$/
                app = app.gsub('.py', '')
            elsif app =~ /.pyc$/
                next
            end
155
            sh("#{pythonpath_prefix} pylint --rcfile=.pylintrc -f parseable #{app} | tee #{report_dir}/#{app}.pylint.report")
156 157 158
        end
    end
    task :pylint => "pylint_#{system}"
159

160 161
end

162
$failed_tests = 0
163

164
def run_under_coverage(cmd, root)
165
    cmd0, cmd_rest = cmd.split(" ", 2)
166 167 168
    # 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}"
169 170 171
    return cmd
end

172
def run_tests(system, report_dir, stop_on_failure=true)
173
    ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
174
    dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
175
    cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each)
176
    sh(run_under_coverage(cmd, system)) do |ok, res|
177 178 179 180 181
        if !ok and stop_on_failure
            abort "Test failed!"
        end
        $failed_tests += 1 unless ok
    end
182 183
end

184
TEST_TASK_DIRS = []
185

186
[:lms, :cms].each do |system|
187
    report_dir = report_dir_path(system)
188

189
    # Per System tasks
190
    desc "Run all django tests on our djangoapps for the #{system}"
191
    task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"]
192

193 194
    # Have a way to run the tests without running collectstatic -- useful when debugging without
    # messing with static files.
195 196 197
    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)
198
    end
199

200
    TEST_TASK_DIRS << system
201 202 203 204 205

    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
206
    task system, [:env, :options] => [:predjango] do |t, args|
207
        args.with_defaults(:env => 'dev', :options => default_options[system])
208 209
        sh(django_admin(system, args.env, 'runserver', args.options))
    end
210 211

    # Per environment tasks
212 213
    Dir["#{system}/envs/**/*.py"].each do |env_file|
        env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
214
        desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors"
215
        task "#{system}:check_settings:#{env}" => :predjango do
216 217
            sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}")
        end
218 219 220

        desc "Run collectstatic in the specified environment"
        task "#{system}:collectstatic:#{env}" => :predjango do
221 222 223 224 225
            sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /tmp/collectstatic.out") do |ok, status|
                if !ok
                    abort "collectstatic failed!"
                end
            end
226
        end
227
    end
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244

    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
245 246
end

247 248 249 250 251 252 253 254 255 256 257 258 259
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

260
Dir["common/lib/*"].select{|lib| File.directory?(lib)}.each do |lib|
261 262
    task_name = "test_#{lib}"

263
    report_dir = report_dir_path(lib)
264 265

    desc "Run tests for common lib #{lib}"
266
    task task_name => report_dir do
267
        ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
268
        cmd = "nosetests #{lib} --logging-clear-handlers --with-xunit"
269 270 271
        sh(run_under_coverage(cmd, lib)) do |ok, res|
            $failed_tests += 1 unless ok
        end
272
    end
273
    TEST_TASK_DIRS << lib
Victor Shnayder committed
274 275

    desc "Run tests for common lib #{lib} (without coverage)"
276
    task "fasttest_#{lib}" do
Victor Shnayder committed
277 278 279
        sh("nosetests #{lib}")
    end

280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
    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
296 297
end

298
task :report_dirs
Victor Shnayder committed
299

300 301 302 303
TEST_TASK_DIRS.each do |dir|
    report_dir = report_dir_path(dir)
    directory report_dir
    task :report_dirs => [REPORT_DIR, report_dir]
304 305 306
end

task :test do
307 308
    TEST_TASK_DIRS.each do |dir|
        Rake::Task["test_#{dir}"].invoke(false)
309 310 311 312 313
    end

    if $failed_tests > 0
        abort "Tests failed!"
    end
314
end
315

316 317
namespace :coverage do
    desc "Build the html coverage reports"
318
    task :html => :report_dirs do
319
        TEST_TASK_DIRS.each do |dir|
320 321 322 323 324 325 326
            report_dir = report_dir_path(dir)

            if !File.file?("#{report_dir}/.coverage")
                next
            end

            sh("coverage html --rcfile=#{dir}/.coveragerc")
327
        end
328 329
    end

330
    desc "Build the xml coverage reports"
331
    task :xml => :report_dirs do
332
        TEST_TASK_DIRS.each do |dir|
333 334 335 336 337
            report_dir = report_dir_path(dir)

            if !File.file?("#{report_dir}/.coverage")
                next
            end
338
            # Why doesn't the rcfile control the xml output file properly??
339
            sh("coverage xml -o #{report_dir}/coverage.xml --rcfile=#{dir}/.coveragerc")
340
        end
341
    end
342 343
end

344 345
task :runserver => :lms

346
desc "Run django-admin <action> against the specified system and environment"
347
task "django-admin", [:action, :system, :env, :options] => [:predjango] do |t, args|
348 349 350 351
    args.with_defaults(:env => 'dev', :system => 'lms', :options => '')
    sh(django_admin(args.system, args.env, args.action, args.options))
end

352 353 354 355 356 357
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

358
task :package do
359
    FileUtils.mkdir_p(BUILD_DIR)
360

361
    Dir.chdir(BUILD_DIR) do
362 363
        afterremove = Tempfile.new('afterremove')
        afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '')
John Jarvis committed
364
        #! /bin/bash
365 366
        set -e
        set -x
367

368
        # to be a little safer this rm is executed
John Jarvis committed
369
        # as the makeitso user
370

John Jarvis committed
371
        if [[ -d "#{INSTALL_DIR_PATH}" ]]; then
372
            sudo rm -rf "#{INSTALL_DIR_PATH}"
373 374 375 376 377
        fi

        AFTERREMOVE
        afterremove.close()
        FileUtils.chmod(0755, afterremove.path)
378

379
        args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
380
            "--after-remove=#{afterremove.path}",
381
            "--prefix=#{INSTALL_DIR_PATH}",
382 383 384
            "--exclude=**/build/**",
            "--exclude=**/rakefile",
            "--exclude=**/.git/**",
385
            "--exclude=**/*.pyc",
386
            "--exclude=**/reports/**",
387 388
            "--exclude=**/test_root/**",
            "--exclude=**/.coverage/**",
389
            "-C", "#{REPO_ROOT}",
390
            "--provides=#{PACKAGE_NAME}",
391
            "--name=#{NORMALIZED_DEPLOY_NAME}",
392
            "--version=#{PKG_VERSION}",
393
            "-a", "all",
394
            "."]
395 396 397
        system(*args) || raise("fpm failed to build the .deb")
    end
end
398 399

task :publish => :package do
400
    sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}")
401
end
402 403

namespace :cms do
404 405
  desc "Clone existing MongoDB based course"
  task :clone do
406

407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
    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
427 428
  desc "Import course data within the given DATA_DIR variable"
  task :import do
429 430 431
    if ENV['DATA_DIR'] and ENV['COURSE_DIR']
      sh(django_admin(:cms, :dev, :import, ENV['DATA_DIR'], ENV['COURSE_DIR']))
    elsif ENV['DATA_DIR']
432 433 434 435 436 437 438
      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
439

440 441 442
namespace :cms do
  desc "Import course data within the given DATA_DIR variable"
  task :xlint do
443 444 445
    if ENV['DATA_DIR'] and ENV['COURSE_DIR']
      sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR']))
    elsif ENV['DATA_DIR']
446 447 448 449 450 451 452 453
      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

454 455 456 457 458 459 460 461 462 463 464 465
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

466 467 468 469 470 471 472 473 474
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
475 476 477 478 479 480 481 482

desc "Invoke sphinx 'make build' to generate docs."
task :builddocs do
    Dir.chdir('docs') do
        sh('make html')
    end
end

483
desc "Show docs in browser (mac and ubuntu)."
484 485
task :showdocs do
    Dir.chdir('docs/build/html') do
486 487 488 489 490 491 492 493 494
        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
495 496 497 498 499
    end
end

desc "Build docs and show them in browser"
task :doc => :builddocs do
500
    Rake::Task["showdocs"].invoke
501
end