gen-testdata 8.36 KB
Newer Older
1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
# Copyright (C) 2017-2024  Nexedi SA and Contributors.
4
#                          Kirill Smelkov <kirr@nexedi.com>
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#
# This program is free software: you can Use, Study, Modify and Redistribute
# it under the terms of the GNU General Public License version 3, or (at your
# option) any later version, as published by the Free Software Foundation.
#
# You can also Link and Combine this program with other software covered by
# the terms of any of the Free Software licenses or any of the Open Source
# Initiative approved licenses and Convey the resulting work. Corresponding
# source of such a combination shall include the source code for all other
# software used.
#
# This program is distributed WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See COPYING file for full licensing terms.
# See https://www.nexedi.com/licensing for rationale and options.
"""generate reference fs1 database and index for tests"""

23 24
from __future__ import print_function

25
from ZODB.FileStorage import FileStorage
26
from ZODB.FileStorage.FileStorage import FILESTORAGE_MAGIC, TxnHeader, DataHeader, TRANS_HDR_LEN
27
from ZODB import DB
28
from ZODB.Connection import TransactionMetaData
29 30 31 32
from zodbtools.test.gen_testdata import gen_testdb, precommit, run_with_all_zodb_pickle_kinds, current_zkind
from os import stat, remove, makedirs
from os.path import exists, relpath
from shutil import copyfile, rmtree
33
from golang.gcompat import qq
34 35 36 37 38 39 40 41 42 43 44 45 46

import struct

# convert numeric oid to/from str
def p64(num):
    return struct.pack('>Q', num)

def unpack64(packed):
    return struct.unpack('>Q', packed)[0]

def hex64(packed):
    return '0x%016x' % unpack64(packed)

47 48 49 50 51 52
# bchar returns bytes corresponding to bytes_data[i].
# it works as bytes_data[i] on py2, but on py3, contrary to builtin behaviour, also returns bytes instead of int.
def bchar(bytes_data, i): # -> bytes
    if i >= len(bytes_data):
        raise IndexError('index out of range')
    return bytes_data[i:i+1]
53

54 55 56 57 58 59 60 61 62

def main2():
    zkind = current_zkind()
    prefix = "testdata/%s" % zkind
    if exists(prefix):
        rmtree(prefix)
    makedirs(prefix)

    outfs = "%s/1.fs" % prefix
63 64 65 66
    gen_testdb(outfs)

    # dump to go what to expect
    stor = FileStorage(outfs, read_only=True)
67
    with open("ztestdata_expect_%s_test.go" % zkind, "w") as f:
68
        def emit(v):
69 70
            print(v, file=f)
        emit("// Code generated by %s; DO NOT EDIT." % relpath(__file__))
71
        emit("package fs1\n")
72
        emit("import \"lab.nexedi.com/kirr/neo/go/zodb\"\n")
73

74 75
        emit("func init() {")

76
        # index
77 78
        emit("\tconst _1fs_indexTopPos = %i" % stor._pos)
        emit("\tvar _1fs_indexEntryv = []indexEntry{")
79
        for k, v in stor._index.iteritems():
80 81
            emit("\t\t{%8i, %8i}," % (unpack64(k), v))
        emit("\t}")
82

83
        # database records
84
        emit("\n\tvar _1fs_dbEntryv = []dbEntry{")
85 86 87 88 89 90 91 92 93 94 95 96
        txnLenPrev = 0
        for txn in stor.iterator():     # txn is TransactionRecord
            # txn.extension is already depickled dict - we want to put raw data from file
            # also we need to access txn record length which is not provided by higher-level iterator
            # do deep-dive into FileStorage
            th = stor._read_txn_header(txn._tpos)
            assert th.tid == txn.tid
            assert th.tlen == txn._tend - txn._tpos

            # fs1/go keeps in RAM whole txn length, not len-8 as it is on disk
            txnLen = th.tlen + 8

97
            emit("\t\t{")
98 99

            # -> TxnHeader
100 101 102 103 104 105 106 107 108 109 110
            emit("\t\t\tTxnHeader{")
            emit("\t\t\t\tPos:\t  %i," % txn._tpos)
            emit("\t\t\t\tLenPrev:  %i," % txnLenPrev)
            emit("\t\t\t\tLen:\t  %i," % txnLen)
            emit("\t\t\t\tTxnInfo:\tzodb.TxnInfo{")
            emit("\t\t\t\t\tTid:\t%s," % hex64(txn.tid))
            emit("\t\t\t\t\tStatus:\t'%s'," % txn.status)
            emit("\t\t\t\t\tUser:\t\t[]byte(%s)," % qq(txn.user))
            emit("\t\t\t\t\tDescription:\t[]byte(%s)," % qq(txn.description))
            emit("\t\t\t\t\tExtension:\t[]byte(%s)," % qq(th.ext))
            emit("\t\t\t\t},")
111 112 113 114 115
            emit("\t\t\t},")

            txnLenPrev = txnLen

            # -> DataHeader + payload
116
            emit("\n\t\t\t[]txnEntry{")
117 118 119 120 121 122 123 124 125

            for drec in txn:            # drec is itemof(TransactionRecordIterator) = Record
                # same as with txn - not everything is possible to get via
                # higher-level api
                dh = stor._read_data_header(drec.pos)
                assert dh.oid == drec.oid
                assert dh.tid == drec.tid
                assert dh.tloc == txn._tpos

126 127 128 129 130
                emit("\t\t\t\t{")
                emit("\t\t\t\t\tDataHeader{")
                emit("\t\t\t\t\t\tPos:\t%i," % drec.pos)
                emit("\t\t\t\t\t\tOid:\t%i," % unpack64(drec.oid))
                emit("\t\t\t\t\t\tTid:\t%s," % hex64(drec.tid))
131

132 133
                emit("\t\t\t\t\t\tPrevRevPos:\t%i," % dh.prev)
                emit("\t\t\t\t\t\tTxnPos:\t%i," % txn._tpos)
134
                assert drec.version == ''
135 136
                emit("\t\t\t\t\t\tDataLen:\t%i," % dh.plen)
                emit("\t\t\t\t\t},")
137 138 139 140 141 142 143
                plen = dh.plen
                if plen == 0:
                    rawdata = p64(dh.back) # back-pointer or 0 (= delete)
                    if drec.data is None:
                        data = "/* deleted */ nil"
                        datatid = "/* deleted */ 0"
                    else:
144
                        data = "[]byte(%s)" % qq(drec.data)
145 146 147 148 149 150
                        datatid = "/* copy from */ " + hex64(drec.data_txn)
                else:
                    rawdata = drec.data
                    data = "/* same as ^^^ */ sameAsRaw"
                    datatid = "/* no copy */ 0"

151 152 153 154
                emit("\t\t\t\t\t[]byte(%s)," % qq(rawdata))
                emit("\t\t\t\t\t%s," % data)
                emit("\t\t\t\t\t%s," % datatid)
                emit("\t\t\t\t},")
155

156
            emit("\t\t\t},")
157
            emit("\t\t},")
158 159 160
        emit("\t}")

        emit("\n\tztestdataReg.Register(%s, %s, &_TestDataOK{_1fs_indexTopPos, _1fs_indexEntryv, _1fs_dbEntryv})" % (qq(zkind), qq(prefix)))
161
        emit("}")
162 163 164 165
    stor.close()


    # prepare file with voted (not fully committed) tail
166
    voted = "%s/1voted.fs" % prefix
167
    copyfile(outfs, voted)
168 169 170 171 172 173 174 175 176 177
    def _():
        vstor = FileStorage(voted)
        vdb   = DB(vstor)
        vconn = vdb.open()
        vroot = vconn.root()
        vroot._p_activate() # to know its current serial

        txn = precommit(u"author", u"description", {'aaa': 'bbb'})
        txn_stormeta = TransactionMetaData(txn.user, txn.description, txn.extension)
        vstor.tpc_begin(txn_stormeta)
178
        vstor.store(vroot._p_oid, vroot._p_serial, b'000 data 000', '', txn_stormeta)
179 180
        vstor.tpc_vote(txn_stormeta)
        # NO tpc_finish here so that status remain 'c' (voted) instead of ' ' (committed)
181
    _()
182 183 184 185 186 187

    st = stat(outfs)
    l  = st.st_size
    vf = open(voted, 'rb')
    vf.seek(l)
    voted_tail = vf.read()
188
    assert bchar(voted_tail, -1+8+8+1) == b'c'  # voted, not finished (' ')
189

190
    with open("%s/1voted.tail" % prefix, "wb") as vt:
191 192 193 194 195 196
        vt.write(voted_tail)

    remove(voted)
    remove(voted+".index")
    remove(voted+".tmp")
    remove(voted+".lock")
197

198

199
    # prepare file with whiteout (deletion of previously non-existing object)
200
    whiteout = "%s/whiteout.fs" % prefix
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
    # as of 20210317 FileStorage.deleteObject verifies that object exists
    # -> prepare magic/transaction/data records manually
    with open(whiteout, "wb") as f:
        oid = p64(1)
        tid = p64(0x17)

        # file header
        f.write(FILESTORAGE_MAGIC)
        tpos = f.tell()

        # data record (see FileStorage.deleteObject)
        dh = DataHeader(oid, tid, 0, tpos, 0, 0)
        drec = dh.asString() + p64(0)

        # emit txn header (see FileStorage.tpc_vote)
        tlen = TRANS_HDR_LEN + 0 + 0 + 0 + len(drec) # empty u,d,e
        th = TxnHeader(tid, tlen, ' ', 0, 0, 0)
        th.user  = b''
        th.descr = b''
        th.ext   = b''
        f.write(th.asString())

        # emit data record
        f.write(drec)

        # emit txn tail
        f.write(p64(tlen))


230 231 232 233 234 235 236 237 238
    # prepare empty.fs
    empty = "%s/empty.fs" % prefix
    with open(empty, "wb") as f:
        f.write(FILESTORAGE_MAGIC)


def main():
    run_with_all_zodb_pickle_kinds(main2)

239 240
if __name__ == '__main__':
    main()