|
@@ -141,7 +141,9 @@ def regression_test(argsrc, tests, driver_settings=None, cleanup_hack=None,
|
141
|
141
|
|
142
|
142
|
|
143
|
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
|
147
|
start = time.time()
|
146
|
148
|
d = driverClass()
|
147
|
149
|
d.setup(driver_settings, only_own=only_own)
|
|
@@ -150,7 +152,9 @@ def get_data_and_stats(driverClass, argset, driver_settings, only_own=False):
|
150
|
152
|
|
151
|
153
|
|
152
|
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
|
158
|
d = driverClass()
|
155
|
159
|
d.setup(driver_settings, only_own=only_own)
|
156
|
160
|
d.run(argset)
|
|
@@ -194,15 +198,25 @@ class RuleOp:
|
194
|
198
|
|
195
|
199
|
@staticmethod
|
196
|
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
|
220
|
if you need to do that.
|
207
|
221
|
"""
|
208
|
222
|
|
|
@@ -226,8 +240,8 @@ class RuleOp:
|
226
|
240
|
class DictPath:
|
227
|
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
|
246
|
MyData(dict, DictPath):
|
233
|
247
|
pass
|
|
@@ -335,12 +349,13 @@ class DictPath:
|
335
|
349
|
# ########################################################################### #
|
336
|
350
|
|
337
|
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
|
361
|
'argset': {}, # argset as fed into `BaseTestDriver.run`
|
|
@@ -350,40 +365,19 @@ class TinyCase(dict, DictPath):
|
350
|
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
|
383
|
def a_exchange(self, action):
|
|
@@ -479,7 +473,57 @@ class TinyCase(dict, DictPath):
|
479
|
473
|
'round': a_round}
|
480
|
474
|
|
481
|
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
|
528
|
def driver_matches(rule):
|
485
|
529
|
if 'drivers' not in rule:
|
|
@@ -557,33 +601,62 @@ class DriverDataError(Exception):
|
557
|
601
|
class BaseTestDriver:
|
558
|
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
|
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
|
661
|
* `self._mandatory_args`, a list of keys that need to be present
|
589
|
662
|
in `args` argument to `run()`
|
|
@@ -591,7 +664,7 @@ class BaseTestDriver:
|
591
|
664
|
* and `self._mandatory_settings`, a list of keys that need to be
|
592
|
665
|
present in the `settings` argument to `__init__`
|
593
|
666
|
|
594
|
|
- * implement methods
|
|
667
|
+ * Implement methods
|
595
|
668
|
|
596
|
669
|
* `_decode_data` and `_normalize_data`, which are intended to decode
|
597
|
670
|
the data from any raw format it is received, and to prepare it
|
|
@@ -683,15 +756,25 @@ class BaseTestDriver:
|
683
|
756
|
|
684
|
757
|
@classmethod
|
685
|
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
|
762
|
for fn in cls.bailouts:
|
688
|
763
|
if fn(args):
|
689
|
764
|
raise NotImplementedError(inspect.getsource(fn))
|
690
|
765
|
|
691
|
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
|
778
|
if only_own:
|
696
|
779
|
for ckey in settings:
|
697
|
780
|
driver_class_name, setting_name = ckey.split(".", 2)
|
|
@@ -702,7 +785,9 @@ class BaseTestDriver:
|
702
|
785
|
self._setup_ok = True
|
703
|
786
|
|
704
|
787
|
def run(self, args):
|
705
|
|
- """validate, run and store data"""
|
|
788
|
+ """
|
|
789
|
+ Validate args, run SUT and store data.
|
|
790
|
+ """
|
706
|
791
|
|
707
|
792
|
self._args = args
|
708
|
793
|
assert self._setup_ok, "run() before setup()?"
|
|
@@ -736,7 +821,9 @@ class MockDriverTrue(BaseTestDriver):
|
736
|
821
|
# ########################################################################### #
|
737
|
822
|
|
738
|
823
|
class StatCounter:
|
739
|
|
- """A simple counter with formulas support."""
|
|
824
|
+ """
|
|
825
|
+ A simple counter with support for custom formulas.
|
|
826
|
+ """
|
740
|
827
|
|
741
|
828
|
def __init__(self):
|
742
|
829
|
self.generic_stats = {}
|
|
@@ -807,18 +894,24 @@ class StatCounter:
|
807
|
894
|
return computed
|
808
|
895
|
|
809
|
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
|
900
|
self.formulas[vname] = formula
|
812
|
901
|
|
813
|
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
|
906
|
if vname in self.generic_stats:
|
816
|
907
|
self.generic_stats[vname] += value
|
817
|
908
|
else:
|
818
|
909
|
self.generic_stats[vname] = value
|
819
|
910
|
|
820
|
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
|
915
|
dname = dclass.__name__
|
823
|
916
|
if dname not in self.driver_stats:
|
824
|
917
|
self._register(dname)
|
|
@@ -828,15 +921,21 @@ class StatCounter:
|
828
|
921
|
self.driver_stats[dname][vname] = value
|
829
|
922
|
|
830
|
923
|
def count(self, vname):
|
831
|
|
- """Alias to add(vname, 1)"""
|
|
924
|
+ """
|
|
925
|
+ Alias to add(vname, 1)
|
|
926
|
+ """
|
832
|
927
|
self.add(vname, 1)
|
833
|
928
|
|
834
|
929
|
def count_for(self, dclass, vname):
|
835
|
|
- """Alias to add_for(vname, 1)"""
|
|
930
|
+ """
|
|
931
|
+ Alias to add_for(vname, 1)
|
|
932
|
+ """
|
836
|
933
|
self.add_for(dclass, vname, 1)
|
837
|
934
|
|
838
|
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
|
939
|
stats = self.generic_stats
|
841
|
940
|
for dname, dstats in self.driver_stats.items():
|
842
|
941
|
for key, value in dstats.items():
|
|
@@ -846,7 +945,8 @@ class StatCounter:
|
846
|
945
|
|
847
|
946
|
|
848
|
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
|
951
|
Best used as a result bearer from `regression_test`, this class keeps
|
852
|
952
|
a simple in-memory "database" of errors seen during the regression
|
|
@@ -861,13 +961,14 @@ class Tracker(dict):
|
861
|
961
|
a dict) that caused the error.
|
862
|
962
|
|
863
|
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
|
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
|
973
|
3. At final stage, you can retrieve statistics as how many (distinct)
|
873
|
974
|
errors have been recorded, what was the duration of the whole test,
|
|
@@ -897,21 +998,29 @@ class Tracker(dict):
|
897
|
998
|
self.driver_stats = {}
|
898
|
999
|
|
899
|
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
|
1004
|
return '%s/%s.csv' % (prefix, self._eid(errstr))
|
902
|
1005
|
|
903
|
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
|
1010
|
return hashlib.sha1(errstr).hexdigest()[:7]
|
906
|
1011
|
|
907
|
1012
|
def _insert(self, errstr, argset):
|
908
|
|
- """Insert the argset into DB."""
|
|
1013
|
+ """
|
|
1014
|
+ Insert the argset into DB.
|
|
1015
|
+ """
|
909
|
1016
|
if errstr not in self._db:
|
910
|
1017
|
self._db[errstr] = []
|
911
|
1018
|
self._db[errstr].append(argset)
|
912
|
1019
|
|
913
|
1020
|
def _format_error(self, errstr, max_aa=0):
|
914
|
|
- """Format single error for output."""
|
|
1021
|
+ """
|
|
1022
|
+ Format single error for output.
|
|
1023
|
+ """
|
915
|
1024
|
argsets_affected = self._db[errstr]
|
916
|
1025
|
num_aa = len(argsets_affected)
|
917
|
1026
|
|
|
@@ -934,11 +1043,15 @@ class Tracker(dict):
|
934
|
1043
|
#
|
935
|
1044
|
|
936
|
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
|
1049
|
return bool(self._db)
|
939
|
1050
|
|
940
|
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
|
1055
|
error_list = "\n".join([self._format_error(e, max_aa=max_aa)
|
943
|
1056
|
for e in self._db])
|
944
|
1057
|
return ("Found %(total_errors)s (%(distinct_errors)s distinct) errors"
|
|
@@ -948,16 +1061,20 @@ class Tracker(dict):
|
948
|
1061
|
+ "\n\n" + error_list)
|
949
|
1062
|
|
950
|
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
|
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
|
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
|
1080
|
def total_errors():
|
|
@@ -978,7 +1095,8 @@ class Tracker(dict):
|
978
|
1095
|
return stats
|
979
|
1096
|
|
980
|
1097
|
def update(self, error, argset):
|
981
|
|
- """Update tracker with test result.
|
|
1098
|
+ """
|
|
1099
|
+ Update tracker with test result.
|
982
|
1100
|
|
983
|
1101
|
If `bool(error)` is true, it is considered error and argset
|
984
|
1102
|
is inserted to DB with `str(error)` as key. This allows for later
|
|
@@ -990,7 +1108,9 @@ class Tracker(dict):
|
990
|
1108
|
self._insert(errstr, argset)
|
991
|
1109
|
|
992
|
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
|
1114
|
stats = self.getstats()
|
995
|
1115
|
colnames = sorted(stats.keys())
|
996
|
1116
|
with open(fname, 'a') as fh:
|
|
@@ -999,12 +1119,14 @@ class Tracker(dict):
|
999
|
1119
|
cw.writerow(stats)
|
1000
|
1120
|
|
1001
|
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
|
1125
|
Each CSV is named with error EID (first 7 chars of SHA1) and lists
|
1005
|
1126
|
all argument sets affected by this error. This is supposed to make
|
1006
|
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
|
1131
|
def get_all_colnames():
|
1010
|
1132
|
cn = {}
|
|
@@ -1028,13 +1150,18 @@ def dataMatch(pattern, data):
|
1028
|
1150
|
|
1029
|
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
|
1161
|
def listMatch(pattern, data):
|
1037
|
|
- """Match list-like objects"""
|
|
1162
|
+ """
|
|
1163
|
+ Match list-like objects
|
|
1164
|
+ """
|
1038
|
1165
|
assert all([hasattr(o, 'append') for o in [pattern, data]])
|
1039
|
1166
|
results = []
|
1040
|
1167
|
for pv in pattern:
|
|
@@ -1045,7 +1172,9 @@ def dataMatch(pattern, data):
|
1045
|
1172
|
return all(results)
|
1046
|
1173
|
|
1047
|
1174
|
def dictMatch(pattern, data):
|
1048
|
|
- """Match dict-like objects"""
|
|
1175
|
+ """
|
|
1176
|
+ Match dict-like objects
|
|
1177
|
+ """
|
1049
|
1178
|
assert all([hasattr(o, 'iteritems') for o in [pattern, data]])
|
1050
|
1179
|
results = []
|
1051
|
1180
|
try:
|
|
@@ -1068,13 +1197,16 @@ def dataMatch(pattern, data):
|
1068
|
1197
|
|
1069
|
1198
|
|
1070
|
1199
|
def jsDump(data):
|
1071
|
|
- """A human-readable JSON dump."""
|
|
1200
|
+ """
|
|
1201
|
+ A human-readable JSON dump.
|
|
1202
|
+ """
|
1072
|
1203
|
return json.dumps(data, sort_keys=True, indent=4,
|
1073
|
1204
|
separators=(',', ': '))
|
1074
|
1205
|
|
1075
|
1206
|
|
1076
|
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
|
1211
|
'''BETA''' version.
|
1080
|
1212
|
|
|
@@ -1235,33 +1367,31 @@ def jsDiff(dira, dirb, namea="A", nameb="B", chara="a", charb="b"):
|
1235
|
1367
|
|
1236
|
1368
|
|
1237
|
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
|
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
|
1396
|
Consider this example:
|
1267
|
1397
|
|