patch 9.1.0797: testing of options can be further improved

Problem:  testing of options can be further improved
Solution: split the generated option test into test_options_all.vim,
          add more test cases, save and restore values, fix use-after-free

closes: #15894

Signed-off-by: Milly <milly.ca@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/main.c b/src/main.c
index e5faaa7..ecc61f4 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1694,7 +1694,11 @@
     }
 
 #ifdef FEAT_VIMINFO
-    if (*p_viminfo != NUL)
+    if (
+# ifdef EXITFREE
+	    entered_free_all_mem == FALSE &&
+# endif
+	    *p_viminfo != NUL)
 	// Write out the registers, history, marks etc, to the viminfo file
 	write_viminfo(NULL, FALSE);
 #endif
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index 67ef641..750c194 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -232,6 +232,7 @@
 	test_normal \
 	test_number \
 	test_options \
+	test_options_all \
 	test_packadd \
 	test_partial \
 	test_paste \
@@ -492,6 +493,7 @@
 	test_normal.res \
 	test_number.res \
 	test_options.res \
+	test_options_all.res \
 	test_packadd.res \
 	test_partial.res \
 	test_paste.res \
diff --git a/src/testdir/Make_ming.mak b/src/testdir/Make_ming.mak
index 16632b3..2ffe402 100644
--- a/src/testdir/Make_ming.mak
+++ b/src/testdir/Make_ming.mak
@@ -22,7 +22,7 @@
 include Make_all.mak
 
 # Explicit dependencies.
-test_options.res test_alot.res: opt_test.vim
+test_options_all.res: opt_test.vim
 
 TEST_OUTFILES = $(SCRIPTS_TINY_OUT)
 DOSTMP = dostmp
@@ -157,7 +157,7 @@
 	$(VIMPROG) -u gui_preinit.vim -U gui_init.vim $(NO_PLUGINS) -S runtest.vim $<
 	@$(DEL) vimcmd
 
-opt_test.vim: gen_opt_test.vim ../optiondefs.h
+opt_test.vim: gen_opt_test.vim ../optiondefs.h ../../runtime/doc/options.txt
 	$(VIMPROG) -e -s -u NONE $(COMMON_ARGS) --nofork -S $^
 	@if test -f test.log; then \
 		cat test.log; \
diff --git a/src/testdir/Make_mvc.mak b/src/testdir/Make_mvc.mak
index bbfd8f0..1a54823 100644
--- a/src/testdir/Make_mvc.mak
+++ b/src/testdir/Make_mvc.mak
@@ -16,7 +16,7 @@
 !include Make_all.mak
 
 # Explicit dependencies.
-test_options.res test_alot.res: opt_test.vim
+test_options_all.res: opt_test.vim
 
 TEST_OUTFILES = $(SCRIPTS_TINY_OUT)
 DOSTMP = dostmp
@@ -151,7 +151,7 @@
 	$(VIMPROG) -u gui_preinit.vim -U gui_init.vim $(NO_PLUGINS) -S runtest.vim $*.vim
 	@del vimcmd
 
-opt_test.vim: gen_opt_test.vim ../optiondefs.h
+opt_test.vim: gen_opt_test.vim ../optiondefs.h ../../runtime/doc/options.txt
 	$(VIMPROG) -e -s -u NONE $(COMMON_ARGS) --nofork -S $**
 	@if exist test.log ( type test.log & exit /b 1 )
 
diff --git a/src/testdir/Makefile b/src/testdir/Makefile
index 66b8f1b..3b665e7 100644
--- a/src/testdir/Makefile
+++ b/src/testdir/Makefile
@@ -30,7 +30,7 @@
 include Make_all.mak
 
 # Explicit dependencies.
-test_options.res test_alot.res: opt_test.vim
+test_options_all.res: opt_test.vim
 
 .SUFFIXES: .in .out .res .vim
 
@@ -160,7 +160,7 @@
 	$(RUN_VIMTEST) -u gui_preinit.vim -U gui_init.vim $(NO_PLUGINS) -S runtest.vim $<
 	@rm vimcmd
 
-GEN_OPT_DEPS = gen_opt_test.vim ../optiondefs.h
+GEN_OPT_DEPS = gen_opt_test.vim ../optiondefs.h ../../runtime/doc/options.txt
 
 opt_test.vim: $(GEN_OPT_DEPS)
 	$(VIMPROG) -e -s -u NONE $(NO_INITS) --nofork --gui-dialog-file guidialog -S $(GEN_OPT_DEPS)
diff --git a/src/testdir/gen_opt_test.vim b/src/testdir/gen_opt_test.vim
index f308e0e..e855602 100644
--- a/src/testdir/gen_opt_test.vim
+++ b/src/testdir/gen_opt_test.vim
@@ -1,4 +1,5 @@
-" Script to generate testdir/opt_test.vim from optiondefs.h
+" Script to generate src/testdir/opt_test.vim from src/optiondefs.h and
+" runtime/doc/options.txt
 
 set cpo=&vim
 
@@ -11,19 +12,65 @@
 
 const K_KENTER = -16715
 
+" Get global-local options.
+" "key" is full-name of the option.
+" "value" is the local value to switch back to the global value.
+b options.txt
+call cursor(1, 1)
+let global_locals = {}
+while search("^'[^']*'.*\\n.*|global-local", 'W')
+  let fullname = getline('.')->matchstr("^'\\zs[^']*")
+  let global_locals[fullname] = ''
+endwhile
+call extend(global_locals, #{
+      \ scrolloff: -1,
+      \ sidescrolloff: -1,
+      \ undolevels: -12345,
+      \})
+
+" Get local-noglobal options.
+" "key" is full-name of the option.
+" "value" is no used.
+b options.txt
+call cursor(1, 1)
+let local_noglobals = {}
+while search("^'[^']*'.*\\n.*|local-noglobal", 'W')
+  let fullname = getline('.')->matchstr("^'\\zs[^']*")
+  let local_noglobals[fullname] = v:true
+endwhile
+
+" Options to skip `setglobal` tests.
+" "key" is full-name of the option.
+" "value" is the reason.
+let skip_setglobal_reasons = #{
+      \ iminsert: 'The global value is always overwritten by the local value',
+      \ imsearch: 'The global value is always overwritten by the local value',
+      \ breakindentopt:	'TODO: fix missing error handling for setglobal',
+      \ colorcolumn:	'TODO: fix missing error handling for setglobal',
+      \ conceallevel:	'TODO: fix missing error handling for setglobal',
+      \ foldcolumn:	'TODO: fix missing error handling for setglobal',
+      \ foldmethod:	'TODO: fix `setglobal fdm=` not given an error',
+      \ iskeyword:	'TODO: fix missing error handling for setglobal',
+      \ numberwidth:	'TODO: fix missing error handling for setglobal',
+      \ scrolloff:	'TODO: fix missing error handling for setglobal',
+      \ shiftwidth:	'TODO: fix missing error handling for setglobal',
+      \ sidescrolloff:	'TODO: fix missing error handling for setglobal',
+      \ tabstop:	'TODO: fix missing error handling for setglobal',
+      \ termwinkey:	'TODO: fix missing error handling for setglobal',
+      \ termwinsize:	'TODO: fix missing error handling for setglobal',
+      \ textwidth:	'TODO: fix missing error handling for setglobal',
+      \}
+
 " The terminal size is restored at the end.
-" Clear out t_WS, we don't want to resize the actual terminal.
 let script = [
       \ '" DO NOT EDIT: Generated with gen_opt_test.vim',
-      \ '" Used by test_options.vim.',
+      \ '" Used by test_options_all.vim.',
       \ '',
-      \ 'let save_columns = &columns',
-      \ 'let save_lines = &lines',
-      \ 'set t_WS=',
+      \ 'scriptencoding utf-8',
       \ ]
 
-/#define p_term
-let end = line('.')
+b optiondefs.h
+const end = search('#define p_term', 'nw')
 
 " font name that works everywhere (hopefully)
 let fontname = has('win32') ? 'fixedsys' : 'fixed'
@@ -295,6 +342,27 @@
       \ 'otherstring': [['', 'xxx'], []],
       \}
 
+" Two lists with values: values that pre- and post-processing in test.
+" Clear out t_WS: we don't want to resize the actual terminal.
+let test_prepost = {
+      \ 'browsedir': [["call mkdir('Xdir with space', 'D')"], []],
+      \ 'columns': [[
+      \		'set t_WS=',
+      \		'let save_columns = &columns'
+      \		], [
+      \		'let &columns = save_columns',
+      \		'set t_WS&'
+      \		]],
+      \ 'lines': [[
+      \		'set t_WS=',
+      \		'let save_lines = &lines'
+      \		], [
+      \		'let &lines = save_lines',
+      \		'set t_WS&'
+      \		]],
+      \ 'verbosefile': [[], ['call delete("Xfile")']],
+      \}
+
 const invalid_options = test_values->keys()
       \->filter({-> v:val !~# '^other' && !exists($"&{v:val}")})
 if !empty(invalid_options)
@@ -302,70 +370,105 @@
 endif
 
 1
-/struct vimoption options
+call search('struct vimoption options')
 while 1
-  /{"
-  if line('.') > end
+  if search('{"', 'W') > end
     break
   endif
   let line = getline('.')
-  let name = substitute(line, '.*{"\([^"]*\)".*', '\1', '')
+  let fullname = substitute(line, '.*{"\([^"]*\)".*', '\1', '')
   let shortname = substitute(line, '.*"\([^"]*\)".*', '\1', '')
 
-  if has_key(test_values, name)
-    let a = test_values[name]
-  elseif line =~ 'P_NUM'
-    let a = test_values['othernum']
-  else
-    let a = test_values['otherstring']
+  let [valid_values, invalid_values] = test_values[
+	\ has_key(test_values, fullname) ? fullname
+	\ : line =~ 'P_NUM' ? 'othernum'
+	\ : 'otherstring']
+
+  if empty(valid_values) && empty(invalid_values)
+    continue
   endif
-  if len(a[0]) > 0 || len(a[1]) > 0
-    if name == 'browsedir'
-      call add(script, 'call mkdir("Xdir with space")')
-    endif
 
-    if line =~ 'P_BOOL'
-      call add(script, 'set ' . name)
-      call add(script, 'set ' . shortname)
-      call add(script, 'set no' . name)
-      call add(script, 'set no' . shortname)
-    else
-      for val in a[0]
-	call add(script, 'set ' . name . '=' . val)
-	call add(script, 'set ' . shortname . '=' . val)
+  call add(script, $"func Test_opt_set_{fullname}()")
+  call add(script, $"if exists('+{fullname}') && execute('set!') =~# '\\n..{fullname}\\([=\\n]\\|$\\)'")
+  call add(script, $"let l:saved = [&g:{fullname}, &l:{fullname}]")
+  call add(script, 'endif')
+
+  let [pre_processing, post_processing] = get(test_prepost, fullname, [[], []])
+  let script += pre_processing
+
+  if line =~ 'P_BOOL'
+    for opt in [fullname, shortname]
+      for cmd in ['set', 'setlocal', 'setglobal']
+	call add(script, $'{cmd} {opt}')
+	call add(script, $'{cmd} no{opt}')
+	call add(script, $'{cmd} inv{opt}')
+	call add(script, $'{cmd} {opt}!')
       endfor
-
-      " setting an option can only fail when it's implemented.
-      call add(script, "if exists('+" . name . "')")
-      for val in a[1]
-	call add(script, "silent! call assert_fails('set " . name . "=" . val . "')")
-	call add(script, "silent! call assert_fails('set " . shortname . "=" . val . "')")
+    endfor
+  else  " P_NUM || P_STRING
+    " Normal tests
+    for opt in [fullname, shortname]
+      for cmd in ['set', 'setlocal', 'setglobal']
+	for val in valid_values
+	  if local_noglobals->has_key(fullname) && cmd ==# 'setglobal'
+	    " Skip `:setglobal {option}={val}` for local-noglobal option.
+	    " It has no effect.
+	    let pre = '" Skip local-noglobal: '
+	  else
+	    let pre = ''
+	  endif
+	  call add(script, $'{pre}{cmd} {opt}={val}')
+	endfor
       endfor
-      call add(script, "endif")
-    endif
+      " Testing to clear the local value and switch back to the global value.
+      if global_locals->has_key(fullname)
+	let swichback_val = global_locals[fullname]
+	call add(script, $'setlocal {opt}={swichback_val}')
+      endif
+    endfor
 
-    " cannot change 'termencoding' in GTK
-    if name != 'termencoding' || !has('gui_gtk')
-      call add(script, 'set ' . name . '&')
-      call add(script, 'set ' . shortname . '&')
-    endif
-    if name == 'browsedir'
-      call add(script, 'call delete("Xdir with space", "d")')
-    elseif name == 'verbosefile'
-      call add(script, 'call delete("Xfile")')
-    endif
-
-    if name == 'more'
-      call add(script, 'set nomore')
-    elseif name == 'lines'
-      call add(script, 'let &lines = save_lines')
-    endif
+    " Failure tests
+    " Setting an option can only fail when it's implemented.
+    call add(script, $"if exists('+{fullname}')")
+    for opt in [fullname, shortname]
+      for cmd in ['set', 'setlocal', 'setglobal']
+	for val in invalid_values
+	  if val is# global_locals->get(fullname, {}) && cmd ==# 'setlocal'
+	    " Skip setlocal switchback-value to global-local option. It will
+	    " not result in failure.
+	    let pre = '" Skip global-local: '
+	  elseif local_noglobals->has_key(fullname) && cmd ==# 'setglobal'
+	    " Skip setglobal to local-noglobal option. It will not result in
+	    " failure.
+	    let pre = '" Skip local-noglobal: '
+	  elseif skip_setglobal_reasons->has_key(fullname) && cmd ==# 'setglobal'
+	    " Skip setglobal to reasoned option. It will not result in failure.
+	    let reason = skip_setglobal_reasons[fullname]
+	    let pre = $'" Skip {reason}: '
+	  else
+	    let pre = ''
+	  endif
+	  let cmdline = $'{cmd} {opt}={val}'
+	  call add(script, $"{pre}silent! call assert_fails({string(cmdline)})")
+	endfor
+      endfor
+    endfor
+    call add(script, "endif")
   endif
+
+  " Cannot change 'termencoding' in GTK
+  if fullname != 'termencoding' || !has('gui_gtk')
+    call add(script, $'set {fullname}&')
+    call add(script, $'set {shortname}&')
+    call add(script, $"if exists('l:saved')")
+    call add(script, $"let [&g:{fullname}, &l:{fullname}] = l:saved")
+    call add(script, 'endif')
+  endif
+
+  let script += post_processing
+  call add(script, 'endfunc')
 endwhile
 
-call add(script, 'let &columns = save_columns')
-call add(script, 'let &lines = save_lines')
-
 call writefile(script, 'opt_test.vim')
 
 " Write error messages if error occurs.
@@ -381,3 +484,5 @@
 endif
 
 qa!
+
+" vim:sw=2:ts=8:noet:nolist:nosta:
diff --git a/src/testdir/test_options.vim b/src/testdir/test_options.vim
index eec71ef..cbc84a4 100644
--- a/src/testdir/test_options.vim
+++ b/src/testdir/test_options.vim
@@ -4,6 +4,8 @@
 source check.vim
 source view_util.vim
 
+scriptencoding utf-8
+
 func Test_whichwrap()
   set whichwrap=b,s
   call assert_equal('b,s', &whichwrap)
@@ -1037,15 +1039,6 @@
   call assert_equal(sort(copy(options)), options)
 endfunc
 
-func Test_set_values()
-  " opt_test.vim is generated from ../optiondefs.h using gen_opt_test.vim
-  if filereadable('opt_test.vim')
-    source opt_test.vim
-  else
-    throw 'Skipped: opt_test.vim does not exist'
-  endif
-endfunc
-
 func Test_renderoptions()
   " Only do this for Windows Vista and later, fails on Windows XP and earlier.
   " Doesn't hurt to do this on a non-Windows system.
diff --git a/src/testdir/test_options_all.vim b/src/testdir/test_options_all.vim
new file mode 100644
index 0000000..a2330ec
--- /dev/null
+++ b/src/testdir/test_options_all.vim
@@ -0,0 +1,13 @@
+" Test for options
+
+" opt_test.vim is generated from src/optiondefs.h and runtime/doc/options.txt
+" using gen_opt_test.vim
+if filereadable('opt_test.vim')
+  source opt_test.vim
+else
+  func Test_set_values()
+    throw 'Skipped: opt_test.vim does not exist'
+  endfunc
+endif
+
+" vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index e979c04..cd49f58 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    797,
+/**/
     796,
 /**/
     795,