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

xcase.sh.skel 34KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175
  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. local lines=() # output lines
  584. __xcase__has enum || {
  585. jat__log_error "case ID enumerator handler not implemented: xcase__enum"
  586. return 3
  587. }
  588. xcase__enum > "$__xcase__tmp/enum"
  589. __xcase__ttl=$(wc -l <"$__xcase__tmp/enum")
  590. lines+=("enumerated $__xcase__ttl cases:")
  591. lines+=("")
  592. while IFS= read -r id;
  593. do
  594. lines+=(" $id")
  595. done <"$__xcase__tmp/enum"
  596. lines+=("")
  597. jat__log_info "${lines[@]}"
  598. __xcase__validate_enum || return $?
  599. test "$__xcase__ttl" -eq 0 && {
  600. jat__log_error "no cases enumerated, nothing to do"
  601. return 1
  602. }
  603. true
  604. }
  605. __xcase__run_all() {
  606. #
  607. # For each case, do all events
  608. #
  609. local __xcase__id # each case ID
  610. local __xcase__hstart # handler start time
  611. local __xcase__htype # handler type
  612. local __xcase__n=0 # case number (for hint)
  613. local __xcase__ttl # total cases (^^)
  614. local __xcase__varcode # variable auto-setting code
  615. local __xcase__plan_es=0 # planner (enum+validate) exit status
  616. local __xcase__sfail # did setup fail?
  617. local __xcase__tfail # did test fail?
  618. if $__xcase__mktemp;
  619. then
  620. __xcase__runpath=$(mktemp -d -t xcase.runpath.XXXXXXXX)
  621. fi
  622. jat__pstarts "xcase plan"
  623. __xcase__plan; __xcase__plan_es=$?
  624. jat__pend
  625. test $__xcase__plan_es -ne 0 && return $__xcase__plan_es
  626. if test -n "$__xcase__runpath";
  627. then
  628. pushd "$__xcase__runpath" >/dev/null || {
  629. jat__log_error "cannot chdir to: $__xcase__runpath"
  630. return 3
  631. }
  632. fi
  633. mkdir -p "xcase-relics" || {
  634. jat__log_error "cannot create relics directory: $PWD/xcase-relics"
  635. return 3
  636. }
  637. pushd "xcase-relics" >/dev/null || {
  638. jat__log_error "cannot chdir to: $PWD/xcase-relics"
  639. return 3
  640. }
  641. for __xcase__id in $(<"$__xcase__tmp/enum");
  642. do
  643. __xcase__sfail=false
  644. __xcase__tfail=false
  645. __xcase__hstart=$(python -c "import time; print time.time()")
  646. if $__xcase__leaves;
  647. then
  648. mkdir -p "$__xcase__id"
  649. pushd "$__xcase__id" >/dev/null || {
  650. jat__log_error "cannot chdir to: $PWD/$__xcase__id"
  651. return 3
  652. }
  653. fi
  654. ((__xcase__n++))
  655. jat__log_info \
  656. " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" \
  657. " /" \
  658. "o $__xcase__n/$__xcase__ttl: $__xcase__id" \
  659. " \\" \
  660. ' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'
  661. echo >&2
  662. __xcase__varcode=$(__xcase__mkvarcode) || {
  663. jat__log_error "variable parser encountered errors"
  664. return 3
  665. }
  666. bash -n <<<"$__xcase__varcode" || {
  667. jat__log_error "variable parser created invalid code: $__xcase__varcode"
  668. return 3
  669. }
  670. for __xcase__htype in setup test diag cleanup;
  671. do
  672. __xcase__wrap_handler
  673. done
  674. __xcase__upload_note
  675. if $__xcase__leaves;
  676. then
  677. popd >/dev/null
  678. fi
  679. __xcase__tmpwrite C.duration "$(python -c "import time; print time.time() - $__xcase__hstart")"
  680. done
  681. popd >/dev/null # from "xcase-relics"
  682. if $__xcase__rball;
  683. then
  684. tar -czf "$__xcase__tmp/relics.tar.gz" \
  685. "xcase-relics"
  686. fi
  687. if test -n "$__xcase__runpath";
  688. then
  689. popd >/dev/null
  690. fi
  691. if $__xcase__mktemp;
  692. then
  693. rm -rf "$__xcase__runpath"
  694. fi
  695. __xcase__results > "$__xcase__tmp/results.yaml"
  696. __xcase__summary
  697. }
  698. __xcase__tmpread() {
  699. #
  700. # Get result metadata from key $1
  701. #
  702. # See __xcase__tmpfile() for key syntax.
  703. #
  704. local key=$1 # key to read
  705. cat "$(__xcase__tmpfile "$key")"
  706. }
  707. __xcase__tmpwrite() {
  708. #
  709. # Save result metadata value $2 under key $1
  710. #
  711. # See __xcase__tmpfile() for key syntax.
  712. #
  713. local key=$1 # key to write
  714. local value=$2 # value to write
  715. local tgt # target file path
  716. tgt=$(__xcase__tmpfile "$key")
  717. echo "$value" > "$tgt"
  718. }
  719. __xcase__tmpfile() {
  720. #
  721. # Dereference temp storage key $1
  722. #
  723. # The key may be prefixed by `C.` or `H.`, meaning "for current case id"
  724. # or "for current handler", respectively. For example, following keys are
  725. # valid:
  726. #
  727. # foo # same file at any time
  728. # C.foo # same file until the end of this subtest (case id)
  729. # H.foo # same file until the end of this handler (eg. setup)
  730. #
  731. # Note: This function has a side effect within the storage structure that
  732. # directory for the data file is automatically created so that caller does
  733. # not need to.
  734. #
  735. local key=$1 # key to dereference
  736. local ns_case # case id infix
  737. local ns_htype # handler type infix
  738. local path # final path
  739. ns_case=id/$__xcase__id
  740. ns_htype=handler/$__xcase__htype
  741. path=$__xcase__tmp/data/
  742. case $key in
  743. C.*) path+="$ns_case/${key#C.}" ;;
  744. H.*) path+="$ns_case/$ns_htype/${key#H.}" ;;
  745. *) path+="$key" ;;
  746. esac
  747. mkdir -p "${path%/*}"
  748. echo "$path"
  749. }
  750. __xcase__nonempty() {
  751. #
  752. # True if directory $1 has files
  753. #
  754. local dir=$1
  755. local es
  756. test -n "$dir" \
  757. || jat__log_error "usage: __xcase__nonempty DIR"
  758. test -d "$dir" \
  759. || jat__log_error "usage: __xcase__nonempty DIR"
  760. test -e "$dir"/* 2>/dev/null; es=$?
  761. case $es in
  762. 0) return 0 ;; # one file
  763. 2) return 0 ;; # more files (`test` usage error)
  764. *) return 1 ;; # anything else
  765. esac
  766. }
  767. __xcase__upload_note() {
  768. #
  769. # Provide note about file uploads (if applicable)
  770. #
  771. local items # of filename list
  772. local item # of filename list
  773. local lines=() # output lines
  774. $__xcase__rball || return 0
  775. __xcase__nonempty . || return 0
  776. items=$(__xcase__tmpfile "C.relist")
  777. lines+=("Items to be auto-collected to xcase-relics.tar.gz:")
  778. find . -mindepth 1 -maxdepth 1 -printf "%P\\n" \
  779. | sort >"$items"
  780. while read -r item;
  781. do
  782. lines+=(" * $item")
  783. done <"$items"
  784. jat__log_info "${lines[@]}"
  785. echo >&2
  786. }
  787. __xcase__wrap_handler() {
  788. #
  789. # Handler wrapper
  790. #
  791. # Set phases, record failures, set up environment...
  792. #
  793. local __xcase__hfails="" # this handler fails num.
  794. local __xcase__hresult=none # this handler result
  795. __xcase__has "$__xcase__htype" || {
  796. __xcase__tmpwrite H.result "none"
  797. return 0
  798. }
  799. eval "$__xcase__varcode"
  800. case $__xcase__htype in
  801. setup)
  802. jat__pstarts -c "$__xcase__id" "$__xcase__id :: setup"
  803. xcase__setup
  804. ;;
  805. test)
  806. jat__pstartt -c "$__xcase__id" "$__xcase__id :: test"
  807. if $__xcase__sfail;
  808. then
  809. jat__log_error "setup failed--skipping test"
  810. __xcase__tmpwrite H.result "abort"
  811. jat__pend
  812. return 1
  813. else
  814. xcase__test
  815. fi
  816. ;;
  817. diag)
  818. jat__pstartd -c "$__xcase__id" "$__xcase__id :: diag"
  819. xcase__diag
  820. ;;
  821. cleanup)
  822. jat__pstartc -c "$__xcase__id" "$__xcase__id :: cleanup"
  823. xcase__cleanup
  824. ;;
  825. esac
  826. __xcase__hfails=$(jat__stat pfailc)
  827. jat__pend
  828. case $__xcase__hfails in
  829. 0) __xcase__hresult=pass ;;
  830. *) __xcase__hresult=fail ;;
  831. esac
  832. __xcase__tmpwrite H.result "$__xcase__hresult"
  833. #shellcheck disable=SC2034
  834. case $__xcase__htype:$__xcase__hresult in
  835. setup:fail) __xcase__sfail=true ;;
  836. test:fail) __xcase__tfail=true ;;
  837. esac
  838. }
  839. __xcase__mkvarcode() {
  840. #
  841. # Parse $__xcase__id and make variable setting code
  842. #
  843. # SIDE EFFECT: write variable cache file for later reference in results
  844. #
  845. $__xcase__vars || return 0
  846. local aname # declaration from $XCASE__ARRAYS
  847. local ciex # case id expression
  848. local vnam # variable name
  849. local vval # variable value
  850. local vcache # variable cache (for results.yaml)
  851. vcache="$(__xcase__tmpfile C.vcache)"
  852. for aname in ${XCASE__ARRAYS//,/ };
  853. do
  854. echo "local $aname=()"
  855. echo "$aname: []" >> "$vcache"
  856. done
  857. for ciex in ${__xcase__id//,/ };
  858. do
  859. case $ciex in
  860. *=*)
  861. vnam=${ciex%%=*}
  862. vval=${ciex#$vnam=}
  863. if grep -qw "$vnam" <<<"$XCASE__ARRAYS";
  864. then # array
  865. # it's already assigned to empty above, so we don't have
  866. # to do anything when we see empty value
  867. if test -n "$vval";
  868. then
  869. #shellcheck disable=SC2027,SC2086
  870. echo "local $vnam=('"${vval//+/"' '"}"')"
  871. sed -i -e "s/^$vnam:.*/$vnam: ['$vval']/" "$vcache"
  872. sed -i -e "/^$vnam:/s/+/', '/" "$vcache"
  873. fi
  874. else
  875. echo "local $vnam=$vval"
  876. echo "$vnam: $vval" >> "$vcache"
  877. fi
  878. ;;
  879. *)
  880. jat__log_error "invalid assignment in case id: $__xcase__id"
  881. return 3
  882. ;;
  883. "")
  884. jat__log_error "empty item in case id: $__xcase__id"
  885. ;;
  886. esac
  887. done
  888. }
  889. __xcase__summary() {
  890. #
  891. # Show summary phase
  892. #
  893. local id # case id
  894. local r_setup # handler result: setup
  895. local r_test # ^^ test
  896. local r_diag # ^^ diag
  897. local r_cleanup # ^^ cleanup
  898. local hdata # path to cached handler data
  899. local num=0 # row number
  900. local lines=() # output lines
  901. jat__pstartd "xcase summary"
  902. lines+=(
  903. "duration: $(__xcase__tstats)"
  904. ""
  905. "============================================="
  906. "setup test diag cleanup | case id"
  907. "--------------------------------|------------"
  908. )
  909. for id in $(<"$__xcase__tmp/enum");
  910. do
  911. ((num++))
  912. hdata="$__xcase__tmp/data/id/$id/handler"
  913. r_setup=$(<"$hdata/setup/result")
  914. r_test=$(<"$hdata/test/result")
  915. r_diag=$(<"$hdata/diag/result")
  916. r_cleanup=$(<"$hdata/cleanup/result")
  917. lines+=("$(
  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. lines+=("=============================================")
  925. jat__log_info "${lines[@]}"
  926. __xcase__stats
  927. jat__log_info ""
  928. jat__pend
  929. }
  930. __xcase__sumrow() {
  931. #
  932. # Format one row of summary table
  933. #
  934. # Rows should look something like this:
  935. #
  936. # ...
  937. # PASS FAIL none PASS | (8/20) some_case
  938. # PASS FAIL none PASS | (9/20) some_other_case
  939. # PASS FAIL none PASS | (10/20) yet_another_one
  940. # PASS FAIL none PASS | (11/20) still_not_done
  941. # ...
  942. #
  943. # Ie. cells formatted to 8 chars, except the last one, which
  944. # is free-width and prefixed by a right-aligned order hint.
  945. #
  946. local r_setup=$1 # handler result: setup
  947. local r_test=$2 # ^^ test
  948. local r_diag=$3 # ^^ diag
  949. local r_cleanup=$4 # ^^ cleanup
  950. local n=$5 # row number
  951. local ttl=$6 # total rows
  952. local id=$7 # case id
  953. local hintw # order hint width
  954. __xcase__sumcell "$r_setup"
  955. __xcase__sumcell "$r_test"
  956. __xcase__sumcell "$r_diag"
  957. __xcase__sumcell "$r_cleanup"
  958. hintw=$((${#ttl} * 2 + 3))
  959. printf "| %${hintw}s " "($n/$ttl)"
  960. echo "$id"
  961. }
  962. __xcase__sumcell() {
  963. #
  964. # Format single value $1, from summary table, maybe yell
  965. #
  966. local value=$1 # value as stored in handler result cache
  967. case $value in
  968. none) echo -n "none " ;;
  969. pass) echo -n "PASS " ;;
  970. fail) echo -n "FAIL " ;;
  971. abort) echo -n "ABORT " ;;
  972. *) echo -n "!AXERR! " ;;
  973. esac
  974. }
  975. __xcase__tstats() {
  976. #
  977. # Time-related stats
  978. #
  979. local count # total number of cases
  980. local dur_ttl # total duration (measured here)
  981. local dur_one # average case duration
  982. local alltimes # list of all durations from result cache
  983. alltimes=$(
  984. grep -hEo "^[0-9]+\.[0-9]{1,3}" "$__xcase__tmp/data/id"/*/duration \
  985. | sort -n
  986. )
  987. count=$(wc -l <<<"$alltimes")
  988. dur_ttl=$(python -c "import time; print time.time() - $__xcase__start")
  989. dur_one=$(python -c "print $dur_ttl/$count")
  990. __xcase__hduration "$dur_ttl" h
  991. echo -n " (~"
  992. __xcase__hduration "$dur_one" m
  993. echo -n " x $count cases,"
  994. echo -n " min=$(head -1 <<<"$alltimes"),"
  995. echo -n " max=$(tail -1 <<<"$alltimes"),"
  996. echo -n " SD=$(awk '{sum+=$1; sumsq+=$1*$1}END{print sqrt(sumsq/NR - (sum/NR)**2)}'<<<"$alltimes"))"
  997. }
  998. __xcase__hduration() {
  999. #
  1000. # Format seconds float $1 as [[hhh:][m]mm]:[s]ss up to unit $2
  1001. #
  1002. # 'h' and 'm' units round seconds to integer value ('1.9' -> '2'),
  1003. # 's' rounds to miliseconds.
  1004. #
  1005. # If unit is 'h', format is 'H:MM:SS'. If unit is 'm', format
  1006. # is 'M:SS'. If unit is 's', format is "S.SSS", i.e. number of
  1007. # seconds with fractional part up to 3 digits (miliseconds).
  1008. #
  1009. local s=$1 # seconds (fractional)
  1010. local unit=$2 # precision unit
  1011. local ws # whole seconds
  1012. local m # minutes
  1013. local h # hours
  1014. ws=$(printf "%.0f" "$s")
  1015. case $unit in
  1016. h) h=$((ws / 3600)); ws=$((ws % 3600))
  1017. m=$((ws / 60)); ws=$((ws % 60))
  1018. printf "%d:%02d:%02d" $h $m $ws
  1019. ;;
  1020. m) m=$((ws / 60)); ws=$((ws % 60))
  1021. printf "%d:%02d" $m $ws
  1022. ;;
  1023. s) printf "%.3f" "$s"
  1024. ;;
  1025. esac
  1026. }
  1027. __xcase__stats() {
  1028. #
  1029. # Print the statistics
  1030. #
  1031. local results # all results (as grep -H output)
  1032. local result # result (not all are interesting)
  1033. local setups # setups with ^^
  1034. local tests # tests ^^
  1035. local diags # diags ^^
  1036. local cleanups # cleanups ^^
  1037. local lines=() # output lines
  1038. results=$(find "$__xcase__tmp/data/id/" -path "*/handler/*/result" -exec grep -H . {} +)
  1039. for result in pass fail abort axerr;
  1040. do
  1041. setups=$(grep -c "setup/result:$result$" <<<"$results")
  1042. tests=$(grep -c "test/result:$result$" <<<"$results")
  1043. diags=$(grep -c "diag/result:$result$" <<<"$results")
  1044. cleanups=$(grep -c "cleanup/result:$result$" <<<"$results")
  1045. test "$((setups + tests + diags + cleanups))" -eq 0 && continue
  1046. lines+=("$(printf '%-8s%-8s%-8s%-8s| x %s\n' "$setups" "$tests" "$diags" "$cleanups" $result)")
  1047. done
  1048. jat__log_info "${lines[@]}"
  1049. }
  1050. __xcase__results() {
  1051. #
  1052. # Collect case meta-data and print report in YAML
  1053. #
  1054. local idlist # path to cached case id list (straight from user's enum)
  1055. local id # each case id
  1056. idlist=$__xcase__tmp/enum
  1057. echo "---"
  1058. echo "cases: $(wc -l < "$idlist")"
  1059. echo "results:"
  1060. for id in $(<"$idlist");
  1061. do
  1062. pushd "$__xcase__tmp/data/id/$id" >/dev/null
  1063. echo " -"
  1064. echo " id: '$id'"
  1065. test -s "vcache" && {
  1066. echo " variables:"
  1067. sed -e "s/^/ /" "vcache"
  1068. }
  1069. echo " handlers:"
  1070. echo " setup: $(<handler/setup/result)"
  1071. echo " test: $(<handler/test/result)"
  1072. echo " diag: $(<handler/diag/result)"
  1073. echo " cleanup: $(<handler/cleanup/result)"
  1074. echo " duration: $(<duration)"
  1075. echo " result: $(<handler/test/result)"
  1076. popd >/dev/null
  1077. done
  1078. }
  1079. __xcase__validate_enum() {
  1080. #
  1081. # Make sure stuff from xcase__enum() has no banned chars
  1082. #
  1083. local allowed='[:alnum:]._,+%=-' # allowed chars in case id
  1084. local es= # exit status of this function
  1085. local lines=() # error message lines
  1086. if grep "[^$allowed]" "$__xcase__tmp/enum";
  1087. then
  1088. if $__xcase__leaves || $__xcase__vars;
  1089. then
  1090. lines+=(
  1091. "Sorry, when leaf directory mode (default) or variable"
  1092. "setting mode is used, range of characters that"
  1093. "xcase__enum() can emit is limited to:"
  1094. ""
  1095. " $allowed"
  1096. ""
  1097. "This is to enable usage of this data as file, directory"
  1098. "and variable names. To disable these modes, use flags"
  1099. "-L and -V, respectively."
  1100. "illegal characters in enumerator"
  1101. )
  1102. es=2
  1103. else
  1104. lines+=("DEPRECATED characters in enumerator, future version will only allow: $allowed")
  1105. es=0
  1106. fi
  1107. lines+=(
  1108. "Note that in order to make best use of xcase, the case id"
  1109. "should not hold any 'real' testing data but rather just"
  1110. "simple generic words to hint *intent* of the test case."
  1111. )
  1112. jat__log_error "${lines[@]}"
  1113. fi
  1114. return $es
  1115. }
  1116. #shellfu module-version=__MKIT_PROJ_VERSION__