diff --git a/ledger/participant-integration-api/src/test/suite/scala/platform/store/interning/RawStringInterningSpec.scala b/ledger/participant-integration-api/src/test/suite/scala/platform/store/interning/RawStringInterningSpec.scala new file mode 100644 index 000000000000..8c57cfbaa3f7 --- /dev/null +++ b/ledger/participant-integration-api/src/test/suite/scala/platform/store/interning/RawStringInterningSpec.scala @@ -0,0 +1,93 @@ +// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.platform.store.interning + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class RawStringInterningSpec extends AnyFlatSpec with Matchers { + + behavior of "RawStringInterning.from" + + it should "start empty" in { + val current = RawStringInterning.from(Nil) + current.map shouldBe empty + current.idMap shouldBe empty + current.lastId shouldBe 0 + } + + it should "append empty entries to existing cache" in { + val previous = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) + val current = RawStringInterning.from(Nil, previous) + current.map shouldBe previous.map + current.idMap shouldBe previous.idMap + current.lastId shouldBe previous.lastId + } + + it should "append non-empty entries to empty cache" in { + val current = RawStringInterning.from(List(1 -> "one")) + current.map shouldBe Map("one" -> 1) + current.idMap shouldBe Map(1 -> "one") + current.lastId shouldBe 1 + } + + it should "append non-empty entries to non-empty cache" in { + val previous = RawStringInterning( + Map("one" -> 1, "two" -> 2), + Map(1 -> "one", 2 -> "two"), + 2, + ) + val current = RawStringInterning.from(List(3 -> "three"), previous) + current.map shouldBe Map("one" -> 1, "two" -> 2, "three" -> 3) + current.idMap shouldBe Map(1 -> "one", 2 -> "two", 3 -> "three") + current.lastId shouldBe 3 + } + + behavior of "RawStringInterning.newEntries" + + it should "return an empty result if the input and previous state is empty" in { + val current = RawStringInterning.from(Nil) + val newEntries = RawStringInterning.newEntries(Iterator.empty, current) + newEntries shouldBe empty + } + + it should "return an empty result if the input is empty" in { + val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) + val newEntries = RawStringInterning.newEntries(Iterator.empty, current) + newEntries shouldBe empty + } + + it should "return an empty result if the input only contains duplicates" in { + val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) + val newEntries = RawStringInterning.newEntries(List("one").iterator, current) + newEntries shouldBe empty + } + + it should "return a new entry if the input is an unknown string" in { + val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) + val newEntries = RawStringInterning.newEntries(List("two").iterator, current) + newEntries shouldBe Vector(2 -> "two") + } + + it should "not return a new entry for known strings" in { + val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) + val newEntries = RawStringInterning.newEntries(List("one", "two").iterator, current) + newEntries shouldBe Vector(2 -> "two") + } + + it should "handle duplicate unknown strings" in { + val current = RawStringInterning(Map("one" -> 1), Map(1 -> "one"), 1) + val newEntries = RawStringInterning.newEntries(List("two", "two", "two").iterator, current) + newEntries shouldBe Vector(2 -> "two") + } + + it should "handle mixed input" in { + val current = RawStringInterning(Map("one" -> 1, "two" -> 2), Map(1 -> "one", 2 -> "two"), 2) + val newEntries = RawStringInterning.newEntries( + List("one", "three", "two", "four", "two", "four").iterator, + current, + ) + newEntries shouldBe Vector(3 -> "three", 4 -> "four") + } +} diff --git a/ledger/participant-integration-api/src/test/suite/scala/platform/store/interning/StringInterningDomainSpec.scala b/ledger/participant-integration-api/src/test/suite/scala/platform/store/interning/StringInterningDomainSpec.scala new file mode 100644 index 000000000000..f6a4e265f2b7 --- /dev/null +++ b/ledger/participant-integration-api/src/test/suite/scala/platform/store/interning/StringInterningDomainSpec.scala @@ -0,0 +1,95 @@ +// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.platform.store.interning + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class StringInterningDomainSpec extends AnyFlatSpec with Matchers { + + behavior of "StringInterningDomain.prefixing" + + case class StringBox(value: String) + object StringBox { + def from(raw: String): StringBox = StringBox(raw) + def to(boxed: StringBox): String = boxed.value + } + + class StaticStringInterningAccessor( + idToString: Map[Int, String], + stringToId: Map[String, Int], + ) extends StringInterningAccessor[String] { + override def internalize(t: String): Int = tryInternalize(t).get + override def tryInternalize(t: String): Option[Int] = stringToId.get(t) + override def externalize(id: Int): String = tryExternalize(id).get + override def tryExternalize(id: Int): Option[String] = idToString.get(id) + } + + object StaticStringInterningAccessor { + def apply(entries: Seq[(Int, String)]): StaticStringInterningAccessor = { + new StaticStringInterningAccessor( + idToString = entries.toMap, + stringToId = entries.map(_.swap).toMap, + ) + } + } + + it should "handle a known string " in { + val accessor = StaticStringInterningAccessor(List(1 -> ".one", 2 -> ".two")) + val domain = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) + + domain.tryExternalize(2) shouldBe Some(StringBox("two")) + domain.externalize(2) shouldBe StringBox("two") + domain.tryInternalize(StringBox("two")) shouldBe Some(2) + domain.internalize(StringBox("two")) shouldBe 2 + + domain.unsafe.tryExternalize(2) shouldBe Some("two") + domain.unsafe.externalize(2) shouldBe "two" + domain.unsafe.tryInternalize("two") shouldBe Some(2) + domain.unsafe.internalize("two") shouldBe 2 + } + + it should "handle an unknown string" in { + val accessor = StaticStringInterningAccessor(List(1 -> ".one", 2 -> ".two")) + val domain = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) + + domain.tryExternalize(3) shouldBe empty + domain.tryInternalize(StringBox("three")) shouldBe empty + + domain.unsafe.tryExternalize(3) shouldBe empty + domain.unsafe.tryInternalize("three") shouldBe empty + } + + it should "work when two different domains share an accessor" in { + val accessor = StaticStringInterningAccessor(List(1 -> "aX", 2 -> "bX")) + val domainA = StringInterningDomain.prefixing("a", accessor, StringBox.from, StringBox.to) + val domainB = StringInterningDomain.prefixing("b", accessor, StringBox.from, StringBox.to) + + domainA.internalize(StringBox("X")) shouldBe 1 + domainB.internalize(StringBox("X")) shouldBe 2 + domainA.externalize(1) shouldBe StringBox("X") + domainB.externalize(2) shouldBe StringBox("X") + + domainA.unsafe.internalize("X") shouldBe 1 + domainB.unsafe.internalize("X") shouldBe 2 + domainA.unsafe.externalize(1) shouldBe "X" + domainB.unsafe.externalize(2) shouldBe "X" + } + + it should "work when two identical domains share an accessor" in { + val accessor = StaticStringInterningAccessor(List(1 -> ".one", 2 -> ".two")) + val domainA = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) + val domainB = StringInterningDomain.prefixing(".", accessor, StringBox.from, StringBox.to) + + domainA.internalize(StringBox("one")) shouldBe 1 + domainB.internalize(StringBox("one")) shouldBe 1 + domainA.externalize(2) shouldBe StringBox("two") + domainB.externalize(2) shouldBe StringBox("two") + + domainA.unsafe.internalize("one") shouldBe 1 + domainB.unsafe.internalize("one") shouldBe 1 + domainA.unsafe.externalize(2) shouldBe "two" + domainB.unsafe.externalize(2) shouldBe "two" + } +}