forked from mirror/buntdb
initial commit
This commit is contained in:
commit
7875d65f2a
|
@ -0,0 +1 @@
|
|||
language: go
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Josh Baker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,314 @@
|
|||
<p align="center">
|
||||
<img
|
||||
src="logo.png"
|
||||
width="307" height="150" border="0" alt="BuntDB">
|
||||
<br>
|
||||
<a href="https://travis-ci.org/tidwall/buntdb"><img src="https://travis-ci.org/tidwall/buntdb.svg?branch=master" alt="Build Status"></a>
|
||||
<img src="https://img.shields.io/badge/coverage-96%25-green.svg?style=flat" alt="Code Coverage">
|
||||
<a href="https://godoc.org/github.com/tidwall/buntdb"><img src="https://godoc.org/github.com/tidwall/buntdb?status.svg" alt="GoDoc"></a>
|
||||
<img src="https://img.shields.io/badge/version-0.1.0-green.svg" alt="Version">
|
||||
</p>
|
||||
|
||||
====
|
||||
|
||||
BuntDB is a low-level, in-memory, key/value store in pure Go.
|
||||
It persists to disk, is ACID compliant, and uses locking for multiple
|
||||
readers and a single writer. It supports custom indexes and geospatial
|
||||
data. It's ideal for projects that need a dependable database and favor
|
||||
speed over data size.
|
||||
|
||||
The desire to create BuntDB stems from the need for a new embeddable
|
||||
database for [Tile38](https://github.com/tidwall/tile38). One that can work
|
||||
both as a performant [Raft Store](https://github.com/tidwall/raft-boltdb),
|
||||
and a Geospatial database.
|
||||
|
||||
Much of the API is inspired by the fantastic [BoltDB](https://github.com/boltdb/bolt),
|
||||
an amazing key/value store that can handle terrabytes of data on disk.
|
||||
|
||||
Features
|
||||
========
|
||||
|
||||
- In-memory database for [fast reads and writes](https://github.com/tidwall/raft-boltdb#benchmarks)
|
||||
- Embeddable with a [simple API](https://godoc.org/github.com/tidwall/buntdb)
|
||||
- [Spatial indexing](#spatial-indexes) for up to 4 dimensions; Useful for Geospatial data
|
||||
- - Create [custom indexes](#custom-indexes) for any data type
|
||||
- [Built-in types](#built-in-types) that are easy to get up & running; String, Uint, Int, Float
|
||||
- Flexible [iteration](#iterating) of data; ascending, descending, and ranges
|
||||
- Durable append-only file format. Adopts the [Redis AOF](http://redis.io/topics/persistence) process
|
||||
- Option to evict old items with an [expiration](#data-expiration) TTL
|
||||
- Tight codebase, under 1K loc using the `cloc` command.
|
||||
- ACID semantics with locking [transactions](#transactions) that support rollbacks
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
## Installing
|
||||
|
||||
To start using BuntDB, install Go and run `go get`:
|
||||
|
||||
```sh
|
||||
$ go get github.com/tidwall/buntdb
|
||||
```
|
||||
|
||||
This will retrieve the library.
|
||||
|
||||
|
||||
## Opening a database
|
||||
|
||||
The primary object in BuntDB is a `DB`. To open or create your
|
||||
database, use the `buntdb.Open()` function:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Open the data.db file. It will be created if it doesn't exist.
|
||||
db, err := buntdb.Open("data.db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
It's important to note that BuntDB does not currently support file locking, so avoid accessing the database from multiple processes.
|
||||
|
||||
## Transactions
|
||||
All reads and writes must be performed from inside a transaction. BuntDB can have one write transaction opened at a time, but can have many concurrent read transactions. Each transaction maintains a stable view of the database. In other words, once a transaction has begun, the data for that transaction cannot be changed by other transactions.
|
||||
|
||||
Transactions run in a function that exposes a `Tx` object, which represents the transaction state. While inside a transaction, all database operations should be performed using this object. You should never access the origin `DB` object while inside a transaction. Doing so may have side-effects, such as blocking your application.
|
||||
|
||||
When a transaction fails, it will roll back, and revert all chanages that ocurred to the database during that transaction. There's a single return value that you can use to close the transaction. For read/write transactions, returning an error this way will force the transaction to roll back. When a read/write transaction succeeds all changes are persisted to disk.
|
||||
|
||||
### Read-only Transactions
|
||||
A read-only transaction should be used when you don't need to make changes to the data. The advantage of a read-only transaction is that there can be many running concurrently.
|
||||
|
||||
```go
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
...
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Read/write Transactions
|
||||
A read/write transaction is used when you need to make changes to your data. There can only be one read/write transaction running at a time. So make sure you close it as soon as you are done with it.
|
||||
|
||||
```go
|
||||
err := db.Update(func(tx *buntdb.Tx) error {
|
||||
...
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
## Setting and getting key/values
|
||||
|
||||
To set a value you must open a read/write tranasction:
|
||||
|
||||
```go
|
||||
err := db.Update(func(tx *buntdb.Tx) error {
|
||||
err := tx.Set("mykey", "myvalue", nil)
|
||||
return err
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
To get the value:
|
||||
|
||||
```go
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
val, err := tx.Get("mykey")
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
fmt.Printf("value is %s\n", val)
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
Getting non-existent values will case an `ErrNotFound` error.
|
||||
|
||||
### Iterating
|
||||
All keys/value pairs are ordered in the database by the key. To iterate over the keys:
|
||||
|
||||
```go
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
err := tx.Ascend("", func(key, value string) bool{
|
||||
fmt.Printf("key: %s, value: %s\n", key, value)
|
||||
})
|
||||
return err
|
||||
})
|
||||
```
|
||||
|
||||
There is also `AscendGreaterOrEqual`, `AscendLessThan`, `AscendRange`, `Descend`, `DescendLessOrEqual`, `DescendGreaterThan`, and `DescendRange`. Please see the [documentation](https://godoc.org/github.com/tidwall/buntdb) for more information on these functions.
|
||||
|
||||
|
||||
## Custom Indexes
|
||||
Initially all data is stored in a single [B-tree](https://en.wikipedia.org/wiki/B-tree) with each item having one key and one value. All of these items are ordered by the key. This is great for quickly getting a value from a key or [iterating](#iterating) over the keys.
|
||||
|
||||
You can also create custom indexes that allow for ordering and [iterating](#iterating) over values. A custom index also uses a B-tree, but it's more flexible because it allows for custom ordering.
|
||||
|
||||
For example, let's say you want to create an index for ordering names:
|
||||
|
||||
```go
|
||||
db.CreateIndex("names", "*", buntdb.IndexString)
|
||||
```
|
||||
|
||||
This will create an index named `names` which stores and sorts all values. The second parameter is a pattern that is used to filter on keys. A `*` wildcard argument means that we want to accept all keys. `IndexString` is a built-in function that performs case-insensitive ordering on the values
|
||||
|
||||
Now you can add various names:
|
||||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("user:0:name", "tom", nil)
|
||||
tx.Set("user:1:name", "Randi", nil)
|
||||
tx.Set("user:2:name", "jane", nil)
|
||||
tx.Set("user:4:name", "Janet", nil)
|
||||
tx.Set("user:5:name", "Paula", nil)
|
||||
tx.Set("user:6:name", "peter", nil)
|
||||
tx.Set("user:7:name", "Terri", nil)
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
Finally you can iterate over the index:
|
||||
|
||||
```go
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
tx.Ascend("names", func(key, val string) bool {
|
||||
fmt.Printf(buf, "%s %s\n", key, val)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
```
|
||||
The output should be:
|
||||
```
|
||||
user:2:name jane
|
||||
user:4:name Janet
|
||||
user:5:name Paula
|
||||
user:6:name peter
|
||||
user:1:name Randi
|
||||
user:7:name Terri
|
||||
user:0:name tom
|
||||
```
|
||||
|
||||
The pattern parameter can be used to filter on keys like this:
|
||||
|
||||
```go
|
||||
db.CreateIndex("names", "user:*", buntdb.IndexString)
|
||||
```
|
||||
|
||||
Now only items with keys that have the prefix `user:` will be added to the `names` index.
|
||||
|
||||
|
||||
### Built-in types
|
||||
Along with `IndexString`, there is also `IndexInt`, `IndexUint`, and `IndexFloat`.
|
||||
These are built-in types for indexing. You can choose to use these or create your own.
|
||||
|
||||
So to create an index that is numerically ordered on an age key, we could use:
|
||||
|
||||
```go
|
||||
db.CreateIndex("ages", "user:*:age", buntdb.IndexInt)
|
||||
```
|
||||
|
||||
And then add values:
|
||||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("user:0:age", "35", nil)
|
||||
tx.Set("user:1:age", "49", nil)
|
||||
tx.Set("user:2:age", "13", nil)
|
||||
tx.Set("user:4:age", "63", nil)
|
||||
tx.Set("user:5:age", "8", nil)
|
||||
tx.Set("user:6:age", "3", nil)
|
||||
tx.Set("user:7:age", "16", nil)
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
```go
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
tx.Ascend("ages", func(key, val string) bool {
|
||||
fmt.Printf(buf, "%s %s\n", key, val)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
The output should be:
|
||||
```
|
||||
user:6:name 3
|
||||
user:5:name 8
|
||||
user:2:name 13
|
||||
user:7:name 16
|
||||
user:0:name 35
|
||||
user:1:name 49
|
||||
user:4:name 63
|
||||
```
|
||||
|
||||
### Spatial Indexes
|
||||
BuntDB has support for spatial indexes by storing rectangles in an [R-tree](https://en.wikipedia.org/wiki/R-tree). An R-tree is organized in a similar manner as a [B-tree](https://en.wikipedia.org/wiki/B-tree), and both are balanaced trees. But, an R-tree is special because it can operate on data that is in multiple dimensions. This is super handy for Geospatial applications.
|
||||
|
||||
To create a spatial index use the `CreateSpatialIndex` function:
|
||||
|
||||
```go
|
||||
db.CreateSpatialIndex("fleet", "fleet:*:pos", buntdb.IndexRect)
|
||||
```
|
||||
|
||||
Then `IndexRect` is a built-in function that converts rect strings to a format that the R-tree can use. It's easy to use this function out of the box, but you might find it better to create a custom one that renders from a different format, such as [Well-known text](https://en.wikipedia.org/wiki/Well-known_text) or [GeoJSON](http://geojson.org/).
|
||||
|
||||
To add some lon,lat points to the `fleet` index:
|
||||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("fleet:0:pos", "[-115.567 33.532]", nil)
|
||||
tx.Set("fleet:1:pos", "[-116.671 35.735]", nil)
|
||||
tx.Set("fleet:2:pos", "[-113.902 31.234]", nil)
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
And then you can run the `Intersects` function on the index:
|
||||
|
||||
```go
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
tx.Intersects("fleet", "[-117 30],[-112 36]", func(key, val string) bool {
|
||||
...
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
This will get all three positions.
|
||||
|
||||
The bracket syntax `[-117 30],[-112 36]` is unique to BuntDB, and it's how the built-in rectangles are processed, but you are not limited to this syntax. Whatever Rect function you choose to use during `CreateSpatialIndex` will be used to process the paramater, in this case it's `IndexRect`.
|
||||
|
||||
|
||||
### Data Expiration
|
||||
Items can be automatically evicted by using the `SetOptions` object in the `Set` function to set a `TTL`.
|
||||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("mykey", "myval", &buntdb.SetOptions{Expires:true, TTL:time.Second})
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
Now `mykey` will automatically be deleted after one second. You can remove the TTL by setting the value again with the same key/value, but with the options parameter set to nil.
|
||||
|
||||
## Contact
|
||||
Josh Baker [@tidwall](http://twitter.com/tidwall)
|
||||
|
||||
## License
|
||||
|
||||
BuntDB source code is available under the MIT [License](/LICENSE).
|
|
@ -0,0 +1,791 @@
|
|||
package buntdb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBackgroudOperations(t *testing.T) {
|
||||
os.RemoveAll("data.db")
|
||||
db, err := Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
defer db.Close()
|
||||
for i := 0; i < 1000; i++ {
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
for j := 0; j < 200; j++ {
|
||||
tx.Set(fmt.Sprintf("hello%d", j), "planet", nil)
|
||||
}
|
||||
tx.Set("hi", "world", &SetOptions{Expires: true, TTL: time.Second / 2})
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
n := 0
|
||||
db.View(func(tx *Tx) error {
|
||||
n, _ = tx.Len()
|
||||
return nil
|
||||
})
|
||||
if n != 201 {
|
||||
t.Fatalf("expecting '%v', got '%v'", 201, n)
|
||||
}
|
||||
time.Sleep(time.Millisecond * 1500)
|
||||
db.Close()
|
||||
db, err = Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
defer db.Close()
|
||||
n = 0
|
||||
db.View(func(tx *Tx) error {
|
||||
n, _ = tx.Len()
|
||||
return nil
|
||||
})
|
||||
if n != 200 {
|
||||
t.Fatalf("expecting '%v', got '%v'", 200, n)
|
||||
}
|
||||
}
|
||||
func TestVariousTx(t *testing.T) {
|
||||
os.RemoveAll("data.db")
|
||||
db, err := Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
defer db.Close()
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.Set("hello", "planet", nil)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errBroken := errors.New("broken")
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.Set("hello", "world", nil)
|
||||
return errBroken
|
||||
}); err != errBroken {
|
||||
t.Fatalf("did not correctly receive the user-defined transaction error.")
|
||||
}
|
||||
var val string
|
||||
db.View(func(tx *Tx) error {
|
||||
val, _ = tx.Get("hello")
|
||||
return nil
|
||||
})
|
||||
if val == "world" {
|
||||
t.Fatal("a rollbacked transaction got through")
|
||||
}
|
||||
if val != "planet" {
|
||||
t.Fatal("expecting '%v', got '%v'", "planet", val)
|
||||
}
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.db = nil
|
||||
if _, _, err := tx.Set("hello", "planet", nil); err != ErrTxClosed {
|
||||
t.Fatal("expecting a tx closed error")
|
||||
}
|
||||
if _, err := tx.Delete("hello"); err != ErrTxClosed {
|
||||
t.Fatal("expecting a tx closed error")
|
||||
}
|
||||
if _, err := tx.Get("hello"); err != ErrTxClosed {
|
||||
t.Fatal("expecting a tx closed error")
|
||||
}
|
||||
tx.db = db
|
||||
tx.writable = false
|
||||
if _, _, err := tx.Set("hello", "planet", nil); err != ErrTxNotWritable {
|
||||
t.Fatal("expecting a tx not writable error")
|
||||
}
|
||||
if _, err := tx.Delete("hello"); err != ErrTxNotWritable {
|
||||
t.Fatal("expecting a tx not writable error")
|
||||
}
|
||||
tx.writable = true
|
||||
if _, err := tx.Get("something"); err != ErrNotFound {
|
||||
t.Fatalf("expecting not found error")
|
||||
}
|
||||
if _, err := tx.Delete("something"); err != ErrNotFound {
|
||||
t.Fatalf("expecting not found error")
|
||||
}
|
||||
tx.Set("var", "val", &SetOptions{Expires: true, TTL: 0})
|
||||
if _, err := tx.Get("var"); err != ErrNotFound {
|
||||
t.Fatalf("expecting not found error")
|
||||
}
|
||||
if _, err := tx.Delete("var"); err != ErrNotFound {
|
||||
tx.unlock()
|
||||
t.Fatalf("expecting not found error")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test for invalid commits
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
// we are going to do some hackery
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
if v.(string) != "managed tx commit not allowed" {
|
||||
t.Fatal(v.(string))
|
||||
}
|
||||
}
|
||||
}()
|
||||
tx.commit()
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test for invalid commits
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
// we are going to do some hackery
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
if v.(string) != "managed tx rollback not allowed" {
|
||||
t.Fatal(v.(string))
|
||||
}
|
||||
}
|
||||
}()
|
||||
tx.rollback()
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test for closed transactions
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.db = nil
|
||||
return nil
|
||||
}); err != ErrTxClosed {
|
||||
t.Fatal("expecting tx closed error")
|
||||
}
|
||||
db.mu.Unlock()
|
||||
|
||||
// test for invalid writes
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.writable = false
|
||||
return nil
|
||||
}); err != ErrTxNotWritable {
|
||||
t.Fatal("expecting tx not writable error")
|
||||
}
|
||||
db.mu.Unlock()
|
||||
// test for closed transactions
|
||||
if err := db.View(func(tx *Tx) error {
|
||||
tx.db = nil
|
||||
return nil
|
||||
}); err != ErrTxClosed {
|
||||
t.Fatal("expecting tx closed error")
|
||||
}
|
||||
db.mu.RUnlock()
|
||||
// flush to unwritable file
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.Set("var1", "val1", nil)
|
||||
tx.db.file.Close()
|
||||
return nil
|
||||
}); err == nil {
|
||||
t.Fatal("should not be able to commit when the file is closed")
|
||||
}
|
||||
db.file, err = os.OpenFile("data.db", os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.file.Seek(0, 2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db.bufw = bufio.NewWriter(db.file)
|
||||
db.CreateIndex("blank", "*", nil)
|
||||
// test scanning
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.Set("nothing", "here", nil)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.View(func(tx *Tx) error {
|
||||
s := ""
|
||||
tx.Ascend("", func(key, val string) bool {
|
||||
s += key + ":" + val + "\n"
|
||||
return true
|
||||
})
|
||||
if s != "hello:planet\nnothing:here\n" {
|
||||
t.Fatal("invalid scan")
|
||||
}
|
||||
tx.db = nil
|
||||
err = tx.Ascend("", func(key, val string) bool { return true })
|
||||
if err != ErrTxClosed {
|
||||
tx.unlock()
|
||||
t.Fatal("expecting tx closed error")
|
||||
}
|
||||
tx.db = db
|
||||
err = tx.Ascend("na", func(key, val string) bool { return true })
|
||||
if err != ErrNotFound {
|
||||
t.Fatal("expecting not found error")
|
||||
}
|
||||
err = tx.Ascend("blank", func(key, val string) bool { return true })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s = ""
|
||||
tx.AscendLessThan("", "liger", func(key, val string) bool {
|
||||
s += key + ":" + val + "\n"
|
||||
return true
|
||||
})
|
||||
if s != "hello:planet\n" {
|
||||
t.Fatal("invalid scan")
|
||||
}
|
||||
|
||||
s = ""
|
||||
tx.Descend("", func(key, val string) bool {
|
||||
s += key + ":" + val + "\n"
|
||||
return true
|
||||
})
|
||||
if s != "nothing:here\nhello:planet\n" {
|
||||
t.Fatal("invalid scan")
|
||||
}
|
||||
|
||||
s = ""
|
||||
tx.DescendLessOrEqual("", "liger", func(key, val string) bool {
|
||||
s += key + ":" + val + "\n"
|
||||
return true
|
||||
})
|
||||
if s != "hello:planet\n" {
|
||||
t.Fatal("invalid scan")
|
||||
}
|
||||
|
||||
s = ""
|
||||
tx.DescendGreaterThan("", "liger", func(key, val string) bool {
|
||||
s += key + ":" + val + "\n"
|
||||
return true
|
||||
})
|
||||
if s != "nothing:here\n" {
|
||||
t.Fatal("invalid scan")
|
||||
}
|
||||
s = ""
|
||||
tx.DescendRange("", "liger", "apple", func(key, val string) bool {
|
||||
s += key + ":" + val + "\n"
|
||||
return true
|
||||
})
|
||||
if s != "hello:planet\n" {
|
||||
t.Fatal("invalid scan")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test some spatial stuff
|
||||
db.CreateSpatialIndex("spat", "rect:*", IndexRect)
|
||||
db.CreateSpatialIndex("junk", "rect:*", nil)
|
||||
db.Update(func(tx *Tx) error {
|
||||
tx.Set("rect:1", "[10 10],[20 20]", nil)
|
||||
tx.Set("rect:2", "[15 15],[25 25]", nil)
|
||||
tx.Set("shape:1", "[12 12],[25 25]", nil)
|
||||
s := ""
|
||||
tx.Intersects("spat", "[5 5],[13 13]", func(key, val string) bool {
|
||||
s += key + ":" + val + "\n"
|
||||
return true
|
||||
})
|
||||
if s != "rect:1:[10 10],[20 20]\n" {
|
||||
t.Fatal("invalid scan")
|
||||
}
|
||||
tx.db = nil
|
||||
err := tx.Intersects("spat", "[5 5],[13 13]", func(key, val string) bool {
|
||||
return true
|
||||
})
|
||||
if err != ErrTxClosed {
|
||||
t.Fatal("expecting tx closed error")
|
||||
}
|
||||
tx.db = db
|
||||
err = tx.Intersects("", "[5 5],[13 13]", func(key, val string) bool {
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tx.Intersects("na", "[5 5],[13 13]", func(key, val string) bool {
|
||||
return true
|
||||
})
|
||||
if err != ErrNotFound {
|
||||
t.Fatal("expecting not found error")
|
||||
}
|
||||
err = tx.Intersects("junk", "[5 5],[13 13]", func(key, val string) bool {
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := tx.Len()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 5 {
|
||||
t.Fatal("expecting %v, got %v", 5, n)
|
||||
}
|
||||
tx.db = nil
|
||||
_, err = tx.Len()
|
||||
if err != ErrTxClosed {
|
||||
t.Fatal("expecting tx closed error")
|
||||
}
|
||||
tx.db = db
|
||||
return nil
|
||||
})
|
||||
|
||||
// test after closing
|
||||
db.Close()
|
||||
if err := db.Update(func(tx *Tx) error { return nil }); err != ErrDatabaseClosed {
|
||||
t.Fatalf("should not be able to perform transactionso on a closed database.")
|
||||
}
|
||||
}
|
||||
func TestNoExpiringItem(t *testing.T) {
|
||||
item := &dbItem{key: "key", val: "val"}
|
||||
if !item.expiresAt().Equal(maxTime) {
|
||||
t.Fatal("item.expiresAt() != maxTime")
|
||||
}
|
||||
if min, max := item.Rect(nil); min != nil || max != nil {
|
||||
t.Fatal("item min,max should both be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// test database format loading
|
||||
func TestDatabaseFormat(t *testing.T) {
|
||||
// should succeed
|
||||
func() {
|
||||
resp := strings.Join([]string{
|
||||
"*3\r\n$3\r\nset\r\n$4\r\nvar1\r\n$4\r\n1234\r\n",
|
||||
"*3\r\n$3\r\nset\r\n$4\r\nvar2\r\n$4\r\n1234\r\n",
|
||||
"*2\r\n$3\r\ndel\r\n$4\r\nvar1\r\n",
|
||||
"*5\r\n$3\r\nset\r\n$3\r\nvar\r\n$3\r\nval\r\n$2\r\nex\r\n$2\r\n10\r\n",
|
||||
}, "")
|
||||
os.RemoveAll("data.db")
|
||||
ioutil.WriteFile("data.db", []byte(resp), 0666)
|
||||
db, err := Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
defer db.Close()
|
||||
}()
|
||||
testBadFormat := func(resp string) {
|
||||
os.RemoveAll("data.db")
|
||||
ioutil.WriteFile("data.db", []byte(resp), 0666)
|
||||
db, err := Open("data.db")
|
||||
if err == nil {
|
||||
db.Close()
|
||||
os.RemoveAll("data.db")
|
||||
t.Fatalf("invalid database should not be allowed")
|
||||
}
|
||||
}
|
||||
testBadFormat("*3\r")
|
||||
testBadFormat("*3\n")
|
||||
testBadFormat("*a\r\n")
|
||||
testBadFormat("*2\r\n")
|
||||
testBadFormat("*2\r\n%3")
|
||||
testBadFormat("*2\r\n$")
|
||||
testBadFormat("*2\r\n$3\r\n")
|
||||
testBadFormat("*2\r\n$3\r\ndel")
|
||||
testBadFormat("*2\r\n$3\r\ndel\r\r")
|
||||
testBadFormat("*0\r\n*2\r\n$3\r\ndel\r\r")
|
||||
testBadFormat("*1\r\n$3\r\nnop\r\n")
|
||||
testBadFormat("*1\r\n$3\r\ndel\r\n")
|
||||
testBadFormat("*1\r\n$3\r\nset\r\n")
|
||||
testBadFormat("*5\r\n$3\r\nset\r\n$3\r\nvar\r\n$3\r\nval\r\n$2\r\nxx\r\n$2\r\n10\r\n")
|
||||
testBadFormat("*5\r\n$3\r\nset\r\n$3\r\nvar\r\n$3\r\nval\r\n$2\r\nex\r\n$2\r\naa\r\n")
|
||||
}
|
||||
|
||||
func TestInsertsAndDeleted(t *testing.T) {
|
||||
os.RemoveAll("data.db")
|
||||
db, err := Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
defer db.Close()
|
||||
db.CreateIndex("any", "*", IndexString)
|
||||
db.CreateSpatialIndex("rect", "*", IndexRect)
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.Set("item1", "value1", &SetOptions{Expires: true, TTL: time.Second})
|
||||
tx.Set("item2", "value2", nil)
|
||||
tx.Set("item3", "value3", &SetOptions{Expires: true, TTL: time.Second})
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test replacing items in the database
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
if _, _, err := tx.Set("item1", "nvalue1", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, err := tx.Set("item2", "nvalue2", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Delete("item3"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// test index compare functions
|
||||
func TestIndexCompare(t *testing.T) {
|
||||
if !IndexFloat("1.5", "1.6") {
|
||||
t.Fatalf("expected true, got false")
|
||||
}
|
||||
if !IndexInt("-1", "2") {
|
||||
t.Fatalf("expected true, got false")
|
||||
}
|
||||
if !IndexUint("10", "25") {
|
||||
t.Fatalf("expected true, got false")
|
||||
}
|
||||
if !IndexBinary("Hello", "hello") {
|
||||
t.Fatalf("expected true, got false")
|
||||
}
|
||||
if IndexString("hello", "hello") {
|
||||
t.Fatalf("expected false, got true")
|
||||
}
|
||||
if IndexString("Hello", "hello") {
|
||||
t.Fatalf("expected false, got true")
|
||||
}
|
||||
if IndexString("hello", "Hello") {
|
||||
t.Fatalf("expected false, got true")
|
||||
}
|
||||
if !IndexString("gello", "Hello") {
|
||||
t.Fatalf("expected true, got false")
|
||||
}
|
||||
if IndexString("Hello", "gello") {
|
||||
t.Fatalf("expected false, got true")
|
||||
}
|
||||
if Rect(IndexRect("[1 2 3 4],[5 6 7 8]")) != "[1 2 3 4],[5 6 7 8]" {
|
||||
t.Fatalf("expected '%v', got '%v'", "[1 2 3 4],[5 6 7 8]", Rect(IndexRect("[1 2 3 4],[5 6 7 8]")))
|
||||
}
|
||||
if Rect(IndexRect("[1 2 3 4]")) != "[1 2 3 4]" {
|
||||
t.Fatalf("expected '%v', got '%v'", "[1 2 3 4]", Rect(IndexRect("[1 2 3 4]")))
|
||||
}
|
||||
if Rect(nil, nil) != "" {
|
||||
t.Fatalf("expected '%v', got '%v'", "", Rect(nil, nil))
|
||||
}
|
||||
if Point(1, 2, 3) != "[1 2 3]" {
|
||||
t.Fatalf("expected '%v', got '%v'", "[1 2 3]", Point(1, 2, 3))
|
||||
}
|
||||
}
|
||||
|
||||
// test opening a folder.
|
||||
func TestOpeningAFolder(t *testing.T) {
|
||||
os.RemoveAll("dir.tmp")
|
||||
os.Mkdir("dir.tmp", 0700)
|
||||
defer os.RemoveAll("dir.tmp")
|
||||
db, err := Open("dir.tmp")
|
||||
if err == nil {
|
||||
db.Close()
|
||||
t.Fatalf("opening a directory should not be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// test opening an invalid resp file.
|
||||
func TestOpeningInvalidDatabaseFile(t *testing.T) {
|
||||
os.RemoveAll("data.db")
|
||||
ioutil.WriteFile("data.db", []byte("invalid\r\nfile"), 0666)
|
||||
defer os.RemoveAll("data.db")
|
||||
db, err := Open("data.db")
|
||||
if err == nil {
|
||||
db.Close()
|
||||
t.Fatalf("invalid database should not be allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// test closing a closed database.
|
||||
func TestOpeningClosedDatabase(t *testing.T) {
|
||||
os.RemoveAll("data.db")
|
||||
db, err := Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Close(); err != ErrDatabaseClosed {
|
||||
t.Fatal("should not be able to close a closed database")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariousIndexOperations(t *testing.T) {
|
||||
os.RemoveAll("data.db")
|
||||
db, err := Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
defer db.Close()
|
||||
// test creating an index with no index name.
|
||||
err = db.CreateIndex("", "", nil)
|
||||
if err == nil {
|
||||
t.Fatal("should not be able to create an index with no name")
|
||||
}
|
||||
// test creating an index with a name that has already been used.
|
||||
err = db.CreateIndex("hello", "", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = db.CreateIndex("hello", "", nil)
|
||||
if err == nil {
|
||||
t.Fatal("should not be able to create a duplicate index")
|
||||
}
|
||||
db.Update(func(tx *Tx) error {
|
||||
tx.Set("user:1", "tom", nil)
|
||||
tx.Set("user:2", "janet", nil)
|
||||
tx.Set("alt:1", "from", nil)
|
||||
tx.Set("alt:2", "there", nil)
|
||||
tx.Set("rect:1", "[1 2],[3 4]", nil)
|
||||
tx.Set("rect:2", "[5 6],[7 8]", nil)
|
||||
return nil
|
||||
})
|
||||
// test creating an index after adding items. use pattern matching. have some items in the match and some not.
|
||||
if err := db.CreateIndex("string", "user:*", IndexString); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// test creating a spatial index after adding items. use pattern matching. have some items in the match and some not.
|
||||
if err := db.CreateSpatialIndex("rect", "rect:*", IndexRect); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// test dropping an index
|
||||
if err := db.DropIndex("hello"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// test dropping an index with no name
|
||||
if err := db.DropIndex(""); err == nil {
|
||||
t.Fatal("should not be allowed to drop an index with no name")
|
||||
}
|
||||
// test dropping an index with no name
|
||||
if err := db.DropIndex("na"); err == nil {
|
||||
t.Fatal("should not be allowed to drop an index that does not exist")
|
||||
}
|
||||
// test retrieving index names
|
||||
names, err := db.Indexes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(names, ",") != "rect,string" {
|
||||
t.Fatalf("expecting '%v', got '%v'", "rect,string", strings.Join(names, ","))
|
||||
}
|
||||
// test creating an index after closing database
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.CreateIndex("new-index", "", nil); err != ErrDatabaseClosed {
|
||||
t.Fatal("should not be able to create an index on a closed database")
|
||||
}
|
||||
// test getting index names after closing database
|
||||
if _, err := db.Indexes(); err != ErrDatabaseClosed {
|
||||
t.Fatal("should not be able to get index names on a closed database")
|
||||
}
|
||||
// test dropping an index after closing database
|
||||
if err := db.DropIndex("rect"); err != ErrDatabaseClosed {
|
||||
t.Fatal("should not be able to drop an index on a closed database")
|
||||
}
|
||||
}
|
||||
|
||||
func test(t *testing.T, a, b bool) {
|
||||
if a != b {
|
||||
t.Fatal("failed, bummer...")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatternMatching(t *testing.T) {
|
||||
test(t, wildcardMatch("hello", "hello"), true)
|
||||
test(t, wildcardMatch("hello", "h*"), true)
|
||||
test(t, wildcardMatch("hello", "h*o"), true)
|
||||
test(t, wildcardMatch("hello", "h*l*o"), true)
|
||||
test(t, wildcardMatch("hello", "h*z*o"), false)
|
||||
test(t, wildcardMatch("hello", "*l*o"), true)
|
||||
test(t, wildcardMatch("hello", "*l*"), true)
|
||||
test(t, wildcardMatch("hello", "*?*"), true)
|
||||
test(t, wildcardMatch("hello", "*"), true)
|
||||
test(t, wildcardMatch("hello", "h?llo"), true)
|
||||
test(t, wildcardMatch("hello", "h?l?o"), true)
|
||||
test(t, wildcardMatch("", "*"), true)
|
||||
test(t, wildcardMatch("", ""), true)
|
||||
test(t, wildcardMatch("h", ""), false)
|
||||
test(t, wildcardMatch("", "?"), false)
|
||||
}
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
os.RemoveAll("data.db")
|
||||
db, err := Open("data.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll("data.db")
|
||||
defer db.Close()
|
||||
|
||||
// create a simple index
|
||||
db.CreateIndex("users", "fun:user:*", IndexString)
|
||||
|
||||
// create a spatial index
|
||||
db.CreateSpatialIndex("rects", "rect:*", IndexRect)
|
||||
if true {
|
||||
db.Update(func(tx *Tx) error {
|
||||
tx.Set("fun:user:0", "tom", nil)
|
||||
tx.Set("fun:user:1", "Randi", nil)
|
||||
tx.Set("fun:user:2", "jane", nil)
|
||||
tx.Set("fun:user:4", "Janet", nil)
|
||||
tx.Set("fun:user:5", "Paula", nil)
|
||||
tx.Set("fun:user:6", "peter", nil)
|
||||
tx.Set("fun:user:7", "Terri", nil)
|
||||
return nil
|
||||
})
|
||||
// add some random items
|
||||
start := time.Now()
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
for _, i := range rand.Perm(100) {
|
||||
tx.Set(fmt.Sprintf("tag:%d", i+100), fmt.Sprintf("val:%d", rand.Int()%100+100), nil)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if false {
|
||||
println(time.Now().Sub(start).String(), db.keys.Len())
|
||||
}
|
||||
// add some random rects
|
||||
if err := db.Update(func(tx *Tx) error {
|
||||
tx.Set("rect:1", Rect([]float64{10, 10}, []float64{20, 20}), nil)
|
||||
tx.Set("rect:2", Rect([]float64{15, 15}, []float64{24, 24}), nil)
|
||||
tx.Set("rect:3", Rect([]float64{17, 17}, []float64{27, 27}), nil)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// verify the data has been created
|
||||
buf := &bytes.Buffer{}
|
||||
db.View(func(tx *Tx) error {
|
||||
tx.Ascend("users", func(key, val string) bool {
|
||||
fmt.Fprintf(buf, "%s %s\n", key, val)
|
||||
return true
|
||||
})
|
||||
err = tx.AscendRange("", "tag:170", "tag:172", func(key, val string) bool {
|
||||
fmt.Fprintf(buf, "%s\n", key)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tx.AscendGreaterOrEqual("", "tag:195", func(key, val string) bool {
|
||||
fmt.Fprintf(buf, "%s\n", key)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = tx.AscendGreaterOrEqual("", "rect:", func(key, val string) bool {
|
||||
if !strings.HasPrefix(key, "rect:") {
|
||||
return false
|
||||
}
|
||||
min, max := IndexRect(val)
|
||||
fmt.Fprintf(buf, "%s: %v,%v\n", key, min, max)
|
||||
return true
|
||||
})
|
||||
expect := make([]string, 2)
|
||||
n := 0
|
||||
tx.Intersects("rects", "[0 0],[15 15]", func(key, val string) bool {
|
||||
if n == 2 {
|
||||
t.Fatalf("too many rects where received, expecting only two")
|
||||
}
|
||||
min, max := IndexRect(val)
|
||||
s := fmt.Sprintf("%s: %v,%v\n", key, min, max)
|
||||
if key == "rect:1" {
|
||||
expect[0] = s
|
||||
} else if key == "rect:2" {
|
||||
expect[1] = s
|
||||
}
|
||||
n++
|
||||
return true
|
||||
})
|
||||
for _, s := range expect {
|
||||
buf.WriteString(s)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
res := `
|
||||
fun:user:2 jane
|
||||
fun:user:4 Janet
|
||||
fun:user:5 Paula
|
||||
fun:user:6 peter
|
||||
fun:user:1 Randi
|
||||
fun:user:7 Terri
|
||||
fun:user:0 tom
|
||||
tag:170
|
||||
tag:171
|
||||
tag:195
|
||||
tag:196
|
||||
tag:197
|
||||
tag:198
|
||||
tag:199
|
||||
rect:1: [10 10],[20 20]
|
||||
rect:2: [15 15],[24 24]
|
||||
rect:3: [17 17],[27 27]
|
||||
rect:1: [10 10],[20 20]
|
||||
rect:2: [15 15],[24 24]
|
||||
`
|
||||
res = strings.Replace(res, "\r", "", -1)
|
||||
if strings.TrimSpace(buf.String()) != strings.TrimSpace(res) {
|
||||
t.Fatalf("expected [%v], got [%v]", strings.TrimSpace(res), strings.TrimSpace(buf.String()))
|
||||
}
|
||||
}
|
||||
func testRectStringer(min, max []float64) error {
|
||||
nmin, nmax := IndexRect(Rect(min, max))
|
||||
if len(nmin) != len(min) {
|
||||
return fmt.Errorf("rect=%v,%v, expect=%v,%v", nmin, nmax, min, max)
|
||||
}
|
||||
for i := 0; i < len(min); i++ {
|
||||
if min[i] != nmin[i] || max[i] != nmax[i] {
|
||||
return fmt.Errorf("rect=%v,%v, expect=%v,%v", nmin, nmax, min, max)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func TestRectStrings(t *testing.T) {
|
||||
test(t, Rect(IndexRect(Point(1))) == "[1]", true)
|
||||
test(t, Rect(IndexRect(Point(1, 2, 3, 4))) == "[1 2 3 4]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("[1 2],[1 2]")))) == "[1 2]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("[1 2],[2 2]")))) == "[1 2],[2 2]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("[1 2],[2 2],[3]")))) == "[1 2],[2 2]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("[1 2]")))) == "[1 2]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("[1.5 2 4.5 5.6]")))) == "[1.5 2 4.5 5.6]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("[1.5 2 4.5 5.6 -1],[]")))) == "[1.5 2 4.5 5.6 -1],[]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("[]")))) == "[]", true)
|
||||
test(t, Rect(IndexRect(Rect(IndexRect("")))) == "", true)
|
||||
if err := testRectStringer(nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testRectStringer([]float64{}, []float64{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testRectStringer([]float64{1}, []float64{2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testRectStringer([]float64{1, 2}, []float64{3, 4}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testRectStringer([]float64{1, 2, 3}, []float64{4, 5, 6}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testRectStringer([]float64{1, 2, 3, 4}, []float64{5, 6, 7, 8}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := testRectStringer([]float64{1, 2, 3, 4, 5}, []float64{6, 7, 8, 9, 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue