rakefile 19 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
BUILD_NUMBER = (ENV["BUILD_NUMBER"] || "dev").chomp()

24
# Set up the clean and clobber tasks
25
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'test_root/*_repo', 'test_root/staticfiles')
26
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
27

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

32 33
def django_admin(system, env, command, *args)
    django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin')
34
    return "#{django_admin} #{command} --traceback --settings=#{system}.envs.#{env} --pythonpath=. #{args.join(' ')}"
35
end
36

37 38 39
# Runs Process.spawn, and kills the process at the end of the rake process
# Expects the same arguments as Process.spawn
def background_process(*command)
Calen Pennington committed
40
    pid = Process.spawn({}, *command, {:pgroup => true})
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

    at_exit do
        puts "Ending process and children"
        pgid = Process.getpgid(pid)
        begin
            Timeout.timeout(5) do
                puts "Terminating process group #{pgid}"
                Process.kill(:SIGTERM, -pgid)
                puts "Waiting on process group #{pgid}"
                Process.wait(-pgid)
                puts "Done waiting on process group #{pgid}"
            end
        rescue Timeout::Error
            puts "Killing process group #{pgid}"
            Process.kill(:SIGKILL, -pgid)
            puts "Waiting on process group #{pgid}"
            Process.wait(-pgid)
            puts "Done waiting on process group #{pgid}"
        end
    end
end

63 64 65 66 67
def django_for_jasmine(system, django_reload)
    if !django_reload
        reload_arg = '--noreload'
    end

68 69
    port = 10000 + rand(40000)
    jasmine_url = "http://localhost:#{port}/_jasmine/"
70 71 72

    background_process(*django_admin(system, 'jasmine', 'runserver', '-v', '0', port.to_s, reload_arg).split(' '))

73
    up = false
74
    start_time = Time.now
75
    until up do
76 77 78
        if Time.now - start_time > 30
            abort "Timed out waiting for server to start to run jasmine tests"
        end
79 80 81 82 83 84 85 86 87 88 89
        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
90
    yield jasmine_url
91
end
92

93 94 95
def template_jasmine_runner(lib)
    coffee_files = Dir["#{lib}/**/js/**/*.coffee", "common/static/coffee/src/**/*.coffee"]
    if !coffee_files.empty?
96
        sh("node_modules/.bin/coffee -c #{coffee_files.join(' ')}")
97 98 99 100
    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")
101 102 103 104 105 106 107 108

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

109 110 111 112 113 114 115 116 117
    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


118 119 120 121
def report_dir_path(dir)
    return File.join(REPORT_DIR, dir.to_s)
end

122
def compile_assets(watch=false, debug=false)
123
    xmodule_cmd = 'xmodule_assets common/static/xmodule'
124
    if watch
125 126 127 128 129 130
        xmodule_cmd = "watchmedo shell-command \
                       --patterns='*.js;*.coffee;*.sass;*.scss;*.css' \
                       --recursive \
                       --command='#{xmodule_cmd}' \
                       common/lib/xmodule"
    end
131
    coffee_cmd = "node_modules/.bin/coffee #{watch ? '--watch' : ''} --compile */static"
132
    sass_cmd = "sass #{debug ? '--debug-info' : '--style compressed'} " +
133 134
                     "--load-path ./common/static/sass " +
                     "--require ./common/static/sass/bourbon/lib/bourbon.rb " +
135
                     "#{watch ? '--watch' : '--update --force'} */static"
136 137 138 139 140 141 142 143 144

    [xmodule_cmd, coffee_cmd, sass_cmd].each do |cmd|
        if watch
            background_process(cmd)
        else
            pid = Process.spawn(cmd)
            puts "Waiting for `#{cmd}` to complete (pid #{pid})"
            Process.wait(pid)
            puts "Completed"
145 146 147
            if !$?.exited? || $?.exitstatus != 0
                abort "`#{cmd}` failed"
            end
148
        end
149 150 151
    end
end

152
task :default => [:test, :pep8, :pylint]
153 154

directory REPORT_DIR
Calen Pennington committed
155

156 157 158 159 160
default_options = {
    :lms => '8000',
    :cms => '8001',
}

161
desc "Install all prerequisites needed for the lms and cms"
162 163
task :install_prereqs => [:install_node_prereqs, :install_ruby_prereqs, :install_python_prereqs]

164
desc "Install all node prerequisites for the lms and cms"
165 166 167 168
task :install_node_prereqs do
    sh('npm install')
end

169
desc "Install all ruby prerequisites for the lms and cms"
170 171 172 173
task :install_ruby_prereqs do
    sh('bundle install')
end

174
desc "Install all python prerequisites for the lms and cms"
175 176
task :install_python_prereqs do
    sh('pip install -r requirements.txt')
Ned Batchelder committed
177 178
    # Check for private-requirements.txt: used to install our libs as working dirs,
    # or personal-use tools.
179 180 181
    if File.file?("private-requirements.txt")
        sh('pip install -r private-requirements.txt')
    end
182 183
end

184 185
task :predjango do
    sh("find . -type f -name *.pyc -delete")
186
    sh('pip install -q --no-index -r local-requirements.txt')
187 188
end

189
task :clean_test_files do
190
    sh("git clean -fqdx test_root")
191 192
end

193
[:lms, :cms, :common].each do |system|
194
    report_dir = report_dir_path(system)
195 196 197 198
    directory report_dir

    desc "Run pep8 on all #{system} code"
    task "pep8_#{system}" => report_dir do
199
        sh("pep8 #{system} | tee #{report_dir}/pep8.report")
200 201 202 203 204
    end
    task :pep8 => "pep8_#{system}"

    desc "Run pylint on all #{system} code"
    task "pylint_#{system}" => report_dir do
205 206 207 208 209
        apps = Dir["#{system}/*.py", "#{system}/djangoapps/*", "#{system}/lib/*"].map do |app|
            File.basename(app)
        end.select do |app|
            app !=~ /.pyc$/
        end.map do |app|
210
            if app =~ /.py$/
211 212 213
                app.gsub('.py', '')
            else
                app
214
            end
215
        end
216 217 218

        pythonpath_prefix = "PYTHONPATH=#{system}:#{system}/djangoapps:#{system}/lib:common/djangoapps:common/lib"
        sh("#{pythonpath_prefix} pylint --rcfile=.pylintrc -f parseable #{apps.join(' ')} | tee #{report_dir}/pylint.report")
219 220
    end
    task :pylint => "pylint_#{system}"
221

222 223
end

224
$failed_tests = 0
225

226
def run_under_coverage(cmd, root)
227
    cmd0, cmd_rest = cmd.split(" ", 2)
228 229 230
    # 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}"
231 232 233
    return cmd
end

234
def run_tests(system, report_dir, stop_on_failure=true)
235
    ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
236
    dirs = Dir["common/djangoapps/*"] + Dir["#{system}/djangoapps/*"]
237
    cmd = django_admin(system, :test, 'test', '--logging-clear-handlers', *dirs.each)
238
    sh(run_under_coverage(cmd, system)) do |ok, res|
239 240 241 242 243
        if !ok and stop_on_failure
            abort "Test failed!"
        end
        $failed_tests += 1 unless ok
    end
244 245
end

246
TEST_TASK_DIRS = []
247

248 249 250 251 252 253
task :fastlms do
    # this is >2 times faster that rake [lms], and does not need web, good for local dev
    django_admin = ENV['DJANGO_ADMIN_PATH'] || select_executable('django-admin.py', 'django-admin')
    sh("#{django_admin} runserver --traceback --settings=lms.envs.dev   --pythonpath=.")
end

254
[:lms, :cms].each do |system|
255
    report_dir = report_dir_path(system)
256

257
    # Per System tasks
258
    desc "Run all django tests on our djangoapps for the #{system}"
259
    task "test_#{system}", [:stop_on_failure] => ["clean_test_files", :predjango, "#{system}:gather_assets:test", "fasttest_#{system}"]
260

261 262
    # Have a way to run the tests without running collectstatic -- useful when debugging without
    # messing with static files.
263 264 265
    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)
266
    end
267

268 269
    task :fasttest => "fasttest_#{system}"

270
    TEST_TASK_DIRS << system
271 272 273 274 275

    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
276
    task system, [:env, :options] => [:predjango] do |t, args|
277
        args.with_defaults(:env => 'dev', :options => default_options[system])
278 279

        # Compile all assets first
280
        compile_assets(watch=false, debug=true)
281 282

        # Listen for any changes to assets
283
        compile_assets(watch=true, debug=true)
284

285 286
        sh(django_admin(system, args.env, 'runserver', args.options))
    end
287 288

    # Per environment tasks
289 290
    Dir["#{system}/envs/**/*.py"].each do |env_file|
        env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
291
        desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors"
292
        task "#{system}:check_settings:#{env}" => :predjango do
293 294
            sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}")
        end
295

296 297
        desc "Compile coffeescript and sass, and then run collectstatic in the specified environment"
        task "#{system}:gather_assets:#{env}" do
298 299
            compile_assets()
            sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /dev/null") do |ok, status|
300 301 302 303
                if !ok
                    abort "collectstatic failed!"
                end
            end
304
        end
305
    end
306 307 308

    desc "Open jasmine tests for #{system} in your default browser"
    task "browse_jasmine_#{system}" do
309
        compile_assets()
310 311 312 313 314 315 316 317 318
        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
319
        compile_assets()
320 321 322 323 324
        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
325 326
end

327 328 329 330 331 332 333 334 335 336 337 338 339
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

340 341 342 343 344 345
desc "Run tests for the internationalization library"
task :test_i18n do
  test = File.join(REPO_ROOT, "i18n", "tests")
  sh("nosetests #{test}")
end

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

349
    report_dir = report_dir_path(lib)
350 351

    desc "Run tests for common lib #{lib}"
352
    task task_name => report_dir do
353
        ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
354
        cmd = "nosetests #{lib}"
355 356 357
        sh(run_under_coverage(cmd, lib)) do |ok, res|
            $failed_tests += 1 unless ok
        end
358
    end
359
    TEST_TASK_DIRS << lib
Victor Shnayder committed
360 361

    desc "Run tests for common lib #{lib} (without coverage)"
362
    task "fasttest_#{lib}" do
Victor Shnayder committed
363 364 365
        sh("nosetests #{lib}")
    end

366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
    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
382 383
end

384
task :report_dirs
Victor Shnayder committed
385

386 387 388 389
TEST_TASK_DIRS.each do |dir|
    report_dir = report_dir_path(dir)
    directory report_dir
    task :report_dirs => [REPORT_DIR, report_dir]
390 391 392
end

task :test do
393 394
    TEST_TASK_DIRS.each do |dir|
        Rake::Task["test_#{dir}"].invoke(false)
395 396 397 398 399
    end

    if $failed_tests > 0
        abort "Tests failed!"
    end
400
end
401

402 403
namespace :coverage do
    desc "Build the html coverage reports"
404
    task :html => :report_dirs do
405
        TEST_TASK_DIRS.each do |dir|
406 407 408 409 410 411 412
            report_dir = report_dir_path(dir)

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

            sh("coverage html --rcfile=#{dir}/.coveragerc")
413
        end
414 415
    end

416
    desc "Build the xml coverage reports"
417
    task :xml => :report_dirs do
418
        TEST_TASK_DIRS.each do |dir|
419 420 421 422 423
            report_dir = report_dir_path(dir)

            if !File.file?("#{report_dir}/.coverage")
                next
            end
424
            # Why doesn't the rcfile control the xml output file properly??
425
            sh("coverage xml -o #{report_dir}/coverage.xml --rcfile=#{dir}/.coveragerc")
426
        end
427
    end
428 429
end

430 431
task :runserver => :lms

432
desc "Run django-admin <action> against the specified system and environment"
433
task "django-admin", [:action, :system, :env, :options] do |t, args|
434 435 436 437
    args.with_defaults(:env => 'dev', :system => 'lms', :options => '')
    sh(django_admin(args.system, args.env, args.action, args.options))
end

438 439 440 441 442 443
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

444
namespace :cms do
445 446
  desc "Clone existing MongoDB based course"
  task :clone do
447

448 449 450 451 452 453 454 455 456
    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

  desc "Delete existing MongoDB based course"
  task :delete_course do
457

458 459 460
    if ENV['LOC'] and ENV['COMMIT']
        sh(django_admin(:cms, :dev, :delete_course, ENV['LOC'], ENV['COMMIT']))
    elsif ENV['LOC']
461 462 463 464 465 466
      sh(django_admin(:cms, :dev, :delete_course, ENV['LOC']))
    else
      raise "You must pass in a LOC parameter"
    end
  end

467 468
  desc "Import course data within the given DATA_DIR variable"
  task :import do
469 470 471
    if ENV['DATA_DIR'] and ENV['COURSE_DIR']
      sh(django_admin(:cms, :dev, :import, ENV['DATA_DIR'], ENV['COURSE_DIR']))
    elsif ENV['DATA_DIR']
472 473 474 475 476 477
      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
478

479 480 481 482 483
  desc "Imports all the templates from the code pack"
  task :update_templates do
    sh(django_admin(:cms, :dev, :update_templates))
  end

484 485
  desc "Import course data within the given DATA_DIR variable"
  task :xlint do
486 487 488
    if ENV['DATA_DIR'] and ENV['COURSE_DIR']
      sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR']))
    elsif ENV['DATA_DIR']
489 490 491 492 493 494 495
      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

496 497 498 499 500 501 502 503 504 505 506
  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

507 508 509 510 511 512 513 514 515
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
516

Steve Strassmann committed
517 518 519 520 521 522 523 524 525 526 527 528 529 530
# --- Internationalization tasks

desc "Extract localizable strings from sources"
task :extract_dev_strings do
  sh(File.join(REPO_ROOT, "i18n", "extract.py"))
end

desc "Compile localizable strings from sources. With optional flag 'extract', will extract strings first."
task :generate_i18n do
  if ARGV.last.downcase == 'extract'
    Rake::Task["extract_dev_strings"].execute
  end
  sh(File.join(REPO_ROOT, "i18n", "generate.py"))
end
531

532 533 534 535 536 537 538 539 540 541
desc "Simulate international translation by generating dummy strings corresponding to source strings."
task :dummy_i18n do
  source_files = Dir["#{REPO_ROOT}/conf/locale/en/LC_MESSAGES/*.po"]
  dummy_locale = 'fr'
  cmd = File.join(REPO_ROOT, "i18n", "make_dummy.py")
  for file in source_files do
    sh("#{cmd} #{file} #{dummy_locale}")
  end
end

Vasyl Nakvasiuk committed
542
# --- Develop and public documentation ---
543
desc "Invoke sphinx 'make build' to generate docs."
Vasyl Nakvasiuk committed
544 545 546 547 548 549 550 551
task :builddocs, [:options] do |t, args|
    if args.options == 'pub'
        path = "doc/public"
    else
        path = "docs"
    end

    Dir.chdir(path) do
552 553 554 555 556
        sh('make html')
    end
end

desc "Show docs in browser (mac and ubuntu)."
Vasyl Nakvasiuk committed
557 558 559 560 561 562 563 564
task :showdocs, [:options] do |t, args|
    if args.options == 'pub'
        path = "doc/public"
    else
        path = "docs"
    end

    Dir.chdir("#{path}/build/html") do
565 566 567 568 569 570
        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.
Vasyl Nakvasiuk committed
571 572
Please use 'rake builddocs' and then manually open
'mitx/#{path}/build/html/index.html."
573 574 575 576 577
        end
    end
end

desc "Build docs and show them in browser"
Vasyl Nakvasiuk committed
578 579
task :doc, [:options] =>  :builddocs do |t, args|
    Rake::Task["showdocs"].invoke(args.options)
580
end
Vasyl Nakvasiuk committed
581
# --- Develop and public documentation ---