test/run.sh (46713 bytes)
1 #!/bin/sh 2 3 # Non-interactive tests for bm and the bm-* tools. Menus are scripted through 4 # test/fakemenu and test/fakefzf, and the clipboard and opener are stubs that 5 # write to files. 6 # usage: sh test/run.sh (SBM_SH=dash sh test/run.sh to pick the shell) 7 8 # Several groups point BOOKMARKS somewhere else inside a subshell on purpose. 9 # shellcheck disable=SC2030,SC2031 10 11 here=$(cd "$(dirname "$0")" && pwd) 12 top=$(cd "$here/.." && pwd) 13 BM="${SBM_SH:-sh} $top/bm" 14 MIGRATE="${SBM_SH:-sh} $top/bm-migrate" 15 IMPORT="${SBM_SH:-sh} $top/bm-import" 16 CHECK="${SBM_SH:-sh} $top/bm-check" 17 HTML="${SBM_SH:-sh} $top/bm-html" 18 TITLE="${SBM_SH:-sh} $top/bm-title" 19 TAB=$(printf '\t') 20 21 work=${TMPDIR:-/tmp}/sbmtest.$$ 22 mkdir -m 700 "$work" || exit 1 23 trap 'rm -rf "$work"' EXIT INT TERM 24 25 export BOOKMARKS="$work/bookmarks" 26 export USERTAGS="$work/usertags" 27 export SBM_TEST_ANSWERS="$work/answers" 28 export SBM_MENU="$here/fakemenu" 29 export SBM_COPY="$work/copy" 30 export SBM_PASTE="$work/paste" 31 export SBM_OPEN="$work/open" 32 export XDG_DATA_HOME="$work/xdg" 33 # Nothing here may touch the network or the user's git setup. 34 export SBM_FETCH=0 35 # bm starts bm-sync after each change when it is installed. This config is 36 # empty, so bm-sync cannot reach the account of the user. 37 export SBM_SYNC_CONFIG="$work/sync.conf" 38 39 cp "$here/../usertags" "$USERTAGS" 40 41 printf '#!/bin/sh\ncat > "%s"\n' "$work/clipboard" > "$SBM_COPY" 42 printf '#!/bin/sh\ncat "%s" 2>/dev/null\n' "$work/clipboard" > "$SBM_PASTE" 43 # shellcheck disable=SC2016 44 printf '#!/bin/sh\nprintf "%%s\\n" "$1" > "%s"\n' "$work/opened" > "$SBM_OPEN" 45 mkdir "$work/bin" 46 cp "$here/fakemenu" "$work/bin/dmenu" 47 chmod +x "$SBM_COPY" "$SBM_PASTE" "$SBM_OPEN" "$work/bin/dmenu" 48 49 pass=0 50 fail=0 51 52 ok () { 53 pass=$((pass + 1)) 54 printf 'ok %s\n' "$1" 55 } 56 57 notok () { 58 fail=$((fail + 1)) 59 printf 'FAIL %s\n' "$1" 60 [ -z "$2" ] || printf ' %s\n' "$2" 61 } 62 63 # eq <name> <actual> <expected> 64 eq () { 65 if [ "$2" = "$3" ]; then 66 ok "$1" 67 else 68 notok "$1" "expected [$3] got [$2]" 69 fi 70 } 71 72 # answers <line>...: script the next menu answers, one per prompt. 73 answers () { 74 : > "$SBM_TEST_ANSWERS" 75 for a in "$@"; do 76 printf '%s\n' "$a" >> "$SBM_TEST_ANSWERS" 77 done 78 } 79 80 reset () { 81 : > "$BOOKMARKS" 82 rm -f "$work/clipboard" "$work/opened" "$BOOKMARKS.bak" 83 } 84 85 # ---- add ---- 86 87 reset 88 answers 'Example site' 'sec code sec' 89 $BM --add https://example.com 2>/dev/null 90 eq 'add writes url<tab>desc<tab>sorted unique tags' \ 91 "$(cat "$BOOKMARKS")" "https://example.com${TAB}Example site${TAB}code sec" 92 93 reset 94 answers '100%s | done %d' 'lib' 95 $BM -a https://percent.example 2>/dev/null 96 eq 'add keeps % and | in the description' \ 97 "$(cut -f2 "$BOOKMARKS")" '100%s | done %d' 98 99 reset 100 printf 'https://clip.example/page\n' > "$work/clipboard" 101 answers '' 'From clipboard' 'org' 102 $BM -a 2>/dev/null 103 eq 'add with a closed url prompt adds nothing' "$(cat "$BOOKMARKS")" '' 104 105 reset 106 printf 'https://clip.example/page\n' > "$work/clipboard" 107 answers 'https://typed.example' 'Typed' 'org' 108 $BM -a 2>/dev/null 109 eq 'add takes the url from the prompt' \ 110 "$(cut -f1 "$BOOKMARKS")" 'https://typed.example' 111 112 reset 113 answers 'No tags' '' 114 $BM -a https://notags.example 2>/dev/null 115 eq 'add without tags leaves the tag field empty' \ 116 "$(cat "$BOOKMARKS")" "https://notags.example${TAB}No tags${TAB}" 117 118 # ---- duplicates ---- 119 120 reset 121 answers 'First' 'code' 122 $BM -a https://www.Example.com/docs/ 2>/dev/null 123 for dup in http://example.com/docs https://example.com/docs/ \ 124 https://WWW.EXAMPLE.COM/docs example.com/docs; do 125 answers 'Second' 'code' 126 $BM -a "$dup" 2>/dev/null 127 rc=$? 128 eq "add rejects duplicate $dup with status 3" "$rc $(wc -l < "$BOOKMARKS" | tr -d ' ')" '3 1' 129 done 130 answers 'Other path' 'code' 131 $BM -a https://example.com/Docs 2>/dev/null 132 eq 'add keeps urls differing only in path case apart' \ 133 "$(wc -l < "$BOOKMARKS" | tr -d ' ')" '2' 134 135 # ---- copy, open, list, tag filter, delete ---- 136 137 reset 138 printf '%s\n' \ 139 "https://b.example${TAB}Bee${TAB}code lib" \ 140 "https://a.example${TAB}Ay${TAB}sec" \ 141 "https://c.example${TAB}Sea${TAB}code" > "$BOOKMARKS" 142 143 answers 'Bee' 144 $BM -c 145 eq 'copy puts only the url on the clipboard' \ 146 "$(cat "$work/clipboard")" 'https://b.example' 147 148 answers '' 149 $BM -c 150 eq 'copy exits 1 when the menu is closed' "$?" '1' 151 152 answers 'Sea' 153 $BM --open 154 eq 'open hands the url to the opener' "$(cat "$work/opened")" 'https://c.example' 155 156 answers 'Ay' 157 $BM --plumb 158 eq '--plumb is an alias for --open' "$(cat "$work/opened")" 'https://a.example' 159 160 eq 'list prints sorted rows' "$($BM -l | cut -f1 | paste -sd ' ' -)" \ 161 'https://a.example https://b.example https://c.example' 162 163 eq 'list honours the tag filter' "$($BM -t code -l | cut -f1 | paste -sd ' ' -)" \ 164 'https://b.example https://c.example' 165 166 eq 'tag filter matches whole tags only' "$($BM -t cod -l)" '' 167 168 answers 'example' 'copy' 169 $BM -t sec 170 eq 'a tag filter narrows the browser' \ 171 "$(cat "$work/clipboard")" 'https://a.example' 172 173 $BM -t nosuchtag -c 2>/dev/null 174 eq 'copy with an unused tag fails' "$?" '1' 175 176 answers 'Sea' 'copy' 177 $BM 178 eq 'no arguments: pick the bookmark, then what to do' \ 179 "$(cat "$work/clipboard")" 'https://c.example' 180 181 answers 'Ay' 182 eq '--print prints the url' "$($BM --print)" 'https://a.example' 183 answers 'no such bookmark' 184 $BM -c 2>/dev/null 185 eq 'copy refuses text that is not a bookmark' "$?" '1' 186 187 answers 'Bee' 188 $BM -d >/dev/null 189 eq 'delete removes only the chosen row' "$(cut -f1 "$BOOKMARKS" | paste -sd ' ' -)" \ 190 'https://a.example https://c.example' 191 192 # ---- dmenu backend: closing the tag menu must not loop forever ---- 193 194 reset 195 answers 'Escaped' 'sec' 196 ( 197 PATH="$work/bin:$PATH" SBM_MENU=dmenu $BM -a https://dmenu.example 2>/dev/null 198 ) & 199 pid=$! 200 ( sleep 10; kill "$pid" 2>/dev/null ) & 201 watchdog=$! 202 wait "$pid" 203 kill "$watchdog" 2>/dev/null 204 eq 'dmenu tag loop ends when the menu is closed' \ 205 "$(cat "$BOOKMARKS")" "https://dmenu.example${TAB}Escaped${TAB}sec" 206 207 # ---- bm-migrate ---- 208 209 reset 210 printf '%s\n' 'https://old.example Old style | code sec' > "$BOOKMARKS" 211 eq 'bm warns about an old format file and names the tool' \ 212 "$($BM -l 2>&1 >/dev/null | grep -c 'bm-migrate')" '1' 213 214 # backups: how many backups sit beside the bookmark file. 215 backups () { 216 set -- "$BOOKMARKS".bak* 217 if [ -e "$1" ]; then echo $#; else echo 0; fi 218 } 219 220 221 old_file () { 222 rm -f "$BOOKMARKS".bak* 223 printf '%s\n' \ 224 '# my bookmarks' \ 225 '' \ 226 'https://old.example Old style | code sec' \ 227 'https://equwal.com Spenser Truex'"'"'s website.' \ 228 'https://pipe.example a | b | lib' \ 229 ' https://indented.example spaced out | org ' \ 230 'https://bare.example' \ 231 'https://100.example 100%s done \n | lib' \ 232 "https://new.example${TAB}Already TSV${TAB}org" > "$BOOKMARKS" 233 } 234 migrated=$(printf '%s\n' \ 235 '# my bookmarks' \ 236 '' \ 237 "https://old.example${TAB}Old style${TAB}code sec" \ 238 "https://equwal.com${TAB}Spenser Truex's website.${TAB}" \ 239 "https://pipe.example${TAB}a | b${TAB}lib" \ 240 "https://indented.example${TAB}spaced out${TAB}org" \ 241 "https://bare.example${TAB}${TAB}" \ 242 "https://100.example${TAB}100%s done \\n${TAB}lib" \ 243 "https://new.example${TAB}Already TSV${TAB}org") 244 245 old_file 246 original=$(cat "$BOOKMARKS") 247 eq 'bm-migrate -n prints the converted file' "$($MIGRATE -n "$BOOKMARKS")" "$migrated" 248 eq 'bm-migrate -n changes nothing' \ 249 "$(cat "$BOOKMARKS")|$(backups)" "$original|0" 250 eq 'bm-migrate - is a filter' "$($MIGRATE - < "$BOOKMARKS")" "$migrated" 251 eq 'bm-migrate reports what it did' "$($MIGRATE "$BOOKMARKS")" \ 252 "migrated 6 lines in $BOOKMARKS (original kept as $BOOKMARKS.bak)" 253 eq 'bm-migrate converts in place' "$(cat "$BOOKMARKS")" "$migrated" 254 eq 'bm-migrate keeps the original as .bak' "$(cat "$BOOKMARKS.bak")" "$original" 255 eq 'bm-migrate has nothing to do the second time' "$($MIGRATE "$BOOKMARKS")" \ 256 "nothing to migrate in $BOOKMARKS" 257 eq 'bm-migrate then leaves file and backup alone' \ 258 "$(cat "$BOOKMARKS")|$(cat "$BOOKMARKS.bak")|$(backups)" \ 259 "$migrated|$original|1" 260 261 printf '%s\n' 'https://late.example Added by hand | org' >> "$BOOKMARKS" 262 $MIGRATE "$BOOKMARKS" >/dev/null 263 eq 'bm-migrate never overwrites an earlier backup' \ 264 "$(cat "$BOOKMARKS.bak")|$(grep -c late.example "$BOOKMARKS.bak.1")" "$original|1" 265 266 old_file 267 eq 'bm-migrate defaults to the BOOKMARKS variable' "$($MIGRATE >/dev/null; cat "$BOOKMARKS")" "$migrated" 268 269 eq 'bm is quiet once the file is migrated' "$($BM -l 2>&1 >/dev/null)" '' 270 271 old_file 272 ln -s "$BOOKMARKS" "$work/link" 273 chmod 600 "$BOOKMARKS" 274 $MIGRATE "$work/link" >/dev/null 275 eq 'bm-migrate keeps a symlinked file a symlink, and its mode' \ 276 "$([ -L "$work/link" ] && echo link) $(find "$BOOKMARKS" -perm 600 | wc -l | tr -d ' ') $(grep -c "$TAB" "$BOOKMARKS")" \ 277 'link 1 7' 278 chmod 644 "$BOOKMARKS" 279 280 $MIGRATE "$work/nosuchfile" 2>/dev/null 281 eq 'bm-migrate fails on a missing file' "$?" '1' 282 $MIGRATE --bogus 2>/dev/null 283 eq 'bm-migrate rejects unknown options' "$?" '2' 284 $MIGRATE a b 2>/dev/null 285 eq 'bm-migrate takes one file' "$?" '2' 286 rm -f "$BOOKMARKS".bak* "$work/link" "$work/link".bak* 287 288 # ---- argument handling ---- 289 290 reset 291 $BM --bogus 2>/dev/null 292 eq 'unknown flag exits 2' "$?" '2' 293 294 answers 'x' 'code' 295 $BM -c -a https://two.example 2>/dev/null 296 eq 'two actions exit 2 without side effects' "$? $(cat "$BOOKMARKS")" '2 ' 297 298 $BM -t 2>/dev/null 299 eq '-t without a tag exits 2' "$?" '2' 300 301 missing= 302 for flag in --copy --open --plumb --print --add --edit --delete --list --merge --tag --sort; do 303 $BM -h | grep -q -e "$flag" || missing="$missing $flag" 304 done 305 eq 'help mentions every flag' "$missing" '' 306 307 for gone in '--import x' --check --migrate '-s x'; do 308 # shellcheck disable=SC2086 309 $BM $gone 2>/dev/null 310 eq "bm no longer does $gone itself" "$?" '2' 311 done 312 313 bad= 314 for script in bm bm-migrate bm-import bm-check bm-html bm-title bm-commit bm-watch; do 315 [ "$(sed -n 1p "$top/$script")" = '#!/bin/sh' ] || bad="$bad $script" 316 done 317 eq 'every script asks for /bin/sh' "$bad" '' 318 319 # ---- sorting and use counts ---- 320 321 seed () { 322 reset 323 rm -f "$BOOKMARKS.usage" 324 printf '%s\n' \ 325 "https://b.example${TAB}Zed${TAB}code lib" \ 326 "https://a.example${TAB}Mid${TAB}sec" \ 327 "https://c.example${TAB}Alpha${TAB}code" > "$BOOKMARKS" 328 } 329 330 # order <bm arguments>: the hosts' first letters, in listed order. 331 order () { 332 $BM "$@" -l | sed 's|https://\(.\).*|\1|' | paste -sd ' ' - 333 } 334 335 seed 336 eq 'default order without use counts is by url' "$(order)" 'a b c' 337 eq 'sort url' "$(order -S url)" 'a b c' 338 eq 'sort recent' "$(order --sort recent)" 'c a b' 339 eq 'sort desc' "$(order -S desc)" 'c a b' 340 eq 'sort tag' "$(order -S tag)" 'c b a' 341 eq 'SBM_SORT sets the default' "$(SBM_SORT=recent order)" 'c a b' 342 $BM -S bogus -l 2>/dev/null 343 eq 'unknown sort order exits 2' "$?" '2' 344 345 answers 'Alpha'; $BM -c 346 answers 'Alpha'; $BM -o 347 answers 'Zed'; $BM -c 348 eq 'sort used puts the most used first' "$(order -S used)" 'c b a' 349 eq 'use counts are kept beside the bookmarks' \ 350 "$(grep -c "^https://c.example${TAB}2${TAB}[0-9]\{10,\}\$" "$BOOKMARKS.usage")" '1' 351 answers 'Alpha'; $BM -d >/dev/null 352 eq 'delete forgets the use count' "$(grep -c 'c.example' "$BOOKMARKS.usage")" '0' 353 354 # A writable file in a directory that is not: temporary files go to $TMPDIR. 355 if [ "$(id -u)" -ne 0 ]; then 356 mkdir "$work/locked" "$work/tmp" 357 printf '%s\n' "https://keep.example${TAB}Keep${TAB}" "https://drop.example${TAB}Drop${TAB}" \ 358 > "$work/locked/bookmarks" 359 : > "$work/locked/bookmarks.usage" 360 chmod 555 "$work/locked" 361 ( 362 BOOKMARKS="$work/locked/bookmarks" TMPDIR="$work/tmp" 363 export BOOKMARKS TMPDIR 364 answers 'Drop'; $BM -d >/dev/null 365 answers 'Keep'; $BM -c 366 printf 'https://merged.example\n' | $BM -m >/dev/null 367 ) 368 eq 'a read-only directory does not stop delete, merge or use counts' \ 369 "$(cut -f1 "$work/locked/bookmarks" | paste -sd ' ' -) $(cut -f1,2 "$work/locked/bookmarks.usage")" \ 370 "https://keep.example https://merged.example https://keep.example${TAB}1" 371 eq 'and no temporary file is left behind' "$(find "$work/tmp" "$work/locked" -name '*sbm*' -o -name '*.tmp.*' | wc -l | tr -d ' ')" '0' 372 ln -s "$work/locked/bookmarks" "$work/tmp/sbm.planted" 373 # tmpfile relies on this: with noclobber, ">" refuses a name that exists, 374 # a symlink included, instead of writing through it. 375 # shellcheck disable=SC2016 376 eq 'a name planted in the temporary directory is refused, not written through' \ 377 "$( if ${SBM_SH:-sh} -c 'set -C; : > "$1"' sh "$work/tmp/sbm.planted" 2>/dev/null 378 then echo written; else echo refused; fi) $(grep -c . "$work/locked/bookmarks")" 'refused 2' 379 chmod 755 "$work/locked" 380 fi 381 382 # ---- bm-title, and add with it installed ---- 383 384 mkdir "$work/net" 385 cat > "$work/net/curl" <<'FAKE' 386 #!/bin/sh 387 printf 'curl %s\n' "$*" >> "$SBM_TEST_CURLLOG" 388 probe= head= 389 for arg in "$@"; do 390 case $arg in 391 -w) probe=1 ;; 392 -sI) head=1 ;; 393 esac 394 url=$arg 395 done 396 if [ -z "$probe" ]; then 397 case $url in 398 *oneline*) 399 # A minified page: one line, the title beyond the first 64k. 400 awk 'BEGIN { for (i = 0; i < 70000; i++) printf "x"; print "<title>too far</title>" }' 401 exit 0 402 ;; 403 *nearline*) 404 awk 'BEGIN { for (i = 0; i < 60000; i++) printf "x"; print "<title>near enough</title>" }' 405 exit 0 406 ;; 407 esac 408 printf '<html><head>\n<meta charset="utf-8"><TITLE lang="en">\n Fetched & decoded\n 'title' </Title></head><body><title>no</title>' 409 exit 0 410 fi 411 case $url in 412 *dead*) printf '404 ' ;; 413 *moved*) printf '301 https://new.example/' ;; 414 *nohead*) if [ -n "$head" ]; then printf '405 '; else printf '206 '; fi ;; 415 *gone*) printf '000 ' ;; 416 *) printf '200 ' ;; 417 esac 418 FAKE 419 chmod +x "$work/net/curl" 420 export SBM_TEST_CURLLOG="$work/curllog" 421 422 eq 'bm-title prints a decoded, squeezed title' \ 423 "$(PATH="$work/net:$PATH" SBM_FETCH=1 $TITLE https://fetch.example)" "Fetched & decoded 'title'" 424 eq 'bm-title stops reading a one-line page at 64k' \ 425 "$(PATH="$work/net:$PATH" SBM_FETCH=1 $TITLE https://oneline.example)|$(PATH="$work/net:$PATH" SBM_FETCH=1 $TITLE https://nearline.example)" \ 426 '|near enough' 427 $TITLE 2>/dev/null 428 eq 'bm-title wants exactly one url' "$?" '2' 429 430 reset 431 : > "$SBM_TEST_CURLLOG" 432 answers '<default>' 'code' 433 PATH="$top:$work/net:$PATH" SBM_FETCH=1 $BM -a https://fetch.example 2>/dev/null 434 eq 'add offers the fetched <title> as the description' \ 435 "$(cut -f2 "$BOOKMARKS")" "Fetched & decoded 'title'" 436 437 reset 438 : > "$SBM_TEST_CURLLOG" 439 answers 'Mine' 'code' 440 PATH="$top:$work/net:$PATH" SBM_FETCH=1 $BM -a https://fetch.example 2>/dev/null 441 eq 'a typed description beats the fetched title' "$(cut -f2 "$BOOKMARKS")" 'Mine' 442 443 reset 444 : > "$SBM_TEST_CURLLOG" 445 answers 'Offline' 'code' 446 PATH="$top:$work/net:$PATH" SBM_FETCH=0 $BM -a https://fetch.example 2>/dev/null 447 eq 'SBM_FETCH=0 never runs curl' "$(cat "$SBM_TEST_CURLLOG")" '' 448 449 reset 450 cp "$here/../usertags" "$USERTAGS" 451 answers 'New tag' 'code brandnew' 452 $BM -a https://newtag.example 2>/dev/null 453 answers 'Again' 'brandnew' 454 $BM -a https://newtag2.example 2>/dev/null 455 eq 'a new tag is used' "$(sed -n 1p "$BOOKMARKS" | cut -f3)" 'brandnew code' 456 eq 'a new tag is remembered once' "$(grep -c '^brandnew | $' "$USERTAGS")" '1' 457 458 reset 459 rm -f "$USERTAGS" 460 answers 'No tag file' 'first' 461 $BM -a https://notagfile.example 2>/dev/null 462 eq 'a missing tag file is created from the first new tag' \ 463 "$(cat "$USERTAGS")|$(cut -f3 "$BOOKMARKS")" 'first | |first' 464 cp "$here/../usertags" "$USERTAGS" 465 466 # ---- bm-import and bm --merge ---- 467 468 reset 469 printf '%s\n' "https://old.example${TAB}Kept${TAB}org" > "$BOOKMARKS" 470 eq 'bm-import html prints rows: entities decoded, folders as tags, web urls only' \ 471 "$($IMPORT "$here/fixtures/netscape.html")" "$(printf '%s\n' \ 472 "https://top.example/${TAB}Top level${TAB}" \ 473 "https://git.example/?a=1&b=2${TAB}Git & friends${TAB}dev-tools" \ 474 "https://sh.example/posix${TAB}POSIX sh${TAB}dev-tools shell" \ 475 "http://www.top.example${TAB}Top level again${TAB}")" 476 eq 'bm-import leaves the bookmark file alone' "$(grep -c . "$BOOKMARKS")" '1' 477 eq 'merge reports its counts' \ 478 "$($IMPORT "$here/fixtures/netscape.html" | $BM --merge)" 'added 3, skipped 1 duplicates' 479 eq 'merge appends only what is new' "$(cut -f1 "$BOOKMARKS" | paste -sd ' ' -)" \ 480 'https://old.example https://top.example/ https://git.example/?a=1&b=2 https://sh.example/posix' 481 eq 'merge teaches the tags' "$(grep -c -e '^dev-tools | $' -e '^shell | $' "$USERTAGS")" '2' 482 eq 'merging twice adds nothing' \ 483 "$($IMPORT "$here/fixtures/netscape.html" | $BM -m)" 'added 0, skipped 4 duplicates' 484 eq 'merge takes a bare list of urls' \ 485 "$(printf 'https://bare.example\n\n# note\n' | $BM -m; tail -n 1 "$BOOKMARKS")" \ 486 "$(printf 'added 1, skipped 0 duplicates\nhttps://bare.example\t\t')" 487 $IMPORT "$work/nosuchfile" 2>/dev/null 488 eq 'bm-import fails on a missing file' "$?" '1' 489 $IMPORT 2>/dev/null 490 eq 'bm-import wants one source' "$?" '2' 491 492 if command -v jq >/dev/null 2>&1; then 493 eq 'bm-import json skips the trash and non-web urls, tags by folder' \ 494 "$($IMPORT "$here/fixtures/chromium.json")" "$(printf '%s\n' \ 495 "https://bar.example/${TAB}Bar site${TAB}" \ 496 "https://blog.example/feed${TAB}A blog${TAB}news-blogs" \ 497 "https://deep.example/${TAB}Deep${TAB}news-blogs tech" \ 498 "http://www.bar.example${TAB}Bar dupe${TAB}")" 499 500 # profiles <dir> <folder>...: a browser directory with these profiles, 501 # each with the fixture as its bookmarks. 502 profiles () { 503 dir=$1 504 shift 505 for folder; do 506 mkdir -p "$dir/$folder" 507 cp "$here/fixtures/chromium.json" "$dir/$folder/Bookmarks" 508 done 509 } 510 json_rows=$(printf '%s\n' \ 511 "https://bar.example/${TAB}Bar site${TAB}" \ 512 "https://blog.example/feed${TAB}A blog${TAB}news-blogs" \ 513 "https://deep.example/${TAB}Deep${TAB}news-blogs tech" \ 514 "http://www.bar.example${TAB}Bar dupe${TAB}") 515 profiles "$work/linux/.config/BraveSoftware/Brave-Browser" Default 'Profile 1' 516 eq 'bm-import brave reads the bookmarks of every profile' \ 517 "$(HOME=$work/linux XDG_CONFIG_HOME='' LOCALAPPDATA='' $IMPORT brave)" \ 518 "$(printf '%s\n%s\n' "$json_rows" "$json_rows")" 519 profiles "$work/mac/Library/Application Support/Google/Chrome" Default 'Profile 2' 520 eq 'bm-import finds a browser in ~/Library/Application Support' \ 521 "$(HOME=$work/mac XDG_CONFIG_HOME='' LOCALAPPDATA='' $IMPORT chrome | wc -l | tr -d ' ')" '8' 522 profiles "$work/win/Microsoft/Edge/User Data" Default 523 eq 'bm-import finds a browser in LOCALAPPDATA' \ 524 "$(HOME=$work/nohome XDG_CONFIG_HOME='' LOCALAPPDATA=$work/win $IMPORT edge)" "$json_rows" 525 HOME=$work/nohome XDG_CONFIG_HOME='' LOCALAPPDATA='' $IMPORT brave 2>/dev/null 526 eq 'bm-import fails when it finds no profiles of the browser' "$?" '1' 527 eq 'bm-import --files lists the bookmark file of each profile' \ 528 "$(HOME=$work/linux XDG_CONFIG_HOME='' LOCALAPPDATA='' $IMPORT --files brave | sed "s|^$work/linux/||")" \ 529 "$(printf '%s\n' '.config/BraveSoftware/Brave-Browser/Default/Bookmarks' \ 530 '.config/BraveSoftware/Brave-Browser/Profile 1/Bookmarks')" 531 $IMPORT --files "$here/fixtures/chromium.json" 2>/dev/null 532 eq 'bm-import --files wants a browser name' "$?" '2' 533 534 # bm-watch runs bm-import and bm by name. 535 WATCH="${SBM_SH:-sh} $top/bm-watch" 536 reset 537 eq 'bm-watch -1 imports the bookmarks of every profile' \ 538 "$(PATH="$top:$PATH" HOME=$work/linux XDG_CONFIG_HOME='' LOCALAPPDATA='' $WATCH -1 brave)" \ 539 'added 3, skipped 5 duplicates' 540 eq 'bm-watch -1 again adds nothing' \ 541 "$(PATH="$top:$PATH" HOME=$work/linux XDG_CONFIG_HOME='' LOCALAPPDATA='' $WATCH -1 brave)" \ 542 'added 0, skipped 8 duplicates' 543 $WATCH -1 2>/dev/null 544 eq 'bm-watch wants a browser' "$?" '2' 545 PATH="$top:$PATH" HOME=$work/nohome XDG_CONFIG_HOME='' LOCALAPPDATA='' $WATCH -1 brave 2>/dev/null 546 eq 'bm-watch fails when it finds no profiles' "$?" '1' 547 548 reset 549 PATH="$top:$PATH" HOME=$work/linux XDG_CONFIG_HOME='' LOCALAPPDATA='' \ 550 $WATCH -i 1 brave >/dev/null 2>&1 & 551 watcher=$! 552 trap 'kill "$watcher" 2>/dev/null; rm -rf "$work"' EXIT INT TERM 553 i=0 554 while [ $i -lt 30 ] && ! grep -q deep.example "$BOOKMARKS"; do 555 sleep 1 556 i=$((i + 1)) 557 done 558 sed 's|https://deep.example/|https://new.example/|' "$here/fixtures/chromium.json" \ 559 > "$work/linux/.config/BraveSoftware/Brave-Browser/Profile 1/Bookmarks" 560 i=0 561 while [ $i -lt 30 ] && ! grep -q new.example "$BOOKMARKS"; do 562 sleep 1 563 i=$((i + 1)) 564 done 565 kill "$watcher" 2>/dev/null 566 trap 'rm -rf "$work"' EXIT INT TERM 567 eq 'bm-watch imports again when a bookmark file changes' \ 568 "$(grep -c new.example "$BOOKMARKS")" '1' 569 else 570 printf 'skip bm-import json: no jq\n' 571 fi 572 cp "$here/../usertags" "$USERTAGS" 573 574 # ---- bm-check ---- 575 576 577 reset 578 printf '%s\n' \ 579 "https://ok.example${TAB}Fine${TAB}" \ 580 "https://dead.example${TAB}Dead${TAB}" \ 581 "https://moved.example${TAB}Moved${TAB}" \ 582 "https://nohead.example${TAB}Refuses HEAD${TAB}" \ 583 "https://gone.example${TAB}No answer${TAB}" > "$BOOKMARKS" 584 out=$(PATH="$work/net:$PATH" $CHECK "$BOOKMARKS" | sort) 585 eq 'bm-check lists dead and moved links only' "$out" "$(printf '%s\n' \ 586 "000${TAB}https://gone.example" \ 587 "301${TAB}https://moved.example${TAB}https://new.example/" \ 588 "404${TAB}https://dead.example")" 589 PATH="$work/net:$PATH" $CHECK "$BOOKMARKS" >/dev/null 590 eq 'bm-check exits 1 when something is wrong' "$?" '1' 591 eq 'bm-check is a filter' \ 592 "$($BM -l | PATH="$work/net:$PATH" $CHECK - | cut -f2 | sort | paste -sd ' ' -)" \ 593 'https://dead.example https://gone.example https://moved.example' 594 eq 'bm-check takes a plain list of urls' \ 595 "$(printf 'https://dead.example\nhttps://ok.example\n' | PATH="$work/net:$PATH" $CHECK -)" \ 596 "404${TAB}https://dead.example" 597 many=$(i=0; while [ $i -lt 40 ]; do printf 'https://dead%d.example\n' $i; i=$((i + 1)); done) 598 eq 'bm-check reports every link of a long list exactly once' \ 599 "$(printf '%s\n' "$many" | PATH="$work/net:$PATH" $CHECK - | cut -f2 | sort -u | wc -l | tr -d ' ')" '40' 600 eq 'bm-check copes with an empty list' "$(: | PATH="$work/net:$PATH" $CHECK -; echo $?)" '0' 601 eq 'bm-check says nothing about healthy links' \ 602 "$(printf 'https://ok.example\n' | PATH="$work/net:$PATH" $CHECK -; echo $?)" '0' 603 604 # ---- bm-commit ---- 605 606 if command -v git >/dev/null 2>&1; then 607 export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null 608 export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t 609 repo="$work/repo" 610 git init -q "$repo" 611 mkdir "$repo/nested" 612 commits () { git -C "$repo" rev-list --count HEAD 2>/dev/null || echo 0; } 613 PATH_WAS=$PATH 614 PATH="$top:$PATH" 615 616 ( 617 BOOKMARKS="$repo/bookmarks" USERTAGS="$repo/usertags" 618 cp "$here/../usertags" "$USERTAGS" 619 answers 'In repo' 'code viagit' 620 $BM -a https://git1.example 2>/dev/null 621 answers 'In repo' 622 $BM -c 623 answers 'In repo' 624 $BM -d >/dev/null 625 ) 626 eq 'add and delete are committed in a dedicated repository' "$(commits)" '2' 627 eq 'commit messages say what happened' \ 628 "$(git -C "$repo" log --format=%s | paste -sd '|' -)" \ 629 'bm: delete https://git1.example|bm: add https://git1.example' 630 eq 'bookmarks and tags are tracked, use counts are not' \ 631 "$(git -C "$repo" ls-files | paste -sd ' ' -)" 'bookmarks usertags' 632 633 ( 634 BOOKMARKS="$repo/nested/bookmarks" 635 answers 'Nested' 'code' 636 $BM -a https://git2.example 2>/dev/null 637 ) 638 eq 'a shared repository is left alone' "$(commits)" '2' 639 ( 640 BOOKMARKS="$repo/nested/bookmarks" 641 answers 'Forced' 'code' 642 SBM_GIT=1 $BM -a https://git3.example 2>/dev/null 643 ) 644 eq 'SBM_GIT=1 commits in a shared repository' "$(commits)" '3' 645 ( 646 BOOKMARKS="$repo/bookmarks" USERTAGS="$repo/usertags" 647 answers 'Off' 'code' 648 SBM_GIT=0 $BM -a https://git4.example 2>/dev/null 649 ) 650 eq 'SBM_GIT=0 never commits' "$(commits)" '3' 651 PATH=$PATH_WAS 652 ( 653 BOOKMARKS="$repo/bookmarks" 654 answers 'Not installed' 'code' 655 $BM -a https://git5.example 2>/dev/null 656 ) 657 eq 'without bm-commit on PATH bm does not touch git' "$(commits)" '3' 658 else 659 printf 'skip git: no git\n' 660 fi 661 662 # ---- fzf backend: the browser ---- 663 664 mkdir "$work/fzfbin" 665 cp "$here/fakefzf" "$work/fzfbin/fzf" 666 chmod +x "$work/fzfbin/fzf" 667 export SBM_TEST_LOG="$work/menulog" 668 669 # fz <bm arguments>: run bm against the fake fzf. 670 fz () { 671 PATH="$work/fzfbin:$PATH" SBM_MENU=fzf $BM "$@" 672 } 673 674 seed 675 answers 'enter:Zed' 676 fz </dev/null 677 eq 'browse: ENTER opens' "$(cat "$work/opened")" 'https://b.example' 678 679 answers 'ctrl-y:Alpha' 680 fz </dev/null 681 eq 'browse: ctrl-y copies' "$(cat "$work/clipboard")" 'https://c.example' 682 683 answers '' 684 fz </dev/null 685 eq 'browse: ESC exits 1' "$?" '1' 686 687 : > "$SBM_TEST_LOG" 688 answers 'ctrl-s:' 'ctrl-s:' '' 689 fz </dev/null 690 eq 'browse: ctrl-s cycles the sort order' \ 691 "$(grep -o 'sort \[[a-z]*\]' "$SBM_TEST_LOG" | paste -sd ' ' -)" \ 692 'sort [used] sort [recent] sort [url]' 693 694 # Tags need no key of their own: fzf matches the tags column like the rest. 695 : > "$SBM_TEST_LOG" 696 answers '' 697 fz </dev/null 698 eq 'browse: no tag filter key' "$(grep -c -e 'ctrl-t' -e '\^T' "$SBM_TEST_LOG")" '0' 699 700 answers 'ctrl-d:Zed' '' 701 printf 'n\n' | fz 2>/dev/null 702 eq 'browse: ctrl-d asks first' "$(grep -c . "$BOOKMARKS")" '3' 703 answers 'ctrl-d:Zed' '' 704 printf 'y\n' | fz 2>/dev/null 705 eq 'browse: ctrl-d deletes after a yes' "$(cut -f1 "$BOOKMARKS" | paste -sd ' ' -)" \ 706 'https://a.example https://c.example' 707 708 answers 'ctrl-a:' 'fresh:' ':code' '' 709 printf 'https://added.example\nAdded here\n' | fz 2>/dev/null 710 eq 'browse: ctrl-a adds, fzf tag picker creates typed tags' \ 711 "$(grep added.example "$BOOKMARKS")" "https://added.example${TAB}Added here${TAB}code fresh" 712 713 answers 'ctrl-a:' 'enter:Mid' 714 out=$(printf 'https://a.example\n\n' | fz 2>&1) 715 eq 'browse: a refused duplicate is shown, then the list comes back' \ 716 "$(printf '%s\n' "$out" | grep -c 'already bookmarked; press ENTER') $(cat "$work/opened")" \ 717 '1 https://a.example' 718 719 rm -f "$work/opened" 720 answers 'enter:posix sh printf' 721 fz </dev/null 722 eq 'browse: text that matches nothing is searched for on the web' \ 723 "$(cat "$work/opened")" 'https://duckduckgo.com/?q=posix+sh+printf' 724 725 : > "$SBM_TEST_LOG" 726 answers 'ctrl-s:' '' 727 fz </dev/null 728 eq 'browse: the preview follows the cursor' \ 729 "$(grep -c -e "--preview=printf '%s.n%s.ntags: %s.nused: %s.n' {1} {2} {3} {4}" "$SBM_TEST_LOG")" '2' 730 731 : > "$SBM_TEST_LOG" 732 answers 'Mid' 733 fz -c </dev/null 734 eq 'fzf: rows are listed by their display column, with a live preview' \ 735 "$(grep -c -e "--with-nth=5.. --preview-window=down:5:wrap --preview=printf '%s" "$SBM_TEST_LOG")" '1' 736 : > "$SBM_TEST_LOG" 737 answers 'Mid' 738 SBM_FZF_OPTS='--height=100%' fz -c </dev/null 739 eq 'fzf: SBM_FZF_OPTS is passed along' "$(grep -c -e '--height=40% --height=100%' "$SBM_TEST_LOG")" '1' 740 cp "$here/../usertags" "$USERTAGS" 741 742 # ---- dmenu backend: its behaviour must not change ---- 743 744 # dm <bm arguments>: run bm against the fake dmenu. 745 dm () { 746 PATH="$work/bin:$PATH" SBM_MENU=dmenu $BM "$@" 747 } 748 749 seed 750 : > "$SBM_TEST_LOG" 751 answers 'Zed' 'copy' 752 dm 753 eq 'dmenu: no arguments asks for the bookmark, then what to do' \ 754 "$(cat "$work/clipboard")" 'https://b.example' 755 eq 'dmenu: the two menus' "$(sed 's/.* -p //' "$SBM_TEST_LOG" | paste -sd ' ' -)" 'bookmark: do:' 756 eq 'dmenu: is called with its usual flags' "$(sed 's/ -p .*//' "$SBM_TEST_LOG" | sort -u)" \ 757 'dmenu -i -l 15' 758 eq 'dmenu: DMENULINES is honoured' \ 759 "$(: > "$SBM_TEST_LOG"; answers 'Zed'; DMENULINES=7 dm -c; cat "$SBM_TEST_LOG")" \ 760 'dmenu -i -l 7 -p copy:' 761 762 answers 'Mid' 763 dm -o 764 eq 'dmenu: open' "$(cat "$work/opened")" 'https://a.example' 765 766 answers 'Alpha' 767 dm -d >/dev/null 768 eq 'dmenu: delete' "$(cut -f1 "$BOOKMARKS" | paste -sd ' ' -)" 'https://b.example https://a.example' 769 770 reset 771 printf 'not a url\n' > "$work/clipboard" 772 : > "$SBM_TEST_LOG" 773 answers 'https://typed-dm.example' 'Typed in dmenu' 'code' 'sec' 'typednew' 'done' 774 dm -a 2>/dev/null 775 eq 'dmenu: add loops over tags until done, typed tags included' \ 776 "$(cat "$BOOKMARKS")" "https://typed-dm.example${TAB}Typed in dmenu${TAB}code sec typednew" 777 eq 'dmenu: prompts keep their order' \ 778 "$(sed 's/.* -p //' "$SBM_TEST_LOG" | paste -sd ' ' -)" \ 779 'url: description: tags: tags: tags: tags:' 780 781 reset 782 printf 'https://selection.example\n' > "$work/clipboard" 783 answers '<default>' 'From selection' 'done' 784 dm -a 2>/dev/null 785 eq 'dmenu: the selection is offered as the url' \ 786 "$(cut -f1 "$BOOKMARKS")" 'https://selection.example' 787 cp "$here/../usertags" "$USERTAGS" 788 789 # Which menu is picked when the user has not said. 790 seed 791 both="$work/bin:$work/fzfbin" 792 : > "$SBM_TEST_LOG" 793 answers 'Zed' 'copy' 794 ( unset SBM_MENU WAYLAND_DISPLAY; PATH="$both:$PATH" DISPLAY=:0 $BM </dev/null ) 795 eq 'a display means dmenu even when fzf exists' \ 796 "$(cut -d' ' -f1 "$SBM_TEST_LOG" | sort -u)" 'dmenu' 797 798 : > "$SBM_TEST_LOG" 799 answers 'Zed' 'copy' 800 ( unset SBM_MENU DISPLAY; PATH="$both:$PATH" WAYLAND_DISPLAY=wayland-0 $BM </dev/null ) 801 eq 'Wayland means dmenu too' "$(cut -d' ' -f1 "$SBM_TEST_LOG" | sort -u)" 'dmenu' 802 803 : > "$SBM_TEST_LOG" 804 answers 'enter:Zed' 805 ( unset SBM_MENU DISPLAY WAYLAND_DISPLAY; PATH="$both:$PATH" $BM </dev/null ) 806 eq 'no display falls back to fzf' "$(cut -d' ' -f1 "$SBM_TEST_LOG" | sort -u)" 'fzf' 807 808 unset SBM_TEST_LOG 809 810 # ---- one-screen browsing without key bindings (dmenu, custom menus) ---- 811 812 seed 813 export SBM_TEST_SEEN="$work/seen" 814 815 answers 'Zed' 'open' 816 $BM 817 eq 'browse: pick, then open' "$(cat "$work/opened")" 'https://b.example' 818 819 : > "$SBM_TEST_SEEN" 820 answers 'Zed' 'copy' 821 $BM 822 eq 'browse: the second menu shows the bookmark in full, below the actions' \ 823 "$(sed -n '/^--- pick do:/,$p' "$SBM_TEST_SEEN" | sed 1d | paste -sd '|' -)" \ 824 'open|copy|edit|delete| https://b.example| Zed| tags: code lib| used: 1' 825 eq 'browse: the list starts with [add] and hides the use counts' \ 826 "$(sed -n '2,3p' "$SBM_TEST_SEEN" | paste -sd '|' -)" \ 827 "[add]|https://b.example${TAB}Zed${TAB}code lib" 828 829 rm -f "$work/opened" 830 answers 'Zed' '' 'Mid' 'open' 831 $BM 832 eq 'browse: closing the second menu goes back to the list' \ 833 "$(cat "$work/opened")" 'https://a.example' 834 835 answers 'Zed' ' tags: code lib' 'Mid' 'open' 836 $BM 837 eq 'browse: picking a detail line does nothing' "$(cat "$work/opened")" 'https://a.example' 838 839 answers 'Alpha' 'delete' '' 840 $BM 841 eq 'browse: delete comes back to the list, closing the list leaves' \ 842 "$? $(cut -f1 "$BOOKMARKS" | paste -sd ' ' -)" '1 https://b.example https://a.example' 843 844 answers '[add]' 'https://viamenu.example' 'Via menu' 'code' '' 845 $BM 2>/dev/null 846 eq 'browse: [add] adds and comes back' "$(grep -c "^https://viamenu.example${TAB}Via menu${TAB}code$" "$BOOKMARKS")" '1' 847 848 : > "$SBM_TEST_SEEN" 849 answers '[add]' 'https://www.a.example/' 'seen' '' 850 $BM 2>/dev/null 851 eq 'browse: a refused duplicate is announced in a menu' \ 852 "$(grep -c '^--- ask already bookmarked; press ENTER' "$SBM_TEST_SEEN")" '1' 853 854 : > "$SBM_TEST_SEEN" 855 answers '[add]' '' '' 856 $BM 2>/dev/null 857 eq 'browse: a cancelled add just comes back' \ 858 "$(grep -c 'already bookmarked' "$SBM_TEST_SEEN") $(grep -c '^--- pick bookmark:' "$SBM_TEST_SEEN")" '0 2' 859 unset SBM_TEST_SEEN 860 861 # ---- web search from the bookmark menu ---- 862 863 seed 864 rm -f "$work/opened" "$BOOKMARKS.usage" 865 printf '%s\n' 'w | https://en.wikipedia.org/w/index.php?search=%s' \ 866 'gh | https://github.com/search?q=%s&type=repositories' > "$work/engines" 867 export SBM_ENGINES="$work/engines" 868 869 # went <typed text> [bm arguments]: where typing that into the menu leads. 870 went () { 871 typed=$1 872 shift 873 rm -f "$work/opened" 874 answers "$typed" 875 $BM "$@" 2>/dev/null 876 cat "$work/opened" 2>/dev/null 877 } 878 879 eq 'open: a bookmark is opened' "$(went Zed -o)" 'https://b.example' 880 eq 'open: other words are searched for' "$(went 'posix sh printf' -o)" \ 881 'https://duckduckgo.com/?q=posix+sh+printf' 882 eq 'open: the query is url-encoded, byte by byte' "$(went 'c++ & más?' -o)" \ 883 'https://duckduckgo.com/?q=c%2B%2B+%26+m%C3%A1s%3F' 884 eq 'open: one word is searched for too' "$(went suckless -o)" \ 885 'https://duckduckgo.com/?q=suckless' 886 eq 'open: something with a dot is an address' "$(went 'example.org/a?b=c' -o)" \ 887 'https://example.org/a?b=c' 888 eq 'open: a url is opened as it is' "$(went 'ftp://files.example/x' -o)" 'ftp://files.example/x' 889 eq 'open: an engine keyword picks the engine' "$(went 'w dynamic menu' -o)" \ 890 'https://en.wikipedia.org/w/index.php?search=dynamic+menu' 891 eq 'open: an engine url may go on after the query' "$(went 'gh dmenu' -o)" \ 892 'https://github.com/search?q=dmenu&type=repositories' 893 eq 'open: a keyword alone is just a word' "$(went w -o)" 'https://duckduckgo.com/?q=w' 894 eq 'open: an unknown keyword is part of the query' "$(went 'zz top' -o)" \ 895 'https://duckduckgo.com/?q=zz+top' 896 eq 'open: SBM_SEARCH sets the default engine' \ 897 "$(SBM_SEARCH='https://search.example/?s=%s' went 'two words' -o)" \ 898 'https://search.example/?s=two+words' 899 eq 'open: works without an engines file' "$(SBM_ENGINES="$work/none" went 'w dynamic menu' -o)" \ 900 'https://duckduckgo.com/?q=w+dynamic+menu' 901 eq 'browse: typed text is searched for as well' "$(went 'w dmenu')" \ 902 'https://en.wikipedia.org/w/index.php?search=dmenu' 903 eq 'only bookmarks are counted as used' "$(cut -f1 "$BOOKMARKS.usage")" 'https://b.example' 904 went '' -o >/dev/null 905 eq 'open: closing the menu exits 1' "$?" '1' 906 unset SBM_ENGINES 907 908 # ---- bm-html ---- 909 910 reset 911 printf '%s\n' \ 912 "https://b.example/?a=1&b=2${TAB}B <b>bold</b> & \"quoted\"${TAB}code lib" \ 913 "https://a.example${TAB}${TAB}" \ 914 '# a comment' \ 915 "javascript:alert(1)${TAB}Bookmarklet${TAB}code" \ 916 "https://c.example/x${TAB}see${TAB}code" > "$BOOKMARKS" 917 page=$($HTML -t 'My <marks>' "$BOOKMARKS") 918 eq 'bm-html lists a bookmark under each of its tags, and the untagged' \ 919 "$(printf '%s\n' "$page" | grep -c '^<li>') $(printf '%s\n' "$page" | grep '<h2' | sed 's/.*id="\([^"]*\)".*/\1/' | paste -sd ' ' -)" \ 920 '4 code lib untagged' 921 eq 'bm-html escapes text and attributes' \ 922 "$(printf '%s\n' "$page" | grep -c '<li><a href="https://b.example/?a=1&b=2">B <b>bold</b> & "quoted"</a> <small>b.example</small>')" '2' 923 eq 'bm-html escapes the title' "$(printf '%s\n' "$page" | grep -c '<title>My <marks></title>')" '1' 924 eq 'bm-html never links to script' "$(printf '%s\n' "$page" | grep -ci 'javascript:')" '0' 925 eq 'bm-html shows the url when there is no description' \ 926 "$(printf '%s\n' "$page" | grep -c '<a href="https://a.example">https://a.example</a>')" '1' 927 eq 'bm-html sorts by description within a tag' \ 928 "$(printf '%s\n' "$page" | sed -n '/id="code"/,/<\/ul>/p' | grep -c .) $(printf '%s\n' "$page" | sed -n '/id="code"/,/<\/ul>/p' | sed -n 3p | grep -c 'B <b')" '5 1' 929 eq 'bm-html has one inline script, a filter box hidden without it, and ?q=' \ 930 "$(printf '%s\n' "$page" | grep -c '<script>') $(printf '%s\n' "$page" | grep -c '<input id=q type=search placeholder="filter 3 bookmarks" hidden>') $(printf '%s\n' "$page" | grep -c 'URLSearchParams(location.search).get("q")')" \ 931 '1 1 1' 932 eq 'bm-html loads nothing from elsewhere' "$(printf '%s\n' "$page" | grep -c -e ' src=' -e '<link' -e '@import')" '0' 933 eq 'bm-html is a filter' "$($BM -t lib -l | $HTML - | grep -c '^<li>')" '2' 934 eq 'bm-html reads the bookmark file by default, whatever stdin is' \ 935 "$($HTML </dev/null | grep -c '^<li>')" '4' 936 eq 'bm-check reads the bookmark file by default too' \ 937 "$(PATH="$work/net:$PATH" $CHECK </dev/null; echo $?)" '0' 938 $HTML --bogus 2>/dev/null </dev/null 939 eq 'bm-html rejects unknown options' "$?" '2' 940 if command -v node >/dev/null 2>&1; then 941 printf '%s\n' "$page" | sed -n '/<script>/,/<\/script>/p' | sed '1d;$d' > "$work/page.js" 942 node --check "$work/page.js" 2>/dev/null 943 eq 'bm-html: the script parses' "$?" '0' 944 fi 945 946 # ---- bm-sync ---- 947 948 # A stand-in for curl that plays an sbm-sync server. The file of the server 949 # is $srv/file. Lines in $srv/other come from another device, once. With 950 # $srv/code, the server refuses with that status. With $srv/touch, bm adds a 951 # bookmark while the request runs. Without a version, the server deletes 952 # nothing, as the real one: it keeps its file and adds the new lines. With 953 # $srv/broken, the answer is a directory, so bm-sync cannot write it. With 954 # $srv/hold, a process takes the write lock of bm while the request runs, 955 # and keeps it; its pid goes to $srv/holder. 956 srv="$work/srv" 957 mkdir "$srv" "$work/syncnet" 958 cat > "$work/syncnet/curl" <<'FAKE' 959 #!/bin/sh 960 printf '%s\n' "$*" >> "$SBM_TEST_SRV/argv" 961 out= head= data= auth= url= password= 962 while [ $# -gt 0 ]; do 963 case $1 in 964 -o) out=$2; shift ;; 965 -D) head=$2; shift ;; 966 -w|--max-time|-X) shift ;; 967 -H) case $2 in @*) auth=$(cat "${2#@}") ;; esac; shift ;; 968 --data-binary) data=${2#@}; shift ;; 969 --data-urlencode) case $2 in password@-) password=$(cat) ;; esac; shift ;; 970 -*) ;; 971 *) url=$1 ;; 972 esac 973 shift 974 done 975 answer () { printf '%s\n' "$2" > "$out"; printf '%s' "$1"; exit 0; } 976 case $url in 977 */api/login) 978 [ "$password" = 'secret pass ' ] || answer 401 'wrong email or password' 979 answer 200 tok123 ;; 980 */api/logout) 981 echo logout >> "$SBM_TEST_SRV/log" 982 exit 0 ;; 983 esac 984 [ "$auth" = 'Authorization: Bearer tok123' ] || answer 401 'not signed in' 985 [ ! -e "$SBM_TEST_SRV/code" ] || answer "$(cat "$SBM_TEST_SRV/code")" 'sync is paused' 986 base=${url#*base=} 987 printf '%s\n' "$base" >> "$SBM_TEST_SRV/bases" 988 if [ -z "$base" ]; then 989 cat "$SBM_TEST_SRV/file" "$data" 2>/dev/null | awk '!seen[$0]++' > "$SBM_TEST_SRV/union" 990 data=$SBM_TEST_SRV/union 991 fi 992 cat "$data" "$SBM_TEST_SRV/other" > "$SBM_TEST_SRV/file" 2>/dev/null 993 rm -f "$SBM_TEST_SRV/other" 994 version=v$(cksum < "$SBM_TEST_SRV/file" | cut -d' ' -f1) 995 echo "$version" >> "$SBM_TEST_SRV/versions" 996 cp "$SBM_TEST_SRV/file" "$out" 997 [ ! -e "$SBM_TEST_SRV/broken" ] || { rm -f "$out"; mkdir "$out"; } 998 printf 'HTTP/1.1 200 OK\r\nsbm-version: %s\r\n\r\n' "$version" > "$head" 999 if [ -e "$SBM_TEST_SRV/touch" ]; then 1000 rm -f "$SBM_TEST_SRV/touch" 1001 printf 'https://c.example\tC\t\n' >> "$BOOKMARKS" 1002 fi 1003 if [ -e "$SBM_TEST_SRV/hold" ]; then 1004 sleep 30 >/dev/null 2>&1 & 1005 echo $! > "$SBM_TEST_SRV/holder" 1006 mkdir "$BOOKMARKS.lock" && echo $! > "$BOOKMARKS.lock/pid" 1007 fi 1008 printf 200 1009 FAKE 1010 chmod +x "$work/syncnet/curl" 1011 export SBM_TEST_SRV="$srv" 1012 bmsync () { 1013 PATH="$work/syncnet:$PATH" ${SBM_SH:-sh} "$top/bm-sync" "$@" 1014 } 1015 1016 reset 1017 printf 'https://a.example\tA\t\n' > "$BOOKMARKS" 1018 bmsync 2>"$work/err" 1019 eq 'bm-sync without an account says how to sign in' "$?:$(grep -c 'bm-sync login' "$work/err")" '1:1' 1020 1021 printf 'me@example.org\nsecret pass \n' | bmsync login https://sync.example/ >/dev/null 2>&1 1022 eq 'bm-sync login keeps the token in a file that only you can read' \ 1023 "$(ls -l "$SBM_SYNC_CONFIG" | cut -c1-10) $(paste -sd ' ' - < "$SBM_SYNC_CONFIG")" \ 1024 '-rw------- server=https://sync.example token=tok123' 1025 eq 'the password and the token never go on the command line' \ 1026 "$(grep -c -e secret -e tok123 "$srv/argv")" '0' 1027 eq 'bm-sync login syncs at once, from no version' \ 1028 "$(cat "$srv/file"):$(sed -n 1p "$srv/bases")" "https://a.example${TAB}A${TAB}:" 1029 1030 printf 'https://b.example\tB\t\n' > "$srv/other" 1031 bmsync -q 1032 eq 'bm-sync brings the bookmarks of other devices' \ 1033 "$(cut -f1 "$BOOKMARKS" | paste -sd ' ' -)" 'https://a.example https://b.example' 1034 eq 'bm-sync sends the version of the last sync' "$(sed -n 2p "$srv/bases")" "$(sed -n 1p "$srv/versions")" 1035 1036 : > "$srv/touch" 1037 bmsync -q 1038 eq 'a bookmark added during a sync gets to the server too' \ 1039 "$(grep -c c.example "$srv/file") $(grep -c c.example "$BOOKMARKS")" '1 1' 1040 1041 echo 402 > "$srv/code" 1042 cp "$BOOKMARKS" "$work/before" 1043 bmsync -q 2>"$work/err" 1044 eq 'bm-sync shows why the server refused, and keeps the file' \ 1045 "$?:$(cat "$work/err"):$(cmp -s "$work/before" "$BOOKMARKS" && echo same)" \ 1046 '1:bm-sync: sync is paused:same' 1047 rm -f "$srv/code" 1048 1049 chmod 644 "$SBM_SYNC_CONFIG" 1050 printf 'me@example.org\nsecret pass \n' | bmsync login https://sync.example/ >/dev/null 2>&1 1051 eq 'bm-sync login makes a config file that exists already private too' \ 1052 "$(ls -l "$SBM_SYNC_CONFIG" | cut -c1-10)" '-rw-------' 1053 1054 # kept: the version that bm-sync sent last, then "kept" if the server still 1055 # has all bookmarks, then "back" if the file has all of them again. 1056 cp "$srv/file" "$work/before" 1057 kept () { 1058 printf '%s:%s:%s' "$(sed -n '$p' "$srv/bases")" \ 1059 "$(cmp -s "$work/before" "$srv/file" && echo kept)" \ 1060 "$(cmp -s "$work/before" "$BOOKMARKS" && echo back)" 1061 } 1062 : > "$BOOKMARKS" 1063 bmsync -q 1064 eq 'bm-sync sends an empty file without a version, so the server deletes nothing' \ 1065 "$(kept)" ':kept:back' 1066 rm -f "$BOOKMARKS" 1067 bmsync -q 1068 eq 'bm-sync sends a missing file without a version too' "$(kept)" ':kept:back' 1069 1070 : > "$srv/broken" 1071 bmsync -q 2>"$work/err" 1072 eq 'when bm-sync cannot write the file, it says so and forgets the version' \ 1073 "$?:$(grep -c 'cannot write' "$work/err"):$([ -e "$BOOKMARKS.sync" ] || echo none)" '1:1:none' 1074 rm -f "$srv/broken" 1075 # The write stopped after the first line. 1076 sed -n 1p "$work/before" > "$BOOKMARKS" 1077 bmsync -q 1078 eq 'after a failed write, the next sync has no version, so the server deletes nothing' \ 1079 "$(kept)" ':kept:back' 1080 1081 # While bm holds its write lock, bm-sync neither reads nor writes the file. 1082 export SBM_LOCK_WAIT=1 1083 cp "$BOOKMARKS" "$work/before" 1084 cp "$BOOKMARKS.sync" "$work/state" 1085 printf 'https://other.example\tOther\t\n' > "$srv/other" 1086 sleep 30 >/dev/null 2>&1 & 1087 holder=$! 1088 mkdir "$BOOKMARKS.lock" && echo "$holder" > "$BOOKMARKS.lock/pid" 1089 bmsync -q 2>"$work/err" 1090 rc=$? 1091 kill "$holder" 1092 rm -rf "$BOOKMARKS.lock" 1093 eq 'bm-sync does not read the file while bm holds the lock, and says why' \ 1094 "$rc:$(grep -c 'in use' "$work/err"):$(cmp -s "$work/before" "$BOOKMARKS" && echo same)" \ 1095 '1:1:same' 1096 # bm takes the lock while the request runs: bm-sync must not write. 1097 : > "$srv/hold" 1098 bmsync -q 2>"$work/err" 1099 rc=$? 1100 kill "$(cat "$srv/holder")" 1101 rm -rf "$BOOKMARKS.lock" "$srv/hold" 1102 eq 'bm-sync does not write while bm holds the lock, and keeps the old version' \ 1103 "$rc:$(cmp -s "$work/before" "$BOOKMARKS" && echo same):$(cmp -s "$work/state" "$BOOKMARKS.sync" && echo same)" \ 1104 '1:same:same' 1105 printf 'https://other.example\tOther\t\n' > "$srv/other" 1106 bmsync -q 1107 eq 'after the lock is free, the next sync brings the change of the other device' \ 1108 "$(grep -c other.example "$BOOKMARKS"):$(ls -d "$BOOKMARKS.lock" 2>/dev/null)" '1:' 1109 unset SBM_LOCK_WAIT 1110 1111 bmsync -q logout 1112 eq 'bm-sync logout forgets the token and signs out on the server' \ 1113 "$([ -e "$SBM_SYNC_CONFIG" ] || echo gone) $(cat "$srv/log")" 'gone logout' 1114 eq 'bm-sync leaves no lock behind' "$(ls -d "$BOOKMARKS.sync.lock" 2>/dev/null)" '' 1115 unset SBM_TEST_SRV 1116 1117 # bm starts bm-sync after each change. A stand-in bm-sync records the config 1118 # that it gets: it must be the one of the tests, never that of the user. 1119 mkdir "$work/stubsync" 1120 # shellcheck disable=SC2016 1121 printf '#!/bin/sh\nprintf "%%s\\n" "$SBM_SYNC_CONFIG" >> "%s"\n' "$work/stubsync.log" \ 1122 > "$work/stubsync/bm-sync" 1123 chmod +x "$work/stubsync/bm-sync" 1124 reset 1125 answers 'Stub' '' 1126 PATH="$work/stubsync:$PATH" $BM -a https://stub.example 2>/dev/null 1127 i=0 1128 while [ ! -s "$work/stubsync.log" ] && [ $i -lt 5 ]; do 1129 sleep 1 1130 i=$((i + 1)) 1131 done 1132 eq 'bm starts bm-sync with the config of the tests, never the real one' \ 1133 "$(sort -u "$work/stubsync.log")" "$work/sync.conf" 1134 1135 # ---- the write lock of the bookmark file ---- 1136 1137 # hold: a live process takes the write lock; its pid goes to $holder. 1138 hold () { 1139 sleep 30 >/dev/null 2>&1 & 1140 holder=$! 1141 mkdir "$BOOKMARKS.lock" && echo "$holder" > "$BOOKMARKS.lock/pid" 1142 } 1143 free () { 1144 kill "$holder" 2>/dev/null 1145 rm -rf "$BOOKMARKS.lock" 1146 } 1147 1148 reset 1149 # The other process adds a line after two seconds, and then frees the lock. 1150 mkdir "$BOOKMARKS.lock" 1151 ( sleep 2; printf 'https://first.example\tFirst\t\n' >> "$BOOKMARKS" 1152 rm -rf "$BOOKMARKS.lock" ) & 1153 first=$! 1154 echo "$first" > "$BOOKMARKS.lock/pid" 1155 answers 'Second' '' 1156 SBM_LOCK_WAIT=10 $BM -a https://second.example 2>/dev/null 1157 wait "$first" 1158 eq 'add waits for the lock, and adds after the change of the other process' \ 1159 "$(cut -f1 "$BOOKMARKS" | paste -sd ' ' -)" 'https://first.example https://second.example' 1160 1161 reset 1162 printf 'https://kept.example\tKept\t\n' > "$BOOKMARKS" 1163 hold 1164 answers 'New' '' 1165 SBM_LOCK_WAIT=1 $BM -a https://new.example 2>"$work/err" 1166 eq 'add stops when the lock stays, says why, and changes nothing' \ 1167 "$?:$(grep -c 'in use' "$work/err"):$(cut -f1 "$BOOKMARKS")" '1:1:https://kept.example' 1168 answers 'Kept' 1169 SBM_LOCK_WAIT=1 $BM -d >/dev/null 2>&1 1170 eq 'delete waits for the lock too' "$(cut -f1 "$BOOKMARKS")" 'https://kept.example' 1171 printf 'https://merged.example\n' | SBM_LOCK_WAIT=1 $BM -m >/dev/null 2>&1 1172 eq 'merge waits for the lock too' "$(cut -f1 "$BOOKMARKS")" 'https://kept.example' 1173 free 1174 1175 reset 1176 sh -c 'exit 0' & 1177 gone=$! 1178 wait "$gone" 1179 mkdir "$BOOKMARKS.lock" && echo "$gone" > "$BOOKMARKS.lock/pid" 1180 answers 'After' '' 1181 SBM_LOCK_WAIT=1 $BM -a https://after.example 2>/dev/null 1182 eq 'add takes over the lock of a process that is gone, and frees it' \ 1183 "$(cut -f1 "$BOOKMARKS"):$(ls -d "$BOOKMARKS.lock" 2>/dev/null)" 'https://after.example:' 1184 1185 reset 1186 printf 'https://ed.example\tEd\t\n' > "$BOOKMARKS" 1187 printf '#!/bin/sh\n[ -d "%s" ] && echo locked > "%s"\n' \ 1188 "$BOOKMARKS.lock" "$work/edit.log" > "$work/editor" 1189 chmod +x "$work/editor" 1190 answers 'Ed' 1191 VISUAL="$work/editor" $BM -e 2>/dev/null 1192 eq 'edit holds the lock while the editor runs, and frees it after' \ 1193 "$(cat "$work/edit.log" 2>/dev/null):$(ls -d "$BOOKMARKS.lock" 2>/dev/null)" 'locked:' 1194 1195 # ---- make install ---- 1196 1197 if command -v make >/dev/null 2>&1; then 1198 make -s -C "$top" install DESTDIR="$work/dest" PREFIX=/usr TOOLS='bm bm-html' >/dev/null 1199 eq 'make install installs what TOOLS names, and only that' \ 1200 "$(cd "$work/dest/usr/bin" && for f in *; do [ -x "$f" ] && printf '%s ' "$f"; done)" 'bm bm-html ' 1201 fi 1202 1203 # ---- defaults ---- 1204 1205 ( 1206 unset BOOKMARKS 1207 $BM -l 1208 ) 1209 eq 'default bookmark file is created under XDG_DATA_HOME' \ 1210 "$([ -f "$work/xdg/sbm/bookmarks" ] && echo yes)" 'yes' 1211 1212 printf '\n%d passed, %d failed\n' "$pass" "$fail" 1213 [ "$fail" -eq 0 ]