snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
0
tools/tests/__init__.py
Normal file
0
tools/tests/__init__.py
Normal file
145
tools/tests/test_capitalization_checker.py
Normal file
145
tools/tests/test_capitalization_checker.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from tools.lib.capitalization import check_capitalization, get_safe_text, is_capitalized
|
||||
|
||||
|
||||
class GetSafeTextTestCase(TestCase):
|
||||
def test_get_safe_text(self) -> None:
|
||||
string = "Zulip Zulip. Zulip some text!"
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, "Zulip zulip. Zulip some text!")
|
||||
|
||||
string = "Zulip Zulip? Zulip some text!"
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, "Zulip zulip? Zulip some text!")
|
||||
|
||||
string = "Zulip Zulip! Zulip some text!"
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, "Zulip zulip! Zulip some text!")
|
||||
|
||||
string = "Zulip Zulip, Zulip some text!"
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, "Zulip zulip, zulip some text!")
|
||||
|
||||
string = "Not Ignored Phrase"
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, "Not Ignored Phrase")
|
||||
|
||||
string = "Not ignored phrase"
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, "Not ignored phrase")
|
||||
|
||||
string = ""
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, "")
|
||||
|
||||
string = """
|
||||
<p>Please re-enter your password to confirm your identity.
|
||||
(<a href="/accounts/password/reset/" target="_blank">Forgotten it?</a>)</p>
|
||||
"""
|
||||
safe_text = get_safe_text(string)
|
||||
soup = BeautifulSoup(safe_text, "lxml")
|
||||
rendered_text = " ".join(soup.text.split())
|
||||
self.assertEqual(safe_text, rendered_text)
|
||||
|
||||
string = "Edited (__last_edit_timestr__)"
|
||||
safe_text = get_safe_text(string)
|
||||
self.assertEqual(safe_text, string)
|
||||
|
||||
|
||||
class IsCapitalizedTestCase(TestCase):
|
||||
def test_process_text(self) -> None:
|
||||
string = "Zulip zulip. Zulip some text!"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Zulip zulip? Zulip some text!"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Zulip zulip! Zulip some text!"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Zulip zulip, Zulip some text!"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Some number 25mib"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Not Ignored Phrase"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertFalse(capitalized)
|
||||
|
||||
string = "Not ignored phrase"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = ""
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Please re-enter your password to confirm your identity. (Forgotten it?)"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Edited (__last_edit_timestr__)"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "Iphone application"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
string = "One two etc_ three"
|
||||
capitalized = is_capitalized(string)
|
||||
self.assertTrue(capitalized)
|
||||
|
||||
|
||||
class CheckCapitalizationTestCase(TestCase):
|
||||
def test_check_capitalization(self) -> None:
|
||||
strings = [
|
||||
"Zulip Zulip. Zulip some text!",
|
||||
"Zulip Zulip? Zulip some text!",
|
||||
"Zulip Zulip! Zulip some text!",
|
||||
"Zulip Zulip, Zulip some text!",
|
||||
"Not Ignored Phrase",
|
||||
"Not ignored phrase",
|
||||
"Some text with realm in it",
|
||||
"Realm in capital case",
|
||||
]
|
||||
errored, ignored, banned = check_capitalization(strings)
|
||||
self.assertEqual(errored, ["Not Ignored Phrase"])
|
||||
self.assertEqual(
|
||||
ignored,
|
||||
sorted(
|
||||
[
|
||||
"Zulip Zulip. Zulip some text!",
|
||||
"Zulip Zulip? Zulip some text!",
|
||||
"Zulip Zulip! Zulip some text!",
|
||||
"Zulip Zulip, Zulip some text!",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
banned,
|
||||
sorted(
|
||||
[
|
||||
(
|
||||
"realm found in 'Some text with realm in it'. "
|
||||
"The term realm should not appear in user-facing "
|
||||
"strings. Use organization instead."
|
||||
),
|
||||
(
|
||||
"realm found in 'Realm in capital case'. "
|
||||
"The term realm should not appear in user-facing "
|
||||
"strings. Use organization instead."
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
83
tools/tests/test_check_rabbitmq_queue.py
Normal file
83
tools/tests/test_check_rabbitmq_queue.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import time
|
||||
from unittest import TestCase, mock
|
||||
|
||||
from scripts.lib.check_rabbitmq_queue import CRITICAL, OK, UNKNOWN, WARNING, analyze_queue_stats
|
||||
|
||||
|
||||
class AnalyzeQueueStatsTests(TestCase):
|
||||
def test_no_stats_available(self) -> None:
|
||||
result = analyze_queue_stats("name", {}, 0)
|
||||
self.assertEqual(result["status"], UNKNOWN)
|
||||
|
||||
def test_queue_stuck(self) -> None:
|
||||
"""Last update > 5 minutes ago and there's events in the queue."""
|
||||
|
||||
result = analyze_queue_stats("name", {"update_time": time.time() - 301}, 100)
|
||||
self.assertEqual(result["status"], CRITICAL)
|
||||
self.assertIn("queue appears to be stuck", result["message"])
|
||||
|
||||
def test_queue_just_started(self) -> None:
|
||||
"""
|
||||
We just started processing a burst of events, and haven't processed enough
|
||||
to log productivity statistics yet.
|
||||
"""
|
||||
result = analyze_queue_stats(
|
||||
"name",
|
||||
{
|
||||
"update_time": time.time(),
|
||||
"recent_average_consume_time": None,
|
||||
},
|
||||
10000,
|
||||
)
|
||||
self.assertEqual(result["status"], OK)
|
||||
|
||||
def test_queue_normal(self) -> None:
|
||||
"""10000 events and each takes a second => it'll take a long time to empty."""
|
||||
result = analyze_queue_stats(
|
||||
"name",
|
||||
{
|
||||
"update_time": time.time(),
|
||||
"queue_last_emptied_timestamp": time.time() - 10000,
|
||||
"recent_average_consume_time": 1,
|
||||
},
|
||||
10000,
|
||||
)
|
||||
self.assertEqual(result["status"], CRITICAL)
|
||||
self.assertIn("clearing the backlog", result["message"])
|
||||
|
||||
# If we're doing 10K/sec, it's OK.
|
||||
result = analyze_queue_stats(
|
||||
"name",
|
||||
{
|
||||
"update_time": time.time(),
|
||||
"queue_last_emptied_timestamp": time.time() - 10000,
|
||||
"recent_average_consume_time": 0.0001,
|
||||
},
|
||||
10000,
|
||||
)
|
||||
self.assertEqual(result["status"], OK)
|
||||
|
||||
# Verify logic around whether it'll take MAX_SECONDS_TO_CLEAR to clear queue.
|
||||
with mock.patch.dict("scripts.lib.check_rabbitmq_queue.MAX_SECONDS_TO_CLEAR", {"name": 10}):
|
||||
result = analyze_queue_stats(
|
||||
"name",
|
||||
{
|
||||
"update_time": time.time(),
|
||||
"queue_last_emptied_timestamp": time.time() - 10000,
|
||||
"recent_average_consume_time": 1,
|
||||
},
|
||||
11,
|
||||
)
|
||||
self.assertEqual(result["status"], WARNING)
|
||||
self.assertIn("clearing the backlog", result["message"])
|
||||
|
||||
result = analyze_queue_stats(
|
||||
"name",
|
||||
{
|
||||
"update_time": time.time(),
|
||||
"queue_last_emptied_timestamp": time.time() - 10000,
|
||||
"recent_average_consume_time": 1,
|
||||
},
|
||||
9,
|
||||
)
|
||||
self.assertEqual(result["status"], OK)
|
||||
66
tools/tests/test_html_branches.py
Normal file
66
tools/tests/test_html_branches.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import tools.lib.template_parser
|
||||
from tools.lib.html_branches import build_id_dict, get_tag_info, split_for_id_and_class
|
||||
|
||||
ZULIP_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
TEST_TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "test_template_data")
|
||||
|
||||
|
||||
class TestHtmlBranches(unittest.TestCase):
|
||||
def test_get_tag_info(self) -> None:
|
||||
html = """<p id="test" class="test1 test2">foo</p>"""
|
||||
|
||||
start_tag, text, end_tag = tools.lib.template_parser.tokenize(html)
|
||||
|
||||
start_tag_info = get_tag_info(start_tag)
|
||||
end_tag_info = get_tag_info(end_tag)
|
||||
|
||||
self.assertEqual(start_tag_info.text(), "p.test1.test2#test")
|
||||
self.assertEqual(end_tag_info.text(), "p")
|
||||
self.assertEqual(text.s, "foo")
|
||||
|
||||
def test_build_id_dict(self) -> None:
|
||||
templates = ["test_template1.html", "test_template2.html"]
|
||||
templates = [os.path.join(TEST_TEMPLATES_DIR, fn) for fn in templates]
|
||||
|
||||
template_id_dict = build_id_dict(templates)
|
||||
|
||||
self.assertEqual(
|
||||
set(template_id_dict.keys()), {"below_navbar", "hello_{{ message }}", "intro"}
|
||||
)
|
||||
self.assertEqual(
|
||||
template_id_dict["hello_{{ message }}"],
|
||||
[
|
||||
f"Line 12:{ZULIP_PATH}/tools/tests/test_template_data/test_template1.html",
|
||||
f"Line 12:{ZULIP_PATH}/tools/tests/test_template_data/test_template2.html",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
template_id_dict["intro"],
|
||||
[
|
||||
f"Line 10:{ZULIP_PATH}/tools/tests/test_template_data/test_template1.html",
|
||||
f"Line 11:{ZULIP_PATH}/tools/tests/test_template_data/test_template1.html",
|
||||
f"Line 11:{ZULIP_PATH}/tools/tests/test_template_data/test_template2.html",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
template_id_dict["below_navbar"],
|
||||
[f"Line 10:{ZULIP_PATH}/tools/tests/test_template_data/test_template2.html"],
|
||||
)
|
||||
|
||||
def test_split_for_id_and_class(self) -> None:
|
||||
id1 = "{{ red|blue }}"
|
||||
id2 = "search_box_{{ page }}"
|
||||
|
||||
class1 = "chat_box message"
|
||||
class2 = "stream_{{ topic }}"
|
||||
class3 = "foo {{ a|b|c }} bar"
|
||||
|
||||
self.assertEqual(split_for_id_and_class(id1), ["{{ red|blue }}"])
|
||||
self.assertEqual(split_for_id_and_class(id2), ["search_box_{{ page }}"])
|
||||
|
||||
self.assertEqual(split_for_id_and_class(class1), ["chat_box", "message"])
|
||||
self.assertEqual(split_for_id_and_class(class2), ["stream_{{ topic }}"])
|
||||
self.assertEqual(split_for_id_and_class(class3), ["foo", "{{ a|b|c }}", "bar"])
|
||||
305
tools/tests/test_pretty_print.py
Normal file
305
tools/tests/test_pretty_print.py
Normal file
@@ -0,0 +1,305 @@
|
||||
import unittest
|
||||
|
||||
from tools.lib.pretty_print import pretty_print_html
|
||||
from tools.lib.template_parser import validate
|
||||
|
||||
# Note that GOOD_HTML isn't necessarily beautiful HTML. Apart
|
||||
# from adjusting indentation, we mostly leave things alone to
|
||||
# respect whatever line-wrapping styles were in place before.
|
||||
|
||||
BAD_HTML = """
|
||||
<!-- test -->
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<!-- test -->
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div><p>Hello<br />world!</p></div>
|
||||
<p>Goodbye<!-- test -->world!</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
</table>
|
||||
<pre>
|
||||
print 'hello world'
|
||||
</pre>
|
||||
<div class = "foo"
|
||||
id = "bar"
|
||||
role = "whatever">{{ bla }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!-- test -->
|
||||
"""
|
||||
|
||||
GOOD_HTML = """
|
||||
<!-- test -->
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<!-- test -->
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div><p>Hello<br />world!</p></div>
|
||||
<p>Goodbye<!-- test -->world!</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
</table>
|
||||
<pre>
|
||||
print 'hello world'
|
||||
</pre>
|
||||
<div class = "foo"
|
||||
id = "bar"
|
||||
role = "whatever">{{ bla }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<!-- test -->
|
||||
"""
|
||||
|
||||
BAD_HTML1 = """
|
||||
<html>
|
||||
<body>
|
||||
foobarfoobarfoo<b>bar</b>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
GOOD_HTML1 = """
|
||||
<html>
|
||||
<body>
|
||||
foobarfoobarfoo<b>bar</b>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
BAD_HTML2 = """
|
||||
<html>
|
||||
<body>
|
||||
{{# foobar area}}
|
||||
foobarfoobarfoo<b>bar</b>
|
||||
{{/ foobar}}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
GOOD_HTML2 = """
|
||||
<html>
|
||||
<body>
|
||||
{{# foobar area}}
|
||||
foobarfoobarfoo<b>bar</b>
|
||||
{{/ foobar}}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# The old GOOD_HTML3 test was flawed.
|
||||
|
||||
BAD_HTML4 = """
|
||||
<div>
|
||||
foo
|
||||
<p>hello</p>
|
||||
bar
|
||||
</div>
|
||||
"""
|
||||
|
||||
GOOD_HTML4 = """
|
||||
<div>
|
||||
foo
|
||||
<p>hello</p>
|
||||
bar
|
||||
</div>
|
||||
"""
|
||||
|
||||
BAD_HTML5 = """
|
||||
<div>
|
||||
foo
|
||||
{{#if foobar}}
|
||||
hello
|
||||
{{else}}
|
||||
bye
|
||||
{{/if}}
|
||||
bar
|
||||
</div>
|
||||
"""
|
||||
|
||||
GOOD_HTML5 = """
|
||||
<div>
|
||||
foo
|
||||
{{#if foobar}}
|
||||
hello
|
||||
{{else}}
|
||||
bye
|
||||
{{/if}}
|
||||
bar
|
||||
</div>
|
||||
"""
|
||||
|
||||
BAD_HTML6 = """
|
||||
<div>
|
||||
<p> <strong> <span class = "whatever">foobar </span> </strong></p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
GOOD_HTML6 = """
|
||||
<div>
|
||||
<p> <strong> <span class = "whatever">foobar </span> </strong></p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
BAD_HTML7 = """
|
||||
<div class="foobar">
|
||||
<input type="foobar" name="temp" value="{{dyn_name}}"
|
||||
{{#unless invite_only}}checked="checked"{{/unless}} /> {{dyn_name}}
|
||||
{{#if invite_only}}<i class="zulip-icon zulip-icon-lock"></i>{{/if}}
|
||||
</div>
|
||||
"""
|
||||
|
||||
GOOD_HTML7 = """
|
||||
<div class="foobar">
|
||||
<input type="foobar" name="temp" value="{{dyn_name}}"
|
||||
{{#unless invite_only}}checked="checked"{{/unless}} /> {{dyn_name}}
|
||||
{{#if invite_only}}<i class="zulip-icon zulip-icon-lock"></i>{{/if}}
|
||||
</div>
|
||||
"""
|
||||
|
||||
BAD_HTML8 = """
|
||||
{{#each test}}
|
||||
{{#with this}}
|
||||
{{#if foobar}}
|
||||
<div class="anything">{{{test_html}}}</div>
|
||||
{{/if}}
|
||||
{{#if foobar2}}
|
||||
{{> teststuff}}
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
"""
|
||||
|
||||
GOOD_HTML8 = """
|
||||
{{#each test}}
|
||||
{{#with this}}
|
||||
{{#if foobar}}
|
||||
<div class="anything">{{{test_html}}}</div>
|
||||
{{/if}}
|
||||
{{#if foobar2}}
|
||||
{{> teststuff}}
|
||||
{{/if}}
|
||||
{{/with}}
|
||||
{{/each}}
|
||||
"""
|
||||
|
||||
BAD_HTML9 = """
|
||||
<form id="foobar" class="whatever">
|
||||
{{! <div class="anothertest"> }}
|
||||
<input value="test" />
|
||||
<button type="button"><i class="test"></i></button>
|
||||
<button type="button"><i class="test"></i></button>
|
||||
{{! </div> }}
|
||||
<div class="test"></div>
|
||||
</form>
|
||||
"""
|
||||
|
||||
GOOD_HTML9 = """
|
||||
<form id="foobar" class="whatever">
|
||||
{{! <div class="anothertest"> }}
|
||||
<input value="test" />
|
||||
<button type="button"><i class="test"></i></button>
|
||||
<button type="button"><i class="test"></i></button>
|
||||
{{! </div> }}
|
||||
<div class="test"></div>
|
||||
</form>
|
||||
"""
|
||||
|
||||
BAD_HTML10 = """
|
||||
{% block portico_content %}
|
||||
<div class="test">
|
||||
<i class='test'></i> foobar
|
||||
</div>
|
||||
<div class="test1">
|
||||
{% for row in data %}
|
||||
<div class="test2">
|
||||
{% for group in (row[0:2], row[2:4]) %}
|
||||
<div class="test2">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
GOOD_HTML10 = """
|
||||
{% block portico_content %}
|
||||
<div class="test">
|
||||
<i class='test'></i> foobar
|
||||
</div>
|
||||
<div class="test1">
|
||||
{% for row in data %}
|
||||
<div class="test2">
|
||||
{% for group in (row[0:2], row[2:4]) %}
|
||||
<div class="test2">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
"""
|
||||
|
||||
BAD_HTML11 = """
|
||||
<div class="test1">
|
||||
<div class="test2">
|
||||
foobar
|
||||
<div class="test2">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
GOOD_HTML11 = """
|
||||
<div class="test1">
|
||||
<div class="test2">
|
||||
foobar
|
||||
<div class="test2">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def pretty_print(html: str, template_format: str | None = None) -> str:
|
||||
fn = "<test str>"
|
||||
tokens = validate(fn=fn, text=html, template_format=template_format)
|
||||
return pretty_print_html(tokens, fn=fn)
|
||||
|
||||
|
||||
class TestPrettyPrinter(unittest.TestCase):
|
||||
def compare(self, a: str, b: str) -> None:
|
||||
self.assertEqual(a.split("\n"), b.split("\n"))
|
||||
|
||||
def test_pretty_print(self) -> None:
|
||||
self.compare(pretty_print(GOOD_HTML), GOOD_HTML)
|
||||
self.compare(pretty_print(BAD_HTML), GOOD_HTML)
|
||||
self.compare(pretty_print(BAD_HTML1), GOOD_HTML1)
|
||||
self.compare(pretty_print(BAD_HTML2, template_format="handlebars"), GOOD_HTML2)
|
||||
self.compare(pretty_print(BAD_HTML4), GOOD_HTML4)
|
||||
self.compare(pretty_print(BAD_HTML5, template_format="handlebars"), GOOD_HTML5)
|
||||
self.compare(pretty_print(BAD_HTML6), GOOD_HTML6)
|
||||
self.compare(pretty_print(BAD_HTML7, template_format="handlebars"), GOOD_HTML7)
|
||||
self.compare(pretty_print(BAD_HTML8, template_format="handlebars"), GOOD_HTML8)
|
||||
self.compare(pretty_print(BAD_HTML9, template_format="handlebars"), GOOD_HTML9)
|
||||
self.compare(pretty_print(BAD_HTML10, template_format="django"), GOOD_HTML10)
|
||||
self.compare(pretty_print(BAD_HTML11), GOOD_HTML11)
|
||||
17
tools/tests/test_template_data/test_template1.html
Normal file
17
tools/tests/test_template_data/test_template1.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- test -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="intro">
|
||||
<p id="intro">
|
||||
<span id="hello_{{ message }}">Hello world!!</span>
|
||||
This is a test file for checking correct working of duplicate id detection module.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
17
tools/tests/test_template_data/test_template2.html
Normal file
17
tools/tests/test_template_data/test_template2.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!-- test -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="below_navbar">
|
||||
<p id="intro">
|
||||
<span id="hello_{{ message }}">Hello world!!</span>
|
||||
This is a test file for checking correct working of duplicate id detection module.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
430
tools/tests/test_template_parser.py
Normal file
430
tools/tests/test_template_parser.py
Normal file
@@ -0,0 +1,430 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
try:
|
||||
from tools.lib.template_parser import (
|
||||
TemplateParserError,
|
||||
is_django_block_tag,
|
||||
tokenize,
|
||||
validate,
|
||||
)
|
||||
except ImportError:
|
||||
print("ERROR!!! You need to run this via tools/test-tools.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class ParserTest(unittest.TestCase):
|
||||
def _assert_validate_error(
|
||||
self,
|
||||
error: str,
|
||||
fn: str | None = None,
|
||||
text: str | None = None,
|
||||
template_format: str | None = None,
|
||||
) -> None:
|
||||
with self.assertRaisesRegex(TemplateParserError, error):
|
||||
validate(fn=fn, text=text, template_format=template_format)
|
||||
|
||||
def test_is_django_block_tag(self) -> None:
|
||||
self.assertTrue(is_django_block_tag("block"))
|
||||
self.assertFalse(is_django_block_tag("not a django tag"))
|
||||
|
||||
def test_validate_vanilla_html(self) -> None:
|
||||
"""
|
||||
Verify that validate() does not raise errors for
|
||||
well-formed HTML.
|
||||
"""
|
||||
my_html = """
|
||||
<table>
|
||||
<tr>
|
||||
<td>foo</td>
|
||||
</tr>
|
||||
</table>"""
|
||||
validate(text=my_html)
|
||||
|
||||
def test_validate_handlebars(self) -> None:
|
||||
my_html = """
|
||||
{{#with stream}}
|
||||
<p>{{stream}}</p>
|
||||
{{/with}}
|
||||
"""
|
||||
validate(text=my_html, template_format="handlebars")
|
||||
|
||||
def test_validate_handlebars_partial_block(self) -> None:
|
||||
my_html = """
|
||||
{{#> generic_thing }}
|
||||
<p>hello!</p>
|
||||
{{/generic_thing}}
|
||||
"""
|
||||
validate(text=my_html, template_format="handlebars")
|
||||
|
||||
def test_validate_bad_handlebars_partial_block(self) -> None:
|
||||
my_html = """
|
||||
{{#> generic_thing }}
|
||||
<p>hello!</p>
|
||||
{{# generic_thing}}
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
"Missing end tag for the token at row 4 13!", text=my_html, template_format="handlebars"
|
||||
)
|
||||
|
||||
def test_validate_comment(self) -> None:
|
||||
my_html = """
|
||||
<!---
|
||||
<h1>foo</h1>
|
||||
-->"""
|
||||
validate(text=my_html)
|
||||
|
||||
def test_validate_django(self) -> None:
|
||||
my_html = """
|
||||
{% include "some_other.html" %}
|
||||
{% if foo %}
|
||||
<p>bar</p>
|
||||
{% endif %}
|
||||
"""
|
||||
validate(text=my_html, template_format="django")
|
||||
|
||||
my_html = """
|
||||
{% block "content" %}
|
||||
{% with className="class" %}
|
||||
{% include 'foobar' %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
"""
|
||||
validate(text=my_html)
|
||||
|
||||
def test_validate_no_start_tag(self) -> None:
|
||||
my_html = """
|
||||
foo</p>
|
||||
"""
|
||||
self._assert_validate_error("No start tag", text=my_html)
|
||||
|
||||
def test_validate_mismatched_tag(self) -> None:
|
||||
my_html = """
|
||||
<b>foo</i>
|
||||
"""
|
||||
self._assert_validate_error(r"Mismatched tags: \(b != i\)", text=my_html)
|
||||
|
||||
def test_validate_bad_indentation(self) -> None:
|
||||
my_html = """
|
||||
<p>
|
||||
foo
|
||||
</p>
|
||||
"""
|
||||
self._assert_validate_error("Indentation for start/end tags does not match.", text=my_html)
|
||||
|
||||
def test_validate_state_depth(self) -> None:
|
||||
my_html = """
|
||||
<b>
|
||||
"""
|
||||
self._assert_validate_error("Missing end tag", text=my_html)
|
||||
|
||||
def test_validate_incomplete_handlebars_tag_1(self) -> None:
|
||||
my_html = """
|
||||
{{# foo
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'''Tag missing "}}" at line 2 col 13:"{{# foo
|
||||
"''',
|
||||
text=my_html,
|
||||
template_format="handlebars",
|
||||
)
|
||||
|
||||
def test_validate_incomplete_handlebars_tag_2(self) -> None:
|
||||
my_html = """
|
||||
{{# foo }
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'Tag missing "}}" at line 2 col 13:"{{# foo }\n"',
|
||||
text=my_html,
|
||||
template_format="handlebars",
|
||||
)
|
||||
|
||||
def test_validate_incomplete_handlebars_tag_3(self) -> None:
|
||||
my_html = "{{# foo}"
|
||||
self._assert_validate_error(
|
||||
'Tag missing "}}" at line 1 col 1:"{{# foo}"',
|
||||
text=my_html,
|
||||
template_format="handlebars",
|
||||
)
|
||||
|
||||
def test_validate_triple_stache_var_1(self) -> None:
|
||||
my_html = """
|
||||
{{{ foo}}
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'Tag missing "}}}" at line 2 col 13:"{{{ foo}}\n"',
|
||||
text=my_html,
|
||||
template_format="handlebars",
|
||||
)
|
||||
|
||||
def test_validate_triple_stache_var_2(self) -> None:
|
||||
my_html = """
|
||||
{{{ foo}~}
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'Tag missing "}}}" at line 2 col 13:"{{{ foo}~}"',
|
||||
text=my_html,
|
||||
template_format="handlebars",
|
||||
)
|
||||
|
||||
def test_validate_triple_stache_var_3(self) -> None:
|
||||
my_html = """
|
||||
{{{ foo }}}
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
"Unescaped variables in triple staches {{{ }}} must be suffixed with `_html`",
|
||||
text=my_html,
|
||||
template_format="handlebars",
|
||||
)
|
||||
|
||||
def test_validate_triple_stache_var_4(self) -> None:
|
||||
my_html = """
|
||||
{{{ foo_html }~}}
|
||||
"""
|
||||
validate(text=my_html, template_format="handlebars")
|
||||
|
||||
def test_validate_triple_stache_var_5(self) -> None:
|
||||
my_html = "{{{ foo_html}}}"
|
||||
validate(text=my_html, template_format="handlebars")
|
||||
|
||||
def test_validate_triple_stache_var_6(self) -> None:
|
||||
my_html = "{{~{ bar_html}}}"
|
||||
validate(text=my_html, template_format="handlebars")
|
||||
|
||||
def test_validate_triple_stache_var_7(self) -> None:
|
||||
my_html = "{{~{ bar_html}~}}"
|
||||
validate(text=my_html, template_format="handlebars")
|
||||
|
||||
def test_validate_incomplete_django_tag_1(self) -> None:
|
||||
my_html = """
|
||||
{% foo
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'''Tag missing "%}" at line 2 col 13:"{% foo
|
||||
"''',
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_validate_incomplete_django_tag_2(self) -> None:
|
||||
my_html = """
|
||||
{% foo %
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'Tag missing "%}" at line 2 col 13:"{% foo %\n"',
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_validate_incomplete_html_tag_1(self) -> None:
|
||||
my_html = """
|
||||
<b
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'''Tag missing ">" at line 2 col 13:"<b
|
||||
"''',
|
||||
text=my_html,
|
||||
)
|
||||
|
||||
def test_validate_incomplete_html_tag_2(self) -> None:
|
||||
my_html = """
|
||||
<a href="
|
||||
"""
|
||||
my_html1 = """
|
||||
<a href=""
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
'''Tag missing ">" at line 2 col 13:"<a href=""
|
||||
"''',
|
||||
text=my_html1,
|
||||
)
|
||||
self._assert_validate_error(
|
||||
'''Unbalanced quotes at line 2 col 13:"<a href="
|
||||
"''',
|
||||
text=my_html,
|
||||
)
|
||||
|
||||
def test_validate_empty_html_tag(self) -> None:
|
||||
my_html = """
|
||||
< >
|
||||
"""
|
||||
self._assert_validate_error("Tag name missing", text=my_html)
|
||||
|
||||
def test_code_blocks(self) -> None:
|
||||
# This is fine.
|
||||
my_html = """
|
||||
<code>
|
||||
x = 5
|
||||
y = x + 1
|
||||
</code>"""
|
||||
validate(text=my_html)
|
||||
|
||||
# This is also fine.
|
||||
my_html = "<code>process_widgets()</code>"
|
||||
validate(text=my_html)
|
||||
|
||||
# This is illegal.
|
||||
my_html = """
|
||||
<code>x =
|
||||
5</code>
|
||||
"""
|
||||
self._assert_validate_error("Code tag is split across two lines.", text=my_html)
|
||||
|
||||
def test_anchor_blocks(self) -> None:
|
||||
# This is allowed, although strange.
|
||||
my_html = """
|
||||
<a href="/some/url">
|
||||
Click here
|
||||
for more info.
|
||||
</a>"""
|
||||
validate(text=my_html)
|
||||
|
||||
# This is fine.
|
||||
my_html = '<a href="/some/url">click here</a>'
|
||||
validate(text=my_html)
|
||||
|
||||
# Even this is fine.
|
||||
my_html = """
|
||||
<a class="twitter-timeline" href="https://twitter.com/ZulipStatus"
|
||||
data-widget-id="443457763394334720"
|
||||
data-screen-name="ZulipStatus"
|
||||
>@ZulipStatus on Twitter</a>.
|
||||
"""
|
||||
validate(text=my_html)
|
||||
|
||||
def test_validate_jinja2_whitespace_markers_1(self) -> None:
|
||||
my_html = """
|
||||
{% if foo -%}
|
||||
this is foo
|
||||
{% endif %}
|
||||
"""
|
||||
validate(
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_validate_jinja2_whitespace_markers_2(self) -> None:
|
||||
my_html = """
|
||||
{% if foo %}
|
||||
this is foo
|
||||
{%- endif %}
|
||||
"""
|
||||
validate(
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_validate_jinja2_whitespace_markers_3(self) -> None:
|
||||
my_html = """
|
||||
{% if foo %}
|
||||
this is foo
|
||||
{% endif -%}
|
||||
"""
|
||||
validate(
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_validate_jinja2_whitespace_markers_4(self) -> None:
|
||||
my_html = """
|
||||
{%- if foo %}
|
||||
this is foo
|
||||
{% endif %}
|
||||
"""
|
||||
validate(
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_validate_mismatch_jinja2_whitespace_markers_1(self) -> None:
|
||||
my_html = """
|
||||
{% if foo %}
|
||||
this is foo
|
||||
{%- if bar %}
|
||||
"""
|
||||
self._assert_validate_error(
|
||||
"Missing end tag",
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_validate_jinja2_whitespace_type2_markers(self) -> None:
|
||||
my_html = """
|
||||
{%- if foo -%}
|
||||
this is foo
|
||||
{% endif %}
|
||||
"""
|
||||
validate(
|
||||
text=my_html,
|
||||
template_format="django",
|
||||
)
|
||||
|
||||
def test_tokenize(self) -> None:
|
||||
tag = "<!DOCTYPE html>"
|
||||
token = tokenize(tag)[0]
|
||||
self.assertEqual(token.kind, "html_doctype")
|
||||
|
||||
tag = "<a>bla"
|
||||
token = tokenize(tag)[0]
|
||||
self.assertEqual(token.kind, "html_start")
|
||||
self.assertEqual(token.tag, "a")
|
||||
|
||||
tag = "<br />bla"
|
||||
token = tokenize(tag)[0]
|
||||
self.assertEqual(token.kind, "html_singleton")
|
||||
self.assertEqual(token.tag, "br")
|
||||
|
||||
tag = "<input>bla"
|
||||
token = tokenize(tag)[0]
|
||||
self.assertEqual(token.kind, "html_start") # We later mark this an error.
|
||||
self.assertEqual(token.tag, "input")
|
||||
|
||||
tag = "<input />bla"
|
||||
token = tokenize(tag)[0]
|
||||
self.assertEqual(token.kind, "html_singleton")
|
||||
self.assertEqual(token.tag, "input")
|
||||
|
||||
tag = "</a>bla"
|
||||
token = tokenize(tag)[0]
|
||||
self.assertEqual(token.kind, "html_end")
|
||||
self.assertEqual(token.tag, "a")
|
||||
|
||||
tag = "{{#with foo}}bla"
|
||||
token = tokenize(tag, template_format="handlebars")[0]
|
||||
self.assertEqual(token.kind, "handlebars_start")
|
||||
self.assertEqual(token.tag, "with")
|
||||
|
||||
tag = "{{/with}}bla"
|
||||
token = tokenize(tag, template_format="handlebars")[0]
|
||||
self.assertEqual(token.kind, "handlebars_end")
|
||||
self.assertEqual(token.tag, "with")
|
||||
|
||||
tag = "{{#> compose_banner }}bla"
|
||||
token = tokenize(tag, template_format="handlebars")[0]
|
||||
self.assertEqual(token.kind, "handlebars_partial_block")
|
||||
self.assertEqual(token.tag, "compose_banner")
|
||||
|
||||
tag = "{% if foo %}bla"
|
||||
token = tokenize(tag, template_format="django")[0]
|
||||
self.assertEqual(token.kind, "django_start")
|
||||
self.assertEqual(token.tag, "if")
|
||||
|
||||
tag = "{% endif %}bla"
|
||||
token = tokenize(tag, template_format="django")[0]
|
||||
self.assertEqual(token.kind, "django_end")
|
||||
self.assertEqual(token.tag, "if")
|
||||
|
||||
tag = "{% if foo -%}bla"
|
||||
token = tokenize(tag, template_format="django")[0]
|
||||
self.assertEqual(token.kind, "jinja2_whitespace_stripped_start")
|
||||
self.assertEqual(token.tag, "if")
|
||||
|
||||
tag = "{%- endif %}bla"
|
||||
token = tokenize(tag, template_format="django")[0]
|
||||
self.assertEqual(token.kind, "jinja2_whitespace_stripped_end")
|
||||
self.assertEqual(token.tag, "if")
|
||||
|
||||
tag = "{%- if foo -%}bla"
|
||||
token = tokenize(tag, template_format="django")[0]
|
||||
self.assertEqual(token.kind, "jinja2_whitespace_stripped_type2_start")
|
||||
self.assertEqual(token.tag, "if")
|
||||
68
tools/tests/test_zulint_custom_rules.py
Normal file
68
tools/tests/test_zulint_custom_rules.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import os
|
||||
from io import StringIO
|
||||
from unittest import TestCase
|
||||
from unittest.mock import patch
|
||||
|
||||
from typing_extensions import override
|
||||
from zulint.custom_rules import RuleList
|
||||
|
||||
from tools.linter_lib.custom_check import non_py_rules, python_rules
|
||||
|
||||
ROOT_DIR = os.path.abspath(os.path.join(__file__, "..", "..", ".."))
|
||||
CHECK_MESSAGE = "Fix the corresponding rule in `tools/linter_lib/custom_check.py`."
|
||||
|
||||
|
||||
class TestRuleList(TestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
all_rules = list(python_rules.rules)
|
||||
for rule in non_py_rules:
|
||||
all_rules.extend(rule.rules)
|
||||
self.all_rules = all_rules
|
||||
|
||||
def test_paths_in_rules(self) -> None:
|
||||
"""Verifies that the paths mentioned in linter rules actually exist"""
|
||||
for rule in self.all_rules:
|
||||
for path in rule.get("exclude", {}):
|
||||
abs_path = os.path.abspath(os.path.join(ROOT_DIR, path))
|
||||
self.assertTrue(
|
||||
os.path.exists(abs_path),
|
||||
f"'{path}' is neither an existing file, nor a directory. {CHECK_MESSAGE}",
|
||||
)
|
||||
|
||||
for line_tuple in rule.get("exclude_line", {}):
|
||||
path = line_tuple[0]
|
||||
abs_path = os.path.abspath(os.path.join(ROOT_DIR, path))
|
||||
self.assertTrue(
|
||||
os.path.isfile(abs_path), f"The file '{path}' doesn't exist. {CHECK_MESSAGE}"
|
||||
)
|
||||
|
||||
for path in rule.get("include_only", {}):
|
||||
if not os.path.splitext(path)[1]:
|
||||
self.assertTrue(
|
||||
path.endswith("/"),
|
||||
f"The path '{path}' should end with '/'. {CHECK_MESSAGE}",
|
||||
)
|
||||
|
||||
def test_rule_patterns(self) -> None:
|
||||
"""Verifies that the search regex specified in a custom rule actually matches
|
||||
the expectation and doesn't throw false positives."""
|
||||
for rule in self.all_rules:
|
||||
pattern = rule["pattern"]
|
||||
for line in rule.get("good_lines", []):
|
||||
with patch("builtins.open", return_value=StringIO(line + "\n\n"), autospec=True):
|
||||
self.assertFalse(
|
||||
RuleList([], [rule]).custom_check_file("foo.bar", "baz", ""),
|
||||
f"The pattern '{pattern}' matched the line '{line}' while it shouldn't.",
|
||||
)
|
||||
|
||||
for line in rule.get("bad_lines", []):
|
||||
for filename in rule.get("include_only", {"foo.bar"}):
|
||||
with (
|
||||
patch("builtins.open", return_value=StringIO(line + "\n\n"), autospec=True),
|
||||
patch("builtins.print"),
|
||||
):
|
||||
self.assertTrue(
|
||||
RuleList([], [rule]).custom_check_file(filename, "baz", ""),
|
||||
f"The pattern '{pattern}' didn't match the line '{line}' while it should.",
|
||||
)
|
||||
Reference in New Issue
Block a user