From 51f646cb2224a7860a3f458fa0a35f81a57c65eb Mon Sep 17 00:00:00 2001 From: Sofia Faro Date: Thu, 18 Nov 2021 16:40:03 +0000 Subject: [PATCH] Add engine tests for interfaces. (#11773) * Add engine tests for interfaces. Adds engine tests using the new commands. This uncovered a few issues: - getDependencies doesn't work when given an interfaceId as a templateId (such as when exercising an interface via the ledger api). I patched it up so it deals with interface ids. We've already discussed a way to simplify getDependencies that will also avoid this in the future, but I'll leave that to the future. - issue #11703 is confirmed via some tests that don't pass (and are currently disabled) - PackageInterface.lookupTemplateChoice returns inherited choices, when it should only return own choices. At least, the typechecker assumes it doesn't -- this affects #11558 -- as does the command preprocessor. I'll leave the cleanup to a separate PR. changelog_begin changelog_end * scalafmt, add missing file * Move interfaces tests to separate file * scalafmt --- daml-lf/engine/BUILD.bazel | 1 + .../engine/preprocessing/Preprocessor.scala | 10 +- .../daml/lf/engine/InterfacesTest.scala | 333 ++++++++++++++++++ daml-lf/tests/BUILD.bazel | 7 + daml-lf/tests/Interfaces.daml | 37 ++ 5 files changed, 386 insertions(+), 2 deletions(-) create mode 100644 daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/InterfacesTest.scala create mode 100644 daml-lf/tests/Interfaces.daml diff --git a/daml-lf/engine/BUILD.bazel b/daml-lf/engine/BUILD.bazel index b2085e7769d9..f3370b327936 100644 --- a/daml-lf/engine/BUILD.bazel +++ b/daml-lf/engine/BUILD.bazel @@ -48,6 +48,7 @@ da_scala_test_suite( "//daml-lf/tests:AuthTests.dar", "//daml-lf/tests:BasicTests.dar", "//daml-lf/tests:Exceptions.dar", + "//daml-lf/tests:Interfaces.dar", "//daml-lf/tests:MultiKeys.dar", "//daml-lf/tests:Optional.dar", "//daml-lf/tests:ReinterpretTests.dar", diff --git a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/preprocessing/Preprocessor.scala b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/preprocessing/Preprocessor.scala index 8e481c340d11..d03cd4e75f2e 100644 --- a/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/preprocessing/Preprocessor.scala +++ b/daml-lf/engine/src/main/scala/com/digitalasset/daml/lf/engine/preprocessing/Preprocessor.scala @@ -133,13 +133,19 @@ private[engine] final class Preprocessor( case tmplId :: tmplsToProcess if tmplsAlreadySeen0(tmplId) => go(Nil, tmplsToProcess, tyConAlreadySeen0, tmplsAlreadySeen0) case tmplId :: tmplsToProcess => - interface.lookupTemplate(tmplId) match { - case Right(template) => + interface.lookupTemplateOrInterface(tmplId) match { + case Right(Left(template)) => val typs0 = template.choices.map(_._2.argBinder._2).toList val typs1 = if (tyConAlreadySeen0(tmplId)) typs0 else Ast.TTyCon(tmplId) :: typs0 val typs2 = template.key.fold(typs1)(_.typ :: typs1) go(typs2, tmplsToProcess, tyConAlreadySeen0, tmplsAlreadySeen0) + case Right(Right(interface)) => + val typs0 = (interface.fixedChoices.values.map(_.argBinder._2) + ++ interface.methods.values.map(_.returnType)).toList + val typs1 = + if (tyConAlreadySeen0(tmplId)) typs0 else Ast.TTyCon(tmplId) :: typs0 + go(typs1, tmplsToProcess, tyConAlreadySeen0, tmplsAlreadySeen0) case Left(LookupError.MissingPackage(pkgId, context)) => pullPackage(pkgId, context) case Left(error) => diff --git a/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/InterfacesTest.scala b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/InterfacesTest.scala new file mode 100644 index 000000000000..d29a876daf60 --- /dev/null +++ b/daml-lf/engine/src/test/scala/com/digitalasset/daml/lf/engine/InterfacesTest.scala @@ -0,0 +1,333 @@ +// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.daml.lf +package engine + +import java.io.File +import com.daml.lf.archive.UniversalArchiveDecoder +import com.daml.bazeltools.BazelRunfiles +import com.daml.lf.data.Ref._ +import com.daml.lf.data._ +import com.daml.lf.language.Ast._ +import com.daml.lf.transaction.GlobalKeyWithMaintainers +import com.daml.lf.value.Value +import Value._ +import com.daml.lf.command._ +import com.daml.lf.transaction.test.TransactionBuilder.assertAsVersionedContract +import org.scalatest.prop.TableDrivenPropertyChecks +import org.scalatest.EitherValues +import org.scalatest.wordspec.AnyWordSpec +import org.scalatest.matchers.should.Matchers + +import scala.language.implicitConversions + +@SuppressWarnings( + Array( + "org.wartremover.warts.Any", + "org.wartremover.warts.Serializable", + "org.wartremover.warts.Product", + ) +) +class InterfacesTest + extends AnyWordSpec + with Matchers + with TableDrivenPropertyChecks + with EitherValues + with BazelRunfiles { + + import InterfacesTest._ + + private def loadPackage(resource: String): (PackageId, Package, Map[PackageId, Package]) = { + val packages = UniversalArchiveDecoder.assertReadFile(new File(rlocation(resource))) + val (mainPkgId, mainPkg) = packages.main + (mainPkgId, mainPkg, packages.all.toMap) + } + + private[this] val suffixLenientEngine = newEngine() + private[this] val preprocessor = + new preprocessing.Preprocessor( + ConcurrentCompiledPackages(suffixLenientEngine.config.getCompilerConfig) + ) + + "interfaces" should { + val (interfacesPkgId, _, allInterfacesPkgs) = loadPackage("daml-lf/tests/Interfaces.dar") + val lookupPackage = allInterfacesPkgs.get(_) + val idI1 = Identifier(interfacesPkgId, "Interfaces:I1") + val idI2 = Identifier(interfacesPkgId, "Interfaces:I2") + val idT1 = Identifier(interfacesPkgId, "Interfaces:T1") + val idT2 = Identifier(interfacesPkgId, "Interfaces:T2") + val let = Time.Timestamp.now() + val submissionSeed = hash("rollback") + val seeding = Engine.initialSeeding(submissionSeed, participant, let) + val cid1 = toContractId("1") + val cid2 = toContractId("2") + val contracts = Map( + cid1 -> assertAsVersionedContract( + ContractInstance( + idT1, + ValueRecord(None, ImmArray((None, ValueParty(party)))), + "", + ) + ), + cid2 -> assertAsVersionedContract( + ContractInstance( + idT2, + ValueRecord(None, ImmArray((None, ValueParty(party)))), + "", + ) + ), + ) + val lookupContract = contracts.get(_) + def lookupKey = (_: GlobalKeyWithMaintainers) => None + def preprocess(cmd: Command) = { + preprocessor + .preprocessCommand(cmd) + .consume( + lookupContract, + lookupPackage, + lookupKey, + ) + } + def run(cmd: Command) = { + val submitters = Set(party) + val Right(speedyCmd) = preprocess(cmd) + suffixLenientEngine + .interpretCommands( + validating = false, + submitters = submitters, + readAs = Set.empty, + commands = ImmArray(speedyCmd), + ledgerTime = let, + submissionTime = let, + seeding = seeding, + ) + .consume( + lookupContract, + lookupPackage, + lookupKey, + ) + } + + /* create by interface tests */ + "be able to create T1 by interface I1" in { + val command = + CreateByInterfaceCommand(idI1, idT1, ValueRecord(None, ImmArray((None, ValueParty(party))))) + run(command) shouldBe a[Right[_, _]] + } + "be able to create T2 by interface I1" in { + val command = + CreateByInterfaceCommand(idI1, idT2, ValueRecord(None, ImmArray((None, ValueParty(party))))) + run(command) shouldBe a[Right[_, _]] + } + "be able to create T2 by interface I2" in { + val command = + CreateByInterfaceCommand(idI2, idT2, ValueRecord(None, ImmArray((None, ValueParty(party))))) + run(command) shouldBe a[Right[_, _]] + } + "be unable to create T1 by interface I2 (stopped in preprocessor)" in { + val command = + CreateByInterfaceCommand(idI2, idT1, ValueRecord(None, ImmArray((None, ValueParty(party))))) + preprocess(command) shouldBe a[Left[_, _]] + } + + /* generic exercise tests */ + "be able to exercise interface I1 on a T1 contract" in { + val command = ExerciseCommand(idI1, cid1, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be able to exercise interface I1 on a T2 contract" in { + val command = ExerciseCommand(idI1, cid2, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be able to exercise interface I2 on a T2 contract" in { + val command = ExerciseCommand(idI2, cid2, "C2", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be unable to exercise interface I2 on a T1 contract" in { + val command = ExerciseCommand(idI2, cid1, "C2", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Left[_, _]] + } + + "be able to exercise T1 by interface I1" in { + val command = ExerciseCommand(idT1, cid1, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be able to exercise T2 by interface I1" in { + val command = ExerciseCommand(idT2, cid2, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be able to exercise T2 by interface I2" in { + val command = ExerciseCommand(idT2, cid2, "C2", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be unable to exercise T1 by interface I2 (stopped in preprocessor)" in { + val command = ExerciseCommand(idT1, cid1, "C2", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + + // TODO https://github.com/digital-asset/daml/issues/11703 + // Enable these tests. + /* + "be unable to exercise T1 (disguised as T2) by interface I1" in { + val command = ExerciseCommand(idT2, cid1, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Left[_, _]] + } + "be unable to exercise T2 (disguised as T1) by interface I1" in { + val command = ExerciseCommand(idT1, cid2, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Left[_, _]] + } + */ + "be unable to exercise T2 (disguised as T1) by interface I2 (stopped in preprocessor)" in { + val command = ExerciseCommand(idT1, cid2, "C2", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + "be unable to exercise T1 (disguised as T2) by interface I2 " in { + val command = ExerciseCommand(idT2, cid1, "C2", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Left[_, _]] + } + + /* exercise template tests */ + "be unable to exercise interface via exercise template (stopped in preprocessor)" in { + val command = ExerciseTemplateCommand(idI1, cid1, "C1", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + // TODO https://github.com/digital-asset/daml/issues/11558 + // Fix lookupTemplateChoice to not return inherited choices. + /* + "be unable to exercise T1 inherited choice via exercise template (stopped in preprocessor)" in { + val command = ExerciseTemplateCommand(idT1, cid1, "C1", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + */ + "be able to exercise T1 own choice via exercise template" in { + val command = + ExerciseTemplateCommand(idT1, cid1, "OwnChoice", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + + /* exercise by interface tests */ + "be unable to exercise interface via 'exercise by interface' (stopped in preprocessor)" in { + val command = + ExerciseByInterfaceCommand(idI1, idI1, cid1, "C1", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + + "be able to exercise T1 by interface I1 via 'exercise by interface'" in { + val command = + ExerciseByInterfaceCommand(idI1, idT1, cid1, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be able to exercise T2 by interface I1 via 'exercise by interface'" in { + val command = + ExerciseByInterfaceCommand(idI1, idT2, cid2, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be able to exercise T2 by interface I2 via 'exercise by interface'" in { + val command = + ExerciseByInterfaceCommand(idI2, idT2, cid2, "C2", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Right[_, _]] + } + "be unable to exercise T1 by interface I2 (stopped in preprocessor)" in { + val command = + ExerciseByInterfaceCommand(idI2, idT1, cid1, "C2", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + + // TODO https://github.com/digital-asset/daml/issues/11703 + // Enable these tests. + /* + "be unable to exercise T1 (disguised as T2) by interface I1 via 'exercise by interface'" in { + val command = ExerciseByInterfaceCommand(idI2, idT2, cid1, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Left[_, _]] + } + "be unable to exercise T2 (disguised as T1) by interface I1 via 'exercise by interface'" in { + val command = ExerciseByInterfaceCommand(idI1, idT1, cid2, "C1", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Left[_, _]] + } + */ + "be unable to exercise T2 (disguised as T1) by interface I2 via 'exercise by interface' (stopped in preprocessor)" in { + val command = + ExerciseByInterfaceCommand(idI2, idT1, cid2, "C2", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + "be unable to exercise T1 (disguised as T2) by interface I2 via 'exercise by interface'" in { + val command = + ExerciseByInterfaceCommand(idI2, idT2, cid1, "C2", ValueRecord(None, ImmArray.empty)) + run(command) shouldBe a[Left[_, _]] + } + + "be unable to exercise T2 choice by the wrong interface (stopped in preprocessor)" in { + val command = + ExerciseByInterfaceCommand(idI1, idT2, cid2, "C2", ValueRecord(None, ImmArray.empty)) + preprocess(command) shouldBe a[Left[_, _]] + } + + /* fetch by interface tests */ + "be able to fetch T1 by interface I1" in { + val command = FetchByInterfaceCommand(idI1, idT1, cid1) + run(command) shouldBe a[Right[_, _]] + } + "be able to fetch T2 by interface I1" in { + val command = FetchByInterfaceCommand(idI1, idT2, cid2) + run(command) shouldBe a[Right[_, _]] + } + "be able to fetch T2 by interface I2" in { + val command = FetchByInterfaceCommand(idI2, idT2, cid2) + run(command) shouldBe a[Right[_, _]] + } + "be unable to fetch T1 by interface I2 (stopped in preprocessor)" in { + val command = FetchByInterfaceCommand(idI2, idT1, cid1) + preprocess(command) shouldBe a[Left[_, _]] + } + "be unable to fetch T1 (disguised as T2) via interface I2" in { + val command = FetchByInterfaceCommand(idI2, idT2, cid1) + run(command) shouldBe a[Left[_, _]] + } + // TODO https://github.com/digital-asset/daml/issues/11703 + // Enable these tests. + /* + "be unable to fetch T1 (disguised as T2) via interface I1" in { + val command = FetchByInterfaceCommand(idI1, idT2, cid1) + run(command) shouldBe a[Left[_, _]] + } + "be unable to fetch T2 (disguised as T1) via interface I1" in { + val command = FetchByInterfaceCommand(idI1, idT1, cid2) + run(command) shouldBe a[Left[_, _]] + } + */ + "be unable to fetch T2 (disguised as T1) by interface I2 (stopped in preprocessor)" in { + val command = FetchByInterfaceCommand(idI2, idT1, cid2) + preprocess(command) shouldBe a[Left[_, _]] + } + } + +} + +object InterfacesTest { + + private def hash(s: String) = crypto.Hash.hashPrivateKey(s) + private def participant = Ref.ParticipantId.assertFromString("participant") + + private val party = Party.assertFromString("Party") + + private def newEngine(requireCidSuffixes: Boolean = false) = + new Engine( + EngineConfig( + allowedLanguageVersions = language.LanguageVersion.DevVersions, + forbidV0ContractId = true, + requireSuffixedGlobalContractId = requireCidSuffixes, + ) + ) + + private implicit def qualifiedNameStr(s: String): QualifiedName = + QualifiedName.assertFromString(s) + + private implicit def toName(s: String): Name = + Name.assertFromString(s) + + private val dummySuffix = Bytes.assertFromString("00") + + private def toContractId(s: String): ContractId = + ContractId.V1.assertBuild(crypto.Hash.hashPrivateKey(s), dummySuffix) +} diff --git a/daml-lf/tests/BUILD.bazel b/daml-lf/tests/BUILD.bazel index 0377e1a551d7..d39188c12a1e 100644 --- a/daml-lf/tests/BUILD.bazel +++ b/daml-lf/tests/BUILD.bazel @@ -43,6 +43,13 @@ daml_compile( visibility = ["//daml-lf:__subpackages__"], ) +daml_compile( + name = "Interfaces", + srcs = ["Interfaces.daml"], + target = "1.dev", + visibility = ["//daml-lf:__subpackages__"], +) + daml_build_test( name = "ReinterpretTests", dar_dict = { diff --git a/daml-lf/tests/Interfaces.daml b/daml-lf/tests/Interfaces.daml new file mode 100644 index 000000000000..7557a66b5b72 --- /dev/null +++ b/daml-lf/tests/Interfaces.daml @@ -0,0 +1,37 @@ +-- Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +-- SPDX-License-Identifier: Apache-2.0 + +module Interfaces where + +interface I1 where + getOwner1 : Party + choice C1 : () + controller getOwner1 this + do pure () + +interface I2 where + getOwner2 : Party + choice C2 : () + controller getOwner2 this + do pure () + +template T1 + with + owner1 : Party + where + signatory owner1 + implements I1 where + let getOwner1 = owner1 + choice OwnChoice : () + controller owner1 + do pure () + +template T2 + with + owner2 : Party + where + signatory owner2 + implements I1 where + let getOwner1 = owner2 + implements I2 where + let getOwner2 = owner2