Browse Source

Clean up and rewrite most docstrings

A big spring clean of the docstrings (sure long time coming!)

 *  Fix typos,

 *  rewrite clumsy explanations,

 *  omit unnecessary and misleading parts,

 *  remove awkward and "smart" language,

 *  use consistent here-doc formatting across small and big docstrings
    (isolate quote mark triplets on own line.)
Alois Mahdal 1 year ago
parent
commit
138b43b17e
2 changed files with 281 additions and 142 deletions
  1. 16
    7
      sznqalibs/bottleneck.py
  2. 265
    135
      sznqalibs/hoover.py

+ 16
- 7
sznqalibs/bottleneck.py View File

2
 
2
 
3
 
3
 
4
 class FrameState:
4
 class FrameState:
5
-    """Abstraction and tracking of frame state"""
5
+    """
6
+    Abstraction and tracking of frame state
7
+    """
6
 
8
 
7
     def __init__(self, max_load, size, debug):
9
     def __init__(self, max_load, size, debug):
8
         self.start = 0
10
         self.start = 0
53
 
55
 
54
 
56
 
55
 class Throttle:
57
 class Throttle:
56
-    """Throttle to allow only certain amount of iteration per given time.
58
+    """
59
+    Throttle to allow only certain amount of iteration per given time.
57
 
60
 
58
     Usage:
61
     Usage:
59
 
62
 
75
     of calls in time.  If your loop takes 1ms and you throttle to 1000 loops
78
     of calls in time.  If your loop takes 1ms and you throttle to 1000 loops
76
     per 10 minutes, all loops will happen in the first second, and the last
79
     per 10 minutes, all loops will happen in the first second, and the last
77
     call will block for 599 seconds.
80
     call will block for 599 seconds.
78
-
79
     """
81
     """
80
 
82
 
81
     def __init__(self, max_load, frame_size=60, debug=False):
83
     def __init__(self, max_load, frame_size=60, debug=False):
82
-        """Create new Throttle.
84
+        """
85
+        Create new Throttle.
83
 
86
 
84
         Only required parameter is `max_load`, which is number of times per
87
         Only required parameter is `max_load`, which is number of times per
85
         frame `Throttle.wait()` returns without blocking.  Optionally you can
88
         frame `Throttle.wait()` returns without blocking.  Optionally you can
95
                                 debug=self.debug)
98
                                 debug=self.debug)
96
 
99
 
97
     def is_closed(self):
100
     def is_closed(self):
98
-        """True if throttle is closed."""
101
+        """
102
+        True if throttle is closed.
103
+        """
99
         return self.frame.is_closed()
104
         return self.frame.is_closed()
100
 
105
 
101
     def is_open(self):
106
     def is_open(self):
102
-        """True if throttle is open."""
107
+        """
108
+        True if throttle is open.
109
+        """
103
         return self.frame.is_open()
110
         return self.frame.is_open()
104
 
111
 
105
     def wait(self):
112
     def wait(self):
106
-        """Return now if throttle is open, otherwise block until it is."""
113
+        """
114
+        Return now if throttle is open, otherwise block until it is.
115
+        """
107
         self.frame.debug()
116
         self.frame.debug()
108
         self.waiting = self.is_closed()
117
         self.waiting = self.is_closed()
109
         while self.waiting:
118
         while self.waiting:

+ 265
- 135
sznqalibs/hoover.py View File

141
 
141
 
142
 
142
 
143
 def get_data_and_stats(driverClass, argset, driver_settings, only_own=False):
143
 def get_data_and_stats(driverClass, argset, driver_settings, only_own=False):
144
-    """Run test with given driver"""
144
+    """
145
+    Run single test, return data and stats.
146
+    """
145
     start = time.time()
147
     start = time.time()
146
     d = driverClass()
148
     d = driverClass()
147
     d.setup(driver_settings, only_own=only_own)
149
     d.setup(driver_settings, only_own=only_own)
150
 
152
 
151
 
153
 
152
 def get_data(driverClass, argset, driver_settings, only_own=False):
154
 def get_data(driverClass, argset, driver_settings, only_own=False):
153
-    """Run test with given driver"""
155
+    """
156
+    Run single test, return data only.
157
+    """
154
     d = driverClass()
158
     d = driverClass()
155
     d.setup(driver_settings, only_own=only_own)
159
     d.setup(driver_settings, only_own=only_own)
156
     d.run(argset)
160
     d.run(argset)
194
 
198
 
195
     @staticmethod
199
     @staticmethod
196
     def Match(pattern, item_ok):
200
     def Match(pattern, item_ok):
197
-        """Evaluate set of logically structured patterns using passed function.
201
+        """
202
+        Evaluate set of logically structured patterns using passed function.
203
+
204
+        *pattern* must be a tuple in form of `(op, items)` where *op* can be
205
+        either `RuleOp.ALL` or `RuleOp.ANY` and *items* is a list of items
206
+        to check using *item_ok* function.
198
 
207
 
199
-        pattern has form of `(op, [item1, item2, ...])` where op can be any of
200
-        pre-defined logical operators (`ALL`/`ANY`, I doubt you will ever need
201
-        more) and item_ok is a function that will be used to evaluate each one
202
-        in the list.  In case an itemN is actually pattern as well, it will be
203
-        recursed into, passing the item_ok on and on.
208
+        *item_ok* is a function that accepts single argument and its return
209
+        value is evaluated for true-ness.
204
 
210
 
205
-        Note that there is no data to evaluate "against",  you can use closure
211
+        Final result is True or False and is computed by combining results
212
+        of individual *item_ok* calls: either all must be true (when `op
213
+        == RuleOp.ALL`) or at least one must be true (when `op == RuleOp.ANY`).
214
+
215
+        The evaluation is done recursively, that is, if an item in the pattern
216
+        is also a pattern itself, it will be evaluated by calling RuleOp.Match
217
+        and passing the same *item_ok* function.
218
+
219
+        Note that there is no data to evaluate "against", you can use closure
206
         if you need to do that.
220
         if you need to do that.
207
         """
221
         """
208
 
222
 
226
 class DictPath:
240
 class DictPath:
227
     """Mixin that adds "path-like" behavior to the top dict of dicts.
241
     """Mixin that adds "path-like" behavior to the top dict of dicts.
228
 
242
 
229
-    Use this class as a mixin for a deep dic-like structure and you can access
230
-    the elements using a path.  For example:
243
+    Use this class as a mixin for a deep dictionary-like structure in order to
244
+    access the elements using a Unix-like path.  For example:
231
 
245
 
232
         MyData(dict, DictPath):
246
         MyData(dict, DictPath):
233
             pass
247
             pass
335
 # ########################################################################### #
349
 # ########################################################################### #
336
 
350
 
337
 class TinyCase(dict, DictPath):
351
 class TinyCase(dict, DictPath):
338
-    """Abstraction of the smallest unit of testing.
352
+    """Test case for hoover.
339
 
353
 
340
-    This class is intended to hold relevant data after the actual test
341
-    and apply transformations (hacks) as defined by rules.
354
+    This class is used as an intermediary container for test parameters,
355
+    oracles and test results.  This is to allow post-test transformations
356
+    ("hacks") to happen before the result is evaluated for pass/fail.
342
 
357
 
343
-    The data form (self) is:
358
+    Instantiate TinyCase with data (self) in following format:
344
 
359
 
345
         {
360
         {
346
             'argset': {},   # argset as fed into `BaseTestDriver.run`
361
             'argset': {},   # argset as fed into `BaseTestDriver.run`
350
             'rname': ""     # name of result driver's class
365
             'rname': ""     # name of result driver's class
351
         }
366
         }
352
 
367
 
353
-    The transformation is done using the `TinyCase.hack()` method to which
354
-    a list of rules is passed.  Each rule is applied, and rules are expected
355
-    to be in a following form:
368
+    Then call TinyCase.hack() with a set of rules which can alter oracles,
369
+    results or both based on the data stored in TinyCase.
356
 
370
 
357
-        {
358
-            'drivers': [{}],        # list of structures to match against self
359
-            'argsets': [{}],        # -ditto-
360
-            'action_name': <Arg>    # an action name with argument
361
-        }
371
+    Typical use cases for 'hacks' are:
372
+
373
+     *  avoid known and tracked bugs,
374
+     *  help normalize results (remove irrelevant details),
375
+     *  solve certain limitations in oracle machines.
362
 
376
 
363
-    For each of patterns ('drivers', argsets') present, match against self
364
-    is done using function `hoover.dataMatch`, which is basically a recursive
365
-    test if the pattern is a subset of the case.  If none of results is
366
-    negative (i.e. both patterns missing results in match), any known actions
367
-    included in the rule are called.  Along with action name a list or a dict
368
-    providing necessary parameters is expected: this is simply passed as only
369
-    parameter to corresponding method.
370
-
371
-    Actions use specific way how to address elements in the structures
372
-    saved in the oracle and result keys provided by `DictPath`, which makes
373
-    it easy to define rules for arbitrarily complex dictionary structures.
374
-    The format resembles to Unix path, where "directories" are dict
375
-    keys and "root" is the `self` of the `TinyCase` instance:
376
-
377
-        /oracle/temperature
378
-        /result/stats/word_count
379
-
380
-    Refer to each action's docstring for descriprion of their function
381
-    as well as expected format of argument.  The name of action as used
382
-    in the reule is the name of method without leading 'a_'.
383
-
384
-    Warning: All actions will silently ignore any paths that are invalid
385
-             or leading to non-existent data!
386
-             (This does not apply to a path leading to `None`.)
377
+    Note that while for most tests, you should strive for zero hacks,
378
+    sometimes they are inevitable.  In such cases, number of hacks can
379
+    be a useful quality metric.   For that reason, 'hoover.regression_test'
380
+    will count the applied hacks and return it in the test report.
387
     """
381
     """
388
 
382
 
389
     def a_exchange(self, action):
383
     def a_exchange(self, action):
479
                      'round': a_round}
473
                      'round': a_round}
480
 
474
 
481
     def hack(self, ruleset):
475
     def hack(self, ruleset):
482
-        """Apply action from each rule, if patterns match."""
476
+        """
477
+        Run any matching actions in the *ruleset*.
478
+
479
+        Each rule must be in in a following form:
480
+
481
+            {
482
+                'drivers': [{}],        # list of structures to match
483
+                                        # against self
484
+                'argsets': [{}],        # -ditto-
485
+                <action_name>: <Arg>    # an action name with argument
486
+                <action_name>: <Arg>    # another action...
487
+            }
488
+
489
+        Each of the rules is first evaluated for match (does it apply to this
490
+        TinyCase?), and if the rule applies, transformation is done.  The
491
+        transformation is defined by `<action_name>: <Arg>` pairs and it can
492
+        alter 'oracle', 'result' or both.
493
+
494
+        The match evaluation is done using `hoover.dataMatch()` -- this is
495
+        basically a recursive pattern match against 'drivers' and 'argsets'.
496
+        Both 'drivers' and 'argsets' are optional, but when specified, all
497
+        items must must match in order for the rule to apply.  (If 'drivers'
498
+        and 'argsets' are both missing or empty, rule will apply to each and
499
+        all test cases.)
500
+
501
+        If rule does not match, `TinyCase.hack()` moves on to next one.
502
+
503
+        If a rule does match, `TinyCase.hack()` will look for actions defined
504
+        in it.  Action consists of action name (key of the rule dictionary,
505
+        <action_name>) and an argument (<Arg>).
506
+
507
+        Action name must be one of: 'remove', 'even_up', 'format_str',
508
+        'exchange' or 'round'.  Each action corresponds to a TinyCase method
509
+        prefixed by 'a_'; for example 'even_up' action corresponds to
510
+        TinyCase.a_even_up method.  Each action expects different argument
511
+        so see the corresponding method docstrings.
512
+
513
+        Because 'oracle' and 'result' can be relatively complex structures,
514
+        actions accept Unix-like paths to specify elements inside them.
515
+        The "root" of the path is the TinyCase instance, and "directories"
516
+        are keys under it.  For example, following would be valid paths
517
+        if test drivers work with dictionaries such as `{'temperature': 50,
518
+        'stats': {'word_count': 15}}`:
519
+
520
+            /oracle/temperature
521
+            /result/stats/word_count
522
+
523
+        Warning: All actions will silently ignore any paths that are invalid
524
+                 or leading to non-existent data!
525
+                 (This does not apply to a path leading to `None`.)
526
+        """
483
 
527
 
484
         def driver_matches(rule):
528
         def driver_matches(rule):
485
             if 'drivers' not in rule:
529
             if 'drivers' not in rule:
557
 class BaseTestDriver:
601
 class BaseTestDriver:
558
     """Base class for test drivers used by `hoover.regression_test` and others.
602
     """Base class for test drivers used by `hoover.regression_test` and others.
559
 
603
 
560
-    This class is used to create a test driver, which is an abstraction
561
-    and encapsulation of the system being tested.  Or, the driver in fact
562
-    can be just a "mock" driver that provides data for comparison with
563
-    a "real" driver.
564
-
565
-    The minimum you need to create a working driver is to implement a working
566
-    `self._get_data` method that sets `self.data`.  Any exception from this
567
-    method will be re-raised as DriverError with additional information.
568
-
569
-    Also, you can set self.duration (in fractional seconds, as returned by
570
-    standard time module) in the _get_data method, but if you don't, it is
571
-    measured for you as time the method call took.  This is useful if you
572
-    need to fetch the data from some other driver or a gateway, and you
573
-    have better mechanism to determine how long the action would take "in
574
-    real life".
575
-
576
-    For example, if we are testing a Java library using a Py4J gateway,
577
-    we need to do some more conversions outside our testing code just to
578
-    be able to use the data in our Python test.  We don't want to include
579
-    this in the "duration", since we are measuring the Java library, not the
580
-    Py4J GW (or our ability to perform the conversions optimally).  So we
581
-    do our measurement within the Java machine and pass the result to the
582
-    Python driver.
604
+    This class tepresents test driver and can be used to:
605
+
606
+     *  Wrap system under test (SUT).
607
+
608
+        Provide simple interface to set up, sandbox and activate the system
609
+        and collect any relevant results.  This can be merely return value
610
+        (purely functional test) but also other characteristics such as
611
+        time to complete.
612
+
613
+     *  Mimic ("mock") the system under test.
614
+
615
+        Also called as oracle machine, this can be used to predict expected
616
+        behavior of SUT under given parameters.
617
+
618
+     *  Wrap an alternative implementation of SUT.
619
+
620
+        As a special case of the previous role, sometimes it's desirable to
621
+        use an alternative implementation of SUT as oracle machine.  This
622
+        can be a legacy implementation, reference implementation or other
623
+        platform implementation.
624
+
625
+    In either case, the driver makes sure that any input arguments are
626
+    interpreted (and passed on) correctly and any results are returned in
627
+    a consistent way.
628
+
629
+    To use this class, sub-class it and implement `_get_data()` method.
630
+    Tge `_get_data()` method must:
631
+
632
+     *  Accept single argument; this contains arguments to the SUT.
633
+
634
+        If using `hoover.regression_test()`, this value will be retrieved
635
+        from the *argsrc* iterator.
636
+
637
+     *  Implement the test case defined by the argument set.
638
+
639
+        The implementation can either be a wrapper to real SUT, alternative
640
+        one, or can be an oracle machine -- i.e. it can figure out the result
641
+        on its own.  Note that this can be much easier as it sounds, given
642
+        that you can "cheat" by crafting the set of test cases so that the
643
+        prediction is easy (but still effective at hitting bugs), or you
644
+        can "hide the answer" in the *args* itself, and define set of
645
+        test cases statically in form of "question, answer" pairs.
646
+
647
+     *  Collect any relevant data and set it to `data` property.
648
+
649
+        Optionally, you can also set `duration` property (in fractional
650
+        seconds, as returned by standard time module).  If you don't
651
+        it will be automatically measured.
652
+
653
+    Any exception from the *_get_data* method will be re-raised as
654
+    DriverError.
583
 
655
 
584
     Optionally, you can:
656
     Optionally, you can:
585
 
657
 
586
-    *   Make an __init__ and after calling base __init__, set
658
+    *   Implement *__init__* method calling base __init__ and setting more
659
+        properties:
587
 
660
 
588
         *   `self._mandatory_args`, a list of keys that need to be present
661
         *   `self._mandatory_args`, a list of keys that need to be present
589
             in `args` argument to `run()`
662
             in `args` argument to `run()`
591
         *   and `self._mandatory_settings`, a list of keys that need to be
664
         *   and `self._mandatory_settings`, a list of keys that need to be
592
             present in the `settings` argument to `__init__`
665
             present in the `settings` argument to `__init__`
593
 
666
 
594
-    *   implement methods
667
+    *   Implement methods
595
 
668
 
596
         *   `_decode_data` and `_normalize_data`, which are intended to decode
669
         *   `_decode_data` and `_normalize_data`, which are intended to decode
597
              the data from any raw format it is received, and to prepare it
670
              the data from any raw format it is received, and to prepare it
683
 
756
 
684
     @classmethod
757
     @classmethod
685
     def check_values(cls, args=None):
758
     def check_values(cls, args=None):
686
-        """check args in advance before running or setting up anything"""
759
+        """
760
+        Check args in advance before running or setting up anything.
761
+        """
687
         for fn in cls.bailouts:
762
         for fn in cls.bailouts:
688
             if fn(args):
763
             if fn(args):
689
                 raise NotImplementedError(inspect.getsource(fn))
764
                 raise NotImplementedError(inspect.getsource(fn))
690
 
765
 
691
     def setup(self, settings, only_own=False):
766
     def setup(self, settings, only_own=False):
692
-        """Load settings. only_own means that only settings that belong to us
693
-        are loaded ("DriverClass.settingName", the first discriminating part
694
-        is removed)"""
767
+        """
768
+        Load settings.
769
+
770
+        If *only_own* is false, *settings* are merely assigned to
771
+        settings attribute.
772
+
773
+        if *only_own* is true, settings are filtered:  Any keys that don't
774
+        begin with the prefix of driver class name and period are ignored.
775
+        Settings that do start with this prefix are assigned to settings
776
+        attribute with the prefix removed.
777
+        """
695
         if only_own:
778
         if only_own:
696
             for ckey in settings:
779
             for ckey in settings:
697
                 driver_class_name, setting_name = ckey.split(".", 2)
780
                 driver_class_name, setting_name = ckey.split(".", 2)
702
         self._setup_ok = True
785
         self._setup_ok = True
703
 
786
 
704
     def run(self, args):
787
     def run(self, args):
705
-        """validate, run and store data"""
788
+        """
789
+        Validate args, run SUT and store data.
790
+        """
706
 
791
 
707
         self._args = args
792
         self._args = args
708
         assert self._setup_ok, "run() before setup()?"
793
         assert self._setup_ok, "run() before setup()?"
736
 # ########################################################################### #
821
 # ########################################################################### #
737
 
822
 
738
 class StatCounter:
823
 class StatCounter:
739
-    """A simple counter with formulas support."""
824
+    """
825
+    A simple counter with support for custom formulas.
826
+    """
740
 
827
 
741
     def __init__(self):
828
     def __init__(self):
742
         self.generic_stats = {}
829
         self.generic_stats = {}
807
         return computed
894
         return computed
808
 
895
 
809
     def add_formula(self, vname, formula):
896
     def add_formula(self, vname, formula):
810
-        """Add a function to work with generic_stats, driver_stats."""
897
+        """
898
+        Add a function to work with generic_stats, driver_stats.
899
+        """
811
         self.formulas[vname] = formula
900
         self.formulas[vname] = formula
812
 
901
 
813
     def add(self, vname, value):
902
     def add(self, vname, value):
814
-        """Add a value to generic stat counter."""
903
+        """
904
+        Add a value to generic stat counter.
905
+        """
815
         if vname in self.generic_stats:
906
         if vname in self.generic_stats:
816
             self.generic_stats[vname] += value
907
             self.generic_stats[vname] += value
817
         else:
908
         else:
818
             self.generic_stats[vname] = value
909
             self.generic_stats[vname] = value
819
 
910
 
820
     def add_for(self, dclass, vname, value):
911
     def add_for(self, dclass, vname, value):
821
-        """Add a value to driver stat counter."""
912
+        """
913
+        Add a value to driver stat counter.
914
+        """
822
         dname = dclass.__name__
915
         dname = dclass.__name__
823
         if dname not in self.driver_stats:
916
         if dname not in self.driver_stats:
824
             self._register(dname)
917
             self._register(dname)
828
             self.driver_stats[dname][vname] = value
921
             self.driver_stats[dname][vname] = value
829
 
922
 
830
     def count(self, vname):
923
     def count(self, vname):
831
-        """Alias to add(vname, 1)"""
924
+        """
925
+        Alias to add(vname, 1)
926
+        """
832
         self.add(vname, 1)
927
         self.add(vname, 1)
833
 
928
 
834
     def count_for(self, dclass, vname):
929
     def count_for(self, dclass, vname):
835
-        """Alias to add_for(vname, 1)"""
930
+        """
931
+        Alias to add_for(vname, 1)
932
+        """
836
         self.add_for(dclass, vname, 1)
933
         self.add_for(dclass, vname, 1)
837
 
934
 
838
     def all_stats(self):
935
     def all_stats(self):
839
-        """Compute stats from formulas and add them to colledted data."""
936
+        """
937
+        Compute stats from formulas and add them to colledted data.
938
+        """
840
         stats = self.generic_stats
939
         stats = self.generic_stats
841
         for dname, dstats in self.driver_stats.items():
940
         for dname, dstats in self.driver_stats.items():
842
             for key, value in dstats.items():
941
             for key, value in dstats.items():
846
 
945
 
847
 
946
 
848
 class Tracker(dict):
947
 class Tracker(dict):
849
-    """Error tracker to allow for usable reports from huge regression tests.
948
+    """
949
+    Error tracker to allow for usable reports from huge regression tests.
850
 
950
 
851
     Best used as a result bearer from `regression_test`, this class keeps
951
     Best used as a result bearer from `regression_test`, this class keeps
852
     a simple in-memory "database" of errors seen during the regression
952
     a simple in-memory "database" of errors seen during the regression
861
             a dict) that caused the error.
961
             a dict) that caused the error.
862
 
962
 
863
             If boolean value of the result is False, the object is thrown away
963
             If boolean value of the result is False, the object is thrown away
864
-            and nothing happen.  Otherwise, its string value is used as a key
964
+            and nothing happens.  Otherwise, its string value is used as a key
865
             under which the argument set is saved.
965
             under which the argument set is saved.
866
 
966
 
867
-            As you can see, the string is supposed to be ''as deterministic
868
-            as possible'', i.e. it should provide as little information
869
-            about the error as is necessary.  Do not include any timestamps
870
-            or "volatile" values.
967
+            The string interpretation of the result is supposed to be
968
+            "as deterministic as possible", i.e. it should provide only
969
+            necessary information about the error:  do not include any
970
+            timestamps or "volatile" values such as PID's, version numbers
971
+            or tempfile names.
871
 
972
 
872
          3. At final stage, you can retrieve statistics as how many (distinct)
973
          3. At final stage, you can retrieve statistics as how many (distinct)
873
             errors have been recorded, what was the duration of the whole test,
974
             errors have been recorded, what was the duration of the whole test,
897
         self.driver_stats = {}
998
         self.driver_stats = {}
898
 
999
 
899
     def _csv_fname(self, errstr, prefix):
1000
     def _csv_fname(self, errstr, prefix):
900
-        """Format name of file for this error string"""
1001
+        """
1002
+        Format name of file for this error string
1003
+        """
901
         return '%s/%s.csv' % (prefix, self._eid(errstr))
1004
         return '%s/%s.csv' % (prefix, self._eid(errstr))
902
 
1005
 
903
     def _eid(self, errstr):
1006
     def _eid(self, errstr):
904
-        """Return EID for the error string (first 7 chars of SHA1)."""
1007
+        """
1008
+        Return EID for the error string (first 7 chars of SHA1).
1009
+        """
905
         return hashlib.sha1(errstr).hexdigest()[:7]
1010
         return hashlib.sha1(errstr).hexdigest()[:7]
906
 
1011
 
907
     def _insert(self, errstr, argset):
1012
     def _insert(self, errstr, argset):
908
-        """Insert the argset into DB."""
1013
+        """
1014
+        Insert the argset into DB.
1015
+        """
909
         if errstr not in self._db:
1016
         if errstr not in self._db:
910
             self._db[errstr] = []
1017
             self._db[errstr] = []
911
         self._db[errstr].append(argset)
1018
         self._db[errstr].append(argset)
912
 
1019
 
913
     def _format_error(self, errstr, max_aa=0):
1020
     def _format_error(self, errstr, max_aa=0):
914
-        """Format single error for output."""
1021
+        """
1022
+        Format single error for output.
1023
+        """
915
         argsets_affected = self._db[errstr]
1024
         argsets_affected = self._db[errstr]
916
         num_aa = len(argsets_affected)
1025
         num_aa = len(argsets_affected)
917
 
1026
 
934
     #
1043
     #
935
 
1044
 
936
     def errors_found(self):
1045
     def errors_found(self):
937
-        """Return number of non-distinct errors in db."""
1046
+        """
1047
+        Return number of non-distinct errors in db.
1048
+        """
938
         return bool(self._db)
1049
         return bool(self._db)
939
 
1050
 
940
     def format_report(self, max_aa=0):
1051
     def format_report(self, max_aa=0):
941
-        """Return complete report formatted as string."""
1052
+        """
1053
+        Return complete report formatted as string.
1054
+        """
942
         error_list = "\n".join([self._format_error(e, max_aa=max_aa)
1055
         error_list = "\n".join([self._format_error(e, max_aa=max_aa)
943
                                 for e in self._db])
1056
                                 for e in self._db])
944
         return ("Found %(total_errors)s (%(distinct_errors)s distinct) errors"
1057
         return ("Found %(total_errors)s (%(distinct_errors)s distinct) errors"
948
                 + "\n\n" + error_list)
1061
                 + "\n\n" + error_list)
949
 
1062
 
950
     def getstats(self):
1063
     def getstats(self):
951
-        """Return basic and driver stats
1064
+        """
1065
+        Return basic and driver stats
1066
+
1067
+        Returns dictionary with following values:
952
 
1068
 
953
-            argsets_done - this should must be raised by outer code,
954
-                           once per each unique argset
955
-            tests_done   - how many times Tracker.update() was called
956
-            distinct_errors - how many distinct errors (same `str(error)`)
1069
+            'tests_done' - how many times Tracker.update() was called
1070
+
1071
+            'distinct_errors' - how many distinct errors (same `str(error)`)
957
                            were seen by Tracker.update()
1072
                            were seen by Tracker.update()
958
-            total_errors - how many times `Tracker.update()` saw an
1073
+
1074
+            'total_errors' - how many times `Tracker.update()` saw an
959
                            error, i.e. how many argsets are in DB
1075
                            error, i.e. how many argsets are in DB
960
-            time         - how long since init (seconds)
1076
+
1077
+            'time'       - how long since init (seconds)
961
         """
1078
         """
962
 
1079
 
963
         def total_errors():
1080
         def total_errors():
978
         return stats
1095
         return stats
979
 
1096
 
980
     def update(self, error, argset):
1097
     def update(self, error, argset):
981
-        """Update tracker with test result.
1098
+        """
1099
+        Update tracker with test result.
982
 
1100
 
983
         If `bool(error)` is true, it is considered error and argset
1101
         If `bool(error)` is true, it is considered error and argset
984
         is inserted to DB with `str(error)` as key.  This allows for later
1102
         is inserted to DB with `str(error)` as key.  This allows for later
990
             self._insert(errstr, argset)
1108
             self._insert(errstr, argset)
991
 
1109
 
992
     def write_stats_csv(self, fname):
1110
     def write_stats_csv(self, fname):
993
-        """Write stats to a simple one row (plus header) CSV."""
1111
+        """
1112
+        Write stats to a simple one row (plus header) CSV.
1113
+        """
994
         stats = self.getstats()
1114
         stats = self.getstats()
995
         colnames = sorted(stats.keys())
1115
         colnames = sorted(stats.keys())
996
         with open(fname, 'a') as fh:
1116
         with open(fname, 'a') as fh:
999
             cw.writerow(stats)
1119
             cw.writerow(stats)
1000
 
1120
 
1001
     def write_args_csv(self, prefix=''):
1121
     def write_args_csv(self, prefix=''):
1002
-        """Write out a set of CSV files, one per distinctive error.
1122
+        """
1123
+        Write out a set of CSV files, one per distinctive error.
1003
 
1124
 
1004
         Each CSV is named with error EID (first 7 chars of SHA1) and lists
1125
         Each CSV is named with error EID (first 7 chars of SHA1) and lists
1005
         all argument sets affected by this error.  This is supposed to make
1126
         all argument sets affected by this error.  This is supposed to make
1006
         easier to further analyse impact and trigerring values of errors,
1127
         easier to further analyse impact and trigerring values of errors,
1007
-        perhaps using a table processor software."""
1128
+        perhaps using a table processor software.
1129
+        """
1008
 
1130
 
1009
         def get_all_colnames():
1131
         def get_all_colnames():
1010
             cn = {}
1132
             cn = {}
1028
 
1150
 
1029
     Supports lists, dictionaries and scalars (int, float, string).
1151
     Supports lists, dictionaries and scalars (int, float, string).
1030
 
1152
 
1031
-    For scalars, simple `==` is used.  Lists are converted to sets and
1032
-    "to match" means "to have a matching subset (e.g. `[1, 2, 3, 4]`
1033
-    matches `[3, 2]`).  Both lists and dictionaries are matched recursively.
1153
+    For scalars, simple `==` is used.
1154
+
1155
+    Lists are converted to sets and "to match" means "to have a matching
1156
+    subset (e.g. `[1, 2, 3, 4]` matches `[3, 2]`).
1157
+
1158
+    Both lists and dictionaries are matched recursively.
1034
     """
1159
     """
1035
 
1160
 
1036
     def listMatch(pattern, data):
1161
     def listMatch(pattern, data):
1037
-        """Match list-like objects"""
1162
+        """
1163
+        Match list-like objects
1164
+        """
1038
         assert all([hasattr(o, 'append') for o in [pattern, data]])
1165
         assert all([hasattr(o, 'append') for o in [pattern, data]])
1039
         results = []
1166
         results = []
1040
         for pv in pattern:
1167
         for pv in pattern:
1045
         return all(results)
1172
         return all(results)
1046
 
1173
 
1047
     def dictMatch(pattern, data):
1174
     def dictMatch(pattern, data):
1048
-        """Match dict-like objects"""
1175
+        """
1176
+        Match dict-like objects
1177
+        """
1049
         assert all([hasattr(o, 'iteritems') for o in [pattern, data]])
1178
         assert all([hasattr(o, 'iteritems') for o in [pattern, data]])
1050
         results = []
1179
         results = []
1051
         try:
1180
         try:
1068
 
1197
 
1069
 
1198
 
1070
 def jsDump(data):
1199
 def jsDump(data):
1071
-    """A human-readable JSON dump."""
1200
+    """
1201
+    A human-readable JSON dump.
1202
+    """
1072
     return json.dumps(data, sort_keys=True, indent=4,
1203
     return json.dumps(data, sort_keys=True, indent=4,
1073
                       separators=(',', ': '))
1204
                       separators=(',', ': '))
1074
 
1205
 
1075
 
1206
 
1076
 def jsDiff(dira, dirb, namea="A", nameb="B", chara="a", charb="b"):
1207
 def jsDiff(dira, dirb, namea="A", nameb="B", chara="a", charb="b"):
1077
-    """JSON-based human-readable diff of two data structures.
1208
+    """
1209
+    JSON-based human-readable diff of two data structures.
1078
 
1210
 
1079
     '''BETA''' version.
1211
     '''BETA''' version.
1080
 
1212
 
1235
 
1367
 
1236
 
1368
 
1237
 class Cartman:
1369
 class Cartman:
1238
-    """Create argument sets from ranges (or ay iterators) of values.
1370
+    """
1371
+    Create argument sets from ranges (or ay iterators) of values.
1239
 
1372
 
1240
     This class is to enable easy definition and generation of dictionary
1373
     This class is to enable easy definition and generation of dictionary
1241
-    argument  sets using Cartesian product.  You only need to define:
1242
-
1243
-     *  structure of argument set (can be more than just flat dict)
1374
+    argument sets using Cartesian product.
1244
 
1375
 
1245
-     *  ranges, or arbitrary iterators of values on each "leaf" of the
1246
-        argument set
1376
+    To use Cartman iterator, you need to define structure of an argument
1377
+    set.  Argument set--typically a dictionary--is a set of values that
1378
+    together constitute a test case.  Within the argument set, values
1379
+    will change from test case to test case, so for each changing value,
1380
+    you will also need to define range of values you want to test on.
1247
 
1381
 
1248
-    Since there is expectation that any argument can have any kind of values
1249
-    even another iterables, the pure logic "iterate it if you can"
1250
-    is insufficient.  Instead, definition is divided in two parts:
1382
+    Cartman initiator expects following arguments:
1251
 
1383
 
1252
-     *  scheme, which is a "prototype" of a final argument set, except
1253
-        that for each value that will change, a `Cartman.Iterable`
1254
-        sentinel is used.  For each leaf that is constant, `Cartman.Scalar`
1255
-        is used
1384
+     *  *scheme*, which is a "prototype" of a final argument set, except
1385
+        that values are replaced by either `Cartman.Iterable` if the
1386
+        value is changing from test case to another, and `Cartman.Scalar`
1387
+        if the value is constant.
1256
 
1388
 
1257
-     *  source, which has the same structure, except that where in scheme
1258
-        is `Iterable`, an iterable object is expected, whereas in places
1259
-        where `Scalar` is used, a value is assigned that does not change
1260
-        during iteration.
1389
+     *  *source*, which has the same structure, except that where in scheme
1390
+        is `Cartman.Iterable`, the source has an iterable.  Where scheme has
1391
+        `Cartman.Scalar`, the source can have any value.
1261
 
1392
 
1262
-    Finally, when such instance is used in loop, argument sets are generated
1263
-    uising Cartesian product of each iterable found.  This allows for
1264
-    relatively easy definition of complex scenarios.
1393
+    Finally, when Cartman instance is used in loop, it uses Cartesian product
1394
+    in order to generate argument sets.
1265
 
1395
 
1266
     Consider this example:
1396
     Consider this example:
1267
 
1397