From 241c2cf43232bbea48fb206db03bcaf1580dcd47 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Thu, 26 Mar 2015 14:48:55 -0600 Subject: [PATCH] Get unit tests working with Karma Establish features for running Karma tests. Separate karma configurations are used due to current overlap of template file structure. You can run the karma tests via: ./run_tests.sh --karma The Karma framework allows for many features like unit test coverage and provides a common configuration for different test runners. Change-Id: I79680ef6369383c148da68e6677945886a48df81 Implements: blueprint karma --- .gitignore | 2 + doc/source/ref/run_tests.rst | 15 +++++ horizon/karma.conf.js | 99 +++++++++++++++++++++++++++++++ openstack_dashboard/karma.conf.js | 85 ++++++++++++++++++++++++++ package.json | 22 +++++++ run_tests.sh | 15 +++++ test-shim.js | 98 ++++++++++++++++++++++++++++++ 7 files changed, 336 insertions(+) create mode 100644 horizon/karma.conf.js create mode 100644 openstack_dashboard/karma.conf.js create mode 100644 package.json create mode 100644 test-shim.js diff --git a/.gitignore b/.gitignore index 925e338c25..9dbd711479 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ doc/source/sourcecode /static/ .venv .tox +node_modules +npm-debug.log build dist AUTHORS diff --git a/doc/source/ref/run_tests.rst b/doc/source/ref/run_tests.rst index 2a703a66a6..59e509d020 100644 --- a/doc/source/ref/run_tests.rst +++ b/doc/source/ref/run_tests.rst @@ -134,6 +134,21 @@ Available options: the dashboard module's directory structure. Default: A new directory within the current directory. +JavaScript +---------- + +You can also run JavaScript unit tests using Karma. Karma is a test +environment that allows for multiple test runners and reporters, including +such features as code coverage. Karma allows developer to run tests live, +as it can watch source and test files for changes. + +To run the Karma tests for Horizon and Dashboard:: + + ./run_tests.sh --karma + +The default configuration also performs coverage reports, which are saved +to ``horizon/.coverage-karma/`` and ``openstack_dashboard/.coverage-karma/``. + Give me metrics! ================ diff --git a/horizon/karma.conf.js b/horizon/karma.conf.js new file mode 100644 index 0000000000..5eabc514a8 --- /dev/null +++ b/horizon/karma.conf.js @@ -0,0 +1,99 @@ +module.exports = function(config){ + + // Path to xstatic pkg path. + var xstaticPath = '../../.venv/lib/python2.7/site-packages/xstatic/pkg/'; + + config.set({ + + preprocessors: { + // Used to collect templates for preprocessing. + // NOTE: the templates must also be listed in the files section below. + './**/*.html': ['ng-html2js'], + // Used to indicate files requiring coverage reports. + './**/!(*spec).js': ['coverage'] + }, + + // Sets up module to process templates. + ngHtml2JsPreprocessor: { + prependPrefix: '/static/', + moduleName: 'templates' + }, + + // Assumes you're in the top-level horizon directory. + basePath : './static/', + + // Contains both source and test files. + files : [ + + // shim, partly stolen from /i18n/js/horizon/ + // Contains expected items not provided elsewhere (dynamically by + // Django or via jasmine template. + '../../test-shim.js', + + // from jasmine.html + xstaticPath + 'jquery/data/jquery.js', + xstaticPath + 'angular/data/angular.js', + xstaticPath + 'angular/data/angular-mocks.js', + xstaticPath + 'angular/data/angular-cookies.js', + xstaticPath + 'angular_bootstrap/data/angular-bootstrap.js', + xstaticPath + 'angular/data/angular-sanitize.js', + xstaticPath + 'd3/data/d3.js', + xstaticPath + 'rickshaw/data/rickshaw.js', + xstaticPath + 'angular_smart_table/data/smart-table.js', + xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js', + xstaticPath + 'spin/data/spin.js', + xstaticPath + 'spin/data/spin.jquery.js', + + + // from jasmine_tests.py; only those that are deps for others + 'horizon/js/horizon.js', + 'horizon/js/angular/hz.api.module.js', + 'horizon/js/angular/services/**/*.js', + 'horizon/js/angular/hz.api.module.js', + 'dashboard-app/dashboard-app.module.js', + 'dashboard-app/utils/utils.module.js', + 'dashboard-app/**/*.js', + 'framework/framework.module.js', + 'framework/widgets/charts/charts.js', + 'framework/widgets/metadata-tree/metadata-tree.js', + 'framework/widgets/table/table.js', + + // Catch-all for stuff that isn't required explicitly by others. + 'framework/**/!(*spec).js', + + // Templates. + './**/*.html', + + // TESTS + '**/*.spec.js', + ], + + autoWatch : true, + + frameworks: ['jasmine'], + + browsers : ['PhantomJS'], + + phantomjsLauncher: { + // Have phantomjs exit if a ResourceError is encountered + // (useful if karma exits without killing phantom) + exitOnResourceError: true + }, + + reporters : [ 'progress', 'coverage' ], + + plugins : [ + 'karma-phantomjs-launcher', + 'karma-jasmine', + 'karma-ng-html2js-preprocessor', + 'karma-coverage' + ], + + coverageReporter: { + type : 'html', + dir : '../.coverage-karma/' + } + + }); +}; + diff --git a/openstack_dashboard/karma.conf.js b/openstack_dashboard/karma.conf.js new file mode 100644 index 0000000000..572575401f --- /dev/null +++ b/openstack_dashboard/karma.conf.js @@ -0,0 +1,85 @@ +module.exports = function(config){ + + // Path to xstatic pkg path. + var xstaticPath = '../../.venv/lib/python2.7/site-packages/xstatic/pkg/'; + + config.set({ + + preprocessors: { + // Used to collect templates for preprocessing. + // NOTE: the templates must also be listed in the files section below. + './**/*.html': ['ng-html2js'], + // Used to indicate files requiring coverage reports. + './**/!(*spec).js': ['coverage'] + }, + + // Sets up module to process templates. + ngHtml2JsPreprocessor: { + prependPrefix: '/static/', + moduleName: 'templates' + }, + + // Assumes you're in the top-level horizon directory. + basePath : './static/', + + // Contains both source and test files. + files : [ + + // shim, partly stolen from /i18n/js/horizon/ + // Contains expected items not provided elsewhere (dynamically by + // Django or via jasmine template. + '../../test-shim.js', + + // from jasmine.html + xstaticPath + 'jquery/data/jquery.js', + xstaticPath + 'angular/data/angular.js', + xstaticPath + 'angular/data/angular-mocks.js', + xstaticPath + 'angular/data/angular-cookies.js', + xstaticPath + 'angular_bootstrap/data/angular-bootstrap.js', + xstaticPath + 'angular/data/angular-sanitize.js', + xstaticPath + 'd3/data/d3.js', + xstaticPath + 'rickshaw/data/rickshaw.js', + xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js', + // Needed by modal spinner + xstaticPath + 'spin/data/spin.js', + xstaticPath + 'spin/data/spin.jquery.js', + + // This one seems to have to come first. + "dashboard/dashboard.module.js", + "dashboard/workflow/workflow.js", + "dashboard/launch-instance/launch-instance.js", + "dashboard/**/*.js", + + // Templates. + './**/*.html', + ], + + autoWatch : true, + + frameworks: ['jasmine'], + + browsers : ['PhantomJS'], + + phantomjsLauncher: { + // Have phantomjs exit if a ResourceError is encountered + // (useful if karma exits without killing phantom) + exitOnResourceError: true + }, + + reporters : [ 'progress', 'coverage' ], + + plugins : [ + 'karma-phantomjs-launcher', + 'karma-jasmine', + 'karma-ng-html2js-preprocessor', + 'karma-coverage' + ], + + coverageReporter: { + type : 'html', + dir : '../.coverage-karma/' + } + + }); +}; + diff --git a/package.json b/package.json new file mode 100644 index 0000000000..0a101010eb --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "version": "0.0.0", + "private": true, + "name": "horizon", + "description": "OpenStack Horizon - Angular", + "repository": "none", + "license": "Apache 2.0", + "devDependencies": { + "jasmine-core": "2.2.0", + "karma": "0.12.31", + "karma-chrome-launcher": "0.1.8", + "karma-coverage": "0.3.1", + "karma-jasmine": "0.3.5", + "karma-ng-html2js-preprocessor": "0.1.2", + "karma-phantomjs-launcher": "0.2.0", + "phantomjs": "^1.9.17" + }, + "scripts": { + "test": "node node_modules/karma/bin/karma start horizon/karma.conf.js --single-run && node node_modules/karma/bin/karma start openstack_dashboard/karma.conf.js --single-run" + }, + "dependencies": {} +} diff --git a/run_tests.sh b/run_tests.sh index 3180396f19..7269684db8 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -28,6 +28,7 @@ function usage { echo " -y, --pylint Just run pylint" echo " -j, --jshint Just run jshint" echo " -s, --jscs Just run jscs" + echo " -k, --karma Just run karma" echo " -q, --quiet Run non-interactively. (Relatively) quiet." echo " Implies -V if -N is not set." echo " --only-selenium Run only the Selenium unit tests" @@ -72,6 +73,7 @@ just_docs=0 just_tabs=0 just_jscs=0 just_jshint=0 +just_karma=0 never_venv=0 quiet=0 restore_env=0 @@ -109,6 +111,7 @@ function process_option { -y|--pylint) just_pylint=1;; -j|--jshint) just_jshint=1;; -s|--jscs) just_jscs=1;; + -k|--karma) just_karma=1;; -f|--force) force=1;; -t|--tabs) just_tabs=1;; -q|--quiet) quiet=1;; @@ -174,6 +177,12 @@ function run_jscs { fi } +function run_karma { + echo "Running karma ..." + npm install + npm run test +} + function warn_on_flake8_without_venv { set +o errexit ${command_wrapper} python -c "import hacking" 2>/dev/null @@ -578,6 +587,12 @@ if [ $just_jscs -eq 1 ]; then exit $? fi +# Karma +if [ $just_karma -eq 1 ]; then + run_karma + exit $? +fi + # Tab checker if [ $just_tabs -eq 1 ]; then tab_check diff --git a/test-shim.js b/test-shim.js new file mode 100644 index 0000000000..e8f185f665 --- /dev/null +++ b/test-shim.js @@ -0,0 +1,98 @@ +/* + * Shim for Javascript unit tests; supplying expected global features. + * This should be removed from the codebase once i18n services are provided. + * Taken from default i18n file provided by Django. + */ + +var angularModuleExtension = []; + + +(function (globals) { + + var django = globals.django || (globals.django = {}); + + + django.pluralidx = function (count) { return (count == 1) ? 0 : 1; }; + + /* gettext identity library */ + + django.gettext = function (msgid) { return msgid; }; + django.ngettext = function (singular, plural, count) { return (count == 1) ? singular : plural; }; + django.gettext_noop = function (msgid) { return msgid; }; + django.pgettext = function (context, msgid) { return msgid; }; + django.npgettext = function (context, singular, plural, count) { return (count == 1) ? singular : plural; }; + + + django.interpolate = function (fmt, obj, named) { + if (named) { + return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); + } else { + return fmt.replace(/%s/g, function(match){return String(obj.shift())}); + } + }; + + + /* formatting library */ + + django.formats = { + "DATETIME_FORMAT": "N j, Y, P", + "DATETIME_INPUT_FORMATS": [ + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M:%S.%f", + "%Y-%m-%d %H:%M", + "%Y-%m-%d", + "%m/%d/%Y %H:%M:%S", + "%m/%d/%Y %H:%M:%S.%f", + "%m/%d/%Y %H:%M", + "%m/%d/%Y", + "%m/%d/%y %H:%M:%S", + "%m/%d/%y %H:%M:%S.%f", + "%m/%d/%y %H:%M", + "%m/%d/%y" + ], + "DATE_FORMAT": "N j, Y", + "DATE_INPUT_FORMATS": [ + "%Y-%m-%d", + "%m/%d/%Y", + "%m/%d/%y" + ], + "DECIMAL_SEPARATOR": ".", + "FIRST_DAY_OF_WEEK": "0", + "MONTH_DAY_FORMAT": "F j", + "NUMBER_GROUPING": "3", + "SHORT_DATETIME_FORMAT": "m/d/Y P", + "SHORT_DATE_FORMAT": "m/d/Y", + "THOUSAND_SEPARATOR": ",", + "TIME_FORMAT": "P", + "TIME_INPUT_FORMATS": [ + "%H:%M:%S", + "%H:%M:%S.%f", + "%H:%M" + ], + "YEAR_MONTH_FORMAT": "F Y" + }; + + django.get_format = function (format_type) { + var value = django.formats[format_type]; + if (typeof(value) == 'undefined') { + return format_type; + } else { + return value; + } + }; + + /* add to global namespace */ + globals.pluralidx = django.pluralidx; + globals.gettext = django.gettext; + globals.ngettext = django.ngettext; + globals.gettext_noop = django.gettext_noop; + globals.pgettext = django.pgettext; + globals.npgettext = django.npgettext; + globals.interpolate = django.interpolate; + globals.get_format = django.get_format; + +}(this)); + + + +