Skip to content

Commit

Permalink
Re-written tests
Browse files Browse the repository at this point in the history
  • Loading branch information
darkfrog26 committed Apr 6, 2024
1 parent 5983af6 commit f9419da
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 5 deletions.
18 changes: 17 additions & 1 deletion core/src/main/scala/lightdb/Collection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ package lightdb
import cats.effect.IO
import fabric.rw.RW

class Collection[D <: Document[D]](val collectionName: String, db: LightDB)(implicit val rw: RW[D]) {
abstract class Collection[D <: Document[D]](val collectionName: String, db: LightDB) {
implicit val rw: RW[D]

protected lazy val store: Store = db.createStore(collectionName)

private var _indexedLinks = List.empty[IndexedLinks[_, D]]

def idStream: fs2.Stream[IO, Id[D]] = store.keyStream

def stream: fs2.Stream[IO, D] = store.streamJson[D]

/**
* Called before set
*/
Expand Down Expand Up @@ -42,9 +48,19 @@ class Collection[D <: Document[D]](val collectionName: String, db: LightDB)(impl
il
}

def size: IO[Int] = store.size

def commit(): IO[Unit] = IO.unit

def dispose(): IO[Unit] = IO.unit
}

object Collection {
def apply[D <: Document[D]](collectionName: String, db: LightDB)(implicit docRW: RW[D]): Collection[D] = new Collection[D](collectionName, db) {
override implicit val rw: RW[D] = docRW
}
}

case class IndexedLinks[V, D <: Document[D]](createKey: V => String,
createV: D => V,
store: Store,
Expand Down
7 changes: 3 additions & 4 deletions core/src/main/scala/lightdb/LightDB.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ abstract class LightDB(directory: Path,

private var stores = List.empty[Store]

protected lazy val backingStore = new Collection[KeyValue]("_backingStore", this)
protected lazy val backingStore: Collection[KeyValue] = Collection[KeyValue]("_backingStore", this)
protected lazy val databaseInitialized: StoredValue[Boolean] = stored[Boolean]("_databaseInitialized", false)
protected lazy val appliedUpgrades: StoredValue[Set[String]] = stored[Set[String]]("_appliedUpgrades", Set.empty)

def initialized: Boolean = _initialized.get()

def collections: List[Collection[_]] = Nil
def collections: List[Collection[_]]
def upgrades: List[DatabaseUpgrade]

protected[lightdb] def verifyInitialized(): Unit = if (!initialized) throw new RuntimeException("Database not initialized!")

Expand Down Expand Up @@ -58,8 +59,6 @@ abstract class LightDB(directory: Path,
store
}

def upgrades: List[DatabaseUpgrade] = Nil

def truncate(): IO[Unit] = collections.map(_.truncate()).parSequence.map(_ => ())

def dispose(): IO[Unit] = for {
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/scala/lightdb/Store.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ case class Store(directory: Path,
Id[D](key) -> record.getValue
}

def streamJson[D: RW]: fs2.Stream[IO, D] = stream[D].map {
case (_, bytes) =>
val jsonString = bytes.string
val json = JsonParser(jsonString)
json.as[D]
}

def get[D](id: Id[D]): IO[Option[Array[Byte]]] = IO {
Option(instance.get(id.bytes))
}
Expand Down
179 changes: 179 additions & 0 deletions core/src/test/scala/spec/SimpleSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package spec

import cats.effect.IO
import cats.effect.testing.scalatest.AsyncIOSpec
import fabric.rw._
import lightdb._
import lightdb.upgrade.DatabaseUpgrade
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec

import java.nio.file.Paths

class SimpleSpec extends AsyncWordSpec with AsyncIOSpec with Matchers {
private val id1 = Id[Person]("john")
private val id2 = Id[Person]("jane")

private val p1 = Person("John Doe", 21, id1)
private val p2 = Person("Jane Doe", 19, id2)

"Simple database" should {
"initialize the database" in {
DB.init(truncate = true)
}
"store John Doe" in {
Person.set(p1).map { p =>
p._id should be(id1)
}
}
"verify John Doe exists" in {
Person.get(id1).map { o =>
o should be(Some(p1))
}
}
"storage Jane Doe" in {
Person.set(p2).map { p =>
p._id should be(id2)
}
}
"verify Jane Doe exists" in {
Person.get(id2).map { o =>
o should be(Some(p2))
}
}
"verify exactly two objects in data" in {
Person.size.map { size =>
size should be(2)
}
}
"flush data" in {
Person.commit()
}
// "verify exactly two objects in index" in {
// db.people.indexer.count().map { size =>
// size should be(2)
// }
// }
"verify exactly two objects in the store" in {
Person.idStream.compile.toList.map { ids =>
ids.toSet should be(Set(id1, id2))
}
}
"search by name for positive result" in {
Person.name.query("Jane Doe").compile.toList.map { people =>
people.length should be(1)
val p = people.head
p._id should be(id2)
p.name should be("Jane Doe")
p.age should be(19)
}
}
"search by age for positive result" in {
Person.age.query(19).compile.toList.map { people =>
people.length should be(1)
val p = people.head
p._id should be(id2)
p.name should be("Jane Doe")
p.age should be(19)
}
}
"search by id for John" in {
Person(id1).map { person =>
person._id should be(id1)
person.name should be("John Doe")
person.age should be(21)
}
}
"delete John" in {
Person.delete(id1)
}
"verify exactly one object in data" in {
Person.size.map { size =>
size should be(1)
}
}
"commit data" in {
Person.commit()
}
// "verify exactly one object in index" in {
// db.people.indexer.count().map { size =>
// size should be(1)
// }
// }
"list all documents" in {
Person.stream.compile.toList.map { people =>
people.length should be(1)
val p = people.head
p._id should be(id2)
p.name should be("Jane Doe")
p.age should be(19)
}
}
// TODO: search for an item by name and by age range
"replace Jane Doe" in {
Person.set(Person("Jane Doe", 20, id2)).map { p =>
p._id should be(id2)
}
}
"verify Jan Doe" in {
Person(id2).map { p =>
p._id should be(id2)
p.name should be("Jan Doe")
p.age should be(20)
}
}
"commit new data" in {
Person.commit()
}
"list new documents" in {
Person.stream.compile.toList.map { results =>
results.length should be(1)
val doc = results.head
doc._id should be(id2)
doc.name should be("Jan Doe")
doc.age should be(20)
}
}
"verify start time has been set" in {
DB.startTime.get().map { startTime =>
startTime should be > 0L
}
}
// TODO: support multiple item types (make sure queries don't return different types)
// TODO: test batch operations: insert, replace, and delete
"dispose" in {
DB.dispose()
}
}

object DB extends LightDB(directory = Paths.get("testdb")) {
// override protected def autoCommit: Boolean = true

val startTime: StoredValue[Long] = stored[Long]("startTime", -1L)

// val people: Collection[Person] = collection("people", Person)

override lazy val collections: List[Collection[_]] = List(
Person
)

override def upgrades: List[DatabaseUpgrade] = List(InitialSetupUpgrade)
}

case class Person(name: String, age: Int, _id: Id[Person] = Id()) extends Document[Person]

object Person extends Collection[Person]("people", DB) {
override implicit val rw: RW[Person] = RW.gen

val name: IndexedLinks[String, Person] = indexedLinks[String]("names", identity, _.name)
val age: IndexedLinks[Int, Person] = indexedLinks[Int]("age", _.toString, _.age)
}

object InitialSetupUpgrade extends DatabaseUpgrade {
override def applyToNew: Boolean = true
override def blockStartup: Boolean = true
override def alwaysRun: Boolean = false

override def upgrade(ldb: LightDB): IO[Unit] = DB.startTime.set(System.currentTimeMillis()).map(_ => ())
}
}

0 comments on commit f9419da

Please sign in to comment.