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

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

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

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


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

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

directory REPORT_DIR
Calen Pennington committed
117

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

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

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

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

    desc "Run pep8 on all #{system} code"
    task "pep8_#{system}" => report_dir do
138
        sh("pep8 #{system} | tee #{report_dir}/pep8.report")
139 140 141 142 143
    end
    task :pep8 => "pep8_#{system}"

    desc "Run pylint on all #{system} code"
    task "pylint_#{system}" => report_dir do
144 145 146 147 148
        apps = Dir["#{system}/*.py", "#{system}/djangoapps/*", "#{system}/lib/*"].map do |app|
            File.basename(app)
        end.select do |app|
            app !=~ /.pyc$/
        end.map do |app|
149
            if app =~ /.py$/
150 151 152
                app.gsub('.py', '')
            else
                app
153
            end
154
        end
155 156 157

        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")
158 159
    end
    task :pylint => "pylint_#{system}"
160

161 162
end

163
$failed_tests = 0
164

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

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

185
TEST_TASK_DIRS = []
186

187 188 189 190 191 192
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

193
[:lms, :cms].each do |system|
194
    report_dir = report_dir_path(system)
195

196
    # Per System tasks
197
    desc "Run all django tests on our djangoapps for the #{system}"
198
    task "test_#{system}", [:stop_on_failure] => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"]
199

200 201
    # Have a way to run the tests without running collectstatic -- useful when debugging without
    # messing with static files.
202 203 204
    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)
205
    end
206

207 208
    task :fasttest => "fasttest_#{system}"

209
    TEST_TASK_DIRS << system
210 211 212 213 214

    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
215
    task system, [:env, :options] => [:predjango] do |t, args|
216
        args.with_defaults(:env => 'dev', :options => default_options[system])
217 218
        sh(django_admin(system, args.env, 'runserver', args.options))
    end
219 220

    # Per environment tasks
221 222
    Dir["#{system}/envs/**/*.py"].each do |env_file|
        env = env_file.gsub("#{system}/envs/", '').gsub(/\.py/, '').gsub('/', '.')
223
        desc "Attempt to import the settings file #{system}.envs.#{env} and report any errors"
224
        task "#{system}:check_settings:#{env}" => :predjango do
225 226
            sh("echo 'import #{system}.envs.#{env}' | #{django_admin(system, env, 'shell')}")
        end
227 228 229

        desc "Run collectstatic in the specified environment"
        task "#{system}:collectstatic:#{env}" => :predjango do
230 231 232 233 234
            sh("#{django_admin(system, env, 'collectstatic', '--noinput')} > /tmp/collectstatic.out") do |ok, status|
                if !ok
                    abort "collectstatic failed!"
                end
            end
235
        end
236
    end
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253

    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
254 255
end

256 257 258 259 260 261 262 263 264 265 266 267 268
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

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

272
    report_dir = report_dir_path(lib)
273 274

    desc "Run tests for common lib #{lib}"
275
    task task_name => report_dir do
276
        ENV['NOSE_XUNIT_FILE'] = File.join(report_dir, "nosetests.xml")
277
        cmd = "nosetests #{lib}"
278 279 280
        sh(run_under_coverage(cmd, lib)) do |ok, res|
            $failed_tests += 1 unless ok
        end
281
    end
282
    TEST_TASK_DIRS << lib
Victor Shnayder committed
283 284

    desc "Run tests for common lib #{lib} (without coverage)"
285
    task "fasttest_#{lib}" do
Victor Shnayder committed
286 287 288
        sh("nosetests #{lib}")
    end

289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    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
305 306
end

307
task :report_dirs
Victor Shnayder committed
308

309 310 311 312
TEST_TASK_DIRS.each do |dir|
    report_dir = report_dir_path(dir)
    directory report_dir
    task :report_dirs => [REPORT_DIR, report_dir]
313 314 315
end

task :test do
316 317
    TEST_TASK_DIRS.each do |dir|
        Rake::Task["test_#{dir}"].invoke(false)
318 319 320 321 322
    end

    if $failed_tests > 0
        abort "Tests failed!"
    end
323
end
324

325 326
namespace :coverage do
    desc "Build the html coverage reports"
327
    task :html => :report_dirs do
328
        TEST_TASK_DIRS.each do |dir|
329 330 331 332 333 334 335
            report_dir = report_dir_path(dir)

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

            sh("coverage html --rcfile=#{dir}/.coveragerc")
336
        end
337 338
    end

339
    desc "Build the xml coverage reports"
340
    task :xml => :report_dirs do
341
        TEST_TASK_DIRS.each do |dir|
342 343 344 345 346
            report_dir = report_dir_path(dir)

            if !File.file?("#{report_dir}/.coverage")
                next
            end
347
            # Why doesn't the rcfile control the xml output file properly??
348
            sh("coverage xml -o #{report_dir}/coverage.xml --rcfile=#{dir}/.coveragerc")
349
        end
350
    end
351 352
end

353 354
task :runserver => :lms

355
desc "Run django-admin <action> against the specified system and environment"
356
task "django-admin", [:action, :system, :env, :options] => [:predjango] do |t, args|
357 358 359 360
    args.with_defaults(:env => 'dev', :system => 'lms', :options => '')
    sh(django_admin(args.system, args.env, args.action, args.options))
end

361 362 363 364 365 366
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

367
task :package do
368
    FileUtils.mkdir_p(BUILD_DIR)
369

370
    Dir.chdir(BUILD_DIR) do
371 372
        afterremove = Tempfile.new('afterremove')
        afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '')
John Jarvis committed
373
        #! /bin/bash
374 375
        set -e
        set -x
376

377
        # to be a little safer this rm is executed
John Jarvis committed
378
        # as the makeitso user
379

John Jarvis committed
380
        if [[ -d "#{INSTALL_DIR_PATH}" ]]; then
381
            sudo rm -rf "#{INSTALL_DIR_PATH}"
382 383 384 385 386
        fi

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

388
        args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
389
            "--after-remove=#{afterremove.path}",
390
            "--prefix=#{INSTALL_DIR_PATH}",
391 392 393
            "--exclude=**/build/**",
            "--exclude=**/rakefile",
            "--exclude=**/.git/**",
394
            "--exclude=**/*.pyc",
395
            "--exclude=**/reports/**",
396 397
            "--exclude=**/test_root/**",
            "--exclude=**/.coverage/**",
398
            "-C", "#{REPO_ROOT}",
399
            "--provides=#{PACKAGE_NAME}",
400
            "--name=#{NORMALIZED_DEPLOY_NAME}",
401
            "--version=#{PKG_VERSION}",
402
            "-a", "all",
403
            "."]
404 405 406
        system(*args) || raise("fpm failed to build the .deb")
    end
end
407 408

task :publish => :package do
409
    sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}")
410
end
411 412

namespace :cms do
413 414
  desc "Clone existing MongoDB based course"
  task :clone do
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
427

428 429 430
    if ENV['LOC'] and ENV['COMMIT']
        sh(django_admin(:cms, :dev, :delete_course, ENV['LOC'], ENV['COMMIT']))
    elsif ENV['LOC']
431 432 433 434 435 436 437 438
      sh(django_admin(:cms, :dev, :delete_course, ENV['LOC']))
    else
      raise "You must pass in a LOC parameter"
    end
  end
end

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

452
namespace :cms do
453 454 455 456 457 458 459
  desc "Imports all the templates from the code pack"
  task :update_templates do
    sh(django_admin(:cms, :dev, :update_templates))
  end
end

namespace :cms do
460 461
  desc "Import course data within the given DATA_DIR variable"
  task :xlint do
462 463 464
    if ENV['DATA_DIR'] and ENV['COURSE_DIR']
      sh(django_admin(:cms, :dev, :xlint, ENV['DATA_DIR'], ENV['COURSE_DIR']))
    elsif ENV['DATA_DIR']
465 466 467 468 469 470 471 472
      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

473 474 475 476 477 478 479 480 481 482 483 484
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

485 486 487 488 489 490 491 492 493
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
494 495


Vasyl Nakvasiuk committed
496
# --- Develop and public documentation ---
497
desc "Invoke sphinx 'make build' to generate docs."
Vasyl Nakvasiuk committed
498 499 500 501 502 503 504 505
task :builddocs, [:options] do |t, args|
    if args.options == 'pub'
        path = "doc/public"
    else
        path = "docs"
    end

    Dir.chdir(path) do
506 507 508 509 510
        sh('make html')
    end
end

desc "Show docs in browser (mac and ubuntu)."
Vasyl Nakvasiuk committed
511 512 513 514 515 516 517 518
task :showdocs, [:options] do |t, args|
    if args.options == 'pub'
        path = "doc/public"
    else
        path = "docs"
    end

    Dir.chdir("#{path}/build/html") do
519 520 521 522 523 524
        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
525 526
Please use 'rake builddocs' and then manually open
'mitx/#{path}/build/html/index.html."
527 528 529 530 531
        end
    end
end

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