Files
stacks-puppet-node/integration_tests/bin/blockstack-test-check-serialization
Jude Nelson dcdb9a8336 allow tests to state that certain fields' values change at certain
blocks (i.e. value hash) in ways that are inconsistent with prior
releases, but still correct
2016-09-29 12:25:09 -04:00

329 lines
10 KiB
Python
Executable File

#!/usr/bin/env python
import os
import sys
import simplejson
def parse_serialize_payload( line ):
"""
Given a SERIALIZE payload, parse the payload.
Return a dict that maps each field name to its value,
where the dict will have a special '__order__' key that
maps to the order in which the fields occur.
"""
cur = 0
ret = {}
order = []
opcode = line[cur]
cur += 1
# should be a separator
assert line[cur] == ':', "Missing separator for opcode"
cur += 1
# will be formatted as a CSV of '$key=$len:$value'
while cur < len(line):
# find next '='
try:
i_key = line[cur:].index('=')
key = line[cur:cur+i_key]
except:
print >> sys.stderr, "cur=%s, line_buf=%s" % (cur, line[cur:])
raise
try:
i_len = line[cur+i_key+1:].index(':')
len_str = line[cur+i_key+1: cur+i_key+1+i_len]
except:
print >> sys.stderr, "cur=%s, i_key=%s, key=%s, line_buf=%s" % (cur, i_key, key, line[cur+i_key+1:])
raise
try:
value_len = int(len_str)
value = line[cur+i_key+1+i_len+1: cur+i_key+1+i_len+1+value_len]
except:
print >> sys.stderr, "cur=%s, i_key=%s, key=%s, i_len=%s, len_str=%s, line_buf=%s" % (cur, i_key, key, i_len, len_str, line[cur+i_key+1:])
raise
order.append(key)
ret[key] = value
# advance
cur += i_key+1 + i_len+1 + value_len+1
ret['__order__'] = order
return ret
def extract_serialize_payload( line ):
"""
Given a line that contains SERIALIZE token, extract the netstring payload
"""
assert " SERIALIZE: " in line, "Missing 'SERIALIZE:' token"
i = line.index(" SERIALIZE: ")
payload = line[i + len(" SERIALIZE: "):]
return payload
def find_serialization_outputs( test_output_path ):
"""
Find the SERIALIZE outputs from the given test log output.
Parse them and return the parsed list.
Add '__block_height__' to each item (the height at which the serialization was found)
Add '__ignore_fields__' list to each item for fields we can ignore at that height (as well as the versions for which it applies)
"""
ret = []
curr_height = 0
ignore_fields = {}
with open(test_output_path, "r") as f:
while True:
line = f.readline()
if len(line) == 0:
break
# if we find this line, then we're out of tests
if "Scenario checks passed; verifying history" in line:
break
# block height?
if "Snapshotting block " in line:
i = line.index("Snapshotting block ")
try:
curr_height = int(line[i + len("Snapshotting block "):])
except:
# only other possible string
assert "consensus hash of " in line[i + len("Snapshotting block "):]
pass
line = line.strip()
if line.startswith("BLOCKSTACK_SERIALIZATION_CHECK_IGNORE"):
# format: BLOCKSTACK_SERIALIZATION_CHECK_IGNORE field
# NOTE: since this gets printed *before* the "Snapshotting block", we'll have to use curr_height+1
parts = line.split()
assert len(parts) == 2
field = parts[1]
if not ignore_fields.has_key(curr_height+1):
ignore_fields[curr_height+1] = []
ignore_fields[curr_height+1].append(field)
if " SERIALIZE: " in line:
# finished this block's data
payload_bin = extract_serialize_payload( line )
payload = parse_serialize_payload( payload_bin )
payload['__block_height__'] = curr_height
payload['__ignore_fields__'] = ignore_fields.get(curr_height, [])[:]
ret.append( payload )
return ret
def find_ignore_serialization_fields( test_output_path ):
"""
Find any occurrences of "SERIALIZATION_FIELD_IGNORE ..."
and return them as a CSV
"""
ret = []
with open(test_output_path, "r") as f:
while True:
line = f.readline()
if len(line) == 0:
break
line = line.strip()
if line.startswith("SERIALIZATION_CHECK_IGNORE"):
parts = line.split(" ")
assert len(parts) == 2
ignore = parts[1]
ret.append(ignore)
return ret
def serialization_eq( height, serialization_1, serialization_2, ignore=[] ):
"""
Given two parsed serialization payloads, verify that they represent the same data
(ignoring fields given in @ignore)
Return (True, None) on success
Return (False, {'error': ...}) on failure
"""
if len(serialization_1['__order__']) != len(serialization_2['__order__']):
return False, {'error': "Order length mismatch (%s != %s)" % (len(serialization_1['__order__']), len(serialization_2['__order__']))}
mismatches_1 = []
mismatches_2 = []
for i in xrange(0, len(serialization_1['__order__'])):
key_1 = serialization_1['__order__'][i]
key_2 = serialization_2['__order__'][i]
if key_1 in ignore or key_2 in ignore:
continue
if serialization_1[key_1] != serialization_2[key_2]:
mismatches_1.append( key_1 )
mismatches_2.append( key_2 )
if len(mismatches_1) > 0:
errstr = ""
for i in xrange(0, len(mismatches_1)):
errstr += "Key mismatch at %s: %s (%s) != %s (%s)\n" % (height, mismatches_1[i], serialization_1[mismatches_1[i]], mismatches_2[i], serialization_2[mismatches_2[i]])
errstr += "First serialization:\n%s\n" % simplejson.dumps(serialization_1, indent=4, sort_keys=True)
errstr += "Second serialization:\n%s\n" % simplejson.dumps(serialization_2, indent=4, sort_keys=True)
return False, {'error': errstr}
return True, None
def group_by_block_height( serializations ):
"""
Given a list of parsed serializations, group them by __block_height__
"""
ret = {}
for s in serializations:
height = s['__block_height__']
if not ret.has_key(height):
ret[height] = []
ret[height].append(s)
return ret
def compare_serializations( test_output_old, test_output_new ):
"""
Given the paths to two different test outputs, verify
that their sequence of SERIALIZEs match (up to txid).
"""
serializations_old = find_serialization_outputs( test_output_old )
serializations_new = find_serialization_outputs( test_output_new )
serialization_ignore = ['txid','consensus_hash','name_consensus_hash','name_hash','vtxindex']
rc = True
if len(serializations_old) != len(serializations_new):
print >> sys.stderr, " Mismatched number of serializations (%s != %s)" % (len(serializations_old), len(serializations_new))
return False
# group by block height (since bitcoind can re-order transactions)
block_serializations_old = group_by_block_height( serializations_old )
block_serializations_new = group_by_block_height( serializations_new )
for height in sorted(block_serializations_old.keys()):
if not block_serializations_new.has_key(height):
print >> sys.stderr, " Missing block height %s in second log" % height
return False
for height in sorted(block_serializations_new.keys()):
if not block_serializations_old.has_key(height):
print >> sys.stderr, " Missing block height %s in first log" % height
return False
for height in sorted(block_serializations_old.keys()):
s_old = block_serializations_old[height]
s_new = block_serializations_new[height]
if len(s_old) != len(s_new):
print >> sys.stderr, " Mismatched number of serializations at block %s (%s != %s)" % (height, len(s_old), len(s_new))
return False
matched = False
err = None
for s1 in s_old:
# has to match one serialization in the second listing
# order doesn't matter, since bitcoind reorders them anyway
for s2 in s_new:
# serializations to ignore (use the fields from the new log to ignore fields in the old log)
ignore = serialization_ignore[:]
ignore += s2['__ignore_fields__']
res, err = serialization_eq( height, s1, s2, ignore=ignore )
if res:
matched = True
s_new.remove(s2)
break
if not matched:
# soldier on here so we can print all mismatches
print >> sys.stderr, " Mismatched serializations in block %s" % height
print >> sys.stderr, err['error']
rc = False
return rc
def is_test_successful( test_output ):
"""
Is this a successful test?
"""
with open(test_output, "r") as f:
while True:
line = f.readline()
if len(line) == 0:
break
line = line.strip()
if line.startswith("SUCCESS "):
return True
return False
def skip_check( test_output ):
"""
Should we skip the serialization test?
i.e. is it expected to fail?
"""
with open(test_output, "r") as f:
while True:
line = f.readline()
if len(line) == 0:
break
line = line.strip()
if line == "BLOCKSTACK_SERIALIZATION_CHANGE_BEHAVIOR":
# this is a known breaking change
return True
return False
if __name__ == "__main__":
try:
test_output_old = sys.argv[1]
test_output_new = sys.argv[2]
except:
print >> sys.stderr, "Usage: %s TEST_OUTPUT_OLD TEST_OUTPUT_NEW" % sys.argv[0]
sys.exit(1)
if not is_test_successful( test_output_old ):
print >> sys.stderr, " %s is a failed old test" % test_output_old
sys.exit(2)
if not is_test_successful( test_output_new ):
print >> sys.stderr, " %s is a failed new test" % test_output_new
sys.exit(1)
if skip_check( test_output_new ):
print >> sys.stderr, " %s is a breaking chnage" % test_output_new
sys.exit(2)
res = compare_serializations( test_output_old, test_output_new )
if res:
sys.exit(0)
else:
sys.exit(1)