Shellfu/Bash/JAT data-driven testing mini-framework https://pagure.io/shellfu-bash-xcase

xcase.sh.skel 35KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166
  1. #!/bin/bash
  2. shellfu import jat
  3. #
  4. # Help perform series of homogenous tests
  5. #
  6. # This module aims to make it easier to build and maintain reasonable
  7. # coverage for your SUT based on various combinations od input factors.
  8. #
  9. # This is opposed to traditional "linear" style, where you repeat
  10. # (copy/paste) the same code over and over. There are multiple
  11. # benefits from this design:
  12. #
  13. # * far less code necessary,
  14. #
  15. # * more flexible (the enumeration function can employ any logic),
  16. #
  17. # * "don't run test if setup failed" logic is used, possibly saving
  18. # lot of resources,
  19. #
  20. # * forget about boring formalities such as `jat__pstart*`,
  21. #
  22. # * splitting code to functions, enables you to short-cut by
  23. # `return` keyword (=> clarity AND resource saving),
  24. #
  25. # * the test is much easier to understand (if done well).
  26. #
  27. #
  28. # =head1 HELP, MY TEST IS FAILING AND I DON'T UNDERSTAND THIS SORCERY!
  29. #
  30. # If you encounter xcase-based test and want to understand what's
  31. # happening, usually the fastest way to get the grip is:
  32. #
  33. # 1. Look at summary table near the end of test log. Alternatively
  34. # there's also 'xcase-results.yaml' attached to the test result.
  35. #
  36. # 2. Identify the case id that is interesing for you. Write it down!
  37. #
  38. # 3. Use full-text search to find the case within the test log. There can
  39. # up to 5 phases executed for the case: dummy (does not do anything),
  40. # 'setup', 'test', 'diag' and 'cleanup'.
  41. #
  42. # 4. Open the test code.
  43. #
  44. # For every phase (setup, test, diag and cleanup), there is one function
  45. # ("handler") named `xcase__test`, etc. (Actually only 'test'
  46. # is mandatory.)
  47. #
  48. # Most handlers will probably refer to output of function
  49. # xcase__id(), which is case id you have written down in
  50. # step 2. Sometimes, case ids can have form of variable assignments.
  51. # In that case handlers may refer to these variables directly.
  52. #
  53. # Remember that actual meaning of case id (or its embedded variables) is
  54. # entirely defined by test author. Should this information be unclear,
  55. # xcase can't help you, the only remaining option is to bug the test author.
  56. #
  57. # PRO TIP: Many xcase tests will be long to run, so interactive debugging
  58. # (e.g. using 1minutetip) could be quite painful. However, function that
  59. # controls *what* is executed is `xcase__enum`. You can add
  60. # arbitrary filter here (grep...) to speed up the loop!
  61. #
  62. #
  63. # =head1 GETTING STARTED
  64. #
  65. # Here's what you need to do:
  66. #
  67. # 1. Construct a list of cases. These can be simple self-explanatory
  68. # names or variable lists to represent combinations of various
  69. # input factors.
  70. #
  71. # 2. Implement function `xcase__enum()` that just
  72. # lists them, one per line.
  73. #
  74. # 3. Implement handlers:
  75. #
  76. # xcase__setup (optional)
  77. # xcase__test
  78. # xcase__diag (optional)
  79. # xcase__cleanup (optional)
  80. #
  81. # Inside these handlers, case id being currently executed can be
  82. # accessed by function `xcase__id()`.
  83. #
  84. # 4. Finally, run a single "magic" function, `xcase__run()`.
  85. # This will:
  86. #
  87. # 1. Call the enumeration function to collect case ids.
  88. #
  89. # 2. For each case id, run all implemented handlers (wrap into
  90. # phases as needed).
  91. #
  92. # 3. Add nice report at the end.
  93. #
  94. #
  95. # =head1 EXAMPLE
  96. #
  97. # xcase__enum() {
  98. # echo none
  99. # echo empty
  100. # echo small
  101. # echo normal
  102. # echo huge
  103. # }
  104. #
  105. # xcase__setup() {
  106. # jat__cmd mkdir /var/ftp || return 1
  107. # case $(xcase__id) in
  108. # small|normal|huge|empty) jat__cmd cp "$(xcase__id)" /var/ftp || return 1;;
  109. # none) true ;;
  110. # esac
  111. # jat__cmd useradd joe || return 1
  112. # jat__cmd rlServiceRestart hypothetical_ftp || return 1
  113. # }
  114. #
  115. # xcase__test() {
  116. # local file=$(xcase__id)
  117. # jat__cmd su -c 'hypo_ftp localhost 25 <<<"get $file"' - joe
  118. # case $(xcase__id) in
  119. # small|normal|huge|empty) jat__cmd diff "/var/ftp/$file" "/home/joe/$file" ;;
  120. # none) jat__cmd -S 1 test -e "/home/joe/$file";;
  121. # esac
  122. # }
  123. #
  124. # xcase__cleanup() {
  125. # jat__cmd rm -rf /var/ftp
  126. # jat__cmd userdel joe
  127. # jat__cmd rlServiceRestart hypothetical_ftp
  128. # }
  129. #
  130. # shellfu import xcase
  131. #
  132. # jat__pstarts
  133. # # some general setup
  134. # jat__pend
  135. #
  136. # xcase__run
  137. #
  138. # jat__pstartc
  139. # # some general cleanup
  140. # jat__pend
  141. #
  142. # Notice that the same test without xcase would probably require repeating all
  143. # the setup/test/cleanup code for each of the file size; that would obviously
  144. # make test harder to understand and extend.
  145. #
  146. # Also we were able to make the setup much more responsive to cases when
  147. # something is terribly wrong: the setup will now stop (causing test
  148. # to be skipped) if any of the commands fail.
  149. #
  150. # For more practical examples, contact author of this module.
  151. #
  152. #
  153. # =head1 ADVANCED USAGE
  154. #
  155. #
  156. # =head2 Variable auto-setting
  157. #
  158. # Since version 0.11, xcase can parse and set variables automatically from
  159. # the case id. For example, if case ids are:
  160. #
  161. # size=m,color=red
  162. # size=l,color=red
  163. # size=xl,color=red
  164. # size=m,color=blue
  165. # ...
  166. #
  167. # xcase will, inside each handler, set variables `$size` amnd `$color` to
  168. # respective value. This means you can directly do things like:
  169. #
  170. # xcase__setup() {
  171. # case $color in
  172. # red) echo "color=#f00" ;;
  173. # green) echo "color=#0f0" ;;
  174. # blue) echo "color=#00f" ;;
  175. # esac > /etc/foo.conf
  176. # }
  177. #
  178. # aiming for even better readability of code and test logs.
  179. #
  180. #
  181. # =head2 Permutation helpers
  182. #
  183. # The color/size example above is pretty small, but if you have
  184. # more variables, doing permutations like that can become tedious.
  185. #
  186. # xcase__permute() and xcase__per() are
  187. # here to help you generate such matrix instead of writing it
  188. # yourself or having couple of nested `for` cycles in each of your
  189. # enumerators.
  190. #
  191. # With help of these functions we could rewrite the example as:
  192. #
  193. # xcase__enum() {
  194. # xcase__permute size m l xl \
  195. # | xcase__per color red green blue
  196. # }
  197. #
  198. # Now, xcase__permute() creates list of name=value
  199. # pairs, one per line. xcase__per() takes each line
  200. # and prints 'red', 'green' and 'blue' version of it, appending
  201. # comma and its own name=value pair.
  202. #
  203. #
  204. # Variables treated as arrays
  205. #
  206. # This variable can be set to a comma-delimited list of variable
  207. # names. Each variable named here is then guaranteed to be declared
  208. # as Bash array. In case ID, this variable can then list its members
  209. # delimited by '+' (plus sign).
  210. #
  211. # For example, if $XCASE__ARRAYS is set to 'foo,bar,baz',
  212. # then following case id:
  213. #
  214. # foo=foo1+foo2,bar=bar1,baz=
  215. #
  216. # will set $foo to two-member array, $bar to a single member array, and
  217. # $baz to an empty array.
  218. #
  219. # Note: it's currently not possible to create array with a single item
  220. # of an empty string, i.e. `foo=("")`. `foo=` means empty array and
  221. # `foo=+` means array with two empty strings.
  222. #
  223. XCASE__ARRAYS=${XCASE__ARRAYS:-}
  224. xcase__id_error() {
  225. #
  226. # Raise test failure due to value of $1.. or whole case id
  227. #
  228. # Usage:
  229. # xcase__id_error [VARNAME]..
  230. #
  231. # This convenience function can be used when you detect unknown
  232. # value of xcase__id() or, in variable auto-setting
  233. # mode, a part of it.
  234. #
  235. #
  236. # For example, instead of:
  237. #
  238. # case $(xcase__id) in
  239. # foo) do_something ;;
  240. # bar) do_something_else ;;
  241. # esac
  242. #
  243. # always remember to account for unknown id:
  244. #
  245. # case $(xcase__id) in
  246. # foo) do_something ;;
  247. # bar) do_something_else ;;
  248. # *) xcase__id_error ;;
  249. # esac
  250. #
  251. # This will ensure that if your xcase__enum() emits
  252. # an unknown value (perhaps when you added a case but forgot to
  253. # account for it), the incident will not go unnoticed.
  254. #
  255. # In variable auto-setting mode, you can create more useful error
  256. # message by passing name of the actual variable that got unknown
  257. # value:
  258. #
  259. # xcase__enum() {
  260. # echo Foo=bar
  261. # echo Foo=baz
  262. # echo Foo=quux
  263. # }
  264. #
  265. # # ...later in eg. xcase__setup()
  266. #
  267. # case $Foo in
  268. # bar) do_something ;;
  269. # baz) do_something_else ;;
  270. # *) xcase__id_error Foo ;;
  271. # esac
  272. #
  273. # You can actually name more variables if you are not sure which
  274. # was wrong:
  275. #
  276. # case $Foo:$Bar in
  277. # off:off) something 0 0 ;;
  278. # on:off) something 0 1 ;;
  279. # off:on) something 1 0 ;;
  280. # on:on) something 1 1 ;;
  281. # *) xcase__id_error Foo Bar ;;
  282. # esac
  283. #
  284. #
  285. local var
  286. case $# in
  287. 0)
  288. jat__log_error "unhandled case id value: $(xcase__id)"
  289. ;;
  290. 1)
  291. var=$1
  292. jat__log_error "unhandled case variable: $var=${!var}"
  293. ;;
  294. *)
  295. jat__log_error "unhandled case variable, one of following:"
  296. for var in "$@";
  297. do
  298. jat__log_error " $var='${!var}'"
  299. done
  300. ;;
  301. esac
  302. jat__log_error "update xcase__enum() or ${FUNCNAME[1]}()"
  303. }
  304. xcase__id() {
  305. #
  306. # Print current case ID
  307. #
  308. # Inside handler, this function will output current case id as
  309. # given by enumerator.
  310. #
  311. echo "$__xcase__id"
  312. }
  313. xcase__per() {
  314. #
  315. # Permute each line on stdin with variable named $1 at values $2..
  316. #
  317. # Usage:
  318. #
  319. # xcase__per NAME [--] VALUE1 [VALUE2]..
  320. # xcase__per NAME -c CMD [ARG]..
  321. #
  322. # In first form, take line from stdin (if any), and repeat it to
  323. # stdout once for each VALUE, adding `,NAME=VALUE` at the end of
  324. # the line.
  325. #
  326. # In second form, do not take VALUEs from arguments but run CMD (witn
  327. # any ARGs) and use each line of its stdout as VALUE.
  328. #
  329. __xcase__doperm per "$@"
  330. }
  331. xcase__permute() {
  332. #
  333. # Permute variable named $1 at values $2..
  334. #
  335. # Usage:
  336. #
  337. # xcase__permute NAME [--] VALUE1 [VALUE2]..
  338. # xcase__permute NAME -f CMD [ARG]..
  339. #
  340. # In the first form, produce line `NAME=VALUE` for each VALUE.
  341. #
  342. # In second form, do not take VALUEs from arguments but run CMD (witn
  343. # any ARGs) and use each line of its stdout as VALUE.
  344. #
  345. __xcase__doperm permute "$@"
  346. }
  347. xcase__run() {
  348. #
  349. # Run all cases from xcase__enum()
  350. #
  351. # Usage:
  352. #
  353. # xcase__run [-v] [-R] [-T] [-L] [-c path/to/chdir]
  354. #
  355. # This function is the main launcher for tests. It will perform roughly
  356. # following steps:
  357. #
  358. # 1. create own temporary directory and chdir there,
  359. #
  360. # 2. for each case id emitted from xcase__enum():
  361. #
  362. # 1. create a subdirectory of that name, and chdir there,
  363. #
  364. # 2. run all available handlers (setup, test, diag, cleanup),
  365. # wrapped in JAT phases,
  366. #
  367. # 3. chdir back
  368. #
  369. # 4. Submit relics and reports:
  370. #
  371. # * `xcase-relics.tar.gz` with copy of the mentioned temporary
  372. # directory,
  373. #
  374. # * `xcase-results.yaml` with structured results and time stats,
  375. #
  376. # * and a nice table in test log
  377. #
  378. # See also "GETTING STARTED" and "ADVANCED USAGE" sections.
  379. #
  380. # The behavior can be altered using options:
  381. #
  382. # '-R' - disable creation and submission of relics tarball.
  383. #
  384. # '-c PATH' - change to PATH (must already exist) for the duration
  385. # of the test (implies '-T').
  386. #
  387. # '-L' - disable creation of "leaf" directory for each case id.
  388. #
  389. # '-T' - disable migration to temporary directory. xcase will still run
  390. # in a subdirectory "xcase-relics" of current directory or the
  391. # directory specified by `-c`.
  392. #
  393. # '-v' - enable variable auto-setting (see "ADVANCED USAGE" section).
  394. #
  395. local __xcase__leaves=true # enable "leaf" mode?
  396. local __xcase__tmp # results cache directory
  397. local __xcase__vars=false # enable variable auto-setting?
  398. local __xcase__runpath # directory path to run in
  399. local __xcase__start # start time
  400. local __xcase__rball=true # collect relics tarball?
  401. local __xcase__mktemp=true # make own temporary run dir?
  402. while true; do case $1 in
  403. -R)
  404. __xcase__rball=false
  405. shift
  406. ;;
  407. -c)
  408. __xcase__runpath="$2"
  409. __xcase__mktemp=false
  410. shift 2 || {
  411. jat__log_error "missing value to -c parameter"
  412. return 2
  413. }
  414. test -d "$__xcase__runpath" || {
  415. jat__log_error "no such directory: $__xcase__runpath"
  416. return 3
  417. }
  418. ;;
  419. -L)
  420. __xcase__leaves=false
  421. shift
  422. ;;
  423. -T)
  424. __xcase__mktemp=false
  425. shift
  426. ;;
  427. -v)
  428. __xcase__vars=true
  429. shift
  430. ;;
  431. -*)
  432. jat__log_error "bad argument: '$1'"
  433. return 2
  434. ;;
  435. "")
  436. break
  437. ;;
  438. *)
  439. jat__log_error "bad argument: '$1'"
  440. return 2
  441. ;;
  442. esac done
  443. __xcase__tmp=$(mktemp -d -t xcase.meta.XXXXXXXX)
  444. __xcase__start=$(python -c "import time; print time.time()")
  445. __xcase__run_all \
  446. || jat__log_error "errors encountered during case traversal"
  447. jat__submit "$__xcase__tmp/results.yaml" "xcase-results.yaml"
  448. $__xcase__rball \
  449. && jat__submit "$__xcase__tmp/relics.tar.gz" "xcase-relics.tar.gz"
  450. echo >&2
  451. rm -rf "$__xcase__tmp"
  452. }
  453. # # #
  454. # TEMPLATES # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
  455. # # #
  456. _xcase__enum() {
  457. #
  458. # Skeleton for enumerator
  459. #
  460. # Body of this function must be implemented by user and it must print
  461. # out one case ID---word ([a-zA-Z_]*) per line.
  462. #
  463. # The case ID is then available inside step handlers as output of
  464. # function xcase__id()
  465. #
  466. echo "case_foo" ...
  467. }
  468. _xcase__setup() {
  469. #
  470. # Skeleton for setup handler
  471. #
  472. # Perform setup tasks for case id.
  473. #
  474. true
  475. }
  476. _xcase__test() {
  477. #
  478. # Skeleton for test handler
  479. #
  480. # Perform tests for case id.
  481. #
  482. true
  483. }
  484. _xcase__diag() {
  485. #
  486. # Skeleton for diag handler
  487. #
  488. # Perform diag tasks for case id.
  489. #
  490. true
  491. }
  492. _xcase__cleanup() {
  493. #
  494. # Skeleton for cleanup handler
  495. #
  496. # Perform cleanup tasks for case id.
  497. #
  498. true
  499. }
  500. # # risk of brick increases 20% #
  501. # INTERNAL # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
  502. # # each time you cross this line #
  503. __xcase__has() {
  504. #
  505. # Check if handler has been implemented
  506. #
  507. type -t "xcase__$1" >/dev/null
  508. }
  509. __xcase__doperm() {
  510. #
  511. # Permutation engine for xcase__per{,mute}
  512. #
  513. # This implements both permutation helpers: xcase__per()
  514. # and xcase__permute(). First parameter says which ('per'
  515. # or 'permute'), rest is processed as described in those functions'
  516. # docstrings.
  517. #
  518. # The gist is that in 'permute' mode, we just generate key=value pairs,
  519. # assumed to be "left-most" in the table. In 'per' mode, we generate
  520. # them but also combuine them with lines on stdin, assumed to be output
  521. # of one of other modes.
  522. #
  523. local mode=$1; shift # mode (permute: first column, per: rest)
  524. local name=$1; shift # variable name
  525. local src=args # value source (args: arguments, cmd: command)
  526. local value # each value
  527. local line # each line of previous content ('per' mode)
  528. local oldifs # IFS backup
  529. local iterfail=false # did iteration command fail?
  530. case $1 in
  531. -c) src=cmd; shift ;;
  532. --) shift ;;
  533. esac
  534. test -n "$name" || { __xcase__perm_usage "$mode"; return 2; }
  535. test -n "$1" || { __xcase__perm_usage "$mode"; return 2; }
  536. case $src in
  537. cmd)
  538. oldifs=$IFS
  539. IFS=$'\n'
  540. values=($("$@")) || iterfail=true
  541. IFS=$oldifs
  542. $iterfail && {
  543. jat__log_error "iteration command returned non-zero: $*"
  544. return 3
  545. }
  546. ;;
  547. args)
  548. values=("$@")
  549. ;;
  550. esac
  551. case $mode in
  552. permute)
  553. for value in "${values[@]}";
  554. do
  555. echo "$name=$value"
  556. done
  557. ;;
  558. per)
  559. while read -r line;
  560. do
  561. for value in "${values[@]}";
  562. do
  563. echo "$line,$name=$value"
  564. done
  565. done
  566. ;;
  567. esac
  568. }
  569. __xcase__perm_usage() {
  570. #
  571. # Print usage message for permutation helper
  572. #
  573. local self=$1
  574. jat__log_error \
  575. "usage: xcase__$self NAME [--] VALUE1 [VALUE2].." \
  576. "usage: xcase__$self NAME -c CMD [ARG].."
  577. }
  578. __xcase__plan() {
  579. #
  580. # Enumerate and validate cases
  581. #
  582. local id # each case id
  583. __xcase__has enum || {
  584. jat__log_error "case ID enumerator handler not implemented: xcase__enum"
  585. return 3
  586. }
  587. xcase__enum > "$__xcase__tmp/enum"
  588. __xcase__ttl=$(wc -l <"$__xcase__tmp/enum")
  589. jat__log_info "enumerated $__xcase__ttl cases:"
  590. jat__log_info ""
  591. while IFS= read -r id;
  592. do
  593. jat__log_info " $id"
  594. done <"$__xcase__tmp/enum"
  595. jat__log_info ""
  596. __xcase__validate_enum || return $?
  597. test "$__xcase__ttl" -eq 0 && {
  598. jat__log_error "no cases enumerated, nothing to do"
  599. return 1
  600. }
  601. true
  602. }
  603. __xcase__run_all() {
  604. #
  605. # For each case, do all events
  606. #
  607. local __xcase__id # each case ID
  608. local __xcase__hstart # handler start time
  609. local __xcase__htype # handler type
  610. local __xcase__n=0 # case number (for hint)
  611. local __xcase__ttl # total cases (^^)
  612. local __xcase__varcode # variable auto-setting code
  613. local __xcase__plan_es=0 # planner (enum+validate) exit status
  614. local __xcase__sfail # did setup fail?
  615. local __xcase__tfail # did test fail?
  616. if $__xcase__mktemp;
  617. then
  618. __xcase__runpath=$(mktemp -d -t xcase.runpath.XXXXXXXX)
  619. fi
  620. jat__pstarts "xcase plan"
  621. __xcase__plan; __xcase__plan_es=$?
  622. jat__pend
  623. test $__xcase__plan_es -ne 0 && return $__xcase__plan_es
  624. if test -n "$__xcase__runpath";
  625. then
  626. pushd "$__xcase__runpath" >/dev/null || {
  627. jat__log_error "cannot chdir to: $__xcase__runpath"
  628. return 3
  629. }
  630. fi
  631. mkdir -p "xcase-relics" || {
  632. jat__log_error "cannot create relics directory: $PWD/xcase-relics"
  633. return 3
  634. }
  635. pushd "xcase-relics" >/dev/null || {
  636. jat__log_error "cannot chdir to: $PWD/xcase-relics"
  637. return 3
  638. }
  639. for __xcase__id in $(<"$__xcase__tmp/enum");
  640. do
  641. __xcase__sfail=false
  642. __xcase__tfail=false
  643. __xcase__hstart=$(python -c "import time; print time.time()")
  644. if $__xcase__leaves;
  645. then
  646. mkdir -p "$__xcase__id"
  647. pushd "$__xcase__id" >/dev/null || {
  648. jat__log_error "cannot chdir to: $PWD/$__xcase__id"
  649. return 3
  650. }
  651. fi
  652. ((__xcase__n++))
  653. jat__log_info " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
  654. jat__log_info " /"
  655. jat__log_info "o $__xcase__n/$__xcase__ttl: $__xcase__id"
  656. jat__log_info " \\"
  657. jat__log_info ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
  658. echo >&2
  659. __xcase__varcode=$(__xcase__mkvarcode) || {
  660. jat__log_error "variable parser encountered errors"
  661. return 3
  662. }
  663. bash -n <<<"$__xcase__varcode" || {
  664. jat__log_error "variable parser created invalid code: $__xcase__varcode"
  665. return 3
  666. }
  667. for __xcase__htype in setup test diag cleanup;
  668. do
  669. __xcase__wrap_handler
  670. done
  671. __xcase__upload_note
  672. if $__xcase__leaves;
  673. then
  674. popd >/dev/null
  675. fi
  676. __xcase__tmpwrite C.duration "$(python -c "import time; print time.time() - $__xcase__hstart")"
  677. done
  678. popd >/dev/null # from "xcase-relics"
  679. if $__xcase__rball;
  680. then
  681. tar -czf "$__xcase__tmp/relics.tar.gz" \
  682. "xcase-relics"
  683. fi
  684. if test -n "$__xcase__runpath";
  685. then
  686. popd >/dev/null
  687. fi
  688. if $__xcase__mktemp;
  689. then
  690. rm -rf "$__xcase__runpath"
  691. fi
  692. __xcase__results > "$__xcase__tmp/results.yaml"
  693. __xcase__summary
  694. }
  695. __xcase__tmpread() {
  696. #
  697. # Get result metadata from key $1
  698. #
  699. # See __xcase__tmpfile() for key syntax.
  700. #
  701. local key=$1 # key to read
  702. cat "$(__xcase__tmpfile "$key")"
  703. }
  704. __xcase__tmpwrite() {
  705. #
  706. # Save result metadata value $2 under key $1
  707. #
  708. # See __xcase__tmpfile() for key syntax.
  709. #
  710. local key=$1 # key to write
  711. local value=$2 # value to write
  712. local tgt # target file path
  713. tgt=$(__xcase__tmpfile "$key")
  714. echo "$value" > "$tgt"
  715. }
  716. __xcase__tmpfile() {
  717. #
  718. # Dereference temp storage key $1
  719. #
  720. # The key may be prefixed by `C.` or `H.`, meaning "for current case id"
  721. # or "for current handler", respectively. For example, following keys are
  722. # valid:
  723. #
  724. # foo # same file at any time
  725. # C.foo # same file until the end of this subtest (case id)
  726. # H.foo # same file until the end of this handler (eg. setup)
  727. #
  728. # Note: This function has a side effect within the storage structure that
  729. # directory for the data file is automatically created so that caller does
  730. # not need to.
  731. #
  732. local key=$1 # key to dereference
  733. local ns_case # case id infix
  734. local ns_htype # handler type infix
  735. local path # final path
  736. ns_case=id/$__xcase__id
  737. ns_htype=handler/$__xcase__htype
  738. path=$__xcase__tmp/data/
  739. case $key in
  740. C.*) path+="$ns_case/${key#C.}" ;;
  741. H.*) path+="$ns_case/$ns_htype/${key#H.}" ;;
  742. *) path+="$key" ;;
  743. esac
  744. mkdir -p "${path%/*}"
  745. echo "$path"
  746. }
  747. __xcase__nonempty() {
  748. #
  749. # True if directory $1 has files
  750. #
  751. local dir=$1
  752. local es
  753. test -n "$dir" \
  754. || jat__log_error "usage: __xcase__nonempty DIR"
  755. test -d "$dir" \
  756. || jat__log_error "usage: __xcase__nonempty DIR"
  757. test -e "$dir"/* 2>/dev/null; es=$?
  758. case $es in
  759. 0) return 0 ;; # one file
  760. 2) return 0 ;; # more files (`test` usage error)
  761. *) return 1 ;; # anything else
  762. esac
  763. }
  764. __xcase__upload_note() {
  765. #
  766. # Provide note about file uploads (if applicable)
  767. #
  768. local item # of filename list
  769. $__xcase__rball || return 0
  770. __xcase__nonempty . || return 0
  771. jat__log_info "Items to be auto-collected to xcase-relics.tar.gz:"
  772. find . -mindepth 1 -maxdepth 1 -printf "%P\\n" \
  773. | sort \
  774. | while read -r item;
  775. do
  776. jat__log_info " * $item"
  777. done
  778. echo >&2
  779. }
  780. __xcase__wrap_handler() {
  781. #
  782. # Handler wrapper
  783. #
  784. # Set phases, record failures, set up environment...
  785. #
  786. local __xcase__hfails="" # this handler fails num.
  787. local __xcase__hresult=none # this handler result
  788. __xcase__has "$__xcase__htype" || {
  789. __xcase__tmpwrite H.result "none"
  790. return 0
  791. }
  792. eval "$__xcase__varcode"
  793. case $__xcase__htype in
  794. setup)
  795. jat__pstarts "$__xcase__id :: setup"
  796. xcase__setup
  797. ;;
  798. test)
  799. jat__pstartt "$__xcase__id :: test"
  800. if $__xcase__sfail;
  801. then
  802. jat__log_error "setup failed--skipping test"
  803. __xcase__tmpwrite H.result "abort"
  804. jat__pend
  805. return 1
  806. else
  807. xcase__test
  808. fi
  809. ;;
  810. diag)
  811. jat__pstartd "$__xcase__id :: diag"
  812. xcase__diag
  813. ;;
  814. cleanup)
  815. jat__pstartc "$__xcase__id :: cleanup"
  816. xcase__cleanup
  817. ;;
  818. esac
  819. __xcase__hfails=$(jat__stat pfailc)
  820. jat__pend
  821. case $__xcase__hfails in
  822. 0) __xcase__hresult=pass ;;
  823. *) __xcase__hresult=fail ;;
  824. esac
  825. __xcase__tmpwrite H.result "$__xcase__hresult"
  826. #shellcheck disable=SC2034
  827. case $__xcase__htype:$__xcase__hresult in
  828. setup:fail) __xcase__sfail=true ;;
  829. test:fail) __xcase__tfail=true ;;
  830. esac
  831. }
  832. __xcase__mkvarcode() {
  833. #
  834. # Parse $__xcase__id and make variable setting code
  835. #
  836. # SIDE EFFECT: write variable cache file for later reference in results
  837. #
  838. $__xcase__vars || return 0
  839. local aname # declaration from $XCASE__ARRAYS
  840. local ciex # case id expression
  841. local vnam # variable name
  842. local vval # variable value
  843. local vcache # variable cache (for results.yaml)
  844. vcache="$(__xcase__tmpfile C.vcache)"
  845. for aname in ${XCASE__ARRAYS//,/ };
  846. do
  847. echo "local $aname=()"
  848. echo "$aname: []" >> "$vcache"
  849. done
  850. for ciex in ${__xcase__id//,/ };
  851. do
  852. case $ciex in
  853. *=*)
  854. vnam=${ciex%%=*}
  855. vval=${ciex#$vnam=}
  856. if grep -qw "$vnam" <<<"$XCASE__ARRAYS";
  857. then # array
  858. # it's already assigned to empty above, so we don't have
  859. # to do anything when we see empty value
  860. if test -n "$vval";
  861. then
  862. #shellcheck disable=SC2027,SC2086
  863. echo "local $vnam=('"${vval//+/"' '"}"')"
  864. sed -i -e "s/^$vnam:.*/$vnam: ['$vval']/" "$vcache"
  865. sed -i -e "/^$vnam:/s/+/', '/" "$vcache"
  866. fi
  867. else
  868. echo "local $vnam=$vval"
  869. echo "$vnam: $vval" >> "$vcache"
  870. fi
  871. ;;
  872. *)
  873. jat__log_error "invalid assignment in case id: $__xcase__id"
  874. return 3
  875. ;;
  876. "")
  877. jat__log_error "empty item in case id: $__xcase__id"
  878. ;;
  879. esac
  880. done
  881. }
  882. __xcase__summary() {
  883. #
  884. # Show summary phase
  885. #
  886. local logfunc # log function for a row
  887. local id # case id
  888. local r_setup # handler result: setup
  889. local r_test # ^^ test
  890. local r_diag # ^^ diag
  891. local r_cleanup # ^^ cleanup
  892. local hdata # path to cached handler data
  893. local num=0 # row number
  894. jat__pstartd "xcase summary"
  895. test -n "$PACKAGE" \
  896. && jat__log_info "PACKAGE version: $(rpm -q "$PACKAGE")"
  897. jat__log_info "duration: $(__xcase__tstats)"
  898. jat__log_info ""
  899. jat__log_info "============================================="
  900. jat__log_info "setup test diag cleanup | case id";
  901. jat__log_info "--------------------------------|------------"
  902. for id in $(<"$__xcase__tmp/enum");
  903. do
  904. ((num++))
  905. hdata="$__xcase__tmp/data/id/$id/handler"
  906. r_setup=$(<"$hdata/setup/result")
  907. r_test=$(<"$hdata/test/result")
  908. r_diag=$(<"$hdata/diag/result")
  909. r_cleanup=$(<"$hdata/cleanup/result")
  910. case $r_setup:$r_test:$r_diag:$r_cleanup in
  911. # internal error or cleanup fail: severe issue
  912. *axerr*) logfunc=jat__log_error ;;
  913. *:fail) logfunc=jat__log_error ;;
  914. # else: ok
  915. *) logfunc=jat__log_info ;;
  916. esac
  917. $logfunc "$(
  918. __xcase__sumrow \
  919. "$r_setup" "$r_test" "$r_diag" "$r_cleanup" \
  920. "$num" "$(wc -l < "$__xcase__tmp/enum")" \
  921. "$id"
  922. )"
  923. done
  924. jat__log_info "============================================="
  925. __xcase__stats
  926. jat__log_info ""
  927. jat__pend
  928. }
  929. __xcase__sumrow() {
  930. #
  931. # Format one row of summary table
  932. #
  933. # Rows should look something like this:
  934. #
  935. # ...
  936. # PASS FAIL none PASS | (8/20) some_case
  937. # PASS FAIL none PASS | (9/20) some_other_case
  938. # PASS FAIL none PASS | (10/20) yet_another_one
  939. # PASS FAIL none PASS | (11/20) still_not_done
  940. # ...
  941. #
  942. # Ie. cells formatted to 8 chars, except the last one, which
  943. # is free-width and prefixed by a right-aligned order hint.
  944. #
  945. local r_setup=$1 # handler result: setup
  946. local r_test=$2 # ^^ test
  947. local r_diag=$3 # ^^ diag
  948. local r_cleanup=$4 # ^^ cleanup
  949. local n=$5 # row number
  950. local ttl=$6 # total rows
  951. local id=$7 # case id
  952. local hintw # order hint width
  953. __xcase__sumcell "$r_setup"
  954. __xcase__sumcell "$r_test"
  955. __xcase__sumcell "$r_diag"
  956. __xcase__sumcell "$r_cleanup"
  957. hintw=$((${#ttl} * 2 + 3))
  958. printf "| %${hintw}s " "($n/$ttl)"
  959. echo "$id"
  960. }
  961. __xcase__sumcell() {
  962. #
  963. # Format single value $1, from summary table, maybe yell
  964. #
  965. local value=$1 # value as stored in handler result cache
  966. case $value in
  967. none) echo -n "none " ;;
  968. pass) echo -n "PASS " ;;
  969. fail) echo -n "FAIL " ;;
  970. abort) echo -n "ABORT " ;;
  971. *) echo -n "!AXERR! " ;;
  972. esac
  973. }
  974. __xcase__tstats() {
  975. #
  976. # Time-related stats
  977. #
  978. local count # total number of cases
  979. local dur_ttl # total duration (measured here)
  980. local dur_one # average case duration
  981. local alltimes # list of all durations from result cache
  982. alltimes=$(
  983. grep -hEo "^[0-9]+\.[0-9]{1,3}" "$__xcase__tmp/data/id"/*/duration \
  984. | sort -n
  985. )
  986. count=$(wc -l <<<"$alltimes")
  987. dur_ttl=$(python -c "import time; print time.time() - $__xcase__start")
  988. dur_one=$(python -c "print $dur_ttl/$count")
  989. __xcase__hduration "$dur_ttl" h
  990. echo -n " (~"
  991. __xcase__hduration "$dur_one" m
  992. echo -n " x $count cases,"
  993. echo -n " min=$(head -1 <<<"$alltimes"),"
  994. echo -n " max=$(tail -1 <<<"$alltimes"),"
  995. echo -n " SD=$(awk '{sum+=$1; sumsq+=$1*$1}END{print sqrt(sumsq/NR - (sum/NR)**2)}'<<<"$alltimes"))"
  996. }
  997. __xcase__hduration() {
  998. #
  999. # Format seconds float $1 as [[hhh:][m]mm]:[s]ss up to unit $2
  1000. #
  1001. # 'h' and 'm' units round seconds to integer value ('1.9' -> '2'),
  1002. # 's' rounds to miliseconds.
  1003. #
  1004. # If unit is 'h', format is 'H:MM:SS'. If unit is 'm', format
  1005. # is 'M:SS'. If unit is 's', format is "S.SSS", i.e. number of
  1006. # seconds with fractional part up to 3 digits (miliseconds).
  1007. #
  1008. local s=$1 # seconds (fractional)
  1009. local unit=$2 # precision unit
  1010. local ws # whole seconds
  1011. local m # minutes
  1012. local h # hours
  1013. ws=$(printf "%.0f" "$s")
  1014. case $unit in
  1015. h) h=$((ws / 3600)); ws=$((ws % 3600))
  1016. m=$((ws / 60)); ws=$((ws % 60))
  1017. printf "%d:%02d:%02d" $h $m $ws
  1018. ;;
  1019. m) m=$((ws / 60)); ws=$((ws % 60))
  1020. printf "%d:%02d" $m $ws
  1021. ;;
  1022. s) printf "%.3f" "$s"
  1023. ;;
  1024. esac
  1025. }
  1026. __xcase__stats() {
  1027. #
  1028. # Print the statistics
  1029. #
  1030. local results # all results (as grep -H output)
  1031. local result # result (not all are interesting)
  1032. local setups # setups with ^^
  1033. local tests # tests ^^
  1034. local diags # diags ^^
  1035. local cleanups # cleanups ^^
  1036. results=$(find "$__xcase__tmp/data/id/" -path "*/handler/*/result" -exec grep -H . {} +)
  1037. for result in pass fail abort axerr;
  1038. do
  1039. setups=$(grep -c "setup/result:$result$" <<<"$results")
  1040. tests=$(grep -c "test/result:$result$" <<<"$results")
  1041. diags=$(grep -c "diag/result:$result$" <<<"$results")
  1042. cleanups=$(grep -c "cleanup/result:$result$" <<<"$results")
  1043. test "$((setups + tests + diags + cleanups))" -eq 0 && continue
  1044. jat__log_info "$(printf '%-8s%-8s%-8s%-8s| x %s\n' "$setups" "$tests" "$diags" "$cleanups" $result)"
  1045. done
  1046. }
  1047. __xcase__results() {
  1048. #
  1049. # Collect case meta-data and print report in YAML
  1050. #
  1051. local idlist # path to cached case id list (straight from user's enum)
  1052. local id # each case id
  1053. idlist=$__xcase__tmp/enum
  1054. echo "---"
  1055. echo "cases: $(wc -l < "$idlist")"
  1056. echo "results:"
  1057. for id in $(<"$idlist");
  1058. do
  1059. pushd "$__xcase__tmp/data/id/$id" >/dev/null
  1060. echo " -"
  1061. echo " id: '$id'"
  1062. test -s "vcache" && {
  1063. echo " variables:"
  1064. sed -e "s/^/ /" "vcache"
  1065. }
  1066. echo " handlers:"
  1067. echo " setup: $(<handler/setup/result)"
  1068. echo " test: $(<handler/test/result)"
  1069. echo " diag: $(<handler/diag/result)"
  1070. echo " cleanup: $(<handler/cleanup/result)"
  1071. echo " duration: $(<duration)"
  1072. echo " result: $(<handler/test/result)"
  1073. popd >/dev/null
  1074. done
  1075. }
  1076. __xcase__validate_enum() {
  1077. #
  1078. # Make sure stuff from xcase__enum() has no banned chars
  1079. #
  1080. local allowed='[:alnum:]._,+%@=-' # allowed chars in case id
  1081. local es= # exit status of this function
  1082. if grep "[^$allowed]" "$__xcase__tmp/enum";
  1083. then
  1084. if $__xcase__leaves || $__xcase__vars;
  1085. then
  1086. jat__log_error "Sorry, when leaf directory mode (default) or variable"
  1087. jat__log_error "setting mode is used, range of characters that"
  1088. jat__log_error "xcase__enum() can emit is limited to:"
  1089. jat__log_error ""
  1090. jat__log_error " $allowed"
  1091. jat__log_error ""
  1092. jat__log_error "This is to enable usage of this data as file, directory"
  1093. jat__log_error "and variable names. To disable these modes, use flags"
  1094. jat__log_error "-L and -V, respectively."
  1095. jat__log_error "illegal characters in enumerator"
  1096. es=2
  1097. else
  1098. jat__log_error "DEPRECATED characters in enumerator, future version will only allow: $allowed"
  1099. es=0
  1100. fi
  1101. jat__log_error "Note that in order to make best use of xcase, the case id"
  1102. jat__log_error "should not hold any 'real' testing data but rather just"
  1103. jat__log_error "simple generic words to hint *intent* of the test case."
  1104. fi
  1105. return $es
  1106. }
  1107. #shellfu module-version=__MKIT_PROJ_VERSION__