Refactor for testing of Syncbase debug viewer.
Previously there was no testing of the Syncbase debug viewer code. This
CL refactors out the non-UI code into a separate package `sbtree` and
adds tests for that code.
Also added to syncbase/testutil a CreateSyncgroup function.
These tests found a wrong-index-variable bug bug similar to the one that
Fred previously found. This Cl aslo fixes that bug.
Change-Id: If273e4035b71fdc3734e52b98e964ff49b5d6365
diff --git a/services/debug/debug/browseserver/assets.go b/services/debug/debug/browseserver/assets.go
index a26d861..0172e73 100644
--- a/services/debug/debug/browseserver/assets.go
+++ b/services/debug/debug/browseserver/assets.go
@@ -143,7 +143,7 @@
return a, nil
}
-var _collectionHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x55\x51\x6f\xd3\x30\x10\x7e\xef\xaf\x38\x55\xa8\x6c\x93\x92\xb0\x21\x78\x80\x34\x68\xeb\x26\x31\x81\xaa\x89\x8e\xbd\x20\x1e\x9c\xf8\xd2\x58\x73\xed\xca\x76\xcb\x4a\xd4\xff\xce\x39\x49\x69\x13\xd2\x89\x27\x24\x22\xb5\xf5\x7d\x77\xdf\x9d\x7d\xf7\xd5\x29\xcb\xe8\x6c\x00\xf4\x4c\xf4\x72\x63\xc4\xbc\x70\x70\xf1\xea\xfc\x2d\xdc\x17\x08\x0f\x4c\x31\x2e\x56\x0b\xb8\x5c\xb9\x42\x1b\x1b\xc2\xa5\x94\x50\x05\x59\x30\x68\xd1\xac\x91\x87\x15\xfb\xab\x45\xd0\x39\xb8\x42\x58\xb0\x7a\x65\x32\x84\x4c\x73\x04\x32\xe7\x7a\x8d\x46\x21\x87\x74\x03\x0c\xae\x66\xd7\x81\x75\x1b\x89\x15\x4d\x8a\x0c\x15\x51\x5d\xc1\x1c\x64\x4c\x41\x8a\x90\xeb\x95\xe2\x20\x14\x81\x08\x9f\x6f\x27\x37\xd3\xd9\x0d\xe4\x42\x62\x38\x38\x8b\xb6\xdb\xc1\xa0\x2c\x39\xe6\x42\x21\x0c\x33\xad\x1c\x2a\x37\xf4\x68\x6c\x31\x73\x42\x2b\xc8\x24\xb3\x76\x3c\x6c\xcc\x20\xa0\x0a\x0e\x0d\x2c\xb8\x0c\xe6\x46\xf0\x61\x52\x55\x8e\x8b\x37\xc9\x44\x4b\xd9\x90\xca\x32\xbc\x66\x8e\xa5\xcc\x62\x78\xcb\xc3\x29\x5b\xe0\x76\x1b\x11\xba\x8f\xd9\xe3\x71\x44\xe4\x3a\x0b\x17\xeb\x5d\x41\x5f\x20\x43\x6a\xd0\x6e\x11\x04\xe7\x17\x41\xa6\x65\x53\xb1\x8e\x97\x7b\xc3\x9b\x2e\xb9\x5c\x2e\xe1\x4a\xa2\xb5\x42\xcd\xdf\x79\x20\xe6\x3c\xe9\x6c\x67\xe7\xf7\xa5\xc9\xdb\xc9\x30\x31\xc8\x9c\x36\x2f\xad\x9f\x82\xe9\x4d\xd6\x3e\xc5\xf3\xe9\xa6\xab\x45\x4a\x69\x68\x9a\x5f\xf4\x0f\x7b\x98\x85\xec\x09\x0d\xc7\xf5\xf3\xee\xb5\x63\x12\x3e\xe1\x06\x66\xe2\x27\x1e\xf2\xc8\x43\xb0\x47\xfb\x99\xe4\xb4\xfe\xb7\x05\xb7\xa2\x08\xa0\x6e\x48\x6c\x63\x1e\x35\x49\xec\x8a\x24\x8e\xfc\x17\x7d\x28\xd5\xef\xf5\x03\x93\x2b\xac\xad\x88\xe2\xfa\xa9\x24\x53\x69\x97\x4c\x8d\x87\xaf\x87\xdd\x10\x20\x59\x88\x1c\x42\xbf\xbf\x3b\x36\xc7\xf0\x23\xb3\x77\x06\xd7\x24\xb7\x6e\x20\xe5\x63\x50\x18\xcc\xc7\x24\xca\x5d\xaf\x3f\xa8\x71\x59\xbe\x08\x67\xfe\x7f\x62\x6a\xe9\x8c\x78\x3a\x3e\x3a\xdc\x11\x57\x5d\x67\xc3\xca\x2a\xd6\xb1\x29\x8e\x32\xf5\xa7\xbb\x66\xf6\x9c\xc9\x3f\xdf\x52\x96\x3d\x7e\xef\x3b\x45\xc4\xfa\xba\x80\x8a\xf7\x1c\x7a\xa4\x52\xbb\x7c\xdf\xed\xeb\xb1\x86\x97\xa5\x61\x6a\x8e\x07\xed\xa4\x05\xcd\xc8\xf6\x64\xae\x87\x53\x89\xe7\x56\x71\x7c\xf2\xba\x21\xb3\x81\x88\x77\x00\xc4\xfe\xa2\xf1\x70\x35\x6f\xef\xa8\x80\xda\xdf\xbb\x0f\x94\x16\x8f\x17\x6d\x29\xe2\x44\x69\x78\xa4\xfd\x9e\x3e\x97\xae\xa7\x39\x7f\xaf\x2e\x89\xea\xa0\x25\x53\x7c\x72\xd5\xf1\xfe\x1b\x85\x8d\x72\x61\xac\xa3\x1e\x8d\xeb\xc9\x74\xce\x71\x44\x80\x27\x8a\x02\x4e\xff\xa5\x00\x09\xe9\xde\x20\xed\xbb\x88\xac\xe6\x72\xa6\x95\x58\x27\x83\x38\x6a\xde\x21\x89\x7f\xe7\xd4\x3b\xf8\x15\x00\x00\xff\xff\xd3\x91\x27\x27\x2f\x07\x00\x00")
+var _collectionHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x55\xd1\x6f\xda\x3e\x10\x7e\xe7\xaf\x38\xa1\x9f\xf8\xb5\x95\x92\xac\x9d\xb6\x87\x2d\x64\x6a\x69\xa5\x55\x9b\x50\x35\xba\xbe\x4c\x7b\x70\xe2\x0b\xb1\x6a\x6c\x64\x1b\x56\x16\xf1\xbf\xef\x9c\x84\x32\xd2\x04\xed\x69\xd2\x22\x01\xbe\xef\xbe\xbb\xb3\xef\x3e\x9c\xb2\x8c\xce\x06\x40\xcf\x44\x2f\x37\x46\xcc\x0b\x07\x17\xaf\xce\xdf\xc2\x7d\x81\xf0\xc0\x14\xe3\x62\xb5\x80\xcb\x95\x2b\xb4\xb1\x21\x5c\x4a\x09\x15\xc9\x82\x41\x8b\x66\x8d\x3c\xac\xa2\xbf\x5a\x04\x9d\x83\x2b\x84\x05\xab\x57\x26\x43\xc8\x34\x47\x20\x73\xae\xd7\x68\x14\x72\x48\x37\xc0\xe0\x6a\x76\x1d\x58\xb7\x91\x58\x85\x49\x91\xa1\xa2\x50\x57\x30\x07\x19\x53\x90\x22\xe4\x7a\xa5\x38\x08\x45\x20\xc2\xe7\xdb\xc9\xcd\x74\x76\x03\xb9\x90\x18\x0e\xce\xa2\xed\x76\x30\x28\x4b\x8e\xb9\x50\x08\xc3\x4c\x2b\x87\xca\x0d\x3d\x1a\x5b\xcc\x9c\xd0\x0a\x32\xc9\xac\x1d\x0f\x1b\x33\x08\xa8\x82\x43\x03\x0b\x2e\x83\xb9\x11\x7c\x98\x54\x95\xe3\xe2\x4d\x32\xd1\x52\x36\x41\x65\x19\xde\x1b\xc4\xf0\x9a\x39\x96\x32\x8b\xe1\x2d\x0f\xa7\x6c\x81\xdb\x6d\xb4\x73\xed\xd9\x7b\x67\x1c\x51\x9a\x3a\x1f\x17\xeb\x5d\x69\x5f\x2a\x43\x6a\xd5\x6e\x11\x04\xe7\x17\x41\xa6\x65\x53\xbb\xe6\xcb\xbd\xe1\x4d\x97\x5c\x2e\x97\x70\x25\xd1\x5a\xa1\xe6\xef\x3c\x10\x73\x9e\x74\x6d\x6c\x47\xf2\xf5\x89\xd2\x4a\x33\x31\xc8\x9c\x36\xff\x5b\x3f\x14\xd3\x9f\xf1\xf0\x3c\xc7\x73\x4e\x57\x8b\x94\x72\xd1\x84\xbf\xe8\x1f\xf6\x45\x2a\x02\x27\x34\x35\xd7\x1d\x7c\xaf\x1d\x93\xf0\x09\x37\x30\x13\x3f\xf1\x45\x30\xb9\xc9\xe7\x5d\xdd\xe1\xe4\xb4\xfe\xf7\x00\x3e\x60\x11\x40\xcd\x91\x78\x88\x79\xd4\x24\xb1\x2b\x92\x38\xf2\x5f\xf4\xa1\x54\xcf\xeb\x07\x26\x57\x58\x5b\x11\xf1\xba\x43\x49\xc4\xd2\x2e\x99\x1a\x0f\x5f\x0f\xdb\x14\x20\xd1\x88\x1c\xea\x43\xf8\x4d\xde\xb1\x39\x86\x1f\x99\xbd\x33\xb8\x26\x45\xb6\xd9\x94\x94\x41\x61\x30\x1f\x93\x6e\x77\xad\xff\xa0\xc6\x65\xf9\x5f\x38\xf3\x7f\x25\x53\x6b\x6a\xc4\xd3\xf1\xf1\xa9\x8f\xb8\xea\x64\x34\xf1\xd9\x3e\xbe\x6f\xc6\xa3\x4c\xf5\x70\xea\x1c\x1d\x87\xf5\xcf\xb7\x94\x65\x8f\xdf\xbb\x4e\x16\xb1\xae\xf6\xa0\xe2\x1d\x8d\x18\xa9\xd4\x2e\xdf\xb7\x1b\xde\x37\x89\xb2\x34\x4c\xcd\xb1\xdd\x67\x5a\xd0\x04\x6d\x47\xfa\x7a\x74\x95\xbe\x6e\x15\xc7\x27\xaf\x2a\x32\x1b\x88\xe2\x7e\x03\x62\x7f\x49\x79\xb8\x52\x83\x77\x54\x40\xed\xef\xdc\x0c\x4a\x8b\xfd\x45\x0f\xf4\x72\xa2\x34\x3c\xd2\x7e\x4f\x8f\xa5\xeb\xe8\xd0\x9f\x6b\x4f\xa2\x6a\xf7\x65\x8a\x4f\xae\x3a\xe3\x3f\xad\xbf\x51\x2e\x8c\x75\xd4\xbc\x67\xd6\xcb\x03\xf6\x68\xf4\x44\x11\xe1\xf4\x6f\x6a\x94\x90\xf6\xed\x73\x78\x8f\x91\xd5\x5c\xf6\xb4\x12\xeb\x64\x10\x47\xcd\xdb\x29\xf1\x6f\xb3\x7a\x07\xbf\x02\x00\x00\xff\xff\x75\xd7\x68\x3b\x89\x07\x00\x00")
func collectionHtmlBytes() ([]byte, error) {
return bindataRead(
@@ -283,7 +283,7 @@
return a, nil
}
-var _syncbaseHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x56\x5b\x6b\x23\x37\x14\x7e\xf7\xaf\x38\x98\x90\x26\xdb\x7a\xdc\x5d\x68\x1f\x16\x7b\x4a\x2e\x5b\x30\x6c\xc2\x42\xd2\xbe\x6b\xa4\x63\x8f\x40\x96\x06\x49\x36\x35\xc3\xfc\xf7\x1e\xcd\x68\x6e\xb6\x93\xae\xcb\xfa\x69\xa4\x73\xfd\xbe\xef\x48\x72\x59\xce\x3f\x4c\x80\x7e\x0f\xa6\x38\x58\xb9\xc9\x3d\x7c\xfa\xf5\xe3\xef\xf0\x9a\x23\xfc\xcd\x34\x13\x72\xb7\x85\xbb\x9d\xcf\x8d\x75\x09\xdc\x29\x05\xb5\x93\x03\x8b\x0e\xed\x1e\x45\x52\x47\xff\xe5\x10\xcc\x1a\x7c\x2e\x1d\x38\xb3\xb3\x1c\x81\x1b\x81\x40\xcb\x8d\xd9\xa3\xd5\x28\x20\x3b\x00\x83\xfb\x97\xc7\x99\xf3\x07\x85\x75\x98\x92\x1c\x35\x85\xfa\x9c\x79\xe0\x4c\x43\x86\xb0\x36\x3b\x2d\x40\x6a\xda\x44\xf8\xba\x7a\xf8\xf2\xfc\xf2\x05\xd6\x52\x61\x32\xf9\x30\xaf\xaa\xc9\xa4\x2c\x05\xae\xa5\x46\x98\x72\xa3\x3d\x6a\x3f\x0d\xbb\x0b\x87\xdc\x4b\xa3\x81\x2b\xe6\xdc\x72\x1a\x97\xb3\x19\x55\xf0\x68\x61\x2b\xd4\x6c\x63\xa5\x98\xa6\x75\xe5\x45\xfe\x5b\xfa\x72\xd0\x3c\x63\x0e\x17\x73\x5a\x34\xbb\x42\xee\xdb\x04\x21\x80\x23\x01\x6e\x3f\x66\xb3\x8f\x9f\x66\xdc\xa8\x98\x01\xe0\x85\x08\x20\x00\x30\x2d\xcb\x24\x7e\x27\x7f\xee\x94\x7a\x66\x5b\xac\xaa\x69\x93\x71\x4e\x29\xd3\xc9\x62\x1e\xfb\x49\x43\xff\x96\xe9\x0d\x42\xf2\x6a\x91\xfc\xc8\xed\xc2\xde\xcb\x12\xae\x44\x06\x9f\x97\x90\x3c\x32\xcf\x02\x84\x64\x25\xa0\x4e\xd5\x20\x6b\xb7\x43\x6b\xe4\x9a\xc4\x8e\x7a\xa0\x97\x42\x05\xb8\x2b\x0a\xb8\x57\xe8\x9c\xd4\x9b\x36\x6d\xbb\x6e\xc1\x76\x70\xff\x5f\x89\x85\x50\xfd\xa2\x63\xe9\xc1\x28\xd5\xf0\xe1\x22\xc2\xd6\xdb\xa7\xbd\xad\x16\x61\x25\x3a\xa4\x64\x1c\xf9\x8a\xf4\xc1\x22\xf3\xc6\xfe\xe4\xc2\xb0\xda\x11\x96\x10\x38\xc0\x42\x20\xc4\x71\xf4\x82\x41\x6e\x71\x4d\xda\xc4\xa1\x99\xf3\xae\xf6\x1f\x7a\x49\x7c\xd4\x23\x80\xb6\x69\xe0\x5a\x64\xcb\x63\x8e\xae\x85\x5e\x0e\xe5\xb8\xe6\xc1\x67\x5c\xfb\x9a\xeb\xe5\x08\x48\xfa\x64\x2c\x26\x49\xb2\x98\xb3\x74\xdc\x57\x59\xa2\x72\x78\x42\xc9\x8d\x36\xd0\xb7\xe6\x6e\x29\xc8\x8f\x82\xb4\xe8\x62\xc8\x16\x19\x1f\xe8\x76\x99\x6a\xe7\x35\x0b\x27\x6b\x63\xcd\xae\x38\x95\xac\x33\x7d\x86\x70\x6c\xda\x55\x0f\xf9\x8c\x74\xa1\x48\xd8\x6e\x79\xaa\x31\x05\xc3\x71\x86\x9e\xc8\x9a\xab\x1a\xdf\x59\x35\x63\xc2\x47\x74\xdc\xca\x22\x30\xd5\xe5\xac\x4f\x73\x81\x3c\x19\x18\xdb\xa1\xf8\xcf\x84\xdf\x76\x99\x92\x2e\x87\xf6\x6a\x81\x80\x69\xd4\x6e\xc8\x1c\xbd\x5a\xa7\x88\xfb\xdd\x02\x83\xe5\x11\xe9\x63\xea\xaf\x3c\xdb\xfc\x02\x57\x8c\x73\x62\xe2\xab\x74\xbe\xbe\x26\x9a\xaa\x68\xb7\x0e\x46\x82\xf4\xb2\xd0\x60\x52\x64\x55\x41\x41\x5e\x92\x48\x8c\x8c\x9c\x71\x16\xe9\xcf\xe4\xdd\x57\x48\x56\x3a\x76\x7f\xd6\x79\x36\x76\x7e\x36\xfe\x2d\xff\xf1\x70\xc6\x0c\xf3\x31\xd6\x77\xc9\xa7\x03\x2a\xff\x41\x77\xca\x77\x34\x7c\xef\x58\x3c\xd1\x13\xe4\x5f\x59\xa6\xce\xe4\x1a\xd8\xbe\x37\xdd\xca\x7d\xb3\x72\xcf\xfc\xe9\x20\x74\x96\x1f\x26\xff\x16\xb7\x19\x5a\x9a\x00\xa9\xd7\xa6\xd6\xfe\xa9\xde\x71\x6f\xe8\xde\x58\xdf\x96\x9a\xc4\x6b\x52\xd2\x68\xdc\x84\x81\xa5\x86\x8d\x95\xfe\x10\xae\xb2\x50\x23\x19\x6e\x92\xd3\xbd\x32\xd9\x23\xee\x5f\x0f\x05\x76\x2e\x83\xbd\xaa\xba\xfd\x31\xd2\xbf\x73\xfd\xb9\xee\xf6\xb9\xf4\xf6\x1b\x3c\xd3\x83\xfc\x97\xfe\xb5\xb8\xf0\xd9\xbb\x79\x36\x20\xe2\x83\xed\x6e\x8f\x1f\xd2\xc5\xb0\xa3\xba\xfb\x49\xf7\xf5\x6f\x00\x00\x00\xff\xff\xdd\x03\x4c\x02\xbd\x09\x00\x00")
+var _syncbaseHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x56\xdf\x6f\x22\x37\x10\x7e\xe7\xaf\x18\xa1\x28\x4d\xae\x65\xe9\x9d\xd4\x3e\x9c\x60\xab\x24\x5c\x25\xa4\x4b\x74\x12\x69\xdf\xbd\xf6\x00\x96\x8c\xbd\xb2\x0d\x2a\x5a\xed\xff\xde\xf1\xae\xf7\x17\x90\xf4\xa8\x2e\x4f\x78\xe6\x9b\x1f\xdf\x37\xb3\x76\x8a\x62\xfa\x61\x04\xf4\xf7\x64\xf2\xa3\x95\x9b\xad\x87\x4f\xbf\x7e\xfc\x1d\x5e\xb7\x08\x7f\x33\xcd\x84\xdc\xef\xe0\x61\xef\xb7\xc6\xba\x04\x1e\x94\x82\x0a\xe4\xc0\xa2\x43\x7b\x40\x91\x54\xd1\x7f\x39\x04\xb3\x06\xbf\x95\x0e\x9c\xd9\x5b\x8e\xc0\x8d\x40\xa0\xe3\xc6\x1c\xd0\x6a\x14\x90\x1d\x81\xc1\xe3\x6a\x31\x71\xfe\xa8\xb0\x0a\x53\x92\xa3\xa6\x50\xbf\x65\x1e\x38\xd3\x90\x21\xac\xcd\x5e\x0b\x90\x9a\x8c\x08\x5f\x97\x4f\x5f\x5e\x56\x5f\x60\x2d\x15\x26\xa3\x0f\xd3\xb2\x1c\x8d\x8a\x42\xe0\x5a\x6a\x84\x31\x37\xda\xa3\xf6\xe3\x60\x9d\x39\xe4\x5e\x1a\x0d\x5c\x31\xe7\xe6\xe3\x78\x9c\x4c\xa8\x82\x47\x0b\x3b\xa1\x26\x1b\x2b\xc5\x38\xad\x2a\xcf\xb6\xbf\xa5\xab\xa3\xe6\x19\x73\x38\x9b\xd2\xa1\xb6\x0a\x79\x68\x12\x84\x00\x8e\x44\xb8\xf9\x31\x99\x7c\xfc\x34\xe1\x46\xc5\x0c\x00\x2b\x12\x80\x08\xc0\xb8\x28\x92\x57\x8b\x98\x44\x43\xf2\xe7\x5e\xa9\x17\xb6\xc3\xb2\x1c\xd7\x69\xa7\x94\x37\x1d\xcd\xa6\xb1\xa9\x34\x90\xb0\x4c\x6f\x10\xea\xc0\x45\xe6\x88\x03\x01\xaf\x23\x51\x14\x70\x23\x32\xf8\x3c\x87\x64\xc1\x3c\x0b\x5c\x92\xa5\x80\x2a\x55\x4d\xb1\x31\x87\x1e\x09\x9a\xc4\xae\x3a\xc6\xd7\x72\x06\x78\xc8\x73\x78\x54\xe8\x9c\xd4\x9b\x26\x6d\x73\x6e\x08\xb7\x94\xff\x5f\x89\x99\x50\xdd\xa1\x55\xea\xc9\x28\x55\xeb\xe1\x22\xc3\x06\xed\xd3\xce\x57\x4d\x63\x29\x5a\xa6\xe4\x1c\x60\x45\xfa\x64\x91\x79\x63\x7f\x72\x61\x6b\xed\x80\x4b\x08\xec\x71\x21\x12\xe2\x34\x7a\xc6\x60\x6b\x71\x4d\xb3\x89\xdb\x33\xe5\x6d\xed\x3f\xf4\x9c\xf4\xa8\xd6\x00\x6d\xdd\xc0\xad\xc8\xe6\xa7\x1a\xdd\x0a\x3d\xef\x8f\xe3\x96\x07\xcc\xb0\xf6\x2d\xd7\xf3\x01\x91\xf4\xd9\x58\x4c\x92\x64\x36\x65\xe9\xb0\xaf\xa2\x40\xe5\xf0\x4c\x92\x3b\x6d\xa0\x6b\xcd\xdd\x53\x90\x1f\x04\x69\xd1\xc6\x90\x2f\x2a\xde\x9b\xdb\x75\x53\xbb\x3c\xb3\xf0\x89\x6d\xac\xd9\xe7\xe7\x23\x6b\x5d\x9f\x09\xde\x01\x3b\xca\x17\x46\x17\x8a\x04\x73\xa3\x53\xc5\x29\x38\x4e\x33\x74\x42\x56\x5a\x55\xfc\x2e\x4e\x33\x26\x5c\xa0\xe3\x56\xe6\x41\xa9\x36\x67\xd8\x87\x55\x8e\x3c\xe9\x39\x9b\xa5\xf8\xcf\x84\xdf\xf6\x99\x92\x6e\x0b\xcd\x1d\x03\x81\xd3\xa0\xdd\x90\x39\xa2\x1a\x50\xe4\xfd\x6e\x81\xde\xf1\x44\xf4\xa1\xf4\x37\x9e\x6d\x7e\x81\x1b\xc6\x39\x29\xf1\x55\x3a\x5f\x5d\x13\x75\x55\xb4\x3b\x07\x83\x81\x74\x63\xa1\xc5\xa4\xc8\xb2\x84\x9c\x50\x92\x44\x8c\x8a\x5c\x00\x8b\xf4\x67\x42\x77\x15\x92\xa5\x8e\xdd\x5f\x04\x4f\x86\xe0\x17\xe3\xdf\xc2\x0f\x97\x33\x66\x98\x0e\xb9\xbe\x2b\x3e\x7d\xa0\xf2\x1f\x74\xe7\x7a\x47\xc7\xf7\xae\xc5\x33\xbd\x45\xfe\x95\x65\xea\x42\xae\x9e\xef\x7b\xd3\x2d\xdd\x37\x2b\x0f\xcc\x9f\x2f\x42\xeb\xf9\x61\xe3\xdf\xe1\x2e\x43\x4b\x1b\x20\xf5\xda\x54\xb3\x7f\xae\x2c\xee\x8d\xb9\xd7\xde\xb7\x47\x4d\xc3\xab\x53\xd2\x6a\xdc\x85\x85\xa5\x86\x8d\x95\xfe\x18\xae\xb2\x50\x23\xe9\x1b\x09\xf4\xa8\x4c\xb6\xc0\xc3\xeb\x31\xc7\x16\xd2\xb3\x95\xe5\xfd\x8f\x19\xfd\x3b\xd7\x9f\x6b\x6f\x9f\x6b\x6f\xbf\xde\x53\xdd\xcb\x7f\xed\xff\x18\x57\x3e\x7b\x77\x2f\x06\x44\x7c\xb0\xdd\xfd\xe9\x43\x3a\xeb\x77\x54\x75\x3f\x6a\x7f\xfd\x1b\x00\x00\xff\xff\x14\x60\xd4\x06\xc6\x09\x00\x00")
func syncbaseHtmlBytes() ([]byte, error) {
return bindataRead(
diff --git a/services/debug/debug/browseserver/assets/collection.html b/services/debug/debug/browseserver/assets/collection.html
index 6eaad84..9d9f271 100644
--- a/services/debug/debug/browseserver/assets/collection.html
+++ b/services/debug/debug/browseserver/assets/collection.html
@@ -7,33 +7,33 @@
{{define "content"}}
<section class="section--center mdl-grid">
- <h5>Collection {{.Database.Id.Name}}/{{.Collection.Id.Name}}</h5>
+ <h5>Collection {{.Tree.Database.Id.Name}}/{{.Tree.Collection.Id.Name}}</h5>
<div class="mdl-cell mdl-cell--12-col">
<dl>
- <dt>App Blessing:<dt><dd>{{.Database.Id.Blessing}}</dd>
- <dt>Creator's User Blessing:<dt><dd>{{.Collection.Id.Blessing}}</dd>
- <dt>Number of Rows:<dt><dd>{{.RowCount}}</dd>
- <dt>Total Key Size:<dt><dd>{{.TotKeySize}}</dd>
+ <dt>App Blessing:<dt><dd>{{.Tree.Database.Id.Blessing}}</dd>
+ <dt>Creator's User Blessing:<dt><dd>{{.Tree.Collection.Id.Blessing}}</dd>
+ <dt>Number of Rows:<dt><dd>{{.Tree.RowCount}}</dd>
+ <dt>Total Key Size:<dt><dd>{{.Tree.TotKeySize}}</dd>
<dt>Keys<dt>
<dd>
<table>
<tr><th></th><th>Key</th><th>Value</th></tr>
<tr><th colspan="3">
- {{if .KeysPage.HasPrev}}
- <a href="collection?n={{$.ServerName}}&db={{.Database.Id.Blessing}}&dn={{.Database.Id.Name}}&cb={{.Collection.Id.Blessing}}&cn={{.Collection.Id.Name}}">
+ {{if .Tree.KeysPage.HasPrev}}
+ <a href="collection?n={{$.ServerName}}&db={{.Tree.Database.Id.Blessing}}&dn={{.Tree.Database.Id.Name}}&cb={{.Tree.Collection.Id.Blessing}}&cn={{.Tree.Collection.Id.Name}}">
[back]
</a>
{{end}}
</th></tr>
- {{range .KeysPage.KeyVals}}
+ {{range .Tree.KeysPage.KeyVals}}
<tr><td>{{.Index}}</td><td>{{.Key}}</td><td><code>{{.Value}}</code></td></tr>
{{else}}
<tr><td colspan="3">(no keys)</td></tr>
{{end}}
<tr><th colspan="3">
- {{if len .KeysPage.NextKey}}
- <a href="collection?n={{$.ServerName}}&db={{.Database.Id.Blessing}}&dn={{.Database.Id.Name}}&cb={{.Collection.Id.Blessing}}&cn={{.Collection.Id.Name}}&firstkey={{.KeysPage.NextKey}}">
+ {{if len .Tree.KeysPage.NextKey}}
+ <a href="collection?n={{$.ServerName}}&db={{.Tree.Database.Id.Blessing}}&dn={{.Tree.Database.Id.Name}}&cb={{.Tree.Collection.Id.Blessing}}&cn={{.Tree.Collection.Id.Name}}&firstkey={{.Tree.KeysPage.NextKey}}">
(next)
</a>
{{end}}
diff --git a/services/debug/debug/browseserver/assets/syncbase.html b/services/debug/debug/browseserver/assets/syncbase.html
index 5b87667..18d1243 100644
--- a/services/debug/debug/browseserver/assets/syncbase.html
+++ b/services/debug/debug/browseserver/assets/syncbase.html
@@ -9,11 +9,11 @@
<section class="section--center mdl-grid">
<h5>Syncbase</h5>
<div class="mdl-cell mdl-cell--12-col">
- Service "{{.Service.FullName}}"
+ Service "{{.Tree.Service.FullName}}"
</div>
</section>
-{{range .Tree}}
+{{range .Tree.Dbs}}
<section class="section--center mdl-grid">
{{ $db := .Database.Id }}
<h5>Database "{{$db.Name}}"</h5>
diff --git a/services/debug/debug/browseserver/browseserver.go b/services/debug/debug/browseserver/browseserver.go
index c0e0140..f4b4797 100644
--- a/services/debug/debug/browseserver/browseserver.go
+++ b/services/debug/debug/browseserver/browseserver.go
@@ -25,17 +25,15 @@
"v.io/v23"
"v.io/v23/context"
"v.io/v23/naming"
- "v.io/v23/rpc/reserved"
"v.io/v23/security"
"v.io/v23/services/logreader"
"v.io/v23/services/stats"
- sbwire "v.io/v23/services/syncbase"
svtrace "v.io/v23/services/vtrace"
- "v.io/v23/syncbase"
"v.io/v23/uniqueid"
"v.io/v23/verror"
"v.io/v23/vom"
"v.io/v23/vtrace"
+ "v.io/x/ref/services/debug/debug/browseserver/sbtree"
"v.io/x/ref/services/internal/pproflib"
)
@@ -825,23 +823,6 @@
// collections, and has links to the detailed collection page.
type syncbaseHandler struct{ *handler }
-func implementsSyncbaseInterface(ctx *context.T, server string) (bool, error) {
- const (
- syncbasePkgPath = "v.io/v23/services/syncbase"
- syncbaseName = "Service"
- )
- interfaces, err := reserved.Signature(ctx, server)
- if err != nil {
- return false, err
- }
- for _, ifc := range interfaces {
- if ifc.Name == syncbaseName && ifc.PkgPath == syncbasePkgPath {
- return true, nil
- }
- }
- return false, nil
-}
-
func internalServerError(w http.ResponseWriter, doing string, err error) {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Problem %s: %v", doing, err)
@@ -862,99 +843,38 @@
return
}
- hasSyncbase, err := implementsSyncbaseInterface(ctx, server)
+ sbTree, err := sbtree.AssembleSyncbaseTree(ctx, server)
+
if err != nil {
- internalServerError(w, "getting interfaces", err)
- return
- }
-
- if !hasSyncbase {
- args := struct {
- ServerName string
- CommandLine string
- Vtrace *Tracer
- }{
- ServerName: server,
- CommandLine: "(no command line)",
- Vtrace: tracer,
- }
- h.execute(h.ctx, w, r, noSyncbaseTmpl, args)
- return
- }
-
- service := syncbase.NewService(server)
-
- dbIds, err := service.ListDatabases(ctx)
- if err != nil {
- internalServerError(w, "listing databases", err)
- return
- }
-
- // Assemble the data to be displayed as tree of nested slices
- type SyncgroupTree struct {
- Syncgroup syncbase.Syncgroup
- Spec sbwire.SyncgroupSpec
- Members map[string]sbwire.SyncgroupMemberInfo
- }
- type databaseTree struct {
- Database syncbase.Database
- Collections []syncbase.Collection
- Syncgroups []SyncgroupTree
- }
- tree := make([]databaseTree, len(dbIds))
- for i := range dbIds {
- // TODO(eobrain) Confirm nil for schema is appropriate
- db := service.DatabaseForId(dbIds[i], nil)
-
- // Assemble collections
- collIds, err := db.ListCollections(ctx)
- if err != nil {
- internalServerError(w, "listing collections", err)
- return
- }
- colls := make([]syncbase.Collection, len(collIds))
- for j := range collIds {
- colls[j] = db.CollectionForId(collIds[i])
- }
-
- // Assemble syncgroups
- sgIds, err := db.ListSyncgroups(ctx)
- if err != nil {
- internalServerError(w, "listing syncgroups", err)
- return
- }
- sgs := make([]SyncgroupTree, len(sgIds))
- for j := range sgIds {
- sg := db.SyncgroupForId(sgIds[j])
- spec, _, err := sg.GetSpec(ctx)
- if err != nil {
- internalServerError(w, "getting spec of syncgroup", err)
- return
+ if err == sbtree.NoSyncbaseError {
+ // Error because no Syncbase, send to no-syncbase page.
+ args := struct {
+ ServerName string
+ CommandLine string
+ Vtrace *Tracer
+ }{
+ ServerName: server,
+ CommandLine: "(no command line)",
+ Vtrace: tracer,
}
- members, err := sg.GetMembers(ctx)
- if err != nil {
- internalServerError(w, "getting members of syncgroup", err)
- return
- }
- sgs[j] = SyncgroupTree{sg, spec, members}
+ h.execute(h.ctx, w, r, noSyncbaseTmpl, args)
+ } else {
+ // Some other error.
+ internalServerError(w, "getting syncbase information", err)
}
-
- tree[i] = databaseTree{db, colls, sgs}
+ return
}
- // Assemble data and send it to the template to generate HTML
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
- Service syncbase.Service
- Tree []databaseTree
+ Tree *sbtree.SyncbaseTree
}{
ServerName: server,
CommandLine: fmt.Sprintf(`debug glob "%s/*"`, server),
Vtrace: tracer,
- Service: service,
- Tree: tree,
+ Tree: sbTree,
}
h.execute(h.ctx, w, r, syncbaseTmpl, args)
}
@@ -963,76 +883,7 @@
// main Syncbase viewer page.
type collectionHandler struct{ *handler }
-type keyVal struct {
- Index int
- Key string
- Value interface{}
-}
-
-// keysPage is a contiguous subset of the keys, used for pagination.
-type keysPage struct {
- HasPrev bool
- KeyVals []keyVal
- NextKey string
-}
-
-// scanCollection gets some statistics, plus a page of key-value pairs starting
-// with firstKey.
-func scanCollection(
- ctx *context.T, coll syncbase.Collection, firstKey string,
-) (rowCount int, totKeySize uint64, page keysPage) {
- const keysPerPage = 7
- page.KeyVals = make([]keyVal, 0, keysPerPage)
-
- stream := coll.Scan(ctx, syncbase.Prefix(""))
-
- // We scan through all the keys lexicographically, and when we come to a
- // key >= firstKey we start gathering a "page" of keys. As we scan, a
- // state machine keeps track of whether we are before the page, in the
- // page, or after the page.
- const (
- before = 0
- gathering = iota
- done = iota
- )
- state := before
- for stream.Advance() {
- key := stream.Key()
- totKeySize += uint64(len(key))
-
- switch state {
- case before:
- if key >= firstKey {
- // First key found: transition to gathering the page
- state = gathering
- } else {
- // There is at least one key before the page
- page.HasPrev = true
- }
- case gathering:
- if len(page.KeyVals) >= keysPerPage {
- // Page full: transition to done. There is at
- // least one key after the page.
- state = done
- page.NextKey = key
- }
- case done:
- // Done gathering. Terminal state.
- }
- if state == gathering {
- // Grab the value, put it and the key into a KeyVal, and
- // add it to the page.
- var value interface{}
- err := stream.Value(&value)
- if err != nil {
- value = fmt.Sprintf("ERROR getting value: %v", err)
- }
- page.KeyVals = append(page.KeyVals, keyVal{rowCount, key, value})
- }
- rowCount++
- }
- return
-}
+const keysPerPage = 7
func (h *collectionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
@@ -1042,8 +893,6 @@
collBlessing = r.FormValue("cb")
collName = r.FormValue("cn")
firstKey = r.FormValue("firstkey")
- dbId = sbwire.Id{dbBlessing, dbName}
- collId = sbwire.Id{collBlessing, collName}
)
ctx, tracer := newTracer(h.ctx)
if len(server) == 0 {
@@ -1051,35 +900,23 @@
return
}
- service := syncbase.NewService(server)
-
- // TODO(eobrain) Confirm nil for schema is appropriate
- db := service.DatabaseForId(dbId, nil)
- coll := db.CollectionForId(collId)
-
- rowCount, totKeySize, page := scanCollection(ctx, coll, firstKey)
+ collTree := sbtree.AssembleCollectionTree(
+ ctx, server,
+ dbBlessing, dbName,
+ collBlessing, collName,
+ firstKey, keysPerPage)
// Assemble data and send it to the template to generate HTML
args := struct {
ServerName string
CommandLine string
Vtrace *Tracer
- Service syncbase.Service
- Database syncbase.Database
- Collection syncbase.Collection
- RowCount int
- TotKeySize uint64
- KeysPage keysPage
+ Tree *sbtree.CollectionTree
}{
ServerName: server,
CommandLine: "(no command line)",
Vtrace: tracer,
- Service: service,
- Database: db,
- Collection: coll,
- RowCount: rowCount,
- TotKeySize: totKeySize,
- KeysPage: page,
+ Tree: collTree,
}
h.execute(h.ctx, w, r, collectionTmpl, args)
}
diff --git a/services/debug/debug/browseserver/sbtree/colltree.go b/services/debug/debug/browseserver/sbtree/colltree.go
new file mode 100644
index 0000000..5fd2c37
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/colltree.go
@@ -0,0 +1,113 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sbtree
+
+import (
+ "fmt"
+
+ "v.io/v23/context"
+ wire "v.io/v23/services/syncbase"
+ "v.io/v23/syncbase"
+)
+
+// CollectionTree has all the data for the collection page of the Syncbase debug
+// viewer.
+type CollectionTree struct {
+ Service syncbase.Service
+ Database syncbase.Database
+ Collection syncbase.Collection
+ RowCount int
+ TotKeySize uint64
+ KeysPage keysPage
+}
+
+type keyVal struct {
+ Index int
+ Key string
+ Value interface{}
+}
+
+// keysPage is a contiguous subset of the keys, used for pagination.
+type keysPage struct {
+ HasPrev bool
+ KeyVals []keyVal
+ NextKey string
+}
+
+// AssembleCollectionTree returns information describing the given Syncbase
+// collection, including a "page" of keys starting at the given first key.
+func AssembleCollectionTree(
+ ctx *context.T, server, dbBlessing, dbName, collBlessing, collName, firstKey string, keysPerPage int,
+) *CollectionTree {
+ var (
+ dbId = wire.Id{dbBlessing, dbName}
+ collId = wire.Id{collBlessing, collName}
+ service = syncbase.NewService(server)
+
+ // TODO(eobrain) Confirm nil for schema is appropriate
+ db = service.DatabaseForId(dbId, nil)
+ coll = db.CollectionForId(collId)
+ )
+
+ rowCount, totKeySize, page := scanCollection(ctx, coll, firstKey, keysPerPage)
+ return &CollectionTree{service, db, coll, rowCount, totKeySize, page}
+}
+
+// scanCollection gets some statistics, plus a page of key-value pairs starting
+// with firstKey.
+func scanCollection(
+ ctx *context.T, coll syncbase.Collection, firstKey string, keysPerPage int,
+) (rowCount int, totKeySize uint64, page keysPage) {
+ page.KeyVals = make([]keyVal, 0, keysPerPage)
+
+ stream := coll.Scan(ctx, syncbase.Prefix(""))
+
+ // We scan through all the keys lexicographically, and when we come to a
+ // key >= firstKey we start gathering a "page" of keys. As we scan, a
+ // state machine keeps track of whether we are before the page, in the
+ // page, or after the page.
+ const (
+ before = 0
+ gathering = iota
+ done = iota
+ )
+ state := before
+ for stream.Advance() {
+ key := stream.Key()
+ totKeySize += uint64(len(key))
+
+ switch state {
+ case before:
+ if key >= firstKey {
+ // First key found: transition to gathering the page
+ state = gathering
+ } else {
+ // There is at least one key before the page
+ page.HasPrev = true
+ }
+ case gathering:
+ if len(page.KeyVals) >= keysPerPage {
+ // Page full: transition to done. There is at
+ // least one key after the page.
+ state = done
+ page.NextKey = key
+ }
+ case done:
+ // Done gathering. Terminal state.
+ }
+ if state == gathering {
+ // Grab the value, put it and the key into a KeyVal, and
+ // add it to the page.
+ var value interface{}
+ err := stream.Value(&value)
+ if err != nil {
+ value = fmt.Sprintf("ERROR getting value: %v", err)
+ }
+ page.KeyVals = append(page.KeyVals, keyVal{rowCount, key, value})
+ }
+ rowCount++
+ }
+ return
+}
diff --git a/services/debug/debug/browseserver/sbtree/colltree_test.go b/services/debug/debug/browseserver/sbtree/colltree_test.go
new file mode 100644
index 0000000..921e0db
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/colltree_test.go
@@ -0,0 +1,351 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sbtree_test
+
+import (
+ "reflect"
+ "testing"
+
+ "v.io/v23/syncbase"
+ _ "v.io/x/ref/runtime/factories/generic"
+ "v.io/x/ref/services/debug/debug/browseserver/sbtree"
+ tu "v.io/x/ref/services/syncbase/testutil"
+)
+
+func TestEmptyCollection(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ db = tu.CreateDatabase(t, ctx, service, "the_db")
+ coll = tu.CreateCollection(t, ctx, db, "the_collection")
+ )
+
+ got := sbtree.AssembleCollectionTree(
+ ctx,
+ serverName,
+ db.Id().Blessing,
+ "the_db",
+ coll.Id().Blessing,
+ "the_collection",
+ "",
+ 100,
+ )
+
+ if got.Service.FullName() != serverName {
+ t.Errorf("got %q, want %q", got.Service.FullName(), serverName)
+ }
+ if got.Database.Id().Name != "the_db" {
+ t.Errorf(`got %q, want "the_db"`, got.Database.Id().Name)
+ }
+ if got.Collection.Id().Name != "the_collection" {
+ t.Errorf(`got %q, want "the_collection"`, got.Database.Id().Name)
+ }
+ if got.RowCount != 0 {
+ t.Errorf("got %d rows, want none", got.RowCount)
+ }
+ if got.TotKeySize != 0 {
+ t.Errorf("got %d bytes of keys, want zero", got.TotKeySize)
+ }
+ if got.KeysPage.HasPrev {
+ t.Error("Got previous page, want no previous page")
+ }
+ if len(got.KeysPage.KeyVals) != 0 {
+ t.Errorf("Want no keys, got %v (length %d)",
+ got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+ }
+ if got.KeysPage.NextKey != "" {
+ t.Errorf("Got %q, want empty string", got.KeysPage.NextKey)
+ }
+}
+
+func TestSingleKeysPage(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ db = tu.CreateDatabase(t, ctx, service, "the_db")
+ coll = tu.CreateCollection(t, ctx, db, "the_collection")
+ wantTotKeySize = len("Bravo") + len("Alfa") + len("Delta") + len("Charlie")
+ )
+ // Put keys of sundry types in non-alphabethical order
+ coll.Put(ctx, "Bravo", int64(9999))
+ coll.Put(ctx, "Alfa", complex(11, 22))
+ coll.Put(ctx, "Delta", "something")
+ coll.Put(ctx, "Charlie", 'x')
+
+ got := sbtree.AssembleCollectionTree(
+ ctx,
+ serverName,
+ db.Id().Blessing,
+ "the_db",
+ coll.Id().Blessing,
+ "the_collection",
+ "",
+ 100,
+ )
+
+ if got.RowCount != 4 {
+ t.Errorf("got %d rows, want 4", got.RowCount)
+ }
+ if int(got.TotKeySize) != wantTotKeySize {
+ t.Errorf("got %d bytes of keys, want %d", got.TotKeySize, wantTotKeySize)
+ }
+ if got.KeysPage.HasPrev {
+ t.Error("Got previous page, want no previous page")
+ }
+ if len(got.KeysPage.KeyVals) != 4 {
+ t.Errorf("Wanted 4 keys, got %v (length %d)",
+ got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+ }
+ for i := 0; i < 4; i++ {
+ if got.KeysPage.KeyVals[i].Index != i {
+ t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i)
+ }
+ }
+ // Make sure keys come back in alphabetical order
+ for i, want := range []string{"Alfa", "Bravo", "Charlie", "Delta"} {
+ if got.KeysPage.KeyVals[i].Key != want {
+ t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+ }
+ }
+ for i, want := range []interface{}{complex(11, 22), int64(9999), 'x', "something"} {
+ if got.KeysPage.KeyVals[i].Value != want {
+ t.Errorf("got %v of type %T, want %v of type %T",
+ got.KeysPage.KeyVals[i].Value, got.KeysPage.KeyVals[i].Value, want, want)
+ }
+ }
+
+ if got.KeysPage.NextKey != "" {
+ t.Errorf("Got %q, want empty string", got.KeysPage.NextKey)
+ }
+}
+
+func TestFirstOfMultiplePages(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ db = tu.CreateDatabase(t, ctx, service, "the_db")
+ coll = tu.CreateCollection(t, ctx, db, "the_collection")
+ )
+ // Ten keys, in pages of four, starting with the first key
+ coll.Put(ctx, "555", 99)
+ coll.Put(ctx, "000", 99)
+ coll.Put(ctx, "999", 99)
+ coll.Put(ctx, "111", 99)
+ coll.Put(ctx, "444", 99)
+ coll.Put(ctx, "666", 99)
+ coll.Put(ctx, "222", 99)
+ coll.Put(ctx, "777", 99)
+ coll.Put(ctx, "333", 99)
+ coll.Put(ctx, "888", 99)
+ got := sbtree.AssembleCollectionTree(
+ ctx,
+ serverName,
+ db.Id().Blessing,
+ "the_db",
+ coll.Id().Blessing,
+ "the_collection",
+ "",
+ 4,
+ )
+
+ if got.RowCount != 10 {
+ t.Errorf("got %d rows, want 10", got.RowCount)
+ }
+ if int(got.TotKeySize) != 30 { // 10 keys, each of length 3
+ t.Errorf("got %d bytes of keys, want 30", got.TotKeySize)
+ }
+ if got.KeysPage.HasPrev {
+ t.Error("Got previous page, want no previous page")
+ }
+ if len(got.KeysPage.KeyVals) != 4 {
+ t.Errorf("Wanted 4 keys, got %v (length %d)",
+ got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+ }
+ for i := 0; i < 4; i++ {
+ if got.KeysPage.KeyVals[i].Index != i {
+ t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i)
+ }
+ }
+ for i, want := range []string{"000", "111", "222", "333"} {
+ if got.KeysPage.KeyVals[i].Key != want {
+ t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+ }
+ }
+ if got.KeysPage.NextKey != "444" {
+ t.Errorf(`Got %q, want "444"`, got.KeysPage.NextKey)
+ }
+}
+
+func TestMiddleOfMultiplePages(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ db = tu.CreateDatabase(t, ctx, service, "the_db")
+ coll = tu.CreateCollection(t, ctx, db, "the_collection")
+ )
+ // Ten keys, in pages of four, starting with fifth key
+ coll.Put(ctx, "555", 99)
+ coll.Put(ctx, "000", 99)
+ coll.Put(ctx, "999", 99)
+ coll.Put(ctx, "111", 99)
+ coll.Put(ctx, "444", 99)
+ coll.Put(ctx, "666", 99)
+ coll.Put(ctx, "222", 99)
+ coll.Put(ctx, "777", 99)
+ coll.Put(ctx, "333", 99)
+ coll.Put(ctx, "888", 99)
+ got := sbtree.AssembleCollectionTree(
+ ctx,
+ serverName,
+ db.Id().Blessing,
+ "the_db",
+ coll.Id().Blessing,
+ "the_collection",
+ "444",
+ 4,
+ )
+
+ if got.RowCount != 10 {
+ t.Errorf("got %d rows, want 10", got.RowCount)
+ }
+ if int(got.TotKeySize) != 30 { // 10 keys, each of length 3
+ t.Errorf("got %d bytes of keys, want 30", got.TotKeySize)
+ }
+ if !got.KeysPage.HasPrev {
+ t.Error("No previous page, want previous page")
+ }
+ if len(got.KeysPage.KeyVals) != 4 {
+ t.Errorf("Wanted 4 keys, got %v (length %d)",
+ got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+ }
+ for i := 0; i < 4; i++ {
+ if got.KeysPage.KeyVals[i].Index != i+4 {
+ t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i+4)
+ }
+ }
+ for i, want := range []string{"444", "555", "666", "777"} {
+ if got.KeysPage.KeyVals[i].Key != want {
+ t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+ }
+ }
+ if got.KeysPage.NextKey != "888" {
+ t.Errorf(`Got %q, want "888"`, got.KeysPage.NextKey)
+ }
+}
+
+func TestLastOfMultiplePages(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ db = tu.CreateDatabase(t, ctx, service, "the_db")
+ coll = tu.CreateCollection(t, ctx, db, "the_collection")
+ )
+ // Ten keys, in pages of four, starting with ninth key.
+ coll.Put(ctx, "555", 99)
+ coll.Put(ctx, "000", 99)
+ coll.Put(ctx, "999", 99)
+ coll.Put(ctx, "111", 99)
+ coll.Put(ctx, "444", 99)
+ coll.Put(ctx, "666", 99)
+ coll.Put(ctx, "222", 99)
+ coll.Put(ctx, "777", 99)
+ coll.Put(ctx, "333", 99)
+ coll.Put(ctx, "888", 99)
+ got := sbtree.AssembleCollectionTree(
+ ctx,
+ serverName,
+ db.Id().Blessing,
+ "the_db",
+ coll.Id().Blessing,
+ "the_collection",
+ "888",
+ 4,
+ )
+
+ if got.RowCount != 10 {
+ t.Errorf("got %d rows, want 10", got.RowCount)
+ }
+ if int(got.TotKeySize) != 30 { // 10 keys, each of length 3
+ t.Errorf("got %d bytes of keys, want 30", got.TotKeySize)
+ }
+ if !got.KeysPage.HasPrev {
+ t.Error("No previous page, want previous page")
+ }
+ if len(got.KeysPage.KeyVals) != 2 {
+ t.Errorf("Wanted 2 keys, got %v (length %d)",
+ got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+ }
+ for i := 0; i < 2; i++ {
+ if got.KeysPage.KeyVals[i].Index != i+8 {
+ t.Errorf("got %d, want %d", got.KeysPage.KeyVals[i].Index, i+8)
+ }
+ }
+ // Make sure keys come back in alphabetical order.
+ for i, want := range []string{"888", "999"} {
+ if got.KeysPage.KeyVals[i].Key != want {
+ t.Errorf("got %q, want %q", got.KeysPage.KeyVals[i].Key, want)
+ }
+ }
+ if got.KeysPage.NextKey != "" {
+ t.Errorf(`Got %q, want empty string`, got.KeysPage.NextKey)
+ }
+}
+
+func TestNonBuiltInType(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ db = tu.CreateDatabase(t, ctx, service, "the_db")
+ coll = tu.CreateCollection(t, ctx, db, "the_collection")
+ )
+ type childType struct {
+ I int64
+ C complex128
+ }
+ type someCustomType struct {
+ S string
+ R rune
+ C childType
+ }
+ err := coll.Put(ctx, "the key",
+ someCustomType{"something", 'x', childType{9999, complex(11, 22)}})
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ got := sbtree.AssembleCollectionTree(
+ ctx,
+ serverName,
+ db.Id().Blessing,
+ "the_db",
+ coll.Id().Blessing,
+ "the_collection",
+ "",
+ 100,
+ )
+
+ if got.RowCount != 1 {
+ t.Fatalf("got %d rows, want 1", got.RowCount)
+ }
+ if len(got.KeysPage.KeyVals) != 1 {
+ t.Errorf("Wanted 1 keys, got %v (length %d)",
+ got.KeysPage.KeyVals, len(got.KeysPage.KeyVals))
+ }
+ value, ok := got.KeysPage.KeyVals[0].Value.(someCustomType)
+ if !ok {
+ t.Fatalf("Got %v of type %T, want of type someCustomType",
+ got.KeysPage.KeyVals[0].Value, got.KeysPage.KeyVals[0].Value)
+ }
+ want := someCustomType{"something", 'x', childType{9999, complex(11, 22)}}
+ if !reflect.DeepEqual(value, want) {
+ t.Errorf("Got %v, want %v", value, want)
+ }
+}
diff --git a/services/debug/debug/browseserver/sbtree/sbtree.go b/services/debug/debug/browseserver/sbtree/sbtree.go
new file mode 100644
index 0000000..e52a34b
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/sbtree.go
@@ -0,0 +1,123 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package syncbase provides data structs used by HTML templates to build the
+// web pages for the Syncbase debug viewer. To minimize mixing of code and
+// presentation, all the data for use in the templates is in a form that is
+// convenient for accessing and iterating over, i.e struct fields, no-arg
+// methods, slices, or maps. In some cases this required mirroring data
+// structures in the public Syncbase API to avoid having the templates deal with
+// context objects, or to avoid the templates needing extra variables to handle
+// indirection.
+package sbtree
+
+import (
+ "errors"
+ "fmt"
+
+ "v.io/v23/context"
+ "v.io/v23/rpc/reserved"
+ wire "v.io/v23/services/syncbase"
+ "v.io/v23/syncbase"
+)
+
+// SyncbaseTree has all the data for the main page of the Syncbase debug viewer.
+type SyncbaseTree struct {
+ Service syncbase.Service
+ Dbs []dbTree
+}
+
+type dbTree struct {
+ Database syncbase.Database
+ Collections []syncbase.Collection
+ Syncgroups []syncgroupTree
+}
+
+type syncgroupTree struct {
+ Syncgroup syncbase.Syncgroup
+ Spec wire.SyncgroupSpec
+ Members map[string]wire.SyncgroupMemberInfo
+}
+
+// NoSyncbaseError returned as error from AssembleSbTree when the given server
+// does not implement the Syncbase RPC interface.
+var NoSyncbaseError = errors.New("Server does not have Syncbase")
+
+// AssembleSbTree returns information describing the Syncbase server running on
+// the given server. One possible error it can return is NoSyncbaseError,
+// indicating that the server does not implement the Syncbase RPC interface.
+func AssembleSyncbaseTree(ctx *context.T, server string) (*SyncbaseTree, error) {
+ hasSyncbase, err := hasSyncbaseService(ctx, server)
+ if err != nil {
+ return nil, fmt.Errorf("Problem getting interfaces: %v", err)
+ }
+ if !hasSyncbase {
+ return nil, NoSyncbaseError
+ }
+
+ service := syncbase.NewService(server)
+
+ dbIds, err := service.ListDatabases(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("Problem listing databases: %v", err)
+ }
+
+ dbTrees := make([]dbTree, len(dbIds))
+ for i := range dbIds {
+ // TODO(eobrain) Confirm nil for schema is appropriate
+ db := service.DatabaseForId(dbIds[i], nil)
+
+ // Assemble collections
+ collIds, err := db.ListCollections(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("Problem listing collections: %v", err)
+ }
+ colls := make([]syncbase.Collection, len(collIds))
+ for j := range collIds {
+ colls[j] = db.CollectionForId(collIds[j])
+ }
+
+ // Assemble syncgroups
+ sgIds, err := db.ListSyncgroups(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("Problem listing syncgroups: %v", err)
+ }
+ sgs := make([]syncgroupTree, len(sgIds))
+ for j := range sgIds {
+ sg := db.SyncgroupForId(sgIds[j])
+ spec, _, err := sg.GetSpec(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("Problem getting spec of syncgroup: %v", err)
+ }
+ members, err := sg.GetMembers(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("Problem getting members of syncgroup: %v", err)
+ }
+ sgs[j] = syncgroupTree{sg, spec, members}
+ }
+
+ dbTrees[i] = dbTree{db, colls, sgs}
+ }
+
+ return &SyncbaseTree{service, dbTrees}, nil
+}
+
+// hasSyncbaseService determines whether the given server implements the
+// Syncbase interface.
+func hasSyncbaseService(ctx *context.T, server string) (bool, error) {
+ const (
+ syncbasePkgPath = "v.io/v23/services/syncbase"
+ syncbaseName = "Service"
+ )
+ interfaces, err := reserved.Signature(ctx, server)
+ if err != nil {
+ return false, err
+ }
+ for _, ifc := range interfaces {
+ if ifc.Name == syncbaseName && ifc.PkgPath == syncbasePkgPath {
+ return true, nil
+ }
+ }
+ return false, nil
+}
diff --git a/services/debug/debug/browseserver/sbtree/sbtree_test.go b/services/debug/debug/browseserver/sbtree/sbtree_test.go
new file mode 100644
index 0000000..0c0f4f3
--- /dev/null
+++ b/services/debug/debug/browseserver/sbtree/sbtree_test.go
@@ -0,0 +1,148 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sbtree_test
+
+import (
+ "testing"
+
+ "v.io/v23/syncbase"
+ _ "v.io/x/ref/runtime/factories/generic"
+ "v.io/x/ref/services/debug/debug/browseserver/sbtree"
+ tu "v.io/x/ref/services/syncbase/testutil"
+ "v.io/x/ref/test"
+)
+
+func TestWithNoServer(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test, because has long timeout.")
+ }
+ ctx, cleanup := test.V23Init()
+ defer cleanup()
+
+ _, err := sbtree.AssembleSyncbaseTree(ctx, "no-such-server")
+
+ if err == nil || err == sbtree.NoSyncbaseError {
+ t.Errorf("Got %v, want not nil and not %v", err, sbtree.NoSyncbaseError)
+ }
+}
+
+func TestWithEmptyService(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+
+ got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if got.Service.FullName() != serverName {
+ t.Errorf("got %q, want %q", got.Service.FullName(), serverName)
+ }
+ if len(got.Dbs) != 0 {
+ t.Errorf("want no databases, got %v", got.Dbs)
+ }
+}
+
+func TestWithMultipleEmptyDbs(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ dbNames = []string{"db_a", "db_b", "db_c"}
+ )
+ for _, dbName := range dbNames {
+ tu.CreateDatabase(t, ctx, service, dbName)
+ }
+
+ got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(got.Dbs) != 3 {
+ t.Fatalf("want 3 databases, got %v", got.Dbs)
+ }
+ for i, db := range got.Dbs {
+ if db.Database.Id().Name != dbNames[i] {
+ t.Errorf("got %q, want %q", db.Database.Id().Name, dbNames[i])
+ }
+ if len(db.Collections) != 0 {
+ t.Errorf("want no collections, got %v", db.Collections)
+ }
+ if len(db.Syncgroups) != 0 {
+ t.Errorf("want no syncgroups, got %v", db.Syncgroups)
+ }
+ }
+}
+
+func TestWithMultipleCollections(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ collNames = []string{"coll_a", "coll_b", "coll_c"}
+ database = tu.CreateDatabase(t, ctx, service, "the_db")
+ )
+ for _, collName := range collNames {
+ tu.CreateCollection(t, ctx, database, collName)
+ }
+
+ got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(got.Dbs) != 1 {
+ t.Fatalf("want 1 database, got %v", got.Dbs)
+ }
+ for i, coll := range got.Dbs[0].Collections {
+ if coll.Id().Name != collNames[i] {
+ t.Errorf("got %q, want %q", coll.Id().Name, collNames[i])
+ }
+ exists, err := coll.Exists(ctx)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !exists {
+ t.Error("want collection to exist, but it does not")
+ }
+ }
+}
+
+func TestWithMultipleSyncgroups(t *testing.T) {
+ ctx, serverName, cleanup := tu.SetupOrDie(nil)
+ defer cleanup()
+ var (
+ service = syncbase.NewService(serverName)
+ sgNames = []string{"syncgroup_a", "syncgroup_b", "syncgroup_c"}
+ sgDescriptions = []string{"AAA", "BBB", "CCC"}
+ database = tu.CreateDatabase(t, ctx, service, "the_db")
+ coll = tu.CreateCollection(t, ctx, database, "the_collection")
+ )
+ for i, sgName := range sgNames {
+ tu.CreateSyncgroup(t, ctx, database, coll, sgName, sgDescriptions[i])
+ }
+
+ got, err := sbtree.AssembleSyncbaseTree(ctx, serverName)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(got.Dbs) != 1 {
+ t.Fatalf("want 1 database, got %v", got.Dbs)
+ }
+ for i, sg := range got.Dbs[0].Syncgroups {
+ if sg.Syncgroup.Id().Name != sgNames[i] {
+ t.Errorf("got %q, want %q", sg.Syncgroup.Id().Name, sgNames[i])
+ }
+ if sg.Spec.Description != sgDescriptions[i] {
+ t.Errorf("got %q, want %q", sg.Spec.Description, sgDescriptions[i])
+ }
+ if sg.Spec.Collections[0].Name != "the_collection" {
+ t.Errorf(`got %q, want "the_collection"`,
+ sg.Spec.Collections[0].Name)
+ }
+ }
+}
diff --git a/services/syncbase/testutil/util.go b/services/syncbase/testutil/util.go
index b35e639..e941d22 100644
--- a/services/syncbase/testutil/util.go
+++ b/services/syncbase/testutil/util.go
@@ -67,6 +67,25 @@
return c
}
+func CreateSyncgroup(
+ t testing.TB,
+ ctx *context.T,
+ d syncbase.Database,
+ c syncbase.Collection,
+ name, description string,
+) syncbase.Syncgroup {
+ sg := d.SyncgroupForId(CxId(name))
+ sgSpec := wire.SyncgroupSpec{
+ Description: description,
+ Collections: []wire.Id{c.Id()},
+ }
+ sgMembership := wire.SyncgroupMemberInfo{}
+ if err := sg.Create(ctx, sgSpec, sgMembership); err != nil {
+ Fatalf(t, "sg.Create() failed: %v", err)
+ }
+ return sg
+}
+
// TODO(sadovsky): Drop the 'perms' argument. The only client that passes
// non-nil, syncgroup_test.go, should use SetupOrDieCustom instead.
func SetupOrDie(perms access.Permissions) (clientCtx *context.T, serverName string, cleanup func()) {