Files
zulip/tools/test-js-with-node
Steve Howell 95d136ca5e Extract search_util.js module.
We probably should have done this a while ago, even
though these functions are pretty tiny.  The goal here
is to make it easier to have more consistent search
semantics.

Our first use case is subs.js.  In this case we
are able to decouple a bit of generic string
matching from the subs-specific code.
2018-07-30 11:25:32 -07:00

235 lines
8.1 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import os
import shutil
import subprocess
import sys
from typing import Dict, Any
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(TOOLS_DIR))
ROOT_DIR = os.path.dirname(TOOLS_DIR)
# check for the venv
from tools.lib import sanity_check
sanity_check.check_venv(__file__)
# Import this after we do the sanity_check so it doesn't crash.
import ujson
USAGE = '''
tools/test-js-with-node - to run all tests
tools/test-js-with-node util.js activity.js - to run just a couple tests
tools/test-js-with-node --coverage - to generage coverage report
'''
enforce_fully_covered = {
'static/js/activity.js',
'static/js/alert_words.js',
'static/js/alert_words_ui.js',
'static/js/bot_data.js',
'static/js/buddy_data.js',
'static/js/buddy_list.js',
'static/js/channel.js',
'static/js/colorspace.js',
'static/js/common.js',
'static/js/components.js',
'static/js/compose_pm_pill.js',
'static/js/compose_state.js',
'static/js/compose_ui.js',
'static/js/composebox_typeahead.js',
'static/js/dict.js',
'static/js/emoji.js',
'static/js/fenced_code.js',
'static/js/fetch_status.js',
'static/js/filter.js',
'static/js/hash_util.js',
'static/js/keydown_util.js',
'static/js/markdown.js',
'static/js/message_store.js',
'static/js/muting.js',
'static/js/narrow_state.js',
'static/js/people.js',
'static/js/pm_conversations.js',
'static/js/pm_list.js',
'static/js/presence.js',
'static/js/reactions.js',
'static/js/recent_senders.js',
'static/js/rtl.js',
'static/js/schema.js',
'static/js/scroll_util.js',
'static/js/search.js',
'static/js/search_suggestion.js',
'static/js/search_util.js',
# Removed because we're migrating code from uncovered other settings pages to here.
# 'static/js/settings_ui.js',
'static/js/settings_muting.js',
'static/js/settings_user_groups.js',
'static/js/stream_data.js',
'static/js/stream_events.js',
'static/js/stream_sort.js',
'static/js/top_left_corner.js',
'static/js/topic_data.js',
'static/js/topic_generator.js',
'static/js/transmit.js',
'static/js/typeahead_helper.js',
'static/js/typing_data.js',
'static/js/typing_status.js',
'static/js/unread.js',
'static/js/user_events.js',
'static/js/user_groups.js',
'static/js/user_pill.js',
'static/js/user_search.js',
'static/js/util.js',
'static/js/widgetize.js',
'static/js/voting_widget.js',
'static/js/search_pill.js',
}
parser = argparse.ArgumentParser(USAGE)
parser.add_argument('--coverage', dest='coverage',
action="store_true",
default=False, help='Get coverage report')
parser.add_argument('--force', dest='force',
action="store_true",
default=False, help='Run tests despite possible problems.')
parser.add_argument('args', nargs=argparse.REMAINDER)
options = parser.parse_args()
from tools.lib.test_script import get_provisioning_status
if not options.force:
ok, msg = get_provisioning_status()
if not ok:
print(msg)
print('If you really know what you are doing, use --force to run anyway.')
sys.exit(1)
os.environ['NODE_PATH'] = 'static'
os.environ['TZ'] = 'UTC'
INDEX_JS = 'frontend_tests/zjsunit/index.js'
# Add ".js" to the end of all the file arguments, so index.js
# can actually verify if the file exists or not.
for index, arg in enumerate(options.args):
if not arg.endswith('.js'):
options.args[index] = arg + '.js'
individual_files = options.args
# The index.js test runner is the real "driver" here, and we launch
# with either nyc or node, depending on whether we want coverage
# reports. Running under nyc is slower and creates funny
# tracebacks, so you generally want to get coverage reports only
# after making sure tests will pass.
node_tests_cmd = ['node', '--stack-trace-limit=100', INDEX_JS]
node_tests_cmd += individual_files
if options.coverage:
coverage_dir = os.path.join(ROOT_DIR, 'var/node-coverage')
coverage_lcov_file = os.path.join(coverage_dir, 'lcov.info')
nyc = os.path.join(ROOT_DIR, 'node_modules/.bin/nyc')
command = [nyc, '--report-dir', coverage_dir]
command += ['--temp-directory', coverage_dir, '-r=text-summary']
command += node_tests_cmd
command += ['&&', 'nyc', 'report', '-r=lcov', '-r=json']
command += ['>', coverage_lcov_file]
else:
# Normal testing, no coverage analysis.
# Run the index.js test runner, which runs all the other tests.
command = node_tests_cmd
print('Starting node tests...')
# If we got this far, we can run the tests!
try:
ret = subprocess.check_call(command)
except OSError:
print('Bad command: %s' % (command,))
raise
except subprocess.CalledProcessError:
print('\n** Tests failed, PLEASE FIX! **\n')
sys.exit(1)
def check_line_coverage(fn, line_coverage, line_mapping, log=True):
# type: (str, Dict[Any, Any], Dict[Any, Any], bool) -> bool
missing_lines = []
for line in line_coverage:
if line_coverage[line] == 0:
actual_line = line_mapping[line]
missing_lines.append(str(actual_line["start"]["line"]))
if missing_lines:
if log:
print("ERROR: %s no longer has complete node test coverage" % (fn,))
print(" Lines missing coverage: %s" % (", ".join(sorted(missing_lines, key=int)),))
print()
return False
return True
NODE_COVERAGE_PATH = 'var/node-coverage/coverage-final.json'
def read_coverage() -> Any:
coverage_json = None
try:
with open(NODE_COVERAGE_PATH, 'r') as f:
coverage_json = ujson.load(f)
except IOError:
print(NODE_COVERAGE_PATH + " doesn't exist. Cannot enforce fully covered files.")
raise
return coverage_json
def enforce_proper_coverage(coverage_json: Any) -> bool:
coverage_lost = False
for relative_path in enforce_fully_covered:
path = ROOT_DIR + "/" + relative_path
if not (path in coverage_json):
print("ERROR: %s has no node test coverage" % (relative_path,))
continue
line_coverage = coverage_json[path]['s']
line_mapping = coverage_json[path]['statementMap']
if not check_line_coverage(relative_path, line_coverage, line_mapping):
coverage_lost = True
if coverage_lost:
print()
print("It looks like your changes lost 100% test coverage in one or more files.")
print("Usually, the right fix for this is to add some tests.")
print("But also check out the include/exclude lists in `tools/test-js-with-node`.")
print("To run this check locally, use `test-js-with-node --coverage`.")
coverage_not_enforced = False
for path in coverage_json:
if '/static/js/' in path:
relative_path = os.path.relpath(path, ROOT_DIR)
line_coverage = coverage_json[path]['s']
line_mapping = coverage_json[path]['statementMap']
if check_line_coverage(relative_path, line_coverage, line_mapping, log=False) \
and not (relative_path in enforce_fully_covered):
coverage_not_enforced = True
print("ERROR: %s has complete node test coverage and is not enforced." % (relative_path,))
if coverage_not_enforced:
print()
print("There are one or more fully covered files that are not enforced.")
print("Add the file(s) to enforce_fully_covered in `tools/test-js-with-node`.")
problems_encountered = (coverage_lost or coverage_not_enforced)
return problems_encountered
if options.coverage and ret == 0:
if not individual_files:
coverage_json = read_coverage()
problems_encountered = enforce_proper_coverage(coverage_json)
if problems_encountered:
ret = 1
print()
if ret == 0:
if options.coverage:
print("View coverage reports at http://127.0.0.1:9991/node-coverage/index.html")
print("Test(s) passed. SUCCESS!")
else:
print("FAIL - Test(s) failed")
sys.exit(ret)