2011-12-22 21:04:30 +00:00
|
|
|
"""
|
|
|
|
Unit-test suite for the S3 fuzzer
|
|
|
|
|
|
|
|
The fuzzer is a grammar-based random S3 operation generator
|
|
|
|
that produces random operation sequences in an effort to
|
|
|
|
crash the server. This unit-test suite does not test
|
|
|
|
S3 servers, but rather the fuzzer infrastructure.
|
|
|
|
|
|
|
|
It works by running the fuzzer off of a simple grammar,
|
|
|
|
and checking the producted requests to ensure that they
|
|
|
|
include the expected sorts of operations in the expected
|
|
|
|
proportions.
|
|
|
|
"""
|
2011-10-13 20:34:23 +00:00
|
|
|
import sys
|
2011-08-11 18:32:18 +00:00
|
|
|
import itertools
|
2011-08-08 23:51:10 +00:00
|
|
|
import nose
|
|
|
|
import random
|
|
|
|
import string
|
|
|
|
import yaml
|
|
|
|
|
2011-10-13 20:34:23 +00:00
|
|
|
from ..headers import *
|
2011-08-08 23:51:10 +00:00
|
|
|
|
|
|
|
from nose.tools import eq_ as eq
|
2011-08-15 21:16:40 +00:00
|
|
|
from nose.tools import assert_true
|
2011-08-08 23:51:10 +00:00
|
|
|
from nose.plugins.attrib import attr
|
|
|
|
|
2011-10-13 20:34:23 +00:00
|
|
|
from ...functional.utils import assert_raises
|
2019-03-22 17:58:30 +00:00
|
|
|
from functools import reduce
|
2011-08-08 23:51:10 +00:00
|
|
|
|
|
|
|
_decision_graph = {}
|
|
|
|
|
|
|
|
def check_access_denied(fn, *args, **kwargs):
|
|
|
|
e = assert_raises(boto.exception.S3ResponseError, fn, *args, **kwargs)
|
|
|
|
eq(e.status, 403)
|
|
|
|
eq(e.reason, 'Forbidden')
|
|
|
|
eq(e.error_code, 'AccessDenied')
|
|
|
|
|
|
|
|
|
2011-08-09 18:56:38 +00:00
|
|
|
def build_graph():
|
|
|
|
graph = {}
|
|
|
|
graph['start'] = {
|
|
|
|
'set': {},
|
2011-08-09 22:44:25 +00:00
|
|
|
'choices': ['node2']
|
2011-08-09 18:56:38 +00:00
|
|
|
}
|
|
|
|
graph['leaf'] = {
|
|
|
|
'set': {
|
|
|
|
'key1': 'value1',
|
|
|
|
'key2': 'value2'
|
|
|
|
},
|
2011-08-11 18:32:18 +00:00
|
|
|
'headers': [
|
|
|
|
['1-2', 'random-header-{random 5-10 printable}', '{random 20-30 punctuation}']
|
|
|
|
],
|
2011-08-09 18:56:38 +00:00
|
|
|
'choices': []
|
|
|
|
}
|
|
|
|
graph['node1'] = {
|
|
|
|
'set': {
|
2011-08-11 18:32:18 +00:00
|
|
|
'key3': 'value3',
|
|
|
|
'header_val': [
|
|
|
|
'3 h1',
|
|
|
|
'2 h2',
|
|
|
|
'h3'
|
|
|
|
]
|
2011-08-09 18:56:38 +00:00
|
|
|
},
|
2011-08-11 18:32:18 +00:00
|
|
|
'headers': [
|
|
|
|
['1-1', 'my-header', '{header_val}'],
|
|
|
|
],
|
2011-08-09 18:56:38 +00:00
|
|
|
'choices': ['leaf']
|
|
|
|
}
|
2011-08-09 22:44:25 +00:00
|
|
|
graph['node2'] = {
|
|
|
|
'set': {
|
|
|
|
'randkey': 'value-{random 10-15 printable}',
|
|
|
|
'path': '/{bucket_readable}',
|
|
|
|
'indirect_key1': '{key1}'
|
|
|
|
},
|
|
|
|
'choices': ['leaf']
|
|
|
|
}
|
2011-08-09 18:56:38 +00:00
|
|
|
graph['bad_node'] = {
|
|
|
|
'set': {
|
|
|
|
'key1': 'value1'
|
|
|
|
},
|
|
|
|
'choices': ['leaf']
|
|
|
|
}
|
2011-08-11 19:25:13 +00:00
|
|
|
graph['nonexistant_child_node'] = {
|
|
|
|
'set': {},
|
|
|
|
'choices': ['leafy_greens']
|
|
|
|
}
|
2011-08-10 22:10:24 +00:00
|
|
|
graph['weighted_node'] = {
|
|
|
|
'set': {
|
|
|
|
'k1': [
|
|
|
|
'foo',
|
|
|
|
'2 bar',
|
|
|
|
'1 baz'
|
|
|
|
]
|
|
|
|
},
|
2011-08-10 20:26:00 +00:00
|
|
|
'choices': [
|
|
|
|
'foo',
|
|
|
|
'2 bar',
|
|
|
|
'1 baz'
|
|
|
|
]
|
|
|
|
}
|
2011-08-11 19:25:13 +00:00
|
|
|
graph['null_choice_node'] = {
|
|
|
|
'set': {},
|
|
|
|
'choices': [None]
|
|
|
|
}
|
2011-08-15 21:16:40 +00:00
|
|
|
graph['repeated_headers_node'] = {
|
|
|
|
'set': {},
|
|
|
|
'headers': [
|
|
|
|
['1-2', 'random-header-{random 5-10 printable}', '{random 20-30 punctuation}']
|
|
|
|
],
|
|
|
|
'choices': ['leaf']
|
|
|
|
}
|
2011-08-11 19:25:13 +00:00
|
|
|
graph['weighted_null_choice_node'] = {
|
|
|
|
'set': {},
|
|
|
|
'choices': ['3 null']
|
|
|
|
}
|
2011-08-09 18:56:38 +00:00
|
|
|
return graph
|
|
|
|
|
|
|
|
|
2011-08-19 21:54:24 +00:00
|
|
|
#def test_foo():
|
|
|
|
#graph_file = open('request_decision_graph.yml', 'r')
|
|
|
|
#graph = yaml.safe_load(graph_file)
|
|
|
|
#eq(graph['bucket_put_simple']['set']['grantee'], 0)
|
|
|
|
|
|
|
|
|
2011-08-09 18:56:38 +00:00
|
|
|
def test_load_graph():
|
2011-08-08 23:51:10 +00:00
|
|
|
graph_file = open('request_decision_graph.yml', 'r')
|
2011-08-09 18:56:38 +00:00
|
|
|
graph = yaml.safe_load(graph_file)
|
|
|
|
graph['start']
|
|
|
|
|
|
|
|
|
|
|
|
def test_descend_leaf_node():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
decision = descend_graph(graph, 'leaf', prng)
|
|
|
|
|
|
|
|
eq(decision['key1'], 'value1')
|
|
|
|
eq(decision['key2'], 'value2')
|
|
|
|
e = assert_raises(KeyError, lambda x: decision[x], 'key3')
|
|
|
|
|
2011-08-09 22:44:25 +00:00
|
|
|
|
2011-08-09 18:56:38 +00:00
|
|
|
def test_descend_node():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
decision = descend_graph(graph, 'node1', prng)
|
2011-08-08 23:51:10 +00:00
|
|
|
|
2011-08-09 18:56:38 +00:00
|
|
|
eq(decision['key1'], 'value1')
|
|
|
|
eq(decision['key2'], 'value2')
|
|
|
|
eq(decision['key3'], 'value3')
|
2011-08-08 23:51:10 +00:00
|
|
|
|
2011-08-09 22:44:25 +00:00
|
|
|
|
2011-08-09 18:56:38 +00:00
|
|
|
def test_descend_bad_node():
|
|
|
|
graph = build_graph()
|
2011-08-08 23:51:10 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-15 21:16:40 +00:00
|
|
|
assert_raises(DecisionGraphError, descend_graph, graph, 'bad_node', prng)
|
2011-08-08 23:51:10 +00:00
|
|
|
|
2011-08-09 22:44:25 +00:00
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
def test_descend_nonexistant_child():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
assert_raises(KeyError, descend_graph, graph, 'nonexistant_child_node', prng)
|
|
|
|
|
|
|
|
|
2011-08-18 19:34:56 +00:00
|
|
|
def test_expand_random_printable():
|
2011-08-09 22:44:25 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand({}, '{random 10-15 printable}', prng)
|
|
|
|
eq(got, '[/pNI$;92@')
|
2011-08-09 22:44:25 +00:00
|
|
|
|
2011-08-10 21:39:25 +00:00
|
|
|
|
2011-08-18 19:34:56 +00:00
|
|
|
def test_expand_random_binary():
|
2011-08-10 21:39:25 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand({}, '{random 10-15 binary}', prng)
|
|
|
|
eq(got, '\xdfj\xf1\xd80>a\xcd\xc4\xbb')
|
2011-08-09 22:44:25 +00:00
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
|
2011-08-23 22:08:18 +00:00
|
|
|
def test_expand_random_printable_no_whitespace():
|
|
|
|
prng = random.Random(1)
|
2019-03-22 17:58:30 +00:00
|
|
|
for _ in range(1000):
|
2011-08-23 22:08:18 +00:00
|
|
|
got = expand({}, '{random 500 printable_no_whitespace}', prng)
|
|
|
|
assert_true(reduce(lambda x, y: x and y, [x not in string.whitespace and x in string.printable for x in got]))
|
|
|
|
|
|
|
|
|
2015-06-15 04:31:30 +00:00
|
|
|
def test_expand_random_binary_no_whitespace():
|
2011-08-23 22:08:18 +00:00
|
|
|
prng = random.Random(1)
|
2019-03-22 17:58:30 +00:00
|
|
|
for _ in range(1000):
|
2011-08-23 22:08:18 +00:00
|
|
|
got = expand({}, '{random 500 binary_no_whitespace}', prng)
|
|
|
|
assert_true(reduce(lambda x, y: x and y, [x not in string.whitespace for x in got]))
|
|
|
|
|
|
|
|
|
2011-08-18 19:34:56 +00:00
|
|
|
def test_expand_random_no_args():
|
2011-08-15 21:16:40 +00:00
|
|
|
prng = random.Random(1)
|
2019-03-22 17:58:30 +00:00
|
|
|
for _ in range(1000):
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand({}, '{random}', prng)
|
|
|
|
assert_true(0 <= len(got) <= 1000)
|
|
|
|
assert_true(reduce(lambda x, y: x and y, [x in string.printable for x in got]))
|
2011-08-15 21:16:40 +00:00
|
|
|
|
|
|
|
|
2011-08-18 19:34:56 +00:00
|
|
|
def test_expand_random_no_charset():
|
2011-08-15 21:16:40 +00:00
|
|
|
prng = random.Random(1)
|
2019-03-22 17:58:30 +00:00
|
|
|
for _ in range(1000):
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand({}, '{random 10-30}', prng)
|
|
|
|
assert_true(10 <= len(got) <= 30)
|
|
|
|
assert_true(reduce(lambda x, y: x and y, [x in string.printable for x in got]))
|
2011-08-15 21:16:40 +00:00
|
|
|
|
|
|
|
|
2011-08-18 19:34:56 +00:00
|
|
|
def test_expand_random_exact_length():
|
2011-08-15 21:16:40 +00:00
|
|
|
prng = random.Random(1)
|
2019-03-22 17:58:30 +00:00
|
|
|
for _ in range(1000):
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand({}, '{random 10 digits}', prng)
|
|
|
|
assert_true(len(got) == 10)
|
|
|
|
assert_true(reduce(lambda x, y: x and y, [x in string.digits for x in got]))
|
2011-08-15 21:16:40 +00:00
|
|
|
|
|
|
|
|
2011-08-18 19:34:56 +00:00
|
|
|
def test_expand_random_bad_charset():
|
2011-08-15 21:16:40 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-18 19:34:56 +00:00
|
|
|
assert_raises(KeyError, expand, {}, '{random 10-30 foo}', prng)
|
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
|
2011-08-18 19:34:56 +00:00
|
|
|
def test_expand_random_missing_length():
|
|
|
|
prng = random.Random(1)
|
|
|
|
assert_raises(ValueError, expand, {}, '{random printable}', prng)
|
2011-08-15 21:16:40 +00:00
|
|
|
|
|
|
|
|
2011-08-09 22:44:25 +00:00
|
|
|
def test_assemble_decision():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
decision = assemble_decision(graph, prng)
|
|
|
|
|
|
|
|
eq(decision['key1'], 'value1')
|
|
|
|
eq(decision['key2'], 'value2')
|
|
|
|
eq(decision['randkey'], 'value-{random 10-15 printable}')
|
|
|
|
eq(decision['indirect_key1'], '{key1}')
|
|
|
|
eq(decision['path'], '/{bucket_readable}')
|
|
|
|
assert_raises(KeyError, lambda x: decision[x], 'key3')
|
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
def test_expand_escape():
|
2011-08-18 19:34:56 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-15 21:16:40 +00:00
|
|
|
decision = dict(
|
|
|
|
foo='{{bar}}',
|
|
|
|
)
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand(decision, '{foo}', prng)
|
2011-08-15 21:16:40 +00:00
|
|
|
eq(got, '{bar}')
|
2011-08-10 18:27:06 +00:00
|
|
|
|
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
def test_expand_indirect():
|
2011-08-18 19:34:56 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-15 21:16:40 +00:00
|
|
|
decision = dict(
|
|
|
|
foo='{bar}',
|
|
|
|
bar='quux',
|
|
|
|
)
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand(decision, '{foo}', prng)
|
2011-08-15 21:16:40 +00:00
|
|
|
eq(got, 'quux')
|
2011-08-10 18:27:06 +00:00
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
def test_expand_indirect_double():
|
2011-08-18 19:34:56 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-15 21:16:40 +00:00
|
|
|
decision = dict(
|
|
|
|
foo='{bar}',
|
|
|
|
bar='{quux}',
|
|
|
|
quux='thud',
|
|
|
|
)
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand(decision, '{foo}', prng)
|
2011-08-15 21:16:40 +00:00
|
|
|
eq(got, 'thud')
|
2011-08-10 18:27:06 +00:00
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
def test_expand_recursive():
|
2011-08-18 19:34:56 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-15 21:16:40 +00:00
|
|
|
decision = dict(
|
|
|
|
foo='{foo}',
|
|
|
|
)
|
2011-08-18 19:34:56 +00:00
|
|
|
e = assert_raises(RecursionError, expand, decision, '{foo}', prng)
|
2011-08-15 21:16:40 +00:00
|
|
|
eq(str(e), "Runaway recursion in string formatting: 'foo'")
|
2011-08-09 22:44:25 +00:00
|
|
|
|
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
def test_expand_recursive_mutual():
|
2011-08-18 19:34:56 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-15 21:16:40 +00:00
|
|
|
decision = dict(
|
|
|
|
foo='{bar}',
|
|
|
|
bar='{foo}',
|
|
|
|
)
|
2011-08-18 19:34:56 +00:00
|
|
|
e = assert_raises(RecursionError, expand, decision, '{foo}', prng)
|
2011-08-15 21:16:40 +00:00
|
|
|
eq(str(e), "Runaway recursion in string formatting: 'foo'")
|
2011-08-09 22:44:25 +00:00
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
|
|
|
|
def test_expand_recursive_not_too_eager():
|
2011-08-18 19:34:56 +00:00
|
|
|
prng = random.Random(1)
|
2011-08-15 21:16:40 +00:00
|
|
|
decision = dict(
|
|
|
|
foo='bar',
|
|
|
|
)
|
2011-08-18 19:34:56 +00:00
|
|
|
got = expand(decision, 100*'{foo}', prng)
|
2011-08-15 21:16:40 +00:00
|
|
|
eq(got, 100*'bar')
|
2011-08-09 22:44:25 +00:00
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
|
2011-08-19 21:54:24 +00:00
|
|
|
def test_make_choice_unweighted_with_space():
|
|
|
|
prng = random.Random(1)
|
|
|
|
choice = make_choice(['foo bar'], prng)
|
|
|
|
eq(choice, 'foo bar')
|
|
|
|
|
2011-08-10 20:26:00 +00:00
|
|
|
def test_weighted_choices():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
|
|
|
|
choices_made = {}
|
2019-03-22 17:58:30 +00:00
|
|
|
for _ in range(1000):
|
2011-08-10 22:10:24 +00:00
|
|
|
choice = make_choice(graph['weighted_node']['choices'], prng)
|
2019-03-22 17:58:30 +00:00
|
|
|
if choice in choices_made:
|
2011-08-10 22:10:24 +00:00
|
|
|
choices_made[choice] += 1
|
|
|
|
else:
|
|
|
|
choices_made[choice] = 1
|
|
|
|
|
|
|
|
foo_percentage = choices_made['foo'] / 1000.0
|
|
|
|
bar_percentage = choices_made['bar'] / 1000.0
|
|
|
|
baz_percentage = choices_made['baz'] / 1000.0
|
|
|
|
nose.tools.assert_almost_equal(foo_percentage, 0.25, 1)
|
|
|
|
nose.tools.assert_almost_equal(bar_percentage, 0.50, 1)
|
|
|
|
nose.tools.assert_almost_equal(baz_percentage, 0.25, 1)
|
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
|
|
|
|
def test_null_choices():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
choice = make_choice(graph['null_choice_node']['choices'], prng)
|
|
|
|
|
|
|
|
eq(choice, '')
|
|
|
|
|
|
|
|
|
|
|
|
def test_weighted_null_choices():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
choice = make_choice(graph['weighted_null_choice_node']['choices'], prng)
|
|
|
|
|
|
|
|
eq(choice, '')
|
|
|
|
|
|
|
|
|
|
|
|
def test_null_child():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
decision = descend_graph(graph, 'null_choice_node', prng)
|
|
|
|
|
|
|
|
eq(decision, {})
|
|
|
|
|
|
|
|
|
2011-08-10 22:10:24 +00:00
|
|
|
def test_weighted_set():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
|
|
|
|
choices_made = {}
|
2019-03-22 17:58:30 +00:00
|
|
|
for _ in range(1000):
|
2011-08-10 22:10:24 +00:00
|
|
|
choice = make_choice(graph['weighted_node']['set']['k1'], prng)
|
2019-03-22 17:58:30 +00:00
|
|
|
if choice in choices_made:
|
2011-08-10 20:26:00 +00:00
|
|
|
choices_made[choice] += 1
|
|
|
|
else:
|
|
|
|
choices_made[choice] = 1
|
|
|
|
|
|
|
|
foo_percentage = choices_made['foo'] / 1000.0
|
|
|
|
bar_percentage = choices_made['bar'] / 1000.0
|
|
|
|
baz_percentage = choices_made['baz'] / 1000.0
|
|
|
|
nose.tools.assert_almost_equal(foo_percentage, 0.25, 1)
|
|
|
|
nose.tools.assert_almost_equal(bar_percentage, 0.50, 1)
|
|
|
|
nose.tools.assert_almost_equal(baz_percentage, 0.25, 1)
|
|
|
|
|
2011-08-11 19:25:13 +00:00
|
|
|
|
2011-08-11 18:32:18 +00:00
|
|
|
def test_header_presence():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
decision = descend_graph(graph, 'node1', prng)
|
|
|
|
|
|
|
|
c1 = itertools.count()
|
|
|
|
c2 = itertools.count()
|
|
|
|
for header, value in decision['headers']:
|
|
|
|
if header == 'my-header':
|
|
|
|
eq(value, '{header_val}')
|
2011-08-15 21:16:40 +00:00
|
|
|
assert_true(next(c1) < 1)
|
2011-08-11 18:32:18 +00:00
|
|
|
elif header == 'random-header-{random 5-10 printable}':
|
|
|
|
eq(value, '{random 20-30 punctuation}')
|
2011-08-15 21:16:40 +00:00
|
|
|
assert_true(next(c2) < 2)
|
2011-08-11 18:32:18 +00:00
|
|
|
else:
|
|
|
|
raise KeyError('unexpected header found: %s' % header)
|
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
assert_true(next(c1))
|
|
|
|
assert_true(next(c2))
|
|
|
|
|
|
|
|
|
|
|
|
def test_duplicate_header():
|
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
assert_raises(DecisionGraphError, descend_graph, graph, 'repeated_headers_node', prng)
|
2011-08-11 18:32:18 +00:00
|
|
|
|
|
|
|
|
2011-08-15 21:16:40 +00:00
|
|
|
def test_expand_headers():
|
2011-08-11 18:32:18 +00:00
|
|
|
graph = build_graph()
|
|
|
|
prng = random.Random(1)
|
|
|
|
decision = descend_graph(graph, 'node1', prng)
|
2011-08-18 19:34:56 +00:00
|
|
|
expanded_headers = expand_headers(decision, prng)
|
2011-08-11 18:32:18 +00:00
|
|
|
|
2019-03-22 17:58:30 +00:00
|
|
|
for header, value in expanded_headers.items():
|
2011-08-11 18:32:18 +00:00
|
|
|
if header == 'my-header':
|
2011-08-15 21:16:40 +00:00
|
|
|
assert_true(value in ['h1', 'h2', 'h3'])
|
2011-08-11 18:32:18 +00:00
|
|
|
elif header.startswith('random-header-'):
|
2011-08-15 21:16:40 +00:00
|
|
|
assert_true(20 <= len(value) <= 30)
|
2011-08-18 19:34:56 +00:00
|
|
|
assert_true(string.strip(value, RepeatExpandingFormatter.charsets['punctuation']) is '')
|
2011-08-11 18:32:18 +00:00
|
|
|
else:
|
2011-08-15 21:16:40 +00:00
|
|
|
raise DecisionGraphError('unexpected header found: "%s"' % header)
|
2011-08-11 18:32:18 +00:00
|
|
|
|