mirror of
https://github.com/servo/servo.git
synced 2025-08-06 14:10:11 +01:00
Update web-platform-tests and CSS tests.
- Update CSS tests to revision e05bfd5e30ed662c2f8a353577003f8eed230180. - Update web-platform-tests to revision a052787dd5c069a340031011196b73affbd68cd9.
This commit is contained in:
parent
fb4f421c8b
commit
296fa2512b
21852 changed files with 2080936 additions and 892894 deletions
628
tests/wpt/css-tests/tools/pytest/testing/test_assertion.py
Normal file
628
tests/wpt/css-tests/tools/pytest/testing/test_assertion.py
Normal file
|
@ -0,0 +1,628 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import _pytest.assertion as plugin
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.assertion import reinterpret
|
||||
from _pytest.assertion import util
|
||||
|
||||
PY3 = sys.version_info >= (3, 0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
class Config(object):
|
||||
verbose = False
|
||||
def getoption(self, name):
|
||||
if name == 'verbose':
|
||||
return self.verbose
|
||||
raise KeyError('Not mocked out: %s' % name)
|
||||
return Config()
|
||||
|
||||
|
||||
def interpret(expr):
|
||||
return reinterpret.reinterpret(expr, _pytest._code.Frame(sys._getframe(1)))
|
||||
|
||||
class TestBinReprIntegration:
|
||||
|
||||
def test_pytest_assertrepr_compare_called(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
l = []
|
||||
def pytest_assertrepr_compare(op, left, right):
|
||||
l.append((op, left, right))
|
||||
def pytest_funcarg__l(request):
|
||||
return l
|
||||
""")
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
assert 0 == 1
|
||||
def test_check(l):
|
||||
assert l == [("==", 0, 1)]
|
||||
""")
|
||||
result = testdir.runpytest("-v")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*test_hello*FAIL*",
|
||||
"*test_check*PASS*",
|
||||
])
|
||||
|
||||
def callequal(left, right, verbose=False):
|
||||
config = mock_config()
|
||||
config.verbose = verbose
|
||||
return plugin.pytest_assertrepr_compare(config, '==', left, right)
|
||||
|
||||
|
||||
class TestAssert_reprcompare:
|
||||
def test_different_types(self):
|
||||
assert callequal([0, 1], 'foo') is None
|
||||
|
||||
def test_summary(self):
|
||||
summary = callequal([0, 1], [0, 2])[0]
|
||||
assert len(summary) < 65
|
||||
|
||||
def test_text_diff(self):
|
||||
diff = callequal('spam', 'eggs')[1:]
|
||||
assert '- spam' in diff
|
||||
assert '+ eggs' in diff
|
||||
|
||||
def test_text_skipping(self):
|
||||
lines = callequal('a'*50 + 'spam', 'a'*50 + 'eggs')
|
||||
assert 'Skipping' in lines[1]
|
||||
for line in lines:
|
||||
assert 'a'*50 not in line
|
||||
|
||||
def test_text_skipping_verbose(self):
|
||||
lines = callequal('a'*50 + 'spam', 'a'*50 + 'eggs', verbose=True)
|
||||
assert '- ' + 'a'*50 + 'spam' in lines
|
||||
assert '+ ' + 'a'*50 + 'eggs' in lines
|
||||
|
||||
def test_multiline_text_diff(self):
|
||||
left = 'foo\nspam\nbar'
|
||||
right = 'foo\neggs\nbar'
|
||||
diff = callequal(left, right)
|
||||
assert '- spam' in diff
|
||||
assert '+ eggs' in diff
|
||||
|
||||
def test_list(self):
|
||||
expl = callequal([0, 1], [0, 2])
|
||||
assert len(expl) > 1
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
['left', 'right', 'expected'], [
|
||||
([0, 1], [0, 2], """
|
||||
Full diff:
|
||||
- [0, 1]
|
||||
? ^
|
||||
+ [0, 2]
|
||||
? ^
|
||||
"""),
|
||||
({0: 1}, {0: 2}, """
|
||||
Full diff:
|
||||
- {0: 1}
|
||||
? ^
|
||||
+ {0: 2}
|
||||
? ^
|
||||
"""),
|
||||
(set([0, 1]), set([0, 2]), """
|
||||
Full diff:
|
||||
- set([0, 1])
|
||||
? ^
|
||||
+ set([0, 2])
|
||||
? ^
|
||||
""" if not PY3 else """
|
||||
Full diff:
|
||||
- {0, 1}
|
||||
? ^
|
||||
+ {0, 2}
|
||||
? ^
|
||||
""")
|
||||
]
|
||||
)
|
||||
def test_iterable_full_diff(self, left, right, expected):
|
||||
"""Test the full diff assertion failure explanation.
|
||||
|
||||
When verbose is False, then just a -v notice to get the diff is rendered,
|
||||
when verbose is True, then ndiff of the pprint is returned.
|
||||
"""
|
||||
expl = callequal(left, right, verbose=False)
|
||||
assert expl[-1] == 'Use -v to get the full diff'
|
||||
expl = '\n'.join(callequal(left, right, verbose=True))
|
||||
assert expl.endswith(textwrap.dedent(expected).strip())
|
||||
|
||||
def test_list_different_lenghts(self):
|
||||
expl = callequal([0, 1], [0, 1, 2])
|
||||
assert len(expl) > 1
|
||||
expl = callequal([0, 1, 2], [0, 1])
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_dict(self):
|
||||
expl = callequal({'a': 0}, {'a': 1})
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_dict_omitting(self):
|
||||
lines = callequal({'a': 0, 'b': 1}, {'a': 1, 'b': 1})
|
||||
assert lines[1].startswith('Omitting 1 identical item')
|
||||
assert 'Common items' not in lines
|
||||
for line in lines[1:]:
|
||||
assert 'b' not in line
|
||||
|
||||
def test_dict_omitting_verbose(self):
|
||||
lines = callequal({'a': 0, 'b': 1}, {'a': 1, 'b': 1}, verbose=True)
|
||||
assert lines[1].startswith('Common items:')
|
||||
assert 'Omitting' not in lines[1]
|
||||
assert lines[2] == "{'b': 1}"
|
||||
|
||||
def test_set(self):
|
||||
expl = callequal(set([0, 1]), set([0, 2]))
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_frozenzet(self):
|
||||
expl = callequal(frozenset([0, 1]), set([0, 2]))
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_Sequence(self):
|
||||
col = py.builtin._tryimport(
|
||||
"collections.abc",
|
||||
"collections",
|
||||
"sys")
|
||||
if not hasattr(col, "MutableSequence"):
|
||||
pytest.skip("cannot import MutableSequence")
|
||||
MutableSequence = col.MutableSequence
|
||||
|
||||
class TestSequence(MutableSequence): # works with a Sequence subclass
|
||||
def __init__(self, iterable):
|
||||
self.elements = list(iterable)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self.elements[item]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.elements)
|
||||
|
||||
def __setitem__(self, item, value):
|
||||
pass
|
||||
|
||||
def __delitem__(self, item):
|
||||
pass
|
||||
|
||||
def insert(self, item, index):
|
||||
pass
|
||||
|
||||
expl = callequal(TestSequence([0, 1]), list([0, 2]))
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_list_tuples(self):
|
||||
expl = callequal([], [(1,2)])
|
||||
assert len(expl) > 1
|
||||
expl = callequal([(1,2)], [])
|
||||
assert len(expl) > 1
|
||||
|
||||
def test_list_bad_repr(self):
|
||||
class A:
|
||||
def __repr__(self):
|
||||
raise ValueError(42)
|
||||
expl = callequal([], [A()])
|
||||
assert 'ValueError' in "".join(expl)
|
||||
expl = callequal({}, {'1': A()})
|
||||
assert 'faulty' in "".join(expl)
|
||||
|
||||
def test_one_repr_empty(self):
|
||||
"""
|
||||
the faulty empty string repr did trigger
|
||||
a unbound local error in _diff_text
|
||||
"""
|
||||
class A(str):
|
||||
def __repr__(self):
|
||||
return ''
|
||||
expl = callequal(A(), '')
|
||||
assert not expl
|
||||
|
||||
def test_repr_no_exc(self):
|
||||
expl = ' '.join(callequal('foo', 'bar'))
|
||||
assert 'raised in repr()' not in expl
|
||||
|
||||
def test_unicode(self):
|
||||
left = py.builtin._totext('£€', 'utf-8')
|
||||
right = py.builtin._totext('£', 'utf-8')
|
||||
expl = callequal(left, right)
|
||||
assert expl[0] == py.builtin._totext("'£€' == '£'", 'utf-8')
|
||||
assert expl[1] == py.builtin._totext('- £€', 'utf-8')
|
||||
assert expl[2] == py.builtin._totext('+ £', 'utf-8')
|
||||
|
||||
def test_nonascii_text(self):
|
||||
"""
|
||||
:issue: 877
|
||||
non ascii python2 str caused a UnicodeDecodeError
|
||||
"""
|
||||
class A(str):
|
||||
def __repr__(self):
|
||||
return '\xff'
|
||||
expl = callequal(A(), '1')
|
||||
assert expl
|
||||
|
||||
def test_format_nonascii_explanation(self):
|
||||
assert util.format_explanation('λ')
|
||||
|
||||
def test_mojibake(self):
|
||||
# issue 429
|
||||
left = 'e'
|
||||
right = '\xc3\xa9'
|
||||
if not isinstance(left, py.builtin.bytes):
|
||||
left = py.builtin.bytes(left, 'utf-8')
|
||||
right = py.builtin.bytes(right, 'utf-8')
|
||||
expl = callequal(left, right)
|
||||
for line in expl:
|
||||
assert isinstance(line, py.builtin.text)
|
||||
msg = py.builtin._totext('\n').join(expl)
|
||||
assert msg
|
||||
|
||||
|
||||
class TestFormatExplanation:
|
||||
|
||||
def test_special_chars_full(self, testdir):
|
||||
# Issue 453, for the bug this would raise IndexError
|
||||
testdir.makepyfile("""
|
||||
def test_foo():
|
||||
assert '\\n}' == ''
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines([
|
||||
"*AssertionError*",
|
||||
])
|
||||
|
||||
def test_fmt_simple(self):
|
||||
expl = 'assert foo'
|
||||
assert util.format_explanation(expl) == 'assert foo'
|
||||
|
||||
def test_fmt_where(self):
|
||||
expl = '\n'.join(['assert 1',
|
||||
'{1 = foo',
|
||||
'} == 2'])
|
||||
res = '\n'.join(['assert 1 == 2',
|
||||
' + where 1 = foo'])
|
||||
assert util.format_explanation(expl) == res
|
||||
|
||||
def test_fmt_and(self):
|
||||
expl = '\n'.join(['assert 1',
|
||||
'{1 = foo',
|
||||
'} == 2',
|
||||
'{2 = bar',
|
||||
'}'])
|
||||
res = '\n'.join(['assert 1 == 2',
|
||||
' + where 1 = foo',
|
||||
' + and 2 = bar'])
|
||||
assert util.format_explanation(expl) == res
|
||||
|
||||
def test_fmt_where_nested(self):
|
||||
expl = '\n'.join(['assert 1',
|
||||
'{1 = foo',
|
||||
'{foo = bar',
|
||||
'}',
|
||||
'} == 2'])
|
||||
res = '\n'.join(['assert 1 == 2',
|
||||
' + where 1 = foo',
|
||||
' + where foo = bar'])
|
||||
assert util.format_explanation(expl) == res
|
||||
|
||||
def test_fmt_newline(self):
|
||||
expl = '\n'.join(['assert "foo" == "bar"',
|
||||
'~- foo',
|
||||
'~+ bar'])
|
||||
res = '\n'.join(['assert "foo" == "bar"',
|
||||
' - foo',
|
||||
' + bar'])
|
||||
assert util.format_explanation(expl) == res
|
||||
|
||||
def test_fmt_newline_escaped(self):
|
||||
expl = '\n'.join(['assert foo == bar',
|
||||
'baz'])
|
||||
res = 'assert foo == bar\\nbaz'
|
||||
assert util.format_explanation(expl) == res
|
||||
|
||||
def test_fmt_newline_before_where(self):
|
||||
expl = '\n'.join(['the assertion message here',
|
||||
'>assert 1',
|
||||
'{1 = foo',
|
||||
'} == 2',
|
||||
'{2 = bar',
|
||||
'}'])
|
||||
res = '\n'.join(['the assertion message here',
|
||||
'assert 1 == 2',
|
||||
' + where 1 = foo',
|
||||
' + and 2 = bar'])
|
||||
assert util.format_explanation(expl) == res
|
||||
|
||||
def test_fmt_multi_newline_before_where(self):
|
||||
expl = '\n'.join(['the assertion',
|
||||
'~message here',
|
||||
'>assert 1',
|
||||
'{1 = foo',
|
||||
'} == 2',
|
||||
'{2 = bar',
|
||||
'}'])
|
||||
res = '\n'.join(['the assertion',
|
||||
' message here',
|
||||
'assert 1 == 2',
|
||||
' + where 1 = foo',
|
||||
' + and 2 = bar'])
|
||||
assert util.format_explanation(expl) == res
|
||||
|
||||
|
||||
def test_python25_compile_issue257(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_rewritten():
|
||||
assert 1 == 2
|
||||
# some comment
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines("""
|
||||
*E*assert 1 == 2*
|
||||
*1 failed*
|
||||
""")
|
||||
|
||||
def test_rewritten(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_rewritten():
|
||||
assert "@py_builtins" in globals()
|
||||
""")
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_reprcompare_notin(mock_config):
|
||||
detail = plugin.pytest_assertrepr_compare(
|
||||
mock_config, 'not in', 'foo', 'aaafoobbb')[1:]
|
||||
assert detail == ["'foo' is contained here:", ' aaafoobbb', '? +++']
|
||||
|
||||
def test_pytest_assertrepr_compare_integration(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
x = set(range(100))
|
||||
y = x.copy()
|
||||
y.remove(50)
|
||||
assert x == y
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
"*def test_hello():*",
|
||||
"*assert x == y*",
|
||||
"*E*Extra items*left*",
|
||||
"*E*50*",
|
||||
])
|
||||
|
||||
def test_sequence_comparison_uses_repr(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
x = set("hello x")
|
||||
y = set("hello y")
|
||||
assert x == y
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
"*def test_hello():*",
|
||||
"*assert x == y*",
|
||||
"*E*Extra items*left*",
|
||||
"*E*'x'*",
|
||||
"*E*Extra items*right*",
|
||||
"*E*'y'*",
|
||||
])
|
||||
|
||||
|
||||
def test_assert_compare_truncate_longmessage(monkeypatch, testdir):
|
||||
testdir.makepyfile(r"""
|
||||
def test_long():
|
||||
a = list(range(200))
|
||||
b = a[::2]
|
||||
a = '\n'.join(map(str, a))
|
||||
b = '\n'.join(map(str, b))
|
||||
assert a == b
|
||||
""")
|
||||
monkeypatch.delenv('CI', raising=False)
|
||||
|
||||
result = testdir.runpytest()
|
||||
# without -vv, truncate the message showing a few diff lines only
|
||||
result.stdout.fnmatch_lines([
|
||||
"*- 1",
|
||||
"*- 3",
|
||||
"*- 5",
|
||||
"*- 7",
|
||||
"*truncated (191 more lines)*use*-vv*",
|
||||
])
|
||||
|
||||
|
||||
result = testdir.runpytest('-vv')
|
||||
result.stdout.fnmatch_lines([
|
||||
"*- 197",
|
||||
])
|
||||
|
||||
monkeypatch.setenv('CI', '1')
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
"*- 197",
|
||||
])
|
||||
|
||||
|
||||
def test_assertrepr_loaded_per_dir(testdir):
|
||||
testdir.makepyfile(test_base=['def test_base(): assert 1 == 2'])
|
||||
a = testdir.mkdir('a')
|
||||
a_test = a.join('test_a.py')
|
||||
a_test.write('def test_a(): assert 1 == 2')
|
||||
a_conftest = a.join('conftest.py')
|
||||
a_conftest.write('def pytest_assertrepr_compare(): return ["summary a"]')
|
||||
b = testdir.mkdir('b')
|
||||
b_test = b.join('test_b.py')
|
||||
b_test.write('def test_b(): assert 1 == 2')
|
||||
b_conftest = b.join('conftest.py')
|
||||
b_conftest.write('def pytest_assertrepr_compare(): return ["summary b"]')
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
'*def test_base():*',
|
||||
'*E*assert 1 == 2*',
|
||||
'*def test_a():*',
|
||||
'*E*assert summary a*',
|
||||
'*def test_b():*',
|
||||
'*E*assert summary b*'])
|
||||
|
||||
|
||||
def test_assertion_options(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
x = 3
|
||||
assert x == 4
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
assert "3 == 4" in result.stdout.str()
|
||||
off_options = (("--no-assert",),
|
||||
("--nomagic",),
|
||||
("--no-assert", "--nomagic"),
|
||||
("--assert=plain",),
|
||||
("--assert=plain", "--no-assert"),
|
||||
("--assert=plain", "--nomagic"),
|
||||
("--assert=plain", "--no-assert", "--nomagic"))
|
||||
for opt in off_options:
|
||||
result = testdir.runpytest_subprocess(*opt)
|
||||
assert "3 == 4" not in result.stdout.str()
|
||||
|
||||
def test_old_assert_mode(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_in_old_mode():
|
||||
assert "@py_builtins" not in globals()
|
||||
""")
|
||||
result = testdir.runpytest_subprocess("--assert=reinterp")
|
||||
assert result.ret == 0
|
||||
|
||||
def test_triple_quoted_string_issue113(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
assert "" == '''
|
||||
'''""")
|
||||
result = testdir.runpytest("--fulltrace")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*1 failed*",
|
||||
])
|
||||
assert 'SyntaxError' not in result.stdout.str()
|
||||
|
||||
def test_traceback_failure(testdir):
|
||||
p1 = testdir.makepyfile("""
|
||||
def g():
|
||||
return 2
|
||||
def f(x):
|
||||
assert x == g()
|
||||
def test_onefails():
|
||||
f(3)
|
||||
""")
|
||||
result = testdir.runpytest(p1, "--tb=long")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*test_traceback_failure.py F",
|
||||
"====* FAILURES *====",
|
||||
"____*____",
|
||||
"",
|
||||
" def test_onefails():",
|
||||
"> f(3)",
|
||||
"",
|
||||
"*test_*.py:6: ",
|
||||
"_ _ _ *",
|
||||
#"",
|
||||
" def f(x):",
|
||||
"> assert x == g()",
|
||||
"E assert 3 == 2",
|
||||
"E + where 2 = g()",
|
||||
"",
|
||||
"*test_traceback_failure.py:4: AssertionError"
|
||||
])
|
||||
|
||||
result = testdir.runpytest(p1) # "auto"
|
||||
result.stdout.fnmatch_lines([
|
||||
"*test_traceback_failure.py F",
|
||||
"====* FAILURES *====",
|
||||
"____*____",
|
||||
"",
|
||||
" def test_onefails():",
|
||||
"> f(3)",
|
||||
"",
|
||||
"*test_*.py:6: ",
|
||||
"",
|
||||
" def f(x):",
|
||||
"> assert x == g()",
|
||||
"E assert 3 == 2",
|
||||
"E + where 2 = g()",
|
||||
"",
|
||||
"*test_traceback_failure.py:4: AssertionError"
|
||||
])
|
||||
|
||||
@pytest.mark.skipif("'__pypy__' in sys.builtin_module_names or sys.platform.startswith('java')" )
|
||||
def test_warn_missing(testdir):
|
||||
testdir.makepyfile("")
|
||||
result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h")
|
||||
result.stderr.fnmatch_lines([
|
||||
"*WARNING*assert statements are not executed*",
|
||||
])
|
||||
result = testdir.run(sys.executable, "-OO", "-m", "pytest", "--no-assert")
|
||||
result.stderr.fnmatch_lines([
|
||||
"*WARNING*assert statements are not executed*",
|
||||
])
|
||||
|
||||
def test_recursion_source_decode(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_something():
|
||||
pass
|
||||
""")
|
||||
testdir.makeini("""
|
||||
[pytest]
|
||||
python_files = *.py
|
||||
""")
|
||||
result = testdir.runpytest("--collect-only")
|
||||
result.stdout.fnmatch_lines("""
|
||||
<Module*>
|
||||
""")
|
||||
|
||||
def test_AssertionError_message(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
x,y = 1,2
|
||||
assert 0, (x,y)
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines("""
|
||||
*def test_hello*
|
||||
*assert 0, (x,y)*
|
||||
*AssertionError: (1, 2)*
|
||||
""")
|
||||
|
||||
@pytest.mark.skipif(PY3, reason='This bug does not exist on PY3')
|
||||
def test_set_with_unsortable_elements():
|
||||
# issue #718
|
||||
class UnsortableKey(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __lt__(self, other):
|
||||
raise RuntimeError()
|
||||
|
||||
def __repr__(self):
|
||||
return 'repr({0})'.format(self.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.name == other.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
left_set = set(UnsortableKey(str(i)) for i in range(1, 3))
|
||||
right_set = set(UnsortableKey(str(i)) for i in range(2, 4))
|
||||
expl = callequal(left_set, right_set, verbose=True)
|
||||
# skip first line because it contains the "construction" of the set, which does not have a guaranteed order
|
||||
expl = expl[1:]
|
||||
dedent = textwrap.dedent("""
|
||||
Extra items in the left set:
|
||||
repr(1)
|
||||
Extra items in the right set:
|
||||
repr(3)
|
||||
Full diff (fallback to calling repr on each item):
|
||||
- repr(1)
|
||||
repr(2)
|
||||
+ repr(3)
|
||||
""").strip()
|
||||
assert '\n'.join(expl) == dedent
|
Loading…
Add table
Add a link
Reference in a new issue