Merge pull request #12611 from janlazo/vim-8.0.1531

vim-patch:8.0.{1531,1544,1589,1591,1712,1745,1747},8.1.{819},8.2.{420,539,893,894,895,899,1114,1118,1169,1170,1171,1172,1173,1177,1179,1180,1181,1187,1188,1196,1198,1211,1214,1215,1222}
This commit is contained in:
Matthieu Coudron 2020-07-19 18:41:40 +02:00 committed by GitHub
commit daa5bffd93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 235 additions and 81 deletions

View File

@ -2019,11 +2019,12 @@ argidx() Number current index in the argument list
arglistid([{winnr} [, {tabnr}]]) Number argument list id
argv({nr} [, {winid}]) String {nr} entry of the argument list
argv([-1, {winid}]) List the argument list
asin({expr}) Float arc sine of {expr}
assert_beeps({cmd}) Number assert {cmd} causes a beep
assert_equal({exp}, {act} [, {msg}])
Number assert {exp} is equal to {act}
assert_equalfile({fname-one}, {fname-two})
Number assert file contents is equal
assert_equalfile({fname-one}, {fname-two} [, {msg}])
Number assert file contents are equal
assert_exception({error} [, {msg}])
Number assert {error} is in v:exception
assert_fails({cmd} [, {error}]) Number assert {cmd} fails
@ -2039,7 +2040,6 @@ assert_notmatch({pat}, {text} [, {msg}])
Number assert {pat} not matches {text}
assert_report({msg}) Number report a test failure
assert_true({actual} [, {msg}]) Number assert {actual} is true
asin({expr}) Float arc sine of {expr}
atan({expr}) Float arc tangent of {expr}
atan2({expr}, {expr}) Float arc tangent of {expr1} / {expr2}
browse({save}, {title}, {initdir}, {default})
@ -2630,7 +2630,7 @@ assert_equal({expected}, {actual}, [, {msg}])
test.vim line 12: Expected 'foo' but got 'bar' ~
*assert_equalfile()*
assert_equalfile({fname-one}, {fname-two})
assert_equalfile({fname-one}, {fname-two} [, {msg}])
When the files {fname-one} and {fname-two} do not contain
exactly the same text an error message is added to |v:errors|.
Also see |assert-return|.

View File

@ -5552,19 +5552,18 @@ void prepare_assert_error(garray_T *gap)
}
}
// Append "str" to "gap", escaping unprintable characters.
// Append "p[clen]" to "gap", escaping unprintable characters.
// Changes NL to \n, CR to \r, etc.
static void ga_concat_esc(garray_T *gap, char_u *str)
static void ga_concat_esc(garray_T *gap, const char_u *p, int clen)
FUNC_ATTR_NONNULL_ALL
{
char_u *p;
char_u buf[NUMBUFLEN];
if (str == NULL) {
ga_concat(gap, (char_u *)"NULL");
return;
}
for (p = str; *p != NUL; p++) {
if (clen > 1) {
memmove(buf, p, clen);
buf[clen] = NUL;
ga_concat(gap, buf);
} else {
switch (*p) {
case BS: ga_concat(gap, (char_u *)"\\b"); break;
case ESC: ga_concat(gap, (char_u *)"\\e"); break;
@ -5585,6 +5584,41 @@ static void ga_concat_esc(garray_T *gap, char_u *str)
}
}
// Append "str" to "gap", escaping unprintable characters.
// Changes NL to \n, CR to \r, etc.
static void ga_concat_shorten_esc(garray_T *gap, const char_u *str)
FUNC_ATTR_NONNULL_ARG(1)
{
char_u buf[NUMBUFLEN];
if (str == NULL) {
ga_concat(gap, (char_u *)"NULL");
return;
}
for (const char_u *p = str; *p != NUL; p++) {
int same_len = 1;
const char_u *s = p;
const int c = mb_ptr2char_adv(&s);
const int clen = s - p;
while (*s != NUL && c == utf_ptr2char(s)) {
same_len++;
s += clen;
}
if (same_len > 20) {
ga_concat(gap, (char_u *)"\\[");
ga_concat_esc(gap, p, clen);
ga_concat(gap, (char_u *)" occurs ");
vim_snprintf((char *)buf, NUMBUFLEN, "%d", same_len);
ga_concat(gap, buf);
ga_concat(gap, (char_u *)" times]");
p = s - 1;
} else {
ga_concat_esc(gap, p, clen);
}
}
}
// Fill "gap" with information about an assert error.
void fill_assert_error(garray_T *gap, typval_T *opt_msg_tv,
char_u *exp_str, typval_T *exp_tv,
@ -5609,10 +5643,10 @@ void fill_assert_error(garray_T *gap, typval_T *opt_msg_tv,
if (exp_str == NULL) {
tofree = (char_u *)encode_tv2string(exp_tv, NULL);
ga_concat_esc(gap, tofree);
ga_concat_shorten_esc(gap, tofree);
xfree(tofree);
} else {
ga_concat_esc(gap, exp_str);
ga_concat_shorten_esc(gap, exp_str);
}
if (atype != ASSERT_NOTEQUAL) {
@ -5624,7 +5658,7 @@ void fill_assert_error(garray_T *gap, typval_T *opt_msg_tv,
ga_concat(gap, (char_u *)" but got ");
}
tofree = (char_u *)encode_tv2string(got_tv, NULL);
ga_concat_esc(gap, tofree);
ga_concat_shorten_esc(gap, tofree);
xfree(tofree);
}
}
@ -5674,6 +5708,9 @@ int assert_equalfile(typval_T *argvars)
IObuff[0] = NUL;
FILE *const fd1 = os_fopen(fname1, READBIN);
char line1[200];
char line2[200];
ptrdiff_t lineidx = 0;
if (fd1 == NULL) {
snprintf((char *)IObuff, IOSIZE, (char *)e_notread, fname1);
} else {
@ -5682,6 +5719,7 @@ int assert_equalfile(typval_T *argvars)
fclose(fd1);
snprintf((char *)IObuff, IOSIZE, (char *)e_notread, fname2);
} else {
int64_t linecount = 1;
for (int64_t count = 0; ; count++) {
const int c1 = fgetc(fd1);
const int c2 = fgetc(fd2);
@ -5693,10 +5731,24 @@ int assert_equalfile(typval_T *argvars)
} else if (c2 == EOF) {
STRCPY(IObuff, "second file is shorter");
break;
} else if (c1 != c2) {
snprintf((char *)IObuff, IOSIZE,
"difference at byte %" PRId64, count);
break;
} else {
line1[lineidx] = c1;
line2[lineidx] = c2;
lineidx++;
if (c1 != c2) {
snprintf((char *)IObuff, IOSIZE,
"difference at byte %" PRId64 ", line %" PRId64,
count, linecount);
break;
}
}
if (c1 == NL) {
linecount++;
lineidx = 0;
} else if (lineidx + 2 == (ptrdiff_t)sizeof(line1)) {
memmove(line1, line1 + 100, lineidx - 100);
memmove(line2, line2 + 100, lineidx - 100);
lineidx -= 100;
}
}
fclose(fd1);
@ -5705,7 +5757,24 @@ int assert_equalfile(typval_T *argvars)
}
if (IObuff[0] != NUL) {
prepare_assert_error(&ga);
if (argvars[2].v_type != VAR_UNKNOWN) {
char *const tofree = encode_tv2echo(&argvars[2], NULL);
ga_concat(&ga, (char_u *)tofree);
xfree(tofree);
ga_concat(&ga, (char_u *)": ");
}
ga_concat(&ga, IObuff);
if (lineidx > 0) {
line1[lineidx] = NUL;
line2[lineidx] = NUL;
ga_concat(&ga, (char_u *)" after \"");
ga_concat(&ga, (char_u *)line1);
if (STRCMP(line1, line2) != 0) {
ga_concat(&ga, (char_u *)"\" vs \"");
ga_concat(&ga, (char_u *)line2);
}
ga_concat(&ga, (char_u *)"\"");
}
assert_error(&ga);
ga_clear(&ga);
return 1;

View File

@ -28,7 +28,7 @@ return {
asin={args=1, func="float_op_wrapper", data="&asin"}, -- WJMc
assert_beeps={args={1, 2}},
assert_equal={args={2, 3}},
assert_equalfile={args=2},
assert_equalfile={args={2, 3}},
assert_exception={args={1, 2}},
assert_fails={args={1, 3}},
assert_false={args={1, 2}},

View File

@ -415,7 +415,7 @@ static void f_assert_equal(typval_T *argvars, typval_T *rettv, FunPtr fptr)
rettv->vval.v_number = assert_equal_common(argvars, ASSERT_EQUAL);
}
// "assert_equalfile(fname-one, fname-two)" function
// "assert_equalfile(fname-one, fname-two[, msg])" function
static void f_assert_equalfile(typval_T *argvars, typval_T *rettv, FunPtr fptr)
{
rettv->vval.v_number = assert_equalfile(argvars);

View File

@ -799,10 +799,14 @@ bool tv_list_equal(list_T *const l1, list_T *const l2, const bool ic,
if (l1 == l2) {
return true;
}
if (l1 == NULL || l2 == NULL) {
if (tv_list_len(l1) != tv_list_len(l2)) {
return false;
}
if (tv_list_len(l1) != tv_list_len(l2)) {
if (tv_list_len(l1) == 0) {
// empty and NULL list are considered equal
return true;
}
if (l1 == NULL || l2 == NULL) {
return false;
}

View File

@ -1029,6 +1029,15 @@ void fast_breakcheck(void)
}
}
// Like line_breakcheck() but check 100 times less often.
void veryfast_breakcheck(void)
{
if (++breakcheck_count >= BREAKCHECK_SKIP * 100) {
breakcheck_count = 0;
os_breakcheck();
}
}
/// os_call_shell() wrapper. Handles 'verbose', :profile, and v:shell_error.
/// Invalidates cached tags.
///

View File

@ -3080,10 +3080,12 @@ void do_put(int regname, yankreg_T *reg, int dir, long count, int flags)
if (ve_flags == VE_ALL
&& (curwin->w_cursor.coladd > 0
|| endcol2 == curwin->w_cursor.col)) {
if (dir == FORWARD && c == NUL)
++col;
if (dir != FORWARD && c != NUL)
++curwin->w_cursor.col;
if (dir == FORWARD && c == NUL) {
col++;
}
if (dir != FORWARD && c != NUL && curwin->w_cursor.coladd > 0) {
curwin->w_cursor.col++;
}
if (c == TAB) {
if (dir == BACKWARD && curwin->w_cursor.col)
curwin->w_cursor.col--;

View File

@ -1134,7 +1134,6 @@ static int read_sal_section(FILE *fd, slang_T *slang)
salitem_T *smp;
int ccnt;
char_u *p;
int c = NUL;
slang->sl_sofo = false;
@ -1158,7 +1157,9 @@ static int read_sal_section(FILE *fd, slang_T *slang)
ga_grow(gap, cnt + 1);
// <sal> : <salfromlen> <salfrom> <saltolen> <salto>
for (; gap->ga_len < cnt; ++gap->ga_len) {
for (; gap->ga_len < cnt; gap->ga_len++) {
int c = NUL;
smp = &((salitem_T *)gap->ga_data)[gap->ga_len];
ccnt = getc(fd); // <salfromlen>
if (ccnt < 0)
@ -1810,7 +1811,8 @@ spell_reload_one (
#define CONDIT_SUF 4 // add a suffix for matching flags
#define CONDIT_AFF 8 // word already has an affix
// Tunable parameters for when the tree is compressed. See 'mkspellmem'.
// Tunable parameters for when the tree is compressed. Filled from the
// 'mkspellmem' option.
static long compress_start = 30000; // memory / SBLOCKSIZE
static long compress_inc = 100; // memory / SBLOCKSIZE
static long compress_added = 500000; // word count
@ -3015,6 +3017,7 @@ static int spell_read_dic(spellinfo_T *spin, char_u *fname, afffile_T *affile)
char_u message[MAXLINELEN + MAXWLEN];
int flags;
int duplicate = 0;
Timestamp last_msg_time = 0;
// Open the file.
fd = os_fopen((char *)fname, "r");
@ -3090,18 +3093,22 @@ static int spell_read_dic(spellinfo_T *spin, char_u *fname, afffile_T *affile)
continue;
}
// This takes time, print a message every 10000 words.
// This takes time, print a message every 10000 words, but not more
// often than once per second.
if (spin->si_verbose && spin->si_msg_count > 10000) {
spin->si_msg_count = 0;
vim_snprintf((char *)message, sizeof(message),
_("line %6d, word %6ld - %s"),
lnum, spin->si_foldwcount + spin->si_keepwcount, w);
msg_start();
msg_puts_long_attr(message, 0);
msg_clr_eos();
msg_didout = FALSE;
msg_col = 0;
ui_flush();
if (os_time() > last_msg_time) {
last_msg_time = os_time();
vim_snprintf((char *)message, sizeof(message),
_("line %6d, word %6ld - %s"),
lnum, spin->si_foldwcount + spin->si_keepwcount, w);
msg_start();
msg_puts_long_attr(message, 0);
msg_clr_eos();
msg_didout = false;
msg_col = 0;
ui_flush();
}
}
// Store the word in the hashtable to be able to find duplicates.
@ -3914,9 +3921,10 @@ static int tree_add_word(spellinfo_T *spin, char_u *word, wordnode_T *root, int
++spin->si_msg_count;
if (spin->si_compress_cnt > 1) {
if (--spin->si_compress_cnt == 1)
if (--spin->si_compress_cnt == 1) {
// Did enough words to lower the block count limit.
spin->si_blocks_cnt += compress_inc;
}
}
// When we have allocated lots of memory we need to compress the word tree
@ -3955,9 +3963,10 @@ static int tree_add_word(spellinfo_T *spin, char_u *word, wordnode_T *root, int
// compression useful, or one of them is small, which means
// compression goes fast. But when filling the soundfold word tree
// there is no keep-case tree.
wordtree_compress(spin, spin->si_foldroot);
if (affixID >= 0)
wordtree_compress(spin, spin->si_keeproot);
wordtree_compress(spin, spin->si_foldroot, "case-folded");
if (affixID >= 0) {
wordtree_compress(spin, spin->si_keeproot, "keep-case");
}
}
return OK;
@ -3990,6 +3999,7 @@ static wordnode_T *get_wordnode(spellinfo_T *spin)
// siblings.
// Returns the number of nodes actually freed.
static int deref_wordnode(spellinfo_T *spin, wordnode_T *node)
FUNC_ATTR_NONNULL_ALL
{
wordnode_T *np;
int cnt = 0;
@ -4009,6 +4019,7 @@ static int deref_wordnode(spellinfo_T *spin, wordnode_T *node)
// Free a wordnode_T for re-use later.
// Only the "wn_child" field becomes invalid.
static void free_wordnode(spellinfo_T *spin, wordnode_T *n)
FUNC_ATTR_NONNULL_ALL
{
n->wn_child = spin->si_first_free;
spin->si_first_free = n;
@ -4016,18 +4027,19 @@ static void free_wordnode(spellinfo_T *spin, wordnode_T *n)
}
// Compress a tree: find tails that are identical and can be shared.
static void wordtree_compress(spellinfo_T *spin, wordnode_T *root)
static void wordtree_compress(spellinfo_T *spin, wordnode_T *root,
const char *name)
FUNC_ATTR_NONNULL_ALL
{
hashtab_T ht;
int n;
int tot = 0;
int perc;
long tot = 0;
long perc;
// Skip the root itself, it's not actually used. The first sibling is the
// start of the tree.
if (root->wn_sibling != NULL) {
hash_init(&ht);
n = node_compress(spin, root->wn_sibling, &ht, &tot);
const long n = node_compress(spin, root->wn_sibling, &ht, &tot);
#ifndef SPELL_PRINTTREE
if (spin->si_verbose || p_verbose > 2)
@ -4040,8 +4052,8 @@ static void wordtree_compress(spellinfo_T *spin, wordnode_T *root)
else
perc = (tot - n) * 100 / tot;
vim_snprintf((char *)IObuff, IOSIZE,
_("Compressed %d of %d nodes; %d (%d%%) remaining"),
n, tot, tot - n, perc);
_("Compressed %s of %ld nodes; %ld (%ld%%) remaining"),
name, tot, tot - n, perc);
spell_message(spin, IObuff);
}
#ifdef SPELL_PRINTTREE
@ -4053,23 +4065,23 @@ static void wordtree_compress(spellinfo_T *spin, wordnode_T *root)
// Compress a node, its siblings and its children, depth first.
// Returns the number of compressed nodes.
static int
node_compress (
static long node_compress(
spellinfo_T *spin,
wordnode_T *node,
hashtab_T *ht,
int *tot // total count of nodes before compressing,
long *tot // total count of nodes before compressing,
// incremented while going through the tree
)
FUNC_ATTR_NONNULL_ALL
{
wordnode_T *np;
wordnode_T *tp;
wordnode_T *child;
hash_T hash;
hashitem_T *hi;
int len = 0;
long len = 0;
unsigned nr, n;
int compressed = 0;
long compressed = 0;
// Go through the list of siblings. Compress each child and then try
// finding an identical child to replace it.
@ -4142,7 +4154,7 @@ node_compress (
node->wn_u1.hashkey[5] = NUL;
// Check for CTRL-C pressed now and then.
fast_breakcheck();
veryfast_breakcheck();
return compressed;
}
@ -4749,7 +4761,7 @@ static void spell_make_sugfile(spellinfo_T *spin, char_u *wfname)
// Compress the soundfold trie.
spell_message(spin, (char_u *)_(msg_compressing));
wordtree_compress(spin, spin->si_foldroot);
wordtree_compress(spin, spin->si_foldroot, "case-folded");
// Write the .sug file.
// Make the file name by changing ".spl" to ".sug".
@ -5219,9 +5231,9 @@ mkspell (
if (!error && !got_int) {
// Combine tails in the tree.
spell_message(&spin, (char_u *)_(msg_compressing));
wordtree_compress(&spin, spin.si_foldroot);
wordtree_compress(&spin, spin.si_keeproot);
wordtree_compress(&spin, spin.si_prefroot);
wordtree_compress(&spin, spin.si_foldroot, "case-folded");
wordtree_compress(&spin, spin.si_keeproot, "keep-case");
wordtree_compress(&spin, spin.si_prefroot, "prefixes");
}
if (!error && !got_int) {
@ -5273,7 +5285,8 @@ theend:
// Display a message for spell file processing when 'verbose' is set or using
// ":mkspell". "str" can be IObuff.
static void spell_message(spellinfo_T *spin, char_u *str)
static void spell_message(const spellinfo_T *spin, char_u *str)
FUNC_ATTR_NONNULL_ALL
{
if (spin->si_verbose || p_verbose > 2) {
if (!spin->si_verbose)

View File

@ -275,7 +275,7 @@ func GetVimCommand(...)
" If using valgrind, make sure every run uses a different log file.
if cmd =~ 'valgrind.*--log-file='
let cmd = substitute(cmd, '--log-file=\(^\s*\)', '--log-file=\1.' . g:valgrind_cnt, '')
let cmd = substitute(cmd, '--log-file=\(\S*\)', '--log-file=\1.' . g:valgrind_cnt, '')
let g:valgrind_cnt += 1
endif

View File

@ -28,7 +28,18 @@ func Test_assert_equalfile()
call writefile(['1234X89'], 'Xone')
call writefile(['1234Y89'], 'Xtwo')
call assert_equal(1, assert_equalfile('Xone', 'Xtwo'))
call assert_match("difference at byte 4", v:errors[0])
call assert_match('difference at byte 4, line 1 after "1234X" vs "1234Y"', v:errors[0])
call remove(v:errors, 0)
call writefile([repeat('x', 234) .. 'X'], 'Xone')
call writefile([repeat('x', 234) .. 'Y'], 'Xtwo')
call assert_equal(1, assert_equalfile('Xone', 'Xtwo'))
let xes = repeat('x', 134)
call assert_match('difference at byte 234, line 1 after "' .. xes .. 'X" vs "' .. xes .. 'Y"', v:errors[0])
call remove(v:errors, 0)
call assert_equal(1, assert_equalfile('Xone', 'Xtwo', 'a message'))
call assert_match("a message: difference at byte 234, line 1 after", v:errors[0])
call remove(v:errors, 0)
call delete('Xone')

View File

@ -800,3 +800,25 @@ func Test_diff_closeoff()
diffoff!
enew!
endfunc
func Test_diff_and_scroll()
" this was causing an ml_get error
set ls=2
for i in range(winheight(0) * 2)
call setline(i, i < winheight(0) - 10 ? i : i + 10)
endfor
vnew
for i in range(winheight(0)*2 + 10)
call setline(i, i < winheight(0) - 10 ? 0 : i)
endfor
diffthis
wincmd p
diffthis
execute 'normal ' . winheight(0) . "\<C-d>"
bwipe!
bwipe!
set ls&
endfunc
" vim: shiftwidth=2 sts=2 expandtab

View File

@ -167,4 +167,22 @@ func Test_set_register()
enew!
endfunc
func Test_ve_blockpaste()
new
set ve=all
0put =['QWERTZ','ASDFGH']
call cursor(1,1)
exe ":norm! \<C-V>3ljdP"
call assert_equal(1, col('.'))
call assert_equal(getline(1, 2), ['QWERTZ', 'ASDFGH'])
call cursor(1,1)
exe ":norm! \<C-V>3ljd"
call cursor(1,1)
norm! $3lP
call assert_equal(5, col('.'))
call assert_equal(getline(1, 2), ['TZ QWER', 'GH ASDF'])
set ve&vim
bwipe!
endfunc
" vim: shiftwidth=2 sts=2 expandtab

View File

@ -47,10 +47,8 @@ describe('NULL', function()
-- Subjectable behaviour
-- FIXME Should return 1
null_expr_test('is equal to empty list', 'L == []', 0, 0)
-- FIXME Should return 1
null_expr_test('is equal to empty list (reverse order)', '[] == L', 0, 0)
null_expr_test('is equal to empty list', 'L == []', 0, 1)
null_expr_test('is equal to empty list (reverse order)', '[] == L', 0, 1)
-- Correct behaviour
null_expr_test('can be indexed with error message for empty list', 'L[0]',

View File

@ -38,6 +38,9 @@ describe('assert function:', function()
call assert_equal(4, n)
let l = [1, 2, 3]
call assert_equal([1, 2, 3], l)
call assert_equal(v:_null_list, v:_null_list)
call assert_equal(v:_null_list, [])
call assert_equal([], v:_null_list)
fu Func()
endfu
let F1 = function('Func')
@ -92,6 +95,11 @@ describe('assert function:', function()
call('assert_equal', 'foo', 'bar', 'testing')
expected_errors({"testing: Expected 'foo' but got 'bar'"})
end)
it('should shorten a long message', function()
call ('assert_equal', 'XxxxxxxxxxxxxxxxxxxxxxX', 'XyyyyyyyyyyyyyyyyyyyyyyyyyX')
expected_errors({"Expected 'X\\[x occurs 21 times]X' but got 'X\\[y occurs 25 times]X'"})
end)
end)
-- assert_notequal({expected}, {actual}[, {msg}])

View File

@ -1234,13 +1234,13 @@ describe('typval.c', function()
local l = list()
local l2 = list()
-- NULL lists are not equal to empty lists
eq(false, lib.tv_list_equal(l, nil, true, false))
eq(false, lib.tv_list_equal(nil, l, false, false))
eq(false, lib.tv_list_equal(nil, l, false, true))
eq(false, lib.tv_list_equal(l, nil, true, true))
-- NULL lists are equal to empty lists
eq(true, lib.tv_list_equal(l, nil, true, false))
eq(true, lib.tv_list_equal(nil, l, false, false))
eq(true, lib.tv_list_equal(nil, l, false, true))
eq(true, lib.tv_list_equal(l, nil, true, true))
-- Yet NULL lists are equal themselves
-- NULL lists are equal themselves
eq(true, lib.tv_list_equal(nil, nil, true, false))
eq(true, lib.tv_list_equal(nil, nil, false, false))
eq(true, lib.tv_list_equal(nil, nil, false, true))
@ -2648,13 +2648,13 @@ describe('typval.c', function()
local l2 = lua2typvalt(empty_list)
local nl = lua2typvalt(null_list)
-- NULL lists are not equal to empty lists
eq(false, lib.tv_equal(l, nl, true, false))
eq(false, lib.tv_equal(nl, l, false, false))
eq(false, lib.tv_equal(nl, l, false, true))
eq(false, lib.tv_equal(l, nl, true, true))
-- NULL lists are equal to empty lists
eq(true, lib.tv_equal(l, nl, true, false))
eq(true, lib.tv_equal(nl, l, false, false))
eq(true, lib.tv_equal(nl, l, false, true))
eq(true, lib.tv_equal(l, nl, true, true))
-- Yet NULL lists are equal themselves
-- NULL lists are equal themselves
eq(true, lib.tv_equal(nl, nl, true, false))
eq(true, lib.tv_equal(nl, nl, false, false))
eq(true, lib.tv_equal(nl, nl, false, true))