"""
Tests for our low-level VOTable interface.
"""

#c Copyright 2008-2024, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


import base64
import datetime
import io
import re
import struct

import numpy
from numpy import rec

from gavo.helpers import testhelpers

from gavo import base
from gavo import utils
from gavo import votable
from gavo.utils import pgsphere
from gavo.votable import common
from gavo.votable import paramval
from gavo.votable import V
from gavo.utils.plainxml import iterparse


class NullFlagsSerTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	def _runTest(self, sample):
		nFields, nullMap, expected = sample
		self.assertEqual(common.NULLFlags(nFields).serialize(nullMap), expected)
	
	samples = [
		(1, [True], b"\x80"),
		(1, [False], b"\x00"),
		(8, [True, False, True, False, False, False, False, False], b"\xa0"),
		(9, [True, False, True, False, False, False, False, False, False],
			b"\xa0\x00"),
		(12, [False]*4+[True]*2+[False]*5+[True], b"\x0c\x10"),
		(16, [False]*15+[True], b"\x00\x01"),
		(65, [False]*64+[True], b"\x00\x00\x00\x00\x00\x00\x00\x00\x80"),
	]


class NullFlagsDeserTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	def _runTest(self, sample):
		nFields, expected, input = sample
		self.assertEqual(common.NULLFlags(nFields).deserialize(input),
			expected)
	
	samples = NullFlagsSerTest.samples


class IterParseTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for our custom iterparser.
	"""

	def _runTest(self, sample):
		xml, parsed = sample
		self.assertEqual(list(iterparse(io.BytesIO(utils.bytify(xml)))), parsed)
	
	samples = [
		("<doc/>", [("start", "doc", {}), ("end", "doc", None)]),
		('<doc href="x"/>',
			[("start", "doc", {"href": "x"}), ("end", "doc", None)]),
		(b'<doc>fl\xc3\xb6ge</doc>', [
			("start", "doc", {}),
			("data", None, "fl\xf6ge"),
			("end", "doc", None)]),
		(b'<doc obj="fl\xc3\xb6ge"/>',
			[("start", "doc", {"obj": "flöge"}), ("end", "doc", None)]),
		('<doc><abc>'+"unu"*10000+'</abc>\n<bcd>klutz</bcd></doc>', [
			("start", "doc", {}),
			("start", "abc", {}),
			("data", None, "unu"*10000),
			("end", "abc", None),
			("data", None, "\n"),
			("start", "bcd", {}),
			("data", None, "klutz"),
			("end", "bcd", None),
			("end", "doc", None)]),
		('<doc xmlns="http://insane"/>', [
			("start", "doc", {'xmlns': 'http://insane'}), ("end", "doc", None),])
	]


class TrivialParseTest(testhelpers.VerboseTest):
	"""tests operating on an empty VOTable.
	"""
	def testTrivialParse(self):
		self.assertEqual(list(votable.parseBytes("<VOTABLE/>")), [])

	def testTrivialWatchlist(self):
		res = list(votable.parseBytes("<VOTABLE/>",
			watchset=[V.VOTABLE]))
		self.assertTrue(isinstance(testhelpers.pickSingle(res), V.VOTABLE))
	
	def testTrivialWithNamespace(self):
		res = list(votable.parseBytes(
			'<VOTABLE xmlns="http://www.ivoa.net/xml/VOTable/v1.2"/>',
			watchset=[V.VOTABLE]))
		self.assertTrue(isinstance(res[0], V.VOTABLE))

	def testTrivialOldNamespace(self):
		res = list(votable.parseBytes(
			'<VOTABLE xmlns="http://www.ivoa.net/xml/VOTable/v1.1"/>',
			watchset=[V.VOTABLE]))
		self.assertTrue(isinstance(res[0], V.VOTABLE))


class TrivialWriteTest(testhelpers.VerboseTest):
	def testEmpty(self):
		res = votable.asBytes(V.VOTABLE(), xmlDecl=True)
		self.assertTrue(b"<?xml version='1.0' encoding='utf-8'?>" in res)
		self.assertTrue(b"<VOTABLE version=" in res)
		self.assertTrue(b"/>" in res)
	
	def testBasicSerialisation(self):
		res = votable.asBytes(V.VOTABLE[V.RESOURCE[V.TABLE(name="kna")[
			V.PARAM(name="a", datatype="char", arraysize="*", value="abc"),
			V.DATA]]])
		self.assertTrue(b'xmlns="http://www.ivoa.net/xml/VOTable/' in res)
		self.assertTrue(b'<TABLE name="kna">' in res)
		self.assertTrue(b'value="abc"' in res)


class ErrorParseTest(testhelpers.VerboseTest):
	"""tests for more-or-less benign behaviour on input errors.
	"""
	def testExpatReporting(self):
		try:
			list(votable.parseBytes("<VOTABLE>"))
		except Exception as raised:
			ex = raised
		self.assertEqual(ex.__class__.__name__, "VOTableParseError")
		self.assertEqual(str(ex),
			"(internal source) no element found: line 1, column 9")

	def testInternalReporting(self):
		table = next(votable.parseBytes("<VOTABLE><RESOURCE><TABLE>\n"
			'<FIELD name="x" datatype="boolean"/>\n'
			'<DATA><TABLEDATA>\n'
			'<TR><TDA>True</TDA></TR>\n'
			'</TABLEDATA></DATA>\n'
			"</TABLE></RESOURCE></VOTABLE>\n"))
		try:
			list(table)
		except Exception as raised:
			ex = raised
		self.assertEqual(ex.__class__.__name__, "VOTableParseError")
		self.assertEqual(str(ex),
			'At IO:\'<VOTABLE><RESOURCE><TABLE> <FIELD name="x" datatype="...\', (4, 4):'
			" Unexpected element TDA")


class TextParseTest(testhelpers.VerboseTest):
	"""tests for parsing elements with text content.
	"""
	def _getInfoItem(self, infoLiteral):
		return list(votable.parseBytes(
			'<VOTABLE>%s</VOTABLE>'%infoLiteral,
			watchset=[V.INFO]))[0]

	def testEmptyInfo(self):
		res = self._getInfoItem('<INFO name="t" value="0"/>')
		self.assertEqual(res.value, "0")
		self.assertEqual(res.name, "t")
	
	def testFullInfo(self):
		res = self._getInfoItem('<INFO name="t" value="0">abc</INFO>')
		self.assertEqual(res.text_, "abc")

	def testUnicode(self):
		# xml defaults to utf-8
		res = self._getInfoItem(
			b'<INFO name="t" value="0">\xc3\xa4rn</INFO>'.decode("utf-8"))
		self.assertEqual(res.text_, "\xe4rn")


class IdTest(testhelpers.VerboseTest):
	"""tests for the management of id attributes.
	"""
	def testSimpleId(self):
		els = list(votable.parseBytes(
			'<VOTABLE><INFO ID="xy">abc</INFO></VOTABLE>',
			watchset=[V.INFO, V.VOTABLE]))
		self.assertTrue(els[0].idmap is els[1].idmap)
		self.assertTrue(els[0].idmap["xy"] is els[0])
	
	def testForwardReference(self):
		iter = votable.parseBytes(
			'<VOTABLE><INFO ID="xy" ref="z">abc</INFO>'
			'<INFO ID="z" ref="xy">zz</INFO></VOTABLE>',
			watchset=[V.INFO])
		info0 = next(iter)
		self.assertRaises(KeyError, lambda: info0.idmap[info0.ref])
		info1 = next(iter)
		self.assertTrue(info0.idmap[info0.ref] is info1)
		self.assertTrue(info1.idmap[info1.ref] is info0)
		self.assertRaises(StopIteration, iter.__next__)


class TabledataReadTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for deserialization of TABLEDATA encoded values.
	"""

	def _runTest(self, sample):
		fielddefs, literals, expected = sample
		table = next(votable.parseBytes(
			'<VOTABLE><RESOURCE><TABLE>'+
			fielddefs+
			'<DATA><TABLEDATA>'+
			'\n'.join('<TR>%s</TR>'%''.join('<TD>%s</TD>'%l
				for l in row) for row in literals)+
			'</TABLEDATA></DATA>'
			'</TABLE></RESOURCE></VOTABLE>'))
		self.assertEqual(list(table), expected)
	
	samples = [(
			'<FIELD name="x" datatype="boolean"/>',
			[['TRue'], ['T'],  ['False'], ['?']],
			[[True],   [True], [False],   [None]]
		), (
			'<FIELD name="x" datatype="unsignedByte"/>'
			'<FIELD name="y" datatype="unsignedByte"><VALUES null="16"/></FIELD>',
			[['',   ''],   ['0x10', '0x10'], ['10', '16'], ['', '']],
			[[None, None], [16,     16],     [10,    None], [None, None]]
		), (
			'<FIELD name="x" datatype="char"/>',
			[[''],   ['a'], ['&apos;'], ['\xe4'], ['&#xe4;'], ['']],
			[[None], ['a'], ["'"],      ['ä'], ['ä'], [None]],
		), (
			'<FIELD name="x" datatype="short"><VALUES null="0"/></FIELD>'
			'<FIELD name="y" datatype="int"/>'
			'<FIELD name="z" datatype="long"><VALUES null="222399322"/></FIELD>',
			[['0', '0', '0'], ['-3', '-300', '222399322'], ['0xff', '0xcafebabe', '0xcafebabedeadbeef'], ['', '', '']],
			[[None, 0,  0],   [-3,    -300,  None],        [255,    -889275714,   -3819410105021120785], [None, None, None]]
		), (
			'<FIELD name="x" datatype="float"><VALUES null="-999."/></FIELD>'
			'<FIELD name="y" datatype="float"/>',
			[['1', '0.5e10'], ['-999.', '']],
			[[1.0, 5e09],     [None, None]]
# 5
		), (
			'<FIELD name="x" datatype="floatComplex"><VALUES null="-999. 0"/></FIELD>'
			'<FIELD name="y" datatype="floatComplex"/>',
			[['1 1', '0.5e10 -2e5'], ['-999. 0', '20']],
			[[(1+1j), 5e09-2e5j],    [None, 20+0j]]
		), (
			'<FIELD name="x" datatype="boolean" arraysize="*"/>',
			[['true false ? T'],        [' T'], ['']],
			[[[True, False, None, True]], [[True,]], [None]]
		), (
			'<FIELD name="y" datatype="unsignedByte" arraysize="*">'
			' <VALUES null="16"/></FIELD>',
			[['10 0x10\t 16 \n 0x16'], ['']],
			[[[10, 16, None, 22]], [None]]
		), (
			'<FIELD name="x" datatype="char" arraysize="4"/>',
			[[''], ['auto'], ['&apos;xx&quot;'], ['\xe4'], ['&#xe4;'], ['']],
			[[None], ['auto'], ["'xx\""], ['ä'], ['ä'], [None]],
		), (
			'<FIELD name="x" datatype="short" arraysize="*"><VALUES null="0"/></FIELD>',
			[['1 2 3 0 1'], [""]],
			[[[1,2,3,None,1]], [None]]
#10
		), (
			'<FIELD name="y" datatype="floatComplex" arraysize="*"/>',
			[['1 1 0.5e10 -2e5'], [""]],
			[[[(1+1j), 5e09-2e5j]], [None]]
		), (
			'<FIELD datatype="short" arraysize="2x3"/>',
			[['0 1 2 3 4 5']],
			[[[[0,1],[2,3],[4,5]]]],
		), (
			'<FIELD name="x" datatype="float"/>',
			[['NaN'], ['']],
			[[None],  [None]]
		), (
			'<FIELD datatype="unicodeChar" arraysize="*"/>',
			[['\xe4'], [""]],
			[['\xe4'], [None]]
		),  (
			'<FIELD datatype="float" arraysize="*"/>',
			[[" "]],
			[[[]]]
		), (
#15
			'<FIELD datatype="double" arraysize="2" xtype="point"/>',
			[[""], ["10 12"]],
			[[None], [pgsphere.SPoint.fromDegrees(10, 12)]],
		), (
			'<FIELD datatype="double" arraysize="3" xtype="circle"/>',
			[[""], ["100 -23 0.25"]],
			[[None], [pgsphere.SCircle.fromDALI([100, -23, 0.25])]]
		), (
			'<FIELD datatype="double" arraysize="*" xtype="polygon"/>',
			[[""], ["0 0 130 20 70 -10"]],
			[[None], [pgsphere.SPoly.fromDALI([0, 0, 130, 20, 70, -10])]]
		), (
			'<FIELD datatype="char" arraysize="*"/>',
			[['\xe4'], [""]],
			[['ä'], [None]]   # this is fallback encoding
		), (
			'<FIELD datatype="char" arraysize="1"/>',
			[['\xe4'], [" "]],
			[['ä'], [' ']]   # this is fallback encoding
		), (
# 20
			'<FIELD name="d" datatype="char" arraysize="1" xtype="timestamp"/>',
			[[''], [" "], ["      "]],
			[[None], [None], [None]]   # this is fallback encoding
		), (
			'<FIELD name="j" datatype="char" arraysize="*" xtype="json"/>',
			[[''], ["{'foo': 'bar'}"]],
			[[None], ["{'foo': 'bar'}"]]
		),
	]


class FloatTDEncodingTest(testhelpers.VerboseTest):
	"""tests for proper handling of special float values.
	"""
	def _decode(self, fielddefs, literals):
		table = next(votable.parseBytes(
			'<VOTABLE><RESOURCE><TABLE>'+
			fielddefs+
			'<DATA><TABLEDATA>'+
			'\n'.join('<TR>%s</TR>'%''.join('<TD>%s</TD>'%l
				for l in row) for row in literals)+
			'</TABLEDATA></DATA>'
			'</TABLE></RESOURCE></VOTABLE>'))
		return list(table)

	def testNAN(self):
		vals = self._decode(
			'<FIELD name="y" datatype="float"/>',
			[['NaN']])[0]
		self.assertTrue(vals[0] is None)
	
	def testInfinities(self):
		vals = self._decode(
			'<FIELD name="y" datatype="float"/>',
			[['+Inf'], ['-Inf']])
		self.assertTrue(vals[0][0]==2*vals[0][0])
		self.assertTrue(vals[1][0]==2*vals[1][0])

	def testWeirdArray(self):
		vals = self._decode(
			'<FIELD name="y" datatype="float" arraysize="3"/>',
			[['NaN +Inf -Inf']])[0]
		self.assertEqual(repr(vals), '[[None, inf, -inf]]')


class TabledataWriteTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for serializing TABLEDATA VOTables.
	"""

	def _runTest(self, sample):
		fielddefs, input, expected = sample
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[fielddefs], input, V.TABLEDATA)]]
		res = votable.asBytes(vot)
		mat = re.search(b"<TABLEDATA>(.*)</TABLEDATA>", res)
		content = mat and mat.group(1)
		self.assertEqual(content, utils.bytify(expected))

	samples = [(
			[V.FIELD(datatype="float")],
			[[1],[None],[float("NaN")], [3]],
			"<TR><TD>1.0</TD></TR><TR><TD>NaN</TD></TR><TR><TD>NaN</TD></TR><TR><TD>3.0</TD></TR>"
		), (
			[V.FIELD(datatype="double")],
			[[1.52587890625e-05], [float("+Inf")]],
			'<TR><TD>1.52587890625e-05</TD></TR><TR><TD>inf</TD></TR>'
		), (
			[V.FIELD(datatype="boolean")],
			[[True], [False], [None]],
			'<TR><TD>1</TD></TR><TR><TD>0</TD></TR><TR><TD>?</TD></TR>'
		), ([
				V.FIELD(datatype="bit"),
				V.FIELD(datatype="unsignedByte"),
				V.FIELD(datatype="short"),
				V.FIELD(datatype="int"),
				V.FIELD(datatype="long")],
			[[0,1,2,3,4]],
			'<TR><TD>0</TD><TD>1</TD><TD>2</TD><TD>3</TD><TD>4</TD></TR>'
		), (
			[V.FIELD(datatype="unicodeChar")],
			['\xe4'],
			b'<TR><TD>\xc3\xa4</TD></TR>'
		), (
# 5
			[V.FIELD(datatype="char")],
			['\xe4'],
			'<TR><TD>?</TD></TR>'
		), (
			[V.FIELD(datatype="floatComplex")],
			[[0.5+0.25j]],
			'<TR><TD>0.5 0.25</TD></TR>'
		), ([
				V.FIELD(datatype="unsignedByte")[V.VALUES(null="23")],
				V.FIELD(datatype="unicodeChar")[V.VALUES(null="\x00")],
				V.FIELD(datatype="float")[V.VALUES(null="-9999")]],
			[[1, "a", 1.5], [None, None, None]],
			'<TR><TD>1</TD><TD>a</TD><TD>1.5</TD></TR>'
			'<TR><TD>23</TD><TD>&x00;</TD><TD>NaN</TD></TR>'
		), (
			[V.FIELD(datatype="unsignedByte", arraysize="2")[V.VALUES(null="0xff")]],
			[[[]], [[2]], [None], [[2, 3, 4]]],
		'<TR><TD>0xff 0xff</TD></TR><TR><TD>2 0xff</TD></TR>'
		'<TR><TD></TD></TR><TR><TD>2 3</TD></TR>'
		), (
			[V.FIELD(datatype="bit", arraysize="*")],
			[[430049293488]],
			'<TR><TD>110010000100000111011110111010010110000</TD></TR>'
		), (
# 10
			[V.FIELD(datatype="doubleComplex", arraysize="2")[V.VALUES(null="0 0")]],
			[[[2+2j, None, 4+4j]]],
			'<TR><TD>2.0 2.0 NaN NaN</TD></TR>'
		), (
			[V.FIELD(datatype="double", arraysize="*")[V.VALUES(null="23")]],
			[[None], [[None]]],
			"<TR><TD></TD></TR><TR><TD>NaN</TD></TR>"
		), (
			[V.FIELD(datatype="char", arraysize="*", xtype="adql:TIMESTAMP")],
			[[None], [datetime.datetime(2010, 10, 12, 23, 10, 0o1)]],
			"<TR><TD></TD></TR><TR><TD>2010-10-12T23:10:01Z</TD></TR>"
		),(
			[V.FIELD(datatype="char", arraysize="*", xtype="timestamp")],
			[[None], [datetime.datetime(2010, 10, 12, 23, 10, 0o1)]],
			"<TR><TD></TD></TR><TR><TD>2010-10-12T23:10:01Z</TD></TR>"
		),(
			[V.FIELD(datatype="double", arraysize="2", xtype="point")],
			[[None], [pgsphere.SPoint.fromDegrees(100, 23)],
				[[1, 2]]],
			"<TR><TD></TD></TR><TR><TD>100.0 23.0</TD></TR><TR><TD>1.0 2.0</TD></TR>"
		),(
# 15
			[V.FIELD(datatype="double", arraysize="3", xtype="circle")],
			[[None], [pgsphere.SCircle.fromDALI([100, -23, 0.25])],
				[[1, 2, 0.5]]],
			"<TR><TD></TD></TR><TR><TD>100.0 -23.0 0.25</TD></TR>"
			"<TR><TD>1.0 2.0 0.5</TD></TR>"
		), (
			[V.FIELD(datatype="double", arraysize="*", xtype="polygon")],
			[[None], [pgsphere.SPoly.fromDALI([0, 0, 130, 20, 70, -10])],
				[[1, 2, 3, 4, 5, 6]]],
			"<TR><TD></TD></TR><TR><TD>0.0 0.0 130.0 20.0 70.0 -10.0</TD></TR>"
			"<TR><TD>1.0 2.0 3.0 4.0 5.0 6.0</TD></TR>"
		), (
			[V.FIELD(datatype="char", arraysize="*", xtype="json")],
			[['{"a": "b"}'], [None], ['["ünk", "✂"]']],
			r'<TR><TD>{"a": "b"}</TD></TR><TR><TD></TD></TR><TR><TD>["\u00fcnk", "\u2702"]</TD></TR>'
		),
	]


class BinaryWriteTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for serializing BINARY VOTables.
	"""

	def _runTest(self, sample):
		fielddefs, input, expected = sample
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[fielddefs], input, V.BINARY)]]
		mat = re.search(b'(?s)<STREAM encoding="base64">(.*)</STREAM>',
			votable.asBytes(vot))
		content = mat and mat.group(1)
		self.assertEqual(base64.b64decode(content), expected)

	samples = [(
			[V.FIELD(datatype="float")],
			[[1],[None],[common.NaN]],
			struct.pack("!fff", 1, common.NaN, common.NaN)
		), (
			[V.FIELD(datatype="double")],
			[[1],[None],[common.NaN]],
			struct.pack("!ddd", 1, common.NaN, common.NaN)
		), (
			[V.FIELD(datatype="boolean")],
			[[True],[False],[None]],
			b"10?"
		), (
			[V.FIELD(datatype="bit")],
			[[1],[0]],
			b"\x01\x00"
		), (
			[V.FIELD(datatype="unsignedByte")],
			[[20]],
			b"\x14"
		), (
# 5
			[V.FIELD(datatype="unsignedByte")[V.VALUES(null="23")]],
			[[20], [None]],
			b"\x14\x17"
		), (
			[V.FIELD(datatype="short")],
			[[20]],
			b"\x00\x14"
		), (
			[V.FIELD(datatype="int")],
			[[-20]],
			b"\xff\xff\xff\xec"
		), (
			[V.FIELD(datatype="long")],
			[[-20]],
			b"\xff\xff\xff\xff\xff\xff\xff\xec"
		), (
			[V.FIELD(datatype="char")[V.VALUES(null="x")]],
			[['a'], [None]],
			b"ax"
		), (
# 10
			[V.FIELD(datatype="unicodeChar")[V.VALUES(null="\u1ead")]],
			[['a'], [b'\xe4'.decode("iso-8859-1")], [None]],
			b"\x00a\x00\xe4\x1e\xad"
		), (
			[V.FIELD(datatype="floatComplex")],
			[[6+7j], [None]],
			struct.pack("!ff", 6, 7)+struct.pack("!ff", common.NaN, common.NaN)
		), (
			[V.FIELD(datatype="bit", arraysize="17")],
			[[1],[2**25-1]],
			b"\x00\x00\x01\xff\xff\xff"
		), (
			[V.FIELD(datatype="bit", arraysize="*")],
			[[1],[2**25-1]],
			b"\x00\x00\x00\x08\x01"
			b"\x00\x00\x00\x20\x01\xff\xff\xff"
		), (
			[V.FIELD(datatype="unsignedByte", arraysize="*")],
			[[[]], [[1]], [[0, 1, 2]]],
			b"\x00\x00\x00\x00"
			b"\x00\x00\x00\x01\x01"
			b"\x00\x00\x00\x03\x00\x01\x02"
		), (
# 15
			[V.FIELD(datatype="unsignedByte", arraysize="2")[V.VALUES(null="255")]],
			[[[]], [[1]], [[0, 1, 2]]],
			b"\xff\xff"
			b"\x01\xff"
			b"\x00\x01"
		), (
			[V.FIELD(datatype="short", arraysize="2*")],
			[[[]], [[1]], [[0, 1, 2]]],
			b"\x00\x00\x00\x00"
			b"\x00\x00\x00\x01\x00\x01"
			b"\x00\x00\x00\x03\x00\x00\x00\x01\x00\x02"
		), (
			[V.FIELD(datatype="char", arraysize="2")],
			[["abc"], ["a"], [None], ["öa"]],
			b"aba   ?a"
		), (
			[V.FIELD(datatype="char", arraysize="*")],
			[["abc"], ["a"], [None], ["öa"]],
			b"\0\0\0\x03abc\0\0\0\x01a\0\0\0\0\0\0\0\x02?a"
		), (
			[V.FIELD(datatype="unicodeChar", arraysize="2")],
			[["\u00e4bc"], ["\u00e4"]],
			b'\x00\xe4\x00b\x00\xe4\x00 '
		), (
# 20
			[V.FIELD(datatype="short", arraysize="3x2")],
			[[[1,2,3,4,5,6]]],
			b'\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06'
		),(
			[V.FIELD(datatype="char", arraysize="*", xtype="adql:TIMESTAMP")],
			[[None], [datetime.datetime(2010, 10, 12, 23, 10, 0o1)]],
			b'\x00\x00\x00\x00\x00\x00\x00\x142010-10-12T23:10:01Z'
		), (
			[V.FIELD(datatype="char", arraysize="*", xtype="timestamp")],
			[[None], [datetime.datetime(2010, 10, 12, 23, 10, 0o1)]],
			b'\x00\x00\x00\x00\x00\x00\x00\x142010-10-12T23:10:01Z'
		), (
			[V.FIELD(datatype="double", arraysize="2", xtype="point")],
			[[None], [pgsphere.SPoint.fromDegrees(100, 23)],
				[[1, 2]]],
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'@Y\x00\x00\x00\x00\x00\x00@7\x00\x00\x00\x00\x00\x00'
			b'?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00'
		),(
			[V.FIELD(datatype="double", arraysize="3", xtype="circle")],
			[[None], [pgsphere.SCircle.fromDALI([100, -23, 0.25])],
				[[1, 2, 0.5]]],
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'@Y\x00\x00\x00\x00\x00\x00\xc07\x00\x00\x00\x00\x00\x00?\xd0\x00\x00\x00\x00\x00\x00'
			b'?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xe0\x00\x00\x00\x00\x00\x00'
		), (
# 25
			[V.FIELD(datatype="double", arraysize="*", xtype="polygon")],
			[[None], [pgsphere.SPoly.fromDALI([0, 0, 130, 20, 70, -10])],
				[[1, 2, 3, 4, 5, 6]]],
			b'\x00\x00\x00\x00'
			b'\x00\x00\x00\x06'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'@`@\x00\x00\x00\x00\x00'
			b'@4\x00\x00\x00\x00\x00\x00'
			b'@Q\x80\x00\x00\x00\x00\x00'
			b'\xc0$\x00\x00\x00\x00\x00\x00'
			b'\x00\x00\x00\x06'
			b'?\xf0\x00\x00\x00\x00\x00\x00'
			b'@\x00\x00\x00\x00\x00\x00\x00'
			b'@\x08\x00\x00\x00\x00\x00\x00'
			b'@\x10\x00\x00\x00\x00\x00\x00'
			b'@\x14\x00\x00\x00\x00\x00\x00'
			b'@\x18\x00\x00\x00\x00\x00\x00'
		), (
			[V.FIELD(datatype="char", arraysize="10x*")],
			[[numpy.array(["grab", "grub"])]],
			b'\x00\x00\x00\x14grab      grub      '
		), (
			[V.FIELD(datatype="char", arraysize="*", xtype="json")],
			[[["Phoenix", "Phönix"]]],
			b'\x00\x00\x00\x1a["Phoenix", "Ph\\u00f6nix"]'
		),
	]


class BinaryReadTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for deserializing BINARY VOTables.
	"""

	def _runTest(self, sample):
		fielddefs, stuff, expected = sample
		table = next(votable.parseBytes(
			b'<VOTABLE><RESOURCE><TABLE>'+
			utils.bytify(fielddefs)+
			b'<DATA><BINARY><STREAM encoding="base64">'+
			base64.b64encode(stuff)+
			b'</STREAM></BINARY></DATA>'
			b'</TABLE></RESOURCE></VOTABLE>'))
		self.assertEqual(list(table), expected)

	samples = [(
			'<FIELD datatype="boolean"/>',
			b"10?",
			[[True],[False],[None]],
		), (
			'<FIELD datatype="bit"/>',
			b"\x01\x00\xff",
			[[1],[0],[1]],
		), (
			'<FIELD datatype="bit" arraysize="9"/>',
			b"\x01\x00\xff\xff",
			[[256],[511]],
		), (
			'<FIELD datatype="bit" arraysize="*"/>',
			b"\x00\x00\x00\x03\xff\x00\x00\x00\x45"
				b"\xff\x00\x00\x00\x00\x00\x00\x00\x01",
			[[7],[0x1f0000000000000001]],
		), (
			'<FIELD datatype="char"><VALUES null="a"/></FIELD>',
			b"x\x00a",
			[['x'],[''], [None]],
		), (
# 05
			'<FIELD datatype="unicodeChar"><VALUES null="&#xbabe;"/></FIELD>',
			b"\x00a\x23\x42\xba\xbe",
			[['a'],['\u2342'], [None]],
		), (
			'<FIELD datatype="unsignedByte"><VALUES null="12"/></FIELD>'
				'<FIELD datatype="short"><VALUES null="12"/></FIELD>'
				'<FIELD datatype="int"><VALUES null="12"/></FIELD>'
				'<FIELD datatype="long"><VALUES null="12"/></FIELD>',
			b"\x0c\x00\x0c\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x0c"
			b"\x0d\x0d\x0d\x00\x0d\x0d\x0c\x00\xdd\x00\x00\x00\x00\x00\x0c", [
				[None, None, None, None],
				[13, 3341, 855308, 62205969853054988]]
		), (
			'<FIELD datatype="float"/>',
			b"\x7f\xc0\x00\x00:\x80\x00\x00",
			[[None], [0.0009765625]]
		), (
			'<FIELD datatype="double"/>',
			b"\x7f\xf8\x00\x00\x00\x00\x00\x00?P\x00\x01\x00\x00\x00\x00",
			[[None], [0.00097656343132257462]]
		), (
			'<FIELD datatype="doubleComplex"/>',
			b"\x7f\xf8\x00\x00\x00\x00\x00\x00?P\x00\x01\x00\x00\x00\x00"
			b'@\x04\x00\x00\x00\x00\x00\x00?\xe0\x00\x00\x00\x00\x00\x00',
			[[None], [2.5+0.5j]]
		), (
# 10
			'<FIELD datatype="char" arraysize="4"><VALUES null="0000"/></FIELD>',
			b"abcd0000",
			[["abcd"], [None]]
		), (
			'<FIELD datatype="char" arraysize="*"/>',
			b"\x00\x00\x00\x00\x00\x00\x00\x03abc",
			[[""], ["abc"]]
		), (
			'<FIELD datatype="unicodeChar" arraysize="*"/>',
			b"\x00\x00\x00\x03\x00a\x23\x42bc",
			[["a\u2342\u6263"]]
		), (
			'<FIELD datatype="unicodeChar" arraysize="2"/>',
			b"\x00a\x23\x42",
			[["a\u2342"]]
		), (
			'<FIELD datatype="unsignedByte" arraysize="2"/>',
			b'\x00\x01',
			[[[0, 1]]],
		), (
# 15
			'<FIELD datatype="short" arraysize="*"><VALUES null="16"/></FIELD>',
			b'\x00\x00\x00\x03\x00\x01\x00\x10\x00\x02',
			[[[1, None, 2]]],
		), (
			'<FIELD datatype="int" arraysize="2"/>',
			b'\x00\x00\x00\x03\x00\x01\x00\x10',
			[[[3, 0x10010]]],
		), (
			'<FIELD datatype="float" arraysize="2"/>',
			b'\x7f\xc0\x00\x00:\x80\x00\x00',
			[[[None, 0.0009765625]]],
		), (
			'<FIELD datatype="double" arraysize="*"/>',
			b'\x00\x00\x00\x02\x7f\xf8\x00\x00\x00\x00\x00\x00'
				b'?P\x00\x01\x00\x00\x00\x00',
			[[[None, 0.00097656343132257462]]]
		), (
			'<FIELD datatype="float" arraysize="2"><VALUES null="2"/></FIELD>',
			b'\x7f\xc0\x00\x00:\x80\x00\x00',
			[[[None, 0.0009765625]]],
		), (
# 20
			'<FIELD datatype="floatComplex" arraysize="2"/>',
			b'\x7f\xc0\x00\x00:\x80\x00\x00'
				b'A\x80\x00\x00A\x0c\x00\x00',
			[[[None, 16+8.75j]]],
		), (
			'<FIELD datatype="short" arraysize="2x3"/>',
			b'\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05',
			[[[[0,1],[2,3],[4,5]]]],
		), (
			'<FIELD datatype="double" arraysize="2" xtype="point"/>',
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00',
			[[None]],
		), (
			'<FIELD datatype="double" arraysize="2" xtype="point"/>',
			b'@Y\x00\x00\x00\x00\x00\x00@7\x00\x00\x00\x00\x00\x00',
			[[pgsphere.SPoint.fromDegrees(100, 23)]],
		), (
			'<FIELD datatype="double" arraysize="3" xtype="circle"/>',
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00',
			[[None]]
		), (
# 25
			'<FIELD datatype="double" arraysize="3" xtype="circle"/>',
			b'@Y\x00\x00\x00\x00\x00\x00'
			b'\xc07\x00\x00\x00\x00\x00\x00'
			b'?\xd0\x00\x00\x00\x00\x00\x00',
			[[pgsphere.SCircle.fromDALI([100, -23, 0.25])]]
		), (
			'<FIELD datatype="double" arraysize="*" xtype="polygon"/>',
			b'\x00\x00\x00\x00',
			[[None]]
		), (
			'<FIELD datatype="double" arraysize="*" xtype="polygon"/>',
			b'\x00\x00\x00\x06'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'@`@\x00\x00\x00\x00\x00'
			b'@4\x00\x00\x00\x00\x00\x00'
			b'@Q\x80\x00\x00\x00\x00\x00'
			b'\xc0$\x00\x00\x00\x00\x00\x00',
			[[pgsphere.SPoly.fromDALI([0, 0, 130, 20, 70, -10])]]
		), (
			'<FIELD datatype="char" arraysize="*" xtype="timestamp"/>',
			b'\0\0\0\0\0\0\0\x142010-10-12T23:10:01Z',
			[[None], [datetime.datetime(2010, 10, 12, 23, 10, 0o1)]]
		), (
			'<FIELD datatype="char"><VALUES null="a"/></FIELD>',
			b"\x00a\x23\x42\xba\xbe",
			[[''],[None], ['#'], ['B'], ['?'], ['?']],
		), (
# 30
			'<FIELD datatype="char" arraysize="10"/>',
			b"abc\x00tail  ",
			[["abc"]]),
	]


class Binary2WriteTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for serializing BINARY2 VOTables.
	"""

	def _runTest(self, sample):
		fielddefs, input, expected = sample
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[fielddefs], input, V.BINARY2)]]
		mat = re.search(b'(?s)<STREAM encoding="base64">(.*)</STREAM>',
			votable.asBytes(vot))
		content = mat and mat.group(1)
		self.assertEqual(base64.b64decode(content), expected)

	samples = [(
			[V.FIELD(datatype="float")],
			[[1.],[None],[common.NaN]],
			struct.pack("!BfBfBf", 0, 1., 0x80, common.NaN, 0, common.NaN)
		), (
			[V.FIELD(datatype="double")],
			[[1.],[None],[common.NaN]],
			struct.pack("!BdBdBd", 0, 1., 0x80, common.NaN, 0, common.NaN)
		), (
			[V.FIELD(datatype="boolean")],
			[[True],[False],[None]],
			b"\x001\x000\x80?"
		), (
			[V.FIELD(datatype="bit")],
			[[1],[0]],
			b"\x00\x01\x00\x00"
		), (
			[V.FIELD(datatype="unsignedByte")],
			[[20], [None]],
			b"\x00\x14\x80\xff"
		), (
# 5
			[V.FIELD(datatype="unsignedByte")[V.VALUES(null="23")]],
			[[20], [None]],
			b"\x00\x14\x80\xff"
		), (
			[V.FIELD(datatype="short")],
			[[20],[None]],
			b"\x00\x00\x14\x80\x00\x00"
		), (
			[V.FIELD(datatype="int")],
			[[-20], [None]],
			b"\x00\xff\xff\xff\xec\x80\x00\x00\x00\x00"
		), (
			[V.FIELD(datatype="long")],
			[[-20], [None]],
			b"\x00\xff\xff\xff\xff\xff\xff\xff\xec\x80\x00\x00\x00\x00\x00\x00\x00\x00"
		), (
			[V.FIELD(datatype="char")[V.VALUES(null="x")]],
			[['a'], [None]],
			b"\x00a\x80\x00"
		), (
# 10
			[V.FIELD(datatype="unicodeChar")[V.VALUES(null="\u1ead")]],
			[['a'], [b'\xe4'.decode("iso-8859-1")], [None]],
			b"\x00\x00a\x00\x00\xe4\x80\x00\x00"
		), (
			[V.FIELD(datatype="floatComplex")],
			[[6+7j], [None]],
			struct.pack("!Bff", 0,  6, 7)+struct.pack(
				"!Bff", 0x80, common.NaN, common.NaN)
		), (
			[V.FIELD(datatype="bit", arraysize="17")],
			[[1], [2**25-1], [None]],
			b"\x00\x00\x00\x01\x00\xff\xff\xff\x80\x00\x00\x00\x00"
		), (
			[V.FIELD(datatype="bit", arraysize="*")],
			[[1],[2**25-1], [None]],
			b"\x00\x00\x00\x00\x08\x01"
			b"\x00\x00\x00\x00\x20\x01\xff\xff\xff"
			b"\x80\x00\x00\x00\x00"
		), (
			[V.FIELD(datatype="unsignedByte", arraysize="*")],
			[[[]], [[1]], [[0, 1, 2]], [None]],
			b"\x00\x00\x00\x00\x00"
			b"\x00\x00\x00\x00\x01\x01"
			b"\x00\x00\x00\x00\x03\x00\x01\x02"
			b"\x80\x00\x00\x00\x00"
		), (
# 15
			[V.FIELD(datatype="unsignedByte", arraysize="2")[V.VALUES(null="255")]],
			[[[]], [[1]], [[0, 1, 2]], [None]],
			b"\x00\xff\xff"
			b"\x00\x01\xff"
			b"\x00\x00\x01"
			b"\x80\xff\xff"
		), (
			[V.FIELD(datatype="short", arraysize="2*")],
			[[[]], [[1]], [[0, 1, 2]], [None]],
			b"\x00\x00\x00\x00\x00"
			b"\x00\x00\x00\x00\x01\x00\x01"
			b"\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x02"
			b"\x80\x00\x00\x00\x00"
		), (
			[V.FIELD(datatype="char", arraysize="2")],
			[["abc"], ["a"], [None], ["öa"]],
			b"\x00ab\x00a\x00\x80\x00\x00\x00?a"
		), (
			[V.FIELD(datatype="char", arraysize="*")],
			[["abc"], ["a"], [None], ["öa"]],
			b"\x00\0\0\0\x03abc\x00\0\0\0\x01a\x80\0\0\0\x00\0\0\0\0\x02?a"
		), (
			[V.FIELD(datatype="unicodeChar", arraysize="2")],
			[["\u00e4bc"], ["\u00e4"], [None]],
			b'\x00\x00\xe4\x00b\x00\x00\xe4\x00\x00\x80\0\0\0\0'
		), (
#20
			[V.FIELD(datatype="short", arraysize="3x2")],
			[[[1,2,3,4,5,6]], [None]],
			b'\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06'
			b'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
		), (
			[V.FIELD(datatype="char", arraysize="*", xtype="adql:TIMESTAMP")],
			[[None], [datetime.datetime(2010, 10, 12, 23, 10, 0o1)]],
			b'\x80\0\0\0\0\0\0\0\0\x142010-10-12T23:10:01Z'
		),(
			[V.FIELD(datatype="char", arraysize="*", xtype="timestamp")],
			[[None], [datetime.datetime(2010, 10, 12, 23, 10, 0o1)]],
			b'\x80\0\0\0\0\0\0\0\0\x142010-10-12T23:10:01Z'
		), (
			[V.FIELD(datatype="double", arraysize="2", xtype="point")],
			[[None], [pgsphere.SPoint.fromDegrees(100, 23)],
				[[1, 2]]],
			b'\x80\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'\x00@Y\x00\x00\x00\x00\x00\x00@7\x00\x00\x00\x00\x00\x00'
			b'\x00?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00'
		),(
			[V.FIELD(datatype="double", arraysize="3", xtype="circle")],
			[[None], [pgsphere.SCircle.fromDALI([100, -23, 0.25])],
				[[1, 2, 0.5]]],
			b'\x80\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'\x00@Y\x00\x00\x00\x00\x00\x00\xc07\x00\x00\x00\x00\x00\x00?\xd0\x00\x00\x00\x00\x00\x00'
			b'\x00?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00?\xe0\x00\x00\x00\x00\x00\x00'
		), (
#25
			[V.FIELD(datatype="double", arraysize="*", xtype="polygon")],
			[[None], [pgsphere.SPoly.fromDALI([0, 0, 130, 20, 70, -10])],
				[[1, 2, 3, 4, 5, 6]]],
			b'\x80\x00\x00\x00\x00'
			b'\x00\x00\x00\x00\x06'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'@`@\x00\x00\x00\x00\x00'
			b'@4\x00\x00\x00\x00\x00\x00'
			b'@Q\x80\x00\x00\x00\x00\x00'
			b'\xc0$\x00\x00\x00\x00\x00\x00'
			b'\x00\x00\x00\x00\x06'
			b'?\xf0\x00\x00\x00\x00\x00\x00'
			b'@\x00\x00\x00\x00\x00\x00\x00'
			b'@\x08\x00\x00\x00\x00\x00\x00'
			b'@\x10\x00\x00\x00\x00\x00\x00'
			b'@\x14\x00\x00\x00\x00\x00\x00'
			b'@\x18\x00\x00\x00\x00\x00\x00'
		), (
			[V.FIELD(datatype="short", arraysize="3x2")],
			[[[1]], [[1, 2, 3, 4, 5, 6]], [[[2, 3, 4], [5, 6, 7]]],
				[list(range(1, 23))]],
			b'\x00'
			b'\x00\x01\x00\x00\x00\x00'
			b'\x00\x00\x00\x00\x00\x00'
			b'\x00'
			b'\x00\x01\x00\x02\x00\x03'
			b'\x00\x04\x00\x05\x00\x06'
			b'\x00'
			b'\x00\x02\x00\x03\x00\x04'
			b'\x00\x05\x00\x06\x00\x07'
			b'\x00'
			b'\x00\x01\x00\x02\x00\x03'
			b'\x00\x04\x00\x05\x00\x06'
			)
	]


class Binary2ReadTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for deserializing BINARY VOTables.
	"""

	def _runTest(self, sample):
		fielddefs, stuff, expected = sample
		table = next(votable.parseBytes(
			b'<VOTABLE><RESOURCE><TABLE>'+
			fielddefs.encode("ascii")+
			b'<DATA><BINARY2><STREAM encoding="base64">'+
			base64.b64encode(stuff)+
			b'</STREAM></BINARY2></DATA>'
			b'</TABLE></RESOURCE></VOTABLE>'))
		self.assertEqual(list(table), expected)

	samples = [(
			'<FIELD datatype="boolean"/>',
			b"\x001\x000\x80?",
			[[True],[False],[None]],
		), (
			'<FIELD datatype="bit"/>',
			b"\x00\x01\x00\x00\x00\xff\x80\x00",
			[[1],[0],[1], [None]],
		), (
			'<FIELD datatype="bit" arraysize="9"/>',
			b"\x00\x01\x00\x00\xff\xff\x80\x00\x00",
			[[256],[511], [None]],
		), (
			'<FIELD datatype="bit" arraysize="*"/>',
			b"\x00\x00\x00\x00\x03\xff\x00\x00\x00\x00\x45"
				b"\xff\x00\x00\x00\x00\x00\x00\x00\x01\x80\x00\x00\x00\x00",
			[[7],[0x1f0000000000000001], [None]],
		), (
			'<FIELD datatype="char"><VALUES null="a"/></FIELD>',
			b"\x00x\x00\x00\x00a\x80\x00",
			[['x'],[''], [None], [None]],
		), (
# 05
			'<FIELD datatype="unicodeChar"><VALUES null="&#xbabe;"/></FIELD>',
			b"\x00\x00a\x00\x23\x42\x00\xba\xbe\x80\x00\x00",
			[['a'],['\u2342'], [None], [None]],
		), (
			'<FIELD datatype="unsignedByte"><VALUES null="12"/></FIELD>'
				'<FIELD datatype="short"><VALUES null="12"/></FIELD>'
				'<FIELD datatype="int"><VALUES null="12"/></FIELD>'
				'<FIELD datatype="long"><VALUES null="12"/></FIELD>',
			b"\x00\x0c\x00\x0c\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x0c"
			b"\xf0\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
			b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
			b"\x00\x0d\x0d\x0d\x00\x0d\x0d\x0c\x00\xdd\x00\x00\x00\x00\x00\x0c", [
				[None, None, None, None],
				[None, None, None, None],
				[0, 0, 0, 0],
				[13, 3341, 855308, 62205969853054988]]
		), (
			'<FIELD datatype="float"/>',
			b"\x00\x7f\xc0\x00\x00\x00:\x80\x00\x00\x80\x00\x00\x00\x00",
			[[None], [0.0009765625], [None]]
		), (
			'<FIELD datatype="double"/>',
			b"\x00\x7f\xf8\x00\x00\x00\x00\x00\x00\x00?P\x00\x01\x00\x00\x00\x00",
			[[None], [0.00097656343132257462]]
		), (
			'<FIELD datatype="doubleComplex"/>',
			b"\x00\x7f\xf8\x00\x00\x00\x00\x00\x00?P\x00\x01\x00\x00\x00\x00"
			b'\x00@\x04\x00\x00\x00\x00\x00\x00?\xe0\x00\x00\x00\x00\x00\x00'
			b'\x80@\x04\x00\x00\x00\x00\x00\x00?\xe0\x00\x00\x00\x00\x00\x00',
			[[None], [2.5+0.5j], [None]]
		), (
# 10
			'<FIELD datatype="char" arraysize="4"><VALUES null="x"/></FIELD>',
			b"\x00abcd\x80\x00\x00\x00\x00",
			[["abcd"], [None]]
		), (
			'<FIELD datatype="char" arraysize="*"/>',
			b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03abc\x80\x00\x00\x00\x00",
			[[""], ["abc"], [None]]
		), (
			'<FIELD datatype="unicodeChar" arraysize="*"/>',
			b"\x00\x00\x00\x00\x03\x00a\x23\x42bc",
			[["a\u2342\u6263"]]
		), (
			'<FIELD datatype="unicodeChar" arraysize="2"/>',
			b"\x00\x00a\x23\x42\x80\x00\x00\x00\x00",
			[["a\u2342"], [None]]
		), (
			'<FIELD datatype="unsignedByte" arraysize="2"/>',
			b'\x00\x00\x01\x80\x00\x00',
			[[[0, 1]], [None]],
		), (
# 15
			'<FIELD datatype="short" arraysize="*"><VALUES null="16"/></FIELD>',
			b'\x00\x00\x00\x00\x03\x00\x01\x00\x10\x00\x02'
			b'\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00',
			[[[1, None, 2]], [None], [[]]],
		), (
			'<FIELD datatype="int" arraysize="2"/>',
			b'\x00\x00\x00\x00\x03\x00\x01\x00\x10\x80\0\0\0\0\0\0\0\0',
			[[[3, 0x10010]], [None]],
		), (
			'<FIELD datatype="float" arraysize="2"/>',
			b'\x00\x7f\xc0\x00\x00:\x80\x00\x00',
			[[[None, 0.0009765625]]],
		), (
			'<FIELD datatype="double" arraysize="*"/>',
			b'\x00\x00\x00\x00\x02\x7f\xf8\x00\x00\x00\x00\x00\x00'
				b'?P\x00\x01\x00\x00\x00\x00',
			[[[None, 0.00097656343132257462]]]
		), (
			'<FIELD datatype="float" arraysize="2"><VALUES null="2"/></FIELD>',
			b'\x00\x7f\xc0\x00\x00:\x80\x00\x00',
			[[[None, 0.0009765625]]],
		), (
# 20
			'<FIELD datatype="floatComplex" arraysize="2"/>',
			b'\x00\x7f\xc0\x00\x00:\x80\x00\x00'
				b'A\x80\x00\x00A\x0c\x00\x00',
			[[[None, 16+8.75j]]],
		), (
			'<FIELD datatype="short" arraysize="2x3"/>',
			b'\x00\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05',
			[[[[0,1],[2,3],[4,5]]]],
		), (
			'<FIELD datatype="double" arraysize="2" xtype="point"/>',
			b'\x80\x7f\xf8\x00\x00\x00\x00\x00\x00\x7f\xf8\x00\x00\x00\x00\x00\x00',
			[[None]],
		), (
			'<FIELD datatype="double" arraysize="2" xtype="point"/>',
			b'\x00@Y\x00\x00\x00\x00\x00\x00@7\x00\x00\x00\x00\x00\x00',
			[[pgsphere.SPoint.fromDegrees(100, 23)]],
		), (
			'<FIELD datatype="double" arraysize="3" xtype="circle"/>',
			b'\x80\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00'
			b'\x7f\xf8\x00\x00\x00\x00\x00\x00',
			[[None]]
		), (
# 25
			'<FIELD datatype="double" arraysize="3" xtype="circle"/>',
			b'\x00@Y\x00\x00\x00\x00\x00\x00'
			b'\xc07\x00\x00\x00\x00\x00\x00'
			b'?\xd0\x00\x00\x00\x00\x00\x00',
			[[pgsphere.SCircle.fromDALI([100, -23, 0.25])]]
		), (
			'<FIELD datatype="double" arraysize="*" xtype="polygon"/>',
			b'\x80\x00\x00\x00\x00',
			[[None]]
		), (
			'<FIELD datatype="double" arraysize="*" xtype="polygon"/>',
			b'\x00\x00\x00\x00\x06'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'\x00\x00\x00\x00\x00\x00\x00\x00'
			b'@`@\x00\x00\x00\x00\x00'
			b'@4\x00\x00\x00\x00\x00\x00'
			b'@Q\x80\x00\x00\x00\x00\x00'
			b'\xc0$\x00\x00\x00\x00\x00\x00',
			[[pgsphere.SPoly.fromDALI([0, 0, 130, 20, 70, -10])]]
		), (
			'<FIELD datatype="char" arraysize="10"/>',
			b"\x00abc\x00tail  ",
			[["abc"]]),
]


class Binary2RoundtripTest(testhelpers.VerboseTest,
		metaclass=testhelpers.SamplesBasedAutoTest):
	"""tests for roundtripping values through BINARY2 VOTables.
	"""

	def _runTest(self, sample):
		fielddefs, input = sample
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[fielddefs], input, V.BINARY2)]]
		valAfter = list(next(votable.parseBytes(
			votable.asBytes(vot))))
		self.assertEqual(input, valAfter)
	
	samples = [
		([V.FIELD(datatype="int"), V.FIELD(datatype="float")],
			[[5, None], [None, 4.25]]),
		([V.FIELD(datatype="char"), V.FIELD(datatype="char", arraysize="*")],
			[['a', None], [None, 'abc']]),
		([V.FIELD(datatype="boolean"), V.FIELD(datatype="doubleComplex")],
			[[True, None], [None, 0.25+3j]]),
		([V.FIELD(datatype="int", arraysize="2")],
			[[[1,2]], [[-2, -3]], [None]]),
		([V.FIELD(datatype="int", arraysize="2*")],
			[[[1,2,3,4]], [[-2, -3, -4]], [None]]),
# 5
		([V.FIELD(datatype="float", arraysize="2x3")],
			[[[[1,2],[3,4],[-2,-3]]], [None]]),
		([V.FIELD(datatype="float", arraysize="2x3*")],
			[[[[1,2],[3,4],[-2,-3]]], [None]]),
		([V.FIELD(datatype="double", arraysize="2", xtype="point")],
			[[pgsphere.SPoint(6, 12)], [None]]),
		([V.FIELD(datatype="char", arraysize="*", xtype="timestamp")],
			[[datetime.datetime(2017, 9, 5, 10, 16)], [None]]),
		([V.FIELD(datatype="double", arraysize="4", xtype="x-box")],
			[[pgsphere.SBox(
				pgsphere.SPoint.fromDegrees(2, -12),
				pgsphere.SPoint.fromDegrees(12, 30))], [None]]),
# 10
		([V.FIELD(datatype="char", arraysize="*", xtype="adql:POINT")],
			[[pgsphere.SPoint.fromDegrees(2, -12)], [None]]),
		# on roundtrip, we'll ASCII-escape any non-ASCII json, so don't
		# test for that.
		([V.FIELD(datatype="char", arraysize="*", xtype="json")],
			[[r'"\u00e4jsonstring"'], [None]]),
	]


class NDArrayTest(testhelpers.VerboseTest):
	def _assertRavels(self, arrayspec, data, expected):
		res = votable.unravelArray(arrayspec, data)
		self.assertEqual(res, expected)

	def testUnravelNull(self):
		self._assertRavels("*", list(range(10)), list(range(10)))
	
	def testUnravelPlain(self):
		self._assertRavels("3x2", list(range(6)), [[0,1,2],[3,4,5]])

	def testUnravelSkewed(self):
		self._assertRavels("3x2", list(range(5)), [[0,1,2],[3,4]])

	def testUnravelOverlong(self):
		self._assertRavels("3x2", list(range(9)), [[0,1,2],[3,4,5],[6,7,8]])

	def testUnravelAccpetsStar(self):
		self._assertRavels("3x2*", list(range(9)), [[0,1,2],[3,4,5],[6,7,8]])

	def testUnravel3d(self):
		self._assertRavels("3x2x2", list(range(12)),
			[[[0,1,2],[3,4,5]], [[6,7,8],[9,10,11]]])

	def assertRoundtrips(self, fieldDef, value, serialization):
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[[fieldDef]], [[value]], serialization)]]
		valAfter = list(next(votable.parseBytes(
			votable.asBytes(vot))))
		self.assertEqual(value, valAfter[0][0])

	def assertRoundtripsBinary(self, fieldDef, value):
		self.assertRoundtrips(fieldDef, value, V.BINARY)

	def assertRoundtripsBinary2(self, fieldDef, value):
		self.assertRoundtrips(fieldDef, value, V.BINARY2)

	def assertRoundtripsTabledata(self, fieldDef, value):
		self.assertRoundtrips(fieldDef, value, V.TABLEDATA)

	def testFloat2DArray(self):
		args = (V.FIELD(name="x", datatype="double", arraysize="3x2"),
			[[2.25, 1.5, 0.], [1.75, -3.5, 9.]])
		self.assertRoundtripsBinary(*args)
		self.assertRoundtripsBinary2(*args)
		self.assertRoundtripsTabledata(*args)

	def testTruncatedFloat2DArray(self):
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[[V.FIELD(name="x", datatype="double", arraysize="3x2")]],
			[[
				[[2.25, 1.5, 0.], [1.75, -3.5]] ]], V.TABLEDATA)]]
		valAfter = list(next(votable.parseBytes(
			votable.asBytes(vot))))
		self.assertEqual(valAfter[0][0],
			[[2.25, 1.5, 0.], [1.75, -3.5, None]])

	def testVarSizeIntArray(self):
		args = (V.FIELD(name="x", datatype="int", arraysize="3x2x*"),
				[[[2, 1, 0], [1, -3, 9]],
					[[3, 0, -1], [0, 4, 8]],
					[[1, 3, 7], [23, 1, -8]]]
			)
		self.assertRoundtripsBinary(*args)
		self.assertRoundtripsBinary2(*args)
		self.assertRoundtripsTabledata(*args)

	def test2DCharArray(self):
		args = (V.FIELD(name="x", datatype="char", arraysize="12x*"),
			["012345678901", "123456789012"])
		self.assertRoundtripsBinary(*args)
		self.assertRoundtripsBinary2(*args)
		self.assertRoundtripsTabledata(*args)

	def test2DUnicodeArray(self):
		args = (V.FIELD(name="x", datatype="unicodeChar", arraysize="12x*"),
			["012345678901", "123456789012"])
		self.assertRoundtripsBinary(*args)
		self.assertRoundtripsBinary2(*args)
		self.assertRoundtripsTabledata(*args)


class VOTableDataValidationTest(testhelpers.VerboseTest):
	def testNoSurrogates(self):
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[V.FIELD(datatype="char", arraysize="*", xtype="json")],
			[['"🄁"']], V.TABLEDATA)]]
		content = votable.asBytes(vot)
		self.assertTrue(b"Will not produce utf-8 surrogates" in content)


class ArraySizeValidationTest(testhelpers.VerboseTest):
	def _decode(self, fieldbody, literal):
		table = next(votable.parseBytes((
			'<VOTABLE><RESOURCE><TABLE>'+
			'<FIELD name="x" '+fieldbody+'/>'
			'<DATA><TABLEDATA>'+
			'<TR><TD>'+literal+'</TD></TR>'
			'</TABLEDATA></DATA>'
			'</TABLE></RESOURCE></VOTABLE>')))
		return list(table)[0][0]

	def test1DMissingRaises(self):
		self.assertRaisesWithMsg(votable.BadVOTableLiteral,
			"Invalid literal for int[2] (field x): '<1 token(s)>'",
			self._decode,
			('datatype="int" arraysize="2"', '1'))

	def test1DOverflowRaises(self):
		self.assertRaisesWithMsg(votable.BadVOTableLiteral,
			"Invalid literal for int[2] (field x): '<3 token(s)>'",
			self._decode,
			('datatype="int" arraysize="2"', '1 2 3'))

	def test1DFlexibleOk(self):
		self.assertEqual(self._decode('datatype="int" arraysize="2*"', '1 2 3'),
			[1, 2, 3])
	
	def test2DMissingRaises(self):
		self.assertRaisesWithMsg(votable.BadVOTableLiteral,
			"Invalid literal for float[2x3] (field x): '<5 token(s)>'",
			self._decode,
			('datatype="float" arraysize="2x3"', '1 2 3 4 5'))

	def test2DMFlexibleRaises(self):
		self.assertRaisesWithMsg(votable.BadVOTableLiteral,
			"Invalid literal for float[2x*] (field x): '<5 token(s)>'",
			self._decode,
			('datatype="float" arraysize="2x*"', '1 2 3 4 5'))

	def test2DMFlexibleOk(self):
		self.assertEqual(self._decode('datatype="float" arraysize="2x*"',
			'1 2 3 4'),
			[[1., 2.], [3., 4.]])

	def testBadArraysize(self):
		self.assertRaisesWithMsg(votable.VOTableError,
			"Invalid arraysize '*x2' specified in field or param name 'x'",
			self._decode,
			('datatype="float" arraysize="*x2"', '1 2 3 4 5'))

	def testMoreBadArraysize(self):
		self.assertRaisesWithMsg(votable.VOTableError,
			"Invalid arraysize fragment '*2' in field or param name 'x'",
			self._decode,
			('datatype="float" arraysize="4x4x*2"', '1 2 3 4 5'))


class WeirdTablesTest(testhelpers.VerboseTest):
	"""tests with malformed tables and fringe cases.
	"""
	def testEmpty(self):
		for data in votable.parseBytes("<VOTABLE/>"):
			self.fail("A table is returned for an empty VOTable")

	def testEmptySimple(self):
		data, metadata = votable.load(io.BytesIO((b"<VOTABLE/>")))
		self.assertTrue(data is None)

	def testBadStructure(self):
		it = votable.parseBytes("<VOTABLE>")
		self.assertRaisesWithMsg(votable.VOTableParseError,
			"(internal source) no element found: line 1, column 9", list, it)

	def testLargeTabledata(self):
		# This test is supposed to exercise multi-chunk parsing.  So,
		# raise the "*20" below when you raise tableparser._StreamData.minChunk
		vot = V.VOTABLE[V.RESOURCE[votable.DelayedTable(
    	V.TABLE[V.FIELD(name="col1", datatype="char", arraysize="*"),],
    	[["a"*1000]]*20, V.BINARY)]]
		dest = io.BytesIO()
		votable.write(vot, dest)
		dest.seek(0)
		data, metadata = votable.load(dest)
		self.assertEqual(len(data), 20)
		self.assertEqual(len(testhelpers.pickSingle(data[0])), 1000)

	def testUnknownAttributesFails(self):
		it = votable.parseBytes('<VOTABLE><RESOURCE class="upper"/></VOTABLE>')
		self.assertRaisesWithMsg(votable.VOTableParseError,
			'At IO:\'<VOTABLE><RESOURCE class="upper"/></VOTABLE>\', (1, 9):'
			' Attribute \'class\' invalid on RESOURCE',
			list,
			(it,))

	def testBadElements(self):
		it = votable.parseBytes("<VOTABLE><FOO/></VOTABLE>")
		self.assertRaisesWithMsg(votable.VOTableParseError,
			"At IO:'<VOTABLE><FOO/></VOTABLE>', (1, 9):"
			" Unknown tag: FOO", list, it)

	def testIgnoringBadElements(self):
		data, metadata = votable.load(io.StringIO(
				'<VOTABLE><FOO><bar><PARAM name="quux"/>"'
				'</bar></FOO></VOTABLE>'),
			raiseOnInvalid=False)
		self.assertTrue(data is None)

	def testIgnoringBadAttributes(self):
		data, metadata = votable.load(io.StringIO(
				'<VOTABLE><RESOURCE class="upper"/></VOTABLE>'),
			raiseOnInvalid=False)
		self.assertTrue(data is None)


class StringArrayTest(testhelpers.VerboseTest):
	"""tests for the extra-special case of 2+D char arrays.
	"""
	def _get2DTable(self, enc):
		return V.VOTABLE[V.RESOURCE[votable.DelayedTable(
			V.TABLE[V.FIELD(name="test", datatype="char", arraysize="2x*")],
			[[["ab", b"c", "def"]]], enc)]]

	def test2dTdencWrite(self):
		ser = votable.asBytes(self._get2DTable(V.TABLEDATA))
		self.assertTrue(b"<TD>abc de</TD>" in ser)


class UnicodeCharStringsTest(testhelpers.VerboseTest):
# These are about making sure we don't bomb when someone hands us
# unicode strings for char tables.
	def _getDataTable(self, enc):
		return votable.asBytes(
			V.VOTABLE[V.RESOURCE[votable.DelayedTable(
				V.TABLE[V.FIELD(name="test", datatype="char", arraysize="*")],
				[["\u03b2"]], enc)]])

	def testInTD(self):
		self.assertTrue(b"<TD>?</TD>" in
			self._getDataTable(V.TABLEDATA))

	def testInBinary(self):
		self.assertTrue(b'STREAM encoding="base64">AAAAAT8'
			in self._getDataTable(V.BINARY))


class SimpleInterfaceTest(testhelpers.VerboseTest):
	def testIterDict(self):
		with open("test_data/importtest.vot") as f:
			data, metadata = votable.load(f)
			res = list(metadata.iterDicts(data))
			self.assertEqual(res[0]["FileName"], "ngc104.dat")
			self.assertEqual(res[1]["apex"], None)

	def testWrite(self):
		with open("test_data/importtest.vot") as f:
			data, metadata = votable.load(f)
			dest = io.BytesIO()
			votable.save(data, metadata.votTable, dest)
			content = dest.getvalue()
			self.assertTrue(b"QFILtsN2C/ZAGBY4hllK" in content)
			self.assertTrue(b'name="n_VHB"' in content)
			self.assertTrue(b'Right Ascension (J2000)</DESCRIPTION>' in content)


class RecordArrayTest(testhelpers.VerboseTest):
	def testPlain(self):
		data, metadata = votable.loads(b"""<VOTABLE>
			<RESOURCE><TABLE>
				<FIELD name="a" datatype="int"/>
				<FIELD name="b" datatype="double"/>
				<FIELD name="c" datatype="char" arraysize="4"/>
				<DATA><TABLEDATA>
					<TR><TD>1</TD><TD>23.25</TD><TD>abcd</TD></TR>
					<TR><TD>2</TD><TD>-2e6</TD><TD>x</TD></TR>
				</TABLEDATA></DATA></TABLE></RESOURCE></VOTABLE>""")
		arr = rec.array([tuple(t) for t in data],
			dtype=votable.makeDtype(metadata))
		self.assertEqual(tuple(arr[0]), (1, 23.25, b'abcd'))
		self.assertEqual(tuple(arr[1]),  (2, -2000000.0, b'x'))

	def testVarLengthStrings(self):
		data, metadata = votable.loads(b"""<VOTABLE>
			<RESOURCE><TABLE>
				<FIELD name="a" datatype="short"/>
				<FIELD name="c" datatype="char" arraysize="*"/>
				<DATA><TABLEDATA>
					<TR><TD>1</TD><TD>short string</TD></TR>
					<TR><TD>2</TD><TD>A long, long string that will have to be trunc...</TD></TR>
				</TABLEDATA></DATA></TABLE></RESOURCE></VOTABLE>""")
		arr = rec.fromrecords([tuple(t) for t in data],
			dtype=votable.makeDtype(metadata))
		self.assertEqual(arr[0][1], b'short string')
		self.assertEqual(arr[1][1], b'A long, long string ')


class StanXMLTest(testhelpers.VerboseTest):
	# make sure VOTable work as normal stanxml trees in a pinch
	def testSimple(self):
		vot = V.VOTABLE[
			V.INFO(name="QUERY_STATUS", value="ERROR")["Nothing: µ-test"]]
		ser = vot.render()
		self.assertEqual(ser, b'<VOTABLE version="1.5" xmlns="http://www.ivoa.net/xml/VOTable/v1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.3 http://vo.ari.uni-heidelberg.de/docs/schemata/VOTable.xsd"><INFO name="QUERY_STATUS" value="ERROR">Nothing: \xc2\xb5-test</INFO></VOTABLE>')


class _BOMB(V._VOTElement):
	def isEmpty(self):
		return False

	def write(self, outputFile):
		raise base.ReportableError("This element is a VOTable Bomb.")


class StackUnwindingTest(testhelpers.VerboseTest):
# these test that even if there's an ugly error during VOTable
# serialization, some semblance of VOTable results, and also that
# there's an appropriate error message
	def testErrorWhileMeta(self):
		vot = V.VOTABLE[
			V.RESOURCE(type="results")[
				V.TABLE[
					V.FIELD(name="junk"),],
				V.DEFINITIONS[_BOMB()]]]
		result = votable.asBytes(vot)
		self.assertTrue(b"</INFO></RESOURCE></VOTABLE>" in result)
		self.assertTrue(b"content is probably incomplete" in result)
		self.assertTrue(b"This element is a VOTable Bomb")
	
	def testErrorWhileTABLEData(self):
		vot = V.VOTABLE[
			V.RESOURCE(type="results")[
				votable.DelayedTable(
					V.TABLE[
						V.FIELD(name="junk", datatype="float"),],
					[[0.2], ["abc"]], V.TABLEDATA)]]
		result = votable.asBytes(vot)
		self.assertTrue(b"<TD>0.2" in result)
		self.assertTrue(b"content is probably incomplete" in result)
		self.assertTrue(b"could not convert string to float: 'abc'" in result)
		self.assertTrue(b"</TABLEDATA>" in result)
		self.assertTrue(b"</RESOURCE></VOTABLE>" in result)

	def testErrorWhileBINARY(self):
		vot = V.VOTABLE[
			V.RESOURCE(type="results")[
				votable.DelayedTable(
					V.TABLE[
						V.FIELD(name="junk", datatype="float"),],
					[[0.2], ["abc"]], V.BINARY)]]
		result = votable.asBytes(vot)
		self.assertTrue(b"PkzMzQ==" in result)
		self.assertTrue(b"content is probably incomplete" in result)
		self.assertTrue(b"is not a float" in result)
		self.assertTrue(b"</BINARY>" in result)
		self.assertTrue(b"</RESOURCE></VOTABLE>" in result)


class ParamTypecodeGuessingTest(testhelpers.SimpleSampleComparisonTest):

	functionToRun = staticmethod(votable.guessParamAttrsForValue)
	
	samples = [
		(15, {"datatype": "short"}),
		("23", {"datatype": "char", "arraysize": "*"}),
		([1, 2], {"datatype": "short", "arraysize": "2"}),
		([[1, 2], [2, 3], [4, 5]], {"datatype": "short", "arraysize": "2x3"}),
		(["abc", "defg", "alles Quatsch"], {"datatype": "char",
			"arraysize": "13x3"}),
# 05
		([datetime.datetime.now(), datetime.datetime.now()],
			{"datatype": "char", "arraysize": "20x2", "xtype": "timestamp"}),
		(pgsphere.SPoint.fromDegrees(10, 20),
			{'arraysize': '2', 'datatype': 'double precision', 'xtype': 'point'}),
		([pgsphere.SPoint.fromDegrees(10, 20)],
			{'datatype': 'double precision', 'arraysize': '2x1', 'xtype': 'point'}),
		(base.NotGiven, base.NotFoundError(repr(base.NotGiven),
			"VOTable type code for", "paramval.py predefined types")),
		(150000, {"datatype": "int"}),
#10
		(150000000000, {"datatype": "long"}),
	]


class ParamValueSerializationTest(testhelpers.SimpleSampleComparisonTest):

	def functionToRun(self, args):
		datatype, arraysize, val = args
		param = V.PARAM(name="tmp", datatype=datatype, arraysize=arraysize)
		votable.serializeToParam(param, val)
		return param.value

	samples = [
		(("int", None, None), "99"),
		(("char", "*", None), ""),
		(("char", "4", None), "xxxx"),
		(("float", "1", None), "NaN"),
		(("double", "2x2", None), "NaN NaN NaN NaN"),
# 5
		(("long", "2x2", None), "99 99 99 99"),
		(("int", None, 33), "33"),
		(("char", "4x*", ["foobar", "wo", "nnnn"]), "foobwo  nnnn"),
		(("int", "2x3x4", list(range(24))), '0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23'),
		(("double", None, 0.25), "0.25"),
# 10
		(("double", "2", [0.25, 1]), "0.25 1.0"),
	]

	def testDatetimeValueLegacy(self):
		param = V.PARAM(name="tmp", datatype="char", arraysize="*",
			xtype="adql:TIMESTAMP")
		votable.serializeToParam(param,
			datetime.datetime(2015, 5, 19, 15, 6, 22, 25))
		self.assertEqual(param.value, "2015-05-19T15:06:22Z")

	def testDatetimeValue(self):
		param = V.PARAM(name="tmp", datatype="char", arraysize="*",
			xtype="timestamp")
		votable.serializeToParam(param,
			datetime.datetime(2015, 5, 19, 15, 6, 22, 25))
		self.assertEqual(param.value, "2015-05-19T15:06:22Z")
	
	def testDatetimeArray(self):
		param = V.PARAM(name="tmp", datatype="char", arraysize="19x2",
			xtype="timestamp")
		votable.serializeToParam(param,
				[datetime.datetime(2020, 1, 1),datetime.datetime(2021, 1, 1)])
		self.assertEqual(param.value, "2020-01-01T00:00:002021-01-01T00:00:00")

	def testDatetimeNULL(self):
		param = V.PARAM(name="tmp", datatype="char", arraysize="*",
			xtype="adql:TIMESTAMP")
		votable.serializeToParam(param, None)
		self.assertEqual(param.value, "")
	
	def testSPointValue(self):
		param = V.PARAM(name="tmp", datatype="char", arraysize="*",
			xtype="adql:POINT")
		votable.serializeToParam(param,
			pgsphere.SPoint.fromDegrees(10, 12))
		self.assertEqual(param.value, "Position UNKNOWNFrame 10. 12.")

	def testSPointModern(self):
		param = V.PARAM(name="tmp", datatype="double", arraysize="2",
			xtype="point")
		votable.serializeToParam(param,
			pgsphere.SPoint.fromDegrees(10, 12))
		self.assertEqual(param.value, '10.0 12.0')

	def testNullIsEmpty(self):
		param = V.PARAM(name="tmp", datatype="char", arraysize="*")
		self.assertTrue(b'value=""' in votable.asBytes(param))

	def testNullOption(self):
		param = V.PARAM(name="tmp", datatype="int", value="2")[
			V.VALUES[
				V.OPTION(name="empty", value=""),
				V.OPTION(name="full", value="2")]]
		serialized = votable.asBytes(param)
		self.assertTrue(b'value="2"' in serialized)
		self.assertTrue(b'value=""' in serialized)

	def testTimeIntervalSerialisation(self):
		param = V.PARAM(name="tmp", datatype="char", arraysize="19x2",
			xtype="timestamp")
		votable.serializeToParam(param,
			[utils.parseISODT("2013-12-01T10:20:30"),
				utils.parseISODT("2013-12-21T11:21:31")])
		self.assertEqual(param.value, "2013-12-01T10:20:302013-12-21T11:21:31")

	def testTimeIntervalParsing(self):
		parser = paramval.getVOTParser("char", "19x2", "timestamp")
		self.assertEqual(
			parser("2013-12-01T10:20:302013-12-21T11:21:31"),
			[utils.parseISODT("2013-12-01T10:20:30"),
				utils.parseISODT("2013-12-21T11:21:31")])


if __name__=="__main__":
	testhelpers.main(BinaryReadTest)
