Files
zulip/tools/test-js-with-node
Steve Howell ba243416e3 stream_color: Make stream color assignment more efficient.
We now let color_data keep its own state for
unused_colors, so that we longer have to pass in
a large list of unused_colors every time we want
to assign a new stream color.

This mostly matters at startup, where we might
be cycling through 5000 streams.  We claim all
the unused colors up front.

Each operation now has an upper bound of expensiveness,
where the worst case scenario is basically popping
off the first element of a list of <= 24 colors.

The algorithm is now deterministic, too, to make
it easier to test.  It's unclear whether random color
assignment ever had much benefit, and it made unit
testing the algorithm difficult.  Now we have 100%
line coverage.

Fixes part of #10902.
2018-11-29 13:00:25 -08:00

238 lines
8.2 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/color_data.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/input_pill.js',
'static/js/list_cursor.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)