program BestTest; { = a 100% systematic exhaustive tester of algorithms } (* BestTest is a fully operational task-adaptable 100% systematic and exhaustive combinatorial tester of (data-driven) algorithms. Combinatorial testing is obtained via backtracking . CopyRight (C) 1988 + 2007 , Jan Hajek , NL , version 5.08 of 2007-12-14 "The more correct the prog, the subtler the bugs" [ JH ] "A minute of reflection is worth a megaton of hardware" Feel free to re-implement BestTest in your favorite programming language and to adapt it to your tasks, subject to the following rule : NO part of this document may be published, implemented, programmed, copied or communicated by any means without an explicit & full reference to this author + the full title (here on the 1st non-empty line) + the website www.humintel.com/hajek for the freshest version + the CopyRight note in texts and in ALL references to this. An implicit, incomplete, indirect, disconnected or unlinked reference (in your text and/or on www) does NOT suffice. ALL based on 1st-hand experience. ALL rights reserved. + 2 modes of generation of data vectors V[..] are available : 1. Straight depth-first (find depth-1st ) ie LIFO generator of data-/parameters-/events-vector V[lo..h =hi] ie a stack. 2. Iterative deepening (Id) of data V[lo..h<=hi] is the recommended mode. Both can be run in a submode as : + a model checking ie algorithm checking ie verification test , or as : + a demo which tests nothing, and only shows how all the subvectors of V[..] are generated . Running demos for 1. & 2. shows that 2. ie Id generates ALL SHORT subvectors first (like a breath-first would), hence Id also uses Vmax much sooner than straight depth-1st mode. These are 2 very good reasons to prefer iterative deepening over the straight depth-1st. Q: why? A: because such extreme cases (shortest vectors with Vmax) are most likely to reveal bugs soon. Iterative deepening ! allows us to use hi-lo+1 and/or Vmax-Vmin+1 so LARGE that a combinatorial testing CANNOT be completed in any realistic time, ! nevertheless, unlike depth-1st mode, iterative deepening starts by exhausting ALL extremely short vectors including Vmax value. ! Moreover, in my BestTest I left it to the user to confirm each next deepening step, so that (s)he can decide to stop eg when no bugs have been found and BestTest would run too long. + 3 methods of checking are available ( repeatedly find relative ) : 1. An "absolute check" ie a direct check of the result in W or X only . 2. A "relative check" ie by comparing the result in W against in X from another (often simpler) algorithm believed to be correct. 3. Moreover we are free to insert assertions at any place and check them at runtime (find Assert , fassert, also serve as executable comments). BestTest (compiles in Borland Pascal BP7, Turbo Pascal TP, in FreePascal FP with Options\Compiler\ set to TP compatible) CATCHES ALL BUGS because : + It generates all test vectors (of test data) of all sizes ( within the limits lo..h ) containing all variations with repetitions of values ( from Vmin to Vmax ). For a data-vector with k components aka atoms, eg V[1..k] , each atom allowed to have any of Vmax-Vmin+1 = n values, the number of variations with repetition is n^k , eg 3^2 = 9 full-size vectors. Since we want to generate and test also all subvectors of all sizes from 1 to k , we test the following 3^1 + 3^2 = 3 + 9 = 12 data-vectors: 1; 2; 3; 1,1; 1,2; 1,3; 2,1; 2,2; 2,3; 3,1; 3,2; 3,3; , if our chosen constants were Vmin=1, Vmax=3, and h-lo+1 = k = 2 . If k = 3 then our series would continue with 1,1,1; and end with 3,3,3. The just shown list of (sub)vectors is in breath-first order. But here we actually generate (sub)vectors in depth-first order. Unlike breadth-1st, depth-1st needs almost no memory and is easy to program as a stack. Here the vector V[lo..r..h] is here treated as a stack V[lo..r..] which grows upon r:=r+1 and shrinks upon r:=r-1. Deeper insight is that V[lo..r..h] can be viewed as 2 complementary stacks : V[lo..r-1] and V[r+1..h] with V[r] moving in between. When one stack grows at r , the other shrinks at r . The 1st stack contains a partial solution, while the 2nd one is a pool of free atoms to be consumed ie waiting to get a next value. Even deeper insight is that the 2nd stack V[r+1..h] needs not to be a stack, because the values of its atoms do not matter yet/anymore, and so it is a SET of yet unassigned/undone values (which might be, but need not to be, reset to some nil, fill, or be left as are, since they simply do not matter yet/anymore ). The consequence of this deeper view is that the SET OF INDEXES [r+1..h] is always allowed (without any special precaution or fix) to be PERMUTED ie REORDERED to our advantage. A reordering changes nothing but the order in which V's atoms are (re)assigned values. Caution: we must keep stored the unchanging positional/structural index of each atom in V, because structural positions of atoms in V are task dependent and not exchangable in general. Then the challenge is to invent a smart task-(in)dependent HEURISTIC which will exploit such a REARRANGEMENT to find asap a solution (eg a bug, when testing). In fact it is not necessary to reorder V[r+1..h] ; it just suffices a heuristic choice of the V[best] (best means finding a solution asap) within V[r+1..h], and swap V[best] with V[r+1] . This is called NONCHRONOLOGICAL BACKTRACKING aka DEPENDENCY-DIRECTED BACKTRACKING utilizing task's constraints . Caution : V[r+1..h] contains irrelevant values, ie they are UNINFOrmative. Hence a simple swap would have no effect. It is the structural/positional index of each atom in V[lo..h] , what does matter. Hence for such swaps the positional index of each atom must be kept preserved, either in the V of "records" with 2 items ( Value proper & its inDex), or in an array D[lo..h] , initially filled by: FOR dx:=lo TO h DO D[dx] := dx . In my backtracker proper we would have to replace all V[r] with V[D[r]] , etc. Such a double indexing aka indirect access aka indirection allows us to swap elements of D only, while the possibly large atoms of V do not move. Again : we must not lose structural positions of atoms in V[lo..h] , but may be FREE to process them in any (smart) order ( look-ahead search in V[r+1..h] to choose the best V[best] , then V[best]:=next value ). Of course, we always must satisfy all task-dependent constraints, if any. I do not do it here, I leave it as an exercise to non-junior programmers, in order to keep it KISS = Keep it simple, students ! :-) So far on FORWARD moves ie on our freedom to pick from V[r+1..h] the V[best] as the next to be processed ie assigned a next value. In 1988 I have invented DYNAMIC BACTRACKING which picks yet another best V[back] from V[lo..r-1] while moving BACKWARD . Here I will not go deeper into its nontrivial algorithmics requiring deeper thoughts and precautions, published in my report on A New Dynamic Backtracking Algorithm for Constraints Satisfaction Problems , January 1990 , 17 pages. In that report I also explain an extremely powerful and task-INdependent ( ! ) Least-Free-Variable-First ( LFVF ) heuristic ( Warnsdorff's rule of 1823 to solve one specific task called the Knight's Tour Problem ). I will not go into it here, however superpowerful it is for constraints satisfaction problems ( CSP ), because I do not believe it can be really exploited for program testing. My final general constructive remark on these matters : Identification & exploitation of CONSTRAINTS & FREEDOMS is one of the key tricks of trade in algorithm design and in software engineering. A common freedom is a COMMUTATIVITY of operations ie preordering or even in-flight ie dynamic reordering of processing. Another case of freedom is a RANDOM choice of a split in all interval bisections (see (a+b) div 2 here below), or partitioning of an array (eg in MergeSort or QuickSort ). + Any task-dependent constraints on data may be imposed by means of the OK := Boolean expression (is true iff V's constraints are satisfied). + Exhaustive runtime testing of extremely short data vectors ( with bounds lo, h, BOTH either extremely low or BOTH extremely high ; "extremely" wrt to a preferably VERY SMALL range of the VAR lo , hi : ShortInt , or a subrange TYPE index = lo..hi ; ) provides for STRESS-TESTing. + Runtime testing of an implementation is more realistic than hand-proving, as it covers all idiosyncrasies of your hardware + OS + compiler + programming language, like eg parameter-passing mechanisms plus all kinds of overflow if you switch their checking on with {$R+,Q+,S+,I+} as I do. Eg, a forgotten VAR in Swap( VAR y , z : atom ) will cause a detected bug. Hints : - Please repeatedly find : user , must , TestTaskNr:= - Programs and algorithms (procedures, functions) become incorrect when programmer's INDUCTIVE THINKING BREAKS DOWN, which typically happens at some discontinuities, or extreme situations, including initialization and especially at the ends or at the bounds of eg data structures. Therefore it is BEST TO TEST : - arrays, strings, files with NONE, 1, 2, few atoms (elements, records); - with LARGE values of VERY SMALL-ranged integers (indexes) likely to overflow or to cause out of bound indexing. - In PASCAL arrays are pointer-less and must have constant bounds. Hence a possible out of bound indexing of V's subvectors may not be caught when testing an incorrect algorithm which sometimes indexes out of subvector's upper bound r < h . Therefore to surely get all thinkable bugs out of your algorithm , I recommend to recompile & run BestTest with several different low values of lo and especially of hi (to force r = hi ). - Tests with 8 or 16 bit integer indexes or with very narrow subrange types (like eg: type index = 126..127; ) are more likely to reveal subtle bugs than are the tests with LongInt. Eg, a poor interval bisection by (a+b) div 2 overflows when (a+b) > MaxInt , which happens whenever ((a < b) and (a > (MaxInt div 2))). Safer is (a + ((b-a) div 2)). Although both bisections are not always the same, they are functionally equivalent whenever any RANDOM partition of an interval is allowed from the correctness point of view. Aha! Quiz : what about b - ((b-a) div 2) ? Dear user , test it here in a smart & safe binary search of your own making, or from a smart book by some smart professor. Notes : + I kept the code straight so that every non-junior programmer can easily adapt it for his own tasks and tests. Examples and exercises are inside. + Find TestTaskNr:= , where a user can choose how to do different tests on other/different tasks. + Repeatedly find the word backtrack to get to the "smart part" (near the end of Tester ) which generates all (un)constrained combinatorial variations with repetitions for all possible (sub)vectors within the ranges specified (by Vmin , Vmax , and by lo , hi ) by the user. + By data-driven algorithms I mean those more common than event-driven algorithms like eg protocols for which I have pioneered verifications aka validations aka model checking by means of a highly dynamic (because driven by a priority queue) yet 100% systematically executed BEST-FIRST search reachability analysis in my APPROVER (see the end of the list of my epapers at www.humintel.com/hajek ). Nevertheless, nothing prevents us to REMAP V[..] into task/prog's INPUT PARAMETERS . But then V[..] might also be used for exhaustive model checking of some EVENT-driven algorithms treated either as-if-simultaneous, or as-if-queued in the vector V[..] ; atoms in V would have only to be remapped into task dependent representations of events. + Though not simplistic, BestTest is simple enough and therefore adaptable ie modifiable, because of no pointers, no recursion, few parameters (user's and in procedures). BestTest is also extremely efficient : negligible use of memory (uses RAM only), blitzy fast. + I checked this source code by means of my rigorous automated static analysis from my post-compile, pre-run static analyzer ANA9. *) {$N-}{ no-op in FP ; in BP/TP $N- better pinpoints RTErrors, float = REAL } {$R+,Q+,S+,I+} uses CRT { needed for readkey } ; const pr = True { = writes to file : } ; OutFileName = 'BestTest.out'; { MinInt = -32768; MaxWord = 65535; } MaxLongInt = 2147483647; var out : text{file} ; ds : char; cFullsize,{ <= }cAllTests, IL, TestTaskNr, mode, submode : LONGint; type shortI = ShortInt ; { tiny ranges catch more bugs sooner } index = ShortI ; { or much narrower & sharper subrange : } {eg}{ index = 126..127 ; }{ should equal (or at least contain) lo..hi } procedure writelnS( S: string ); begin writeln( S ); if pr then writeln( out, S ) end; procedure writelnSNrS( S0: string; Nr: LONGint; S: string ); begin writeln( S0, Nr, S ); if pr then writeln( out, S0, Nr, S ) end; procedure ASSert( condition: boolean; txt: string ) ; begin if not condition then begin writeln( '>>> False ASSert ',txt); if pr then writeln(out,'>>> False ASSert ',txt); ds:=readkey end end; (* function fassert( condition: boolean; txt: string ) : boolean; begin if condition then fassert:=true else begin writeln( '>>> False fassert: ',txt); if pr then writeln(out,'>>> False fassert: ',txt); fassert:= False; ds:=readkey end end; procedure BreakS( txt: string ); begin writeln('Breakpoint in/at: ',txt); ds:=readkey end; *) function HpowerL( x,{power} y : REAL ): REAL ; begin assert( y < 38,'y = h-lo+1 >= 38 is a too big REAL nr'); assert( y*LN(x) < LN( MaxLONGint ), 'HpowerL < MaxLONGint; Ctrl+Break; do lower h-lo or Vmax-Vmin'); HpowerL := exp( y * LN(x) ) end; procedure Tester { ( VAR V,W,X: kind; lo,hi: index; Vmin,Vmax: atom ) } ; const { tiny bounds also do avoid too long runs } Vmin = 1; {<=} Vmax = 3; { tiny diff Vmax-Vmin avoids HpowerI overflow } lo = 125; {<=} hi = 127; { tiny diff hi-lo avoids HpowerI overflow } { -128 <= ShortInt <= 127 in BP7 / TP / FP }{ find subrange } type { The shorter the type, the more bugs it is likely to catch sooner } atom = ShortInt; { or BYTE, coz tiny ranges catch more bugs sooner } kind = array[lo..hi] of atom; var V, W, X : kind; Value : atom; label DONE ; const fillV = 88 ; { = a fill of V, W, X visualizes ends of subvectors } var jx , { plain index } h, h1st, { <= hi } r : index ; { r is running index of atoms within X, W, V[lo..r..h] } OK : Boolean; { OK is value of a constraint, if any imposed on vector V } procedure ShowV( r: index; st : string ); { for mode = Demo } var kx : index ; begin write( st,' V[lo..r] = '); if pr then write(out,st,' V[lo..r] = '); for kx := lo to r { r hides fillV , h would show fillV } do begin write( V[kx]:3,' '); if pr then write (out, V[kx]:3,' '); end; writeln; if pr then writeln(out); if (cAlltests mod 15) = 0 then begin writeln('have a look, note the depth-1st (sub)order, ' + 'press a key (or Ctrl+break) : '); readkey end end; procedure TestedTasks( st: string ); { user supplied } const noX = false; YesX = true; var ix, jj, p1, p2 : index; procedure ShowBug( r: index; showX : boolean ); var kx : index; begin { To keep this code simple, p1 and p2 may be uninitialized for tasks where p1 and p2 doNt matter } writeln( 'Bug=??? at case=',cAllTests,' : lo=',lo,' <= r =',r, ' <= h =',h,' ; p1=',p1,' p2=',p2,' :'); if pr then writeln(out,'Bug=??? at case=',cAlltests,' : lo=',lo,' <= r =',r, ' <= h =',h,' ; p1=',p1,' p2=',p2,' :'); write ( st,' V[lo..r] = '); if pr then write (out, st,' V[lo..r] = '); for kx := lo to r { r hides fillV , h would show fillV } do { ^^^^^^ lo..h MUST FIT within subrange of kx = index } begin write ( V[kx],' '); if pr then write (out, V[kx],' ') end; writeln; if pr then writeln(out); write ( st,' ??? W[lo..r] = '); if pr then write (out, st,' ??? W[lo..r] = '); for kx := lo to r { r hides fillV , h would show fillV } do begin write ( W[kx],' '); if pr then write (out, W[kx],' ') end; writeln; if pr then writeln(out); if showX then begin write ( st,' ??? X[lo..r] = '); if pr then write (out, st,' ??? X[lo..r] = '); for kx := lo to r { r hides fillV , h would show fillV } do begin write ( X[kx],' '); if pr then write (out, X[kx],' ') end; writeln; if pr then writeln(out) end; writeln; if pr then writeln(out); readkey end{ ShowBug }; procedure swap( VAR{= must } y, z : atom ); var temp : atom; begin temp:=y; y:=z; z:=temp end; procedure InSortUpJH( VAR Ary: kind ; from, upto: index ); var j, k: index; T: atom; { T is a temporary cache } begin { Ary[from] serves as a sentry ie sentinel for the 1st repeat-loop } for k := from +1 to upto do if Ary[k-1] > Ary[k] then { this compare is out of inner repeat-loop } begin j := k; T := Ary[k]; { MUST SAVE record not to lose in shuffle } if T < Ary[from] { 1st loop top trimmed by Hajek: no records compared } then repeat Ary[j]:=Ary[j-1]; j:=j-1 until j = from {or for-downto } else repeat Ary[j]:=Ary[j-1]; j:=j-1 until T >= Ary[j-1];{ 2nd loop} Ary[j] := T { store the SAVED record at not necessarily a final place } end{if for} end; procedure BinInSortUp ( VAR Ary: kind ; from, upto: index ); var j, k: index; T: atom; { T is temporary cache } begin assert( false,'user has to write own BinSortUp'); { dear user , you are invited to put here your Insertion sort which employs a binary search over the ordered part of the array Ary. Then test it to see if you can program a simple algorithm } end; function CheckIfUpSorted( VAR Ary: kind ; from, upto: index ) : boolean; begin assert(false,'user has to write own check of upsortedness of Ary'); CheckIfUpSorted := FALSE { to be changed by the user } end; Begin { TestedTasks : } W:=V; { we must keep array V intact for the backtracker by doing the TestedTasks computations in W } X:=V; { must for relative checking of the result in W against 2nd algorithm which stores its result in X , or vice versa, both results are compared. V must never be changed by tested tasks } case TestTaskNr of { check your checks too } 0: begin { empty slot shows counters only , checks nothing } end; 1: begin InSortUpJH( W, lo, r ); if not(CheckIfUpSorted( W, lo, r )) then ShowBug( r, noX); end; 2: begin BinInSortUp ( W, lo, r ); if not(CheckIfUpSorted( W, lo, r )) then ShowBug( r, noX); end; 3: begin { Reference task1 stores result in W : } InSortUpJH( W, lo, r ); { serves as a reference task } BinInSortUp ( X, lo, r ); for jj := lo to h { to h checks beyond r } do if X[jj] <> W[jj] then ShowBug( r, YesX) ; end; 4: if lo < r then { lo < r is due to the definition of task2 : } begin { Test task2 when distinct indexes p1 and p2 are given : } { Tested task2 : swap the smallest X[p1] into X[lo] and the 2nd smallest X[p2] into X[lo+1] , so that thus rearranged array will contain the same atoms : } { Giving p1 and p2 : } { find p1:} p1:=lo; for ix:=lo to r do if X[ix] < X[p1] then p1:=ix ; { find p2 as the 2nd lowest index other than p1 : } if p1=lo then p2:=lo+1 else p2:=lo; for ix:=lo to r do if (ix <> p1)and(X[ix] < X[p2]) then p2:=ix ; InSortUpJH( W, lo, r ); { serves as a reference for checking because upsorted W solves task2 too } { task2 proper : can you program a correct swapper ? test IT } begin { This swapper is by a poor programmer, not me :-) } swap( X[lo] , X[p1] ); swap( X[lo+1] , X[p2] ); end; { checks:} if (X[lo] <> W[lo])or(X[lo+1] <> W[lo+1]) then ShowBug( r, YesX) end; 5: begin assert(false,'bad case TestTaskNr , free slot is empty') end; 6: begin assert(false,'bad case TestTaskNr , free slot is empty') end; 7: begin assert(false,'bad case TestTaskNr , free slot is empty') end; Else{case} assert(false,'wrong case TestTasknr'); end{case} End{ TestedTasks } ; BEgin writeln('Exhaustively combinatorial testing is up & running...'); write ('wish submode: Demo = 0 ; Test = 1 : '); read( submode ); if submode <> 1 then begin writeln('submode = Demo (no test, just shows generated ' + 'Values; fillV=',fillV,' must never show)'); submode:=0 { demo enforced if no Test = 1 } end; write('wish to run : Straight depth-1st = 1 ; Iterative deepening = 2 : '); read( mode ); h1st := hi { = depth-1st = default } ; if mode=1 then writeln('runs Straight depth-1st data-generator...') else if mode=2 then begin h1st := lo ; writeln('runs Iterative deepening...'); end else begin mode:=1; assert(false,'bad mode cmded, we do Straight :') end ; (* cAllTests:=0; { combinatorial nr of unconstrained partial & full vectors } for IL :=1 to hi-lo+1 do cAllTests := cAllTests + round( HpowerL(Vmax-Vmin+1, IL) ); writeln( cAlltests,' = Total nr of tests to do;' + ' if ok then PRESS A KEY else Ctrl+Break : '); readkey; writelnSNrS('',cAllTests,' = Total of unconstrained (sub)vectors to be' + ' generated & tested :'); assert( 0 < cAllTests,'0 < cAllTests'); *) for jx:=lo to hi{!} do V[jx]:=fillV; W:=V; X:=V; { for readable output } assert( lo <= h1st,'lo <= h1st'); assert( h1st <= hi,'h1st <= hi'); FOR h := h1st to hi do { for iterative deepening iff h1st < hi } Begin { Combinatorial generator-tester proper : } if h1st < hi then begin write ('Next iterative deepening ',lo,'...h=',h ); if h < hi then write(' < ') else if h = hi then write(' = ') else assert(false,'h <= hi'); writeln( hi,'=hi ; a key or Ctrl+break : '); readkey; for jx:=lo to hi{!} do V[jx]:=fillV; W:=V; X:=V; { for readable output } end; writeln(' vector V[ lo=',lo,'..',h,'=h ]; Vmin=',Vmin,', Vmax=',Vmax, ' limit values in each V[.]'); assert( lo <= h,'lo <= h in Tester'); assert( h-lo+1 > 0, 'h-lo+1 > 0'); assert( h-lo+1 < 38,'p = h-lo+1 is too BIG a power in (Vmax-Vmin+1)^p'); assert( Vmin <= Vmax,'Vmin <= Vmax in Tester'); cFullsize := round( HpowerL(Vmax-Vmin+1, h-lo+1) ); assert( 0 < cFullsize,'0 < cFullsize'); writelnSNrS('', cFullsize, ' = Unconstrained full-size variations w/ repetition to be tested'); assert( cFullsize < 1000000000,'cFullsize > 1 Giga , may overflow' ); assert( cFullsize < 1000000,'cFullsize > 1 Mega may run too long'); assert( Vmax-Vmin+1 <= 7,'Vmax-Vmin+1 > 7 may be superfluously high' + ' & run too long'); assert( h-lo+1 <= 7,'h-lo+1 > 7 may be superfluously high & run too long'); cAllTests:=0; cFullsize:=0; { counters of actually tested cases } r := lo; V[r]:=Vmin { = 1st value of the 1st element } ; { after V[r]:= , V[r] can be remapped into eg Y[r] } Repeat { in V[..] all values' variations w/ repetition are generated : } OK := True; { OK = True iff vector V satisfies CONSTRaints, if any specified; use OK to impose task-specific CONSTRaints, if any } if OK and ( r < h {!} ) then begin cAllTests := cAllTests + 1; { stats only } if submode = 0 then ShowV( r,' Demo: part') { shows PARTial vectors ie r < h } else TestedTasks(' Part:'); { tests PARTial vectors ie r < h } { } { CONSTRaints, if any, should be recorded in V[r] here. } r:=r+1; { insert = extend partial vector V[lo..r] = push to stack } V[r]:= Vmin; { next atom V[r] got its 1st value } end else { CONSTRaint not ok , or r = h } begin if OK { = true if CONSTRaint is ok } then begin cFullsize:=cFullsize+1; cAllTests:=cAllTests+1; {stats only} if submode = 0 then ShowV( r,' Demo: full') { shows full-size vectors ie r=h } else TestedTasks(' Full:'); { tests full-size vectors ie r=h } end ; WHILE V[r] = Vmax DO { backtracking loop proper } begin if lo < r then begin V[r]:=fillV; { not needed but if fillV shows up it is KO } r:=r-1; { pop = pull = delete = shortens subvector V[lo..r..] } { undo }{ here CONSTRaints imposed on V[r] should be undone } end else if lo = r then GOTO DONE { backtrack exhausted all possibilities } else assert( false,'r < lo must never occur !') ; end{od}; V[r] := V[r] + 1; { V[r] got the next value } end Until false; DONE : End{for} ; writelnSNrS('',cFullsize,' = Actually generated Full-size' + ' variations w/ repetition'); writelnSNrS('',cAllTests,' = Actual count of all partial and' + ' full vectors tested ; done.'); readkey ENd{ Tester }; BEGIN { main : } assign(out, OutFileName); rewrite(out); writelnS(''); writelnS('Fresh run : BestsJ2 source code is CopyRight (C) 1988+2007,' + ' Jan Hajek, NL'); repeat write('Ctrl+break or wish a TestTaskNr = '); read( TestTaskNr ); assert( 0 <= TestTaskNr, '0 <= TestTaskNr'); assert( TestTaskNr < 999,'TestTaskNr < 999' ); TESTER { ( V, W, X, lo, h, Vmin, Vmax ) } ; writeln('-.-'); if pr then writeln(out,'-.-') until false { = 4ever until killed by Ctrl+break } ; Close(out) END.