""" This is a python library that uses the "jenny" program to produce a list of combinatorial tests. See http://burtleburtle.net/bob/math/jenny.html for a description of jenny. The difference between this script and the jenny program is that this script lets you use human-readable strings, instead of just numbers and letters. The only thing of interest is the print_test_cases function. Everything else is just a helper function. Example usage: import jenny dims = [ ["win32", "linux", "solaris"], ["ls", "rm", "cp", "del", "pwd"], ["telnet", "ssh", "local-machine"], ] incompats = [ # windows doesn't support the ls,rm,cp commands ["win32", "ls", "rm", "cp"] ] reqs = [ # the del command only works on win32 ["del", "win32"] ] # print tests that cover all feature pairs, except the incompatible ones jenny.print_test_cases(dims, 2, incompats=incompats, reqs=reqs) """ import os, re, sys def find_dim_of_feat(readable_feature, dims): for dim in dims: for feat in dim: if feat == readable_feature: return dim raise "Unable to find feature '%s'" % readable_feature def readable_feat_to_jenny_feat(readable_feature, dims): """converts "feature name" to "1a" format """ for dim_index in range(len(dims)): dim = dims[dim_index] for feat_index in range(len(dim)): if dim[feat_index] == readable_feature: return "%d%s" % (dim_index+1, chr(feat_index + ord('a'))) raise "Unable to find feature '%s'" % readable_feature def jenny_feat_to_readable_feat(jenny_feature, dims): """converts "1a" format to "feature name" """ pattern = re.compile('([0-9]+)([a-z]+)') dim_index, feature_index = pattern.match(jenny_feature).group(1, 2) dim = dims[int(dim_index) - 1] feature_name = dim[ord(feature_index) - ord('a')] return feature_name def jenny_test_to_readable_test(line, dims): """ converts "1a 2c 3d" to "(this one) (that one) (the other one)" """ try: could_not_cover = 0 if line.startswith("Could not cover tuple "): line = line[21:] could_not_cover = 1 ret = "" for feature in line.strip(" ").rstrip("\r\n ").split(" "): feature_name = jenny_feat_to_readable_feat(feature, dims) if ret: ret += " " ret += "(%s)" % feature_name if could_not_cover: sys.stderr.write("Could not cover tuple %s\n" % ret) return None return ret except: sys.stderr.write("Error parsing jenny output line: '%s'\n" % line) raise def print_test_cases(dims, arity, incompats=[], reqs=[]): """ dims is an array of arrays of feature names. incompats is an array of arrays of feature names. The first feature in each list should not be used with any of the subsequent features. reqs is an array of feature name pairs. The first feature in each pair requires the second in the pair; meaning it is incompatible with all the other features in the same dimension as the second feature. arity=1 means test each feature at least once arity=2 means test each feature in combination with every other feature arity=3 means test all feature triples ... and so on. """ cmd = "jenny -n%d" % arity for dim in dims: cmd += " %d" % len(dim) for depender_feat, dependee_feat in reqs: for incompat_feat in find_dim_of_feat(dependee_feat, dims): if incompat_feat <> dependee_feat: incompats.append([depender_feat, incompat_feat]) for incompat in incompats: feat_1 = incompat[0] jenny_feat_1 = readable_feat_to_jenny_feat(feat_1, dims) for feat_2 in incompat[1:]: jenny_feat_2 = readable_feat_to_jenny_feat(feat_2, dims) cmd += " -w"+jenny_feat_1+jenny_feat_2 sys.stderr.write("jenny command: " + cmd + "\n") (jenny_stdin, jenny_stdout) = os.popen4(cmd, None) jenny_tests = jenny_stdout.readlines() num_tests = 0 for test in jenny_tests: readable_test = jenny_test_to_readable_test(test, dims) if readable_test <> None: print readable_test + "\n" num_tests += 1 sys.stderr.write("Total test cases required: %d\n" % num_tests)