Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unit tests for BlockReceiver effects #3741

Merged
merged 33 commits into from Jul 14, 2022

Conversation

stanislavlyalin
Copy link
Collaborator

@stanislavlyalin stanislavlyalin commented Jun 16, 2022

Overview

Unit-tests for BlockReceiver. Continuation of PR #3738, but for effects tests.

Since the interaction with the BlockReceiver performs via Streams/Queues, the tests checks if the desired blocks get into the output stream.

Most tests check that a block is dropped for various reasons: invalid hash or block signature or shard name, etc.

The last test checks the full logic of the BlockReceiver.

Please make sure that this PR:

Bors cheat-sheet:

  • bors r+ runs integration tests and merges the PR (if it's approved),
  • bors try runs integration tests for the PR,
  • bors delegate+ enables non-maintainer PR authors to run the above.

@stanislavlyalin stanislavlyalin self-assigned this Jun 16, 2022
@stanislavlyalin stanislavlyalin added this to In progress in Core team via automation Jun 16, 2022
@stanislavlyalin stanislavlyalin linked an issue Jun 16, 2022 that may be closed by this pull request
2 tasks
@stanislavlyalin stanislavlyalin force-pushed the block-receiver-tests branch 2 times, most recently from 6dca6f3 to 0f3fe38 Compare June 22, 2022 14:04
@stanislavlyalin stanislavlyalin marked this pull request as ready for review June 22, 2022 14:24
@stanislavlyalin stanislavlyalin moved this from In progress to Review in progress in Core team Jun 22, 2022
@stanislavlyalin stanislavlyalin force-pushed the block-receiver-tests branch 2 times, most recently from 60255d3 to aa0a042 Compare July 5, 2022 07:31
@tgrospic tgrospic changed the title Unit-tests for BlockReceiver Unit tests for BlockReceiver effects Jul 8, 2022
bs.contains(*) wasCalled once
br.ackReceived(*) wasCalled once
bds.getRepresentation wasCalled twice
outList.length shouldBe 1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title of the test says that correct block is passed to output stream and here it's only checking the length.

Together with that, we are interested in checking calls to effectful components so better to emphasize that in the title of the test.

project/CompilerSettings.scala Show resolved Hide resolved
@tgrospic
Copy link
Collaborator

tgrospic commented Jul 8, 2022

@tgrospic, I have done changes related to you review. In some cases the last test fails with error message

[info] - should pass to output blocks with resolved dependencies *** FAILED *** (159 milliseconds)
[info]   java.lang.NullPointerException:
[info]   at flatMap @ coop.rchain.casper.blocks.BlockReceiver$.$anonfun$apply$22(BlockReceiver.scala:255)
[info]   at flatMap @ coop.rchain.casper.blocks.BlockReceiver$.$anonfun$apply$20(BlockReceiver.scala:252)
[info]   at map @ coop.rchain.casper.batch2.BlockReceiverEffectsSpec.$anonfun$blockStoreMock$1(BlockReceiverEffectsSpec.scala:167)
[info]   at map @ coop.rchain.store.KeyValueTypedStoreOps$.contains$extension(KeyValueTypedStoreSyntax.scala:44)
[info]   at map @ coop.rchain.catscontrib.BooleanF$.$tilde$up(BooleanF.scala:24)
[info]   at map @ coop.rchain.casper.blocks.BlockReceiver$.$anonfun$apply$18(BlockReceiver.scala:250)
[info]   at flatTraverse @ fs2.internal.CompileScope.$anonfun$lease$1(CompileScope.scala:346)
[info]   at flatTraverse @ fs2.internal.CompileScope.$anonfun$lease$1(CompileScope.scala:346)
[info]   at traverse @ coop.rchain.casper.blocks.BlockReceiver$.$anonfun$apply$17(BlockReceiver.scala:249)
[info]   at flatMap @ coop.rchain.casper.blocks.BlockReceiver$.$anonfun$apply$17(BlockReceiver.scala:248)
[info]   ...

Can it be related to using shared state outside of F-context?

val state = Ref.unsafe[F, Map[BlockHash, BlockMessage]](Map())
val bsMock = mock[BlockStore[F]]
bsMock.contains(*) answers { keys: Seq[BlockHash] =>
state.get.map(s => Seq(s.contains(keys.head)))
}
bsMock.put(*) answers { kvPairs: Seq[(BlockHash, BlockMessage)] =>
state.update(s => kvPairs.foldLeft(s) { case (acc, item) => acc + item })
}

or to concurrent code in BlockReceiver?

There is no shared state between test cases. withEnv instantiate new state for every call.

scalacOptions in (Test, console) := (scalacOptions in (Compile, console)).value,
// To prevent "dead code following this construct" error when using * mockito-scala library
// https://github.com/mockito/mockito-scala/#dead-code-warning
scalacOptions in Test -= "-Ywarn-dead-code"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot compile the project in IntelliJ because of dead code error in tests.

I found this old bug which would explain the behavior. I noticed that scalaOptions set for Compile and Test configuration are loaded differently in IntelliJ.
https://youtrack.jetbrains.com/issue/SCL-11824

I'll try to load separately options for Compile and Test configuration without using -= which doesn't have effect in IntelliJ.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find a workaround, it seems that IntelliJ cannot use separate configuration for Test scope. This is another related issue I found.
https://youtrack.jetbrains.com/issue/IDEA-232043

@stanislavlyalin Let's disable dead code detection completely, this seems the only solution for now. Please add this additional information in the comments also.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I returned initial version of my code and added some related info.

implicit val (bsImp, brImp, bdsImp) = (bs, br, bds)
BlockReceiver(state, incomingBlockStream, validatedBlocksStream, shardId)
}
res <- f(incomingBlockQueue, validatedBlocksQueue, blockReceiver, bs, br, bds)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing mocks to f directly is not beautiful solution but I cannot figure out how to declare that f should take implicit parameters 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is completely fine. Implicit parameters cannot be specified function parameter.

scalacOptions in (Test, console) := (scalacOptions in (Compile, console)).value,
// To prevent "dead code following this construct" error when using * mockito-scala library
// https://github.com/mockito/mockito-scala/#dead-code-warning
scalacOptions in Test -= "-Ywarn-dead-code"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I returned initial version of my code and added some related info.

}
}

it should "pass to output blocks with resolved dependencies" in withEnv[Task]("root") {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is problem with the test as we can see in log.
When running locally, the test passes in most cases, but fails when run on github.
I have been debugging it for 2 days, but have not yet reached the results, since the bug does not appear regularly. To detect it, I wrote a loop in the shell to run BlockReceiver tests, but sbt is initialized every time it runs, which is very long.

// The arrows show the lines of code that the log refers to.
...
for {
  // Save block to block store, resolve parents to request
  _ <- BlockStore[F].put(block)
  parents <- block.justifications  // <-
              .traverse { hash =>
                BlockStore[F].contains(hash).not.map((hash, _))  // <-
              }
  pendingRequests <- state.modify(_.endStored(block.blockHash, parents))  // <-

  // Notify BlockRetriever of finished validation of block
  _ <- BlockRetriever[F].ackReceived(block.blockHash)  // <- 

  // Send request for missing dependencies
  _ <- requestMissingDependencies(pendingRequests)

  // Check if block have all dependencies in the DAG
  dag <- BlockDagStorage[F].getRepresentation
  hasAllDeps = pendingRequests.isEmpty &&
    block.justifications.forall(dag.contains)

  _ <- receiverOutputQueue.enqueue1(block.blockHash).whenA(hasAllDeps)
} yield ()
...

Judging by the fact that the problem does not appear regularly, it is related to concurrent code execution, and the NullPointerException error says that some object we are accessing has been destroyed.
Judging by the log, either the block or the mocks are destroyed. I can't say for sure yet.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this can be a big issue. Did you find similar cases already reported with Mockito library?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem could something similar as described here.
https://www.javafixing.com/2022/03/fixed-mockito-nullpointerexception-when.html

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this can be a big issue. Did you find similar cases already reported with Mockito library?

No, I never thought it can be error in Mockito library.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I resolved the problem. The problem was that the admitHash method was not implemented, although it was called in some scenarios. The Mockito library did not indicate this, but only threw a NullPointerException. I was able to spot the problem by manually implementing mocks and noticing that a NotImplementedError was fired on the admitHash method that had an implementation of ???.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome catch! It would be nice that Mockito can tell which method is called in that case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgrospic, I was happy to find that this is not a bug in Mockito as the library is very handy. It allows you to write less manual code and contains a large set of validation methods that read like natural language 🙂

Copy link
Collaborator

@tgrospic tgrospic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work! 🎉

@tgrospic tgrospic assigned nzpr and unassigned stanislavlyalin Jul 12, 2022
} yield {
val bsPutCaptor = ArgCaptor[Seq[(BlockHash, BlockMessage)]]
bs.put(bsPutCaptor) wasCalled twice
bsPutCaptor.values should contain allOf (Seq((a1.blockHash, a1)), Seq((a2.blockHash, a2)))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be shortened in two lines?

bs.put(Seq((a1.blockHash, a1))) wasCalled once
bs.put(Seq((a2.blockHash, a2))) wasCalled once

Although, I'd like to leave at least one use of ArgCaptor as an example.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tgrospic, yes, it can be. Nice idea!

Comment on lines 230 to 234
private def addBlock[F[_]: Sync](bs: BlockStore[F]): F[BlockMessage] =
for {
block <- Sync[F].delay(makeBlock())
_ <- bs.put(Seq((block.blockHash, block)))
} yield block
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a comment - this can be done shorter

private def addBlock[F[_]: Sync](bs: BlockStore[F]): F[BlockMessage] = {
   val block = makeBlock()
   bs.put(Seq((block.blockHash, block))).as(block)
 }

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nzpr Is this function now referentially transparent?

Let's imagine that makeBlock() is heavy operation and possibly do some logging to make this example more extreme.

What would be executed when this assignment is created in the old and new version of addBlock function?

val makeBlockInStore = addBlock(blockStore)

Copy link
Collaborator

@nzpr nzpr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

@stanislavlyalin
Copy link
Collaborator Author

bors merge

@bors
Copy link
Contributor

bors bot commented Jul 14, 2022

Build succeeded:

@bors bors bot merged commit b293b98 into rchain:dev Jul 14, 2022
Core team automation moved this from Review in progress to Done Jul 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development

Successfully merging this pull request may close these issues.

Tests for BlockReceiver
3 participants