# Run acceptance tests that use the bok-choy framework
# http://bok-choy.readthedocs.org/en/latest/
require 'dalli'


# Mongo databases that will be dropped before/after the tests run
BOK_CHOY_MONGO_DATABASE = "test"

# Control parallel test execution with environment variables
# Process timeout is the maximum amount of time to wait for results from a particular test case
BOK_CHOY_NUM_PARALLEL = ENV.fetch('NUM_PARALLEL', 1).to_i
BOK_CHOY_TEST_TIMEOUT = ENV.fetch("TEST_TIMEOUT", 300).to_f

# Ensure that we have a directory to put logs and reports
BOK_CHOY_DIR = File.join(REPO_ROOT, "common", "test", "acceptance")
BOK_CHOY_TEST_DIR = File.join(BOK_CHOY_DIR, "tests")
BOK_CHOY_LOG_DIR = File.join(REPO_ROOT, "test_root", "log")
directory BOK_CHOY_LOG_DIR

# Reports
BOK_CHOY_REPORT_DIR = report_dir_path("bok_choy")
BOK_CHOY_XUNIT_REPORT = File.join(BOK_CHOY_REPORT_DIR, "xunit.xml")
BOK_CHOY_COVERAGE_RC = File.join(BOK_CHOY_DIR, ".coveragerc")
directory BOK_CHOY_REPORT_DIR


BOK_CHOY_SERVERS = {
    :lms => { :port =>  8003, :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_lms.log") },
    :cms => { :port => 8031, :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_studio.log") }
}

BOK_CHOY_STUBS = {
    :xqueue => { :port => 8040, :log => File.join(BOK_CHOY_LOG_DIR, "bok_choy_xqueue.log") }
}

# For the time being, stubs are used by both the bok-choy and lettuce acceptance tests
# For this reason, the stubs package is currently located in the Django app called "terrain"
# where other lettuce configuration is stored.
BOK_CHOY_STUB_DIR = File.join(REPO_ROOT, "common", "djangoapps", "terrain")

BOK_CHOY_CACHE = Dalli::Client.new('localhost:11211')


# Start the servers we will run tests on
def start_servers()
    BOK_CHOY_SERVERS.each do | service, info |
        address = "0.0.0.0:#{info[:port]}"
        cmd = "coverage run --rcfile=#{BOK_CHOY_COVERAGE_RC} -m manage #{service} --settings bok_choy runserver #{address} --traceback --noreload"
        singleton_process(cmd, logfile=info[:log])
    end

    BOK_CHOY_STUBS.each do | service, info |
        Dir.chdir(BOK_CHOY_STUB_DIR) do
            singleton_process(
                "python -m stubs.start #{service} #{info[:port]}",
                logfile=info[:log]
            )
        end
    end
end


# Wait until we get a successful response from the servers or time out
def wait_for_test_servers()
    BOK_CHOY_SERVERS.merge(BOK_CHOY_STUBS).each do | service, info |
        ready = wait_for_server("0.0.0.0", info[:port])
        if not ready
            fail("Could not contact #{service} test server")
        end
    end
end


def is_mongo_running()
    # The mongo command will connect to the service,
    # failing with a non-zero exit code if it cannot connect.
    output = `mongo --eval "print('running')"`
    return (output and output.include? "running")
end


def is_memcache_running()
    # We use a Ruby memcache client to attempt to set a key
    # in memcache.  If we cannot do so because the service is not
    # available, then this will raise an exception.
    BOK_CHOY_CACHE.set('test', 'test')
    return true
rescue Dalli::DalliError
    return false
end


def is_mysql_running()
    # We use the MySQL CLI client and capture its stderr
    # If the client cannot connect successfully, stderr will be non-empty
    output = `mysql -e "" 2>&1`
    return output == ""
end


# Run the bok choy tests
# `test_spec` is a nose-style test specifier relative to the test directory
# Examples:
# - path/to/test.py
# - path/to/test.py:TestFoo
# - path/to/test.py:TestFoo.test_bar
# It can also be left blank to run all tests in the suite.
def run_bok_choy(test_spec)

    # Default to running all tests if no specific test is specified
    if test_spec.nil?
        test_spec = BOK_CHOY_TEST_DIR
    else
        test_spec = File.join(BOK_CHOY_TEST_DIR, test_spec)
    end

    # Construct the nosetests command, specifying where to save screenshots and XUnit XML reports
    cmd = [
        "SCREENSHOT_DIR='#{BOK_CHOY_LOG_DIR}'", "nosetests", test_spec,
        "--with-xunit", "--xunit-file=#{BOK_CHOY_XUNIT_REPORT}"
    ]

    # Configure parallel test execution, if specified
    if BOK_CHOY_NUM_PARALLEL > 1
        cmd += ["--processes=#{BOK_CHOY_NUM_PARALLEL}", "--process-timeout=#{BOK_CHOY_TEST_TIMEOUT}"]
    end

    # Run the nosetests command
    sh(cmd.join(" "))
end


def clear_mongo()
    sh("mongo #{BOK_CHOY_MONGO_DATABASE} --eval 'db.dropDatabase()' > /dev/null")
end


# Clean up data we created in the databases
def cleanup()
    sh(django_admin('lms', 'bok_choy', 'flush', '--noinput'))
    clear_mongo()
end


namespace :'test:bok_choy' do

    # Check that required services are running
    task :check_services do
        if not is_mongo_running()
            fail("Mongo is not running locally.")
        end

        if not is_memcache_running()
            fail("Memcache is not running locally.")
        end

        if not is_mysql_running()
            fail("MySQL is not running locally.")
        end
    end

    desc "Process assets and set up database for bok-choy tests"
    task :setup => [:check_services, :install_prereqs, BOK_CHOY_LOG_DIR] do

        # Clear any test data already in Mongo
        clear_mongo()

        # Invalidate the cache
        BOK_CHOY_CACHE.flush()

        # HACK: Since the CMS depends on the existence of some database tables
        # that are now in common but used to be in LMS (Role/Permissions for Forums)
        # we need to create/migrate the database tables defined in the LMS.
        # We might be able to address this by moving out the migrations from
        # lms/django_comment_client, but then we'd have to repair all the existing
        # migrations from the upgrade tables in the DB.
        # But for now for either system (lms or cms), use the lms
        # definitions to sync and migrate.
        sh(django_admin('lms', 'bok_choy', 'reset_db', '--noinput'))
        sh(django_admin('lms', 'bok_choy', 'syncdb', '--noinput'))
        sh(django_admin('lms', 'bok_choy', 'migrate', '--noinput'))

        # Collect static assets
        Rake::Task["gather_assets"].invoke('lms', 'bok_choy')
        Rake::Task["gather_assets"].reenable
        Rake::Task["gather_assets"].invoke('cms', 'bok_choy')
    end

    desc "Run acceptance tests that use the bok-choy framework but skip setup"
    task :fast, [:test_spec] => [
        :check_services, BOK_CHOY_LOG_DIR, BOK_CHOY_REPORT_DIR, :clean_reports_dir
    ] do |t, args|

        # Ensure the test servers are available
        puts "Starting test servers...".red
        start_servers()
        puts "Waiting for servers to start...".red
        wait_for_test_servers()

        begin
            puts "Running test suite...".red
            run_bok_choy(args.test_spec)
        rescue
            puts "Tests failed!".red
            exit 1
        ensure
            puts "Cleaning up databases...".red
            cleanup()
        end
    end

    desc "Generate coverage reports for bok-choy tests"
    task :coverage => BOK_CHOY_REPORT_DIR do | t, args |
        puts "Combining coverage reports".red
        sh("coverage combine --rcfile=#{BOK_CHOY_COVERAGE_RC}")

        puts "Generating coverage reports".red
        sh("coverage html --rcfile=#{BOK_CHOY_COVERAGE_RC}")
        sh("coverage xml --rcfile=#{BOK_CHOY_COVERAGE_RC}")
        sh("coverage report --rcfile=#{BOK_CHOY_COVERAGE_RC}")
    end

end


# Default: set up and run the tests
desc "Run acceptance tests that use the bok-choy framework"
task :'test:bok_choy', [:test_spec] => [:'test:bok_choy:setup'] do |t, args|
    Rake::Task["test:bok_choy:fast"].invoke(args.test_spec)
end