write_spec.lua (12576B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 4 local eq, eval, clear, write_file, source, insert = 5 t.eq, n.eval, n.clear, t.write_file, n.source, n.insert 6 local pcall_err = t.pcall_err 7 local command = n.command 8 local feed_command = n.feed_command 9 local fn = n.fn 10 local api = n.api 11 local skip = t.skip 12 local is_os = t.is_os 13 local is_ci = t.is_ci 14 local read_file = t.read_file 15 16 local fname = 'Xtest-functional-ex_cmds-write' 17 local fname_bak = fname .. '~' 18 local fname_broken = fname_bak .. 'broken' 19 20 describe(':write', function() 21 local function cleanup() 22 n.rmdir('Xtest_write') 23 os.remove('Xtest_bkc_file.txt') 24 os.remove('Xtest_bkc_link.txt') 25 os.remove('Xtest_fifo') 26 os.remove(fname) 27 os.remove(fname_bak) 28 os.remove(fname_broken) 29 end 30 before_each(function() 31 clear() 32 cleanup() 33 end) 34 after_each(function() 35 cleanup() 36 end) 37 38 it('&backupcopy=auto preserves symlinks', function() 39 command('set backupcopy=auto') 40 write_file('Xtest_bkc_file.txt', 'content0') 41 if is_os('win') then 42 command('silent !mklink Xtest_bkc_link.txt Xtest_bkc_file.txt') 43 else 44 command('silent !ln -s Xtest_bkc_file.txt Xtest_bkc_link.txt') 45 end 46 if eval('v:shell_error') ~= 0 then 47 pending('Cannot create symlink') 48 end 49 source([[ 50 edit Xtest_bkc_link.txt 51 call setline(1, ['content1']) 52 write 53 ]]) 54 eq(eval("['content1']"), eval("readfile('Xtest_bkc_file.txt')")) 55 eq(eval("['content1']"), eval("readfile('Xtest_bkc_link.txt')")) 56 end) 57 58 it('&backupcopy=no replaces symlink with new file', function() 59 skip(is_ci('cirrus')) 60 command('set backupcopy=no') 61 write_file('Xtest_bkc_file.txt', 'content0') 62 if is_os('win') then 63 command('silent !mklink Xtest_bkc_link.txt Xtest_bkc_file.txt') 64 else 65 command('silent !ln -s Xtest_bkc_file.txt Xtest_bkc_link.txt') 66 end 67 if eval('v:shell_error') ~= 0 then 68 pending('Cannot create symlink') 69 end 70 source([[ 71 edit Xtest_bkc_link.txt 72 call setline(1, ['content1']) 73 write 74 ]]) 75 eq(eval("['content0']"), eval("readfile('Xtest_bkc_file.txt')")) 76 eq(eval("['content1']"), eval("readfile('Xtest_bkc_link.txt')")) 77 end) 78 79 it('appends FIFO file', function() 80 -- mkfifo creates read-only .lnk files on Windows 81 if is_os('win') or eval("executable('mkfifo')") == 0 then 82 pending('missing "mkfifo" command') 83 end 84 85 local text = 'some fifo text from write_spec' 86 assert(os.execute('mkfifo Xtest_fifo')) 87 insert(text) 88 89 -- Blocks until a consumer reads the FIFO. 90 feed_command('write >> Xtest_fifo') 91 92 -- Read the FIFO, this will unblock the :write above. 93 local fifo = assert(io.open('Xtest_fifo')) 94 eq(text .. '\n', fifo:read('*all')) 95 fifo:close() 96 end) 97 98 it('++p creates missing parent directories', function() 99 eq(0, eval("filereadable('p_opt.txt')")) 100 command('write ++p p_opt.txt') 101 eq(1, eval("filereadable('p_opt.txt')")) 102 os.remove('p_opt.txt') 103 104 eq(0, eval("filereadable('p_opt.txt')")) 105 command('write ++p ./p_opt.txt') 106 eq(1, eval("filereadable('p_opt.txt')")) 107 os.remove('p_opt.txt') 108 109 eq(0, eval("filereadable('Xtest_write/write/p_opt.txt')")) 110 command('write ++p Xtest_write/write/p_opt.txt') 111 eq(1, eval("filereadable('Xtest_write/write/p_opt.txt')")) 112 113 eq(0, eval("filereadable('Xtest_write/write2/p_opt.txt')")) 114 eq(0, eval("filereadable('Xtest_write/write2/p_opt2.txt')")) 115 eq(0, eval("filereadable('Xtest_write/write3/p_opt3.txt')")) 116 command('file Xtest_write/write2/p_opt.txt') 117 command('set modified') 118 command('sp Xtest_write/write2/p_opt2.txt') 119 command('set modified') 120 command('sp Xtest_write/write3/p_opt3.txt') 121 -- don't set p_opt3.txt modified - assert it isn't written 122 -- and that write3/ isn't created 123 command('wall ++p') 124 eq(1, eval("filereadable('Xtest_write/write2/p_opt.txt')")) 125 eq(1, eval("filereadable('Xtest_write/write2/p_opt2.txt')")) 126 eq(0, eval("filereadable('Xtest_write/write3/p_opt3.txt')")) 127 128 eq('Vim(write):E32: No file name', pcall_err(command, 'write ++p Xotherdir/')) 129 if not is_os('win') then 130 eq( 131 ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), 132 pcall_err(command, 'write ++p .') 133 ) 134 eq( 135 ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), 136 pcall_err(command, 'write ++p ./') 137 ) 138 end 139 end) 140 141 it('errors out correctly', function() 142 skip(is_ci('cirrus')) 143 command('let $HOME=""') 144 eq(fn.fnamemodify('.', ':p:h'), fn.fnamemodify('.', ':p:h:~')) 145 -- Message from check_overwrite 146 if not is_os('win') then 147 eq( 148 ('Vim(write):E17: "' .. fn.fnamemodify('.', ':p:h') .. '" is a directory'), 149 pcall_err(command, 'write .') 150 ) 151 end 152 api.nvim_set_option_value('writeany', true, {}) 153 -- Message from buf_write 154 eq('Vim(write):E502: "." is a directory', pcall_err(command, 'write .')) 155 fn.mkdir(fname_bak) 156 api.nvim_set_option_value('backupdir', '.', {}) 157 api.nvim_set_option_value('backup', true, {}) 158 write_file(fname, 'content0') 159 command('edit ' .. fname) 160 fn.setline(1, 'TTY') 161 eq("Vim(write):E510: Can't make backup file (add ! to override)", pcall_err(command, 'write')) 162 api.nvim_set_option_value('backup', false, {}) 163 fn.setfperm(fname, 'r--------') 164 eq( 165 'Vim(write):E505: "Xtest-functional-ex_cmds-write" is read-only (add ! to override)', 166 pcall_err(command, 'write') 167 ) 168 if is_os('win') then 169 eq(0, os.execute('del /q/f ' .. fname)) 170 eq(0, os.execute('rd /q/s ' .. fname_bak)) 171 else 172 eq(true, os.remove(fname)) 173 eq(true, os.remove(fname_bak)) 174 end 175 write_file(fname_bak, 'TTYX') 176 skip(is_os('win'), [[FIXME: exc_exec('write!') outputs 0 in Windows]]) 177 vim.uv.fs_symlink(fname_bak .. ('/xxxxx'):rep(20), fname) 178 eq("Vim(write):E166: Can't open linked file for writing", pcall_err(command, 'write!')) 179 end) 180 181 it('fails converting a trailing incomplete sequence', function() 182 -- From https://github.com/neovim/neovim/issues/36990, an invalid UTF-8 sequence at the end of 183 -- the file during conversion testing can overwrite the rest of the file during the real 184 -- conversion. 185 186 api.nvim_buf_set_lines(0, 0, 1, true, { 'line 1', 'line 2', 'aaabbb\235\128' }) 187 command('set noendofline nofixendofline') 188 189 eq( 190 "Vim(write):E513: Write error, conversion failed in line 3 (make 'fenc' empty to override)", 191 pcall_err(command, 'write ++enc=latin1 ' .. fname) 192 ) 193 end) 194 195 it('converts to latin1 with an invalid sequence at buffer boundary', function() 196 -- From https://github.com/neovim/neovim/issues/36990, an invalid UTF-8 sequence that falls 197 -- right at the end of the 8 KiB buffer used for encoding conversions causes subsequent data to 198 -- be overwritten. 199 200 local content = string.rep('a', 1024 * 8 - 1) .. '\251' .. string.rep('b', 20) 201 api.nvim_buf_set_lines(0, 0, 1, true, { content }) 202 command('set noendofline nofixendofline fenc=latin1') 203 command('write ' .. fname) 204 205 local tail = string.sub(read_file(fname) or '', -10) 206 eq('bbbbbbbbbb', tail) 207 end) 208 209 it('converts to CP1251 with iconv', function() 210 api.nvim_buf_set_lines( 211 0, 212 0, 213 1, 214 true, 215 { 'Привет, мир!', 'Это простой тест.' } 216 ) 217 command('write ++enc=cp1251 ++ff=unix ' .. fname) 218 219 eq( 220 '\207\240\232\226\229\242, \236\232\240!\n' 221 .. '\221\242\238 \239\240\238\241\242\238\233 \242\229\241\242.\n', 222 read_file(fname) 223 ) 224 end) 225 226 it('converts to GB18030 with iconv', function() 227 api.nvim_buf_set_lines(0, 0, 1, true, { '你好,世界!', '这是一个测试。' }) 228 command('write ++enc=gb18030 ++ff=unix ' .. fname) 229 230 eq( 231 '\196\227\186\195\163\172\202\192\189\231\163\161\n' 232 .. '\213\226\202\199\210\187\184\246\178\226\202\212\161\163\n', 233 read_file(fname) 234 ) 235 end) 236 237 it('converts to Shift_JIS with iconv', function() 238 api.nvim_buf_set_lines( 239 0, 240 0, 241 1, 242 true, 243 { 'こんにちは、世界!', 'これはテストです。' } 244 ) 245 command('write ++enc=sjis ++ff=unix ' .. fname) 246 247 eq( 248 '\130\177\130\241\130\201\130\191\130\205\129A\144\162\138E\129I\n' 249 .. '\130\177\130\234\130\205\131e\131X\131g\130\197\130\183\129B\n', 250 read_file(fname) 251 ) 252 end) 253 254 it('fails converting an illegal sequence with iconv', function() 255 api.nvim_buf_set_lines(0, 0, 1, true, { 'line 1', 'aaa\128bbb' }) 256 257 eq( 258 "Vim(write):E513: Write error, conversion failed (make 'fenc' empty to override)", 259 pcall_err(command, 'write ++enc=cp1251 ' .. fname) 260 ) 261 end) 262 263 it('handles a multi-byte sequence crossing the buffer boundary converting with iconv', function() 264 local content = string.rep('a', 1024 * 8 - 1) .. 'Дbbbbb' 265 api.nvim_buf_set_lines(0, 0, 1, true, { content }) 266 -- Skip the backup so we're testing the "checking" phase also. 267 command('set nowritebackup') 268 command('write ++enc=cp1251 ++ff=unix ' .. fname) 269 270 local expected = string.rep('a', 1024 * 8 - 1) .. '\196bbbbb\n' 271 eq(expected, read_file(fname)) 272 end) 273 end) 274 275 describe(':update', function() 276 before_each(function() 277 clear() 278 fn.mkdir('Xtest_update', 'p') 279 end) 280 281 after_each(function() 282 fn.delete('Xtest_update', 'rf') 283 end) 284 285 it('works for a new buffer', function() 286 command('edit Xtest_update/foo/bar/nonexist.taz | update ++p') 287 eq(1, eval("filereadable('Xtest_update/foo/bar/nonexist.taz')")) 288 end) 289 290 it('writes modified buffer', function() 291 command('edit Xtest_update/modified.txt') 292 command('call setline(1, "hello world")') 293 command('update') 294 eq({ 'hello world' }, fn.readfile('Xtest_update/modified.txt')) 295 end) 296 297 it('does not write unmodified existing file', function() 298 local filename = 'Xtest_update/existing.txt' 299 local fd = io.open(filename, 'w') 300 fd:write('content') 301 fd:close() 302 local mtime_before = fn.getftime(filename) 303 command('edit ' .. filename) 304 command('update') 305 eq(mtime_before, fn.getftime(filename)) 306 end) 307 308 it('creates parent directories with ++p', function() 309 command('edit Xtest_update/deep/nested/path/file.txt') 310 command('call setline(1, "content")') 311 command('update ++p') 312 eq(1, eval("filereadable('Xtest_update/deep/nested/path/file.txt')")) 313 end) 314 315 it('fails gracefully for unnamed buffer', function() 316 command('enew') 317 command('call setline(1, "some content")') 318 eq('Vim(update):E32: No file name', pcall_err(command, 'update')) 319 end) 320 321 it('respects readonly files', function() 322 local filename = 'Xtest_update/readonly.txt' 323 local fd = io.open(filename, 'w') 324 fd:write('readonly content') 325 fd:close() 326 command('edit ' .. filename) 327 command('set readonly') 328 command('call setline(1, "modified")') 329 eq( 330 "Vim(update):E45: 'readonly' option is set (add ! to override)", 331 pcall_err(command, 'update') 332 ) 333 334 command('update!') 335 eq({ 'modified' }, fn.readfile('Xtest_update/readonly.txt')) 336 end) 337 338 it('can write line ranges', function() 339 command('edit Xtest_update/range.txt') 340 command('call setline(1, ["line1", "line2", "line3", "line4"])') 341 command('2,3update!') 342 eq({ 'line2', 'line3' }, fn.readfile('Xtest_update/range.txt')) 343 end) 344 345 it('can append to existing file', function() 346 local filename = 'Xtest_update/append.txt' 347 local fd = io.open(filename, 'w') 348 fd:write('existing\n') 349 fd:close() 350 command('edit Xtest_update/new_content.txt') 351 command('call setline(1, "new content")') 352 command('update >> ' .. filename) 353 354 eq({ 'existing', 'new content' }, fn.readfile('Xtest_update/append.txt')) 355 end) 356 357 it('triggers autocmds properly', function() 358 command('autocmd BufWritePre * let g:write_pre = 1') 359 command('autocmd BufWritePost * let g:write_post = 1') 360 361 command('edit Xtest_update/autocmd.txt') 362 command('call setline(1, "trigger autocmds")') 363 command('update') 364 365 eq(1, eval('g:write_pre')) 366 eq(1, eval('g:write_post')) 367 end) 368 369 it('does not write acwrite buffer when unchanged', function() 370 command('file remote://test') 371 command('setlocal buftype=acwrite') 372 command('let g:triggered = 0 | autocmd BufWriteCmd remote://* let g:triggered = 1') 373 command('update') 374 eq(0, eval('g:triggered')) 375 command('call setline(1, ["hello"])') 376 command('update') 377 eq(1, eval('g:triggered')) 378 end) 379 end)