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
Conversation
6dca6f3
to
0f3fe38
Compare
casper/src/test/scala/coop/rchain/casper/blocks/proposer/BlockReceiverSpec.scala
Outdated
Show resolved
Hide resolved
casper/src/test/scala/coop/rchain/casper/blocks/proposer/BlockReceiverSpec.scala
Outdated
Show resolved
Hide resolved
casper/src/test/scala/coop/rchain/casper/blocks/proposer/BlockReceiverSpec.scala
Outdated
Show resolved
Hide resolved
60255d3
to
aa0a042
Compare
d01b03c
to
be26576
Compare
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverSpec.scala
Outdated
Show resolved
Hide resolved
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverSpec.scala
Outdated
Show resolved
Hide resolved
bs.contains(*) wasCalled once | ||
br.ackReceived(*) wasCalled once | ||
bds.getRepresentation wasCalled twice | ||
outList.length shouldBe 1 |
There was a problem hiding this comment.
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.
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverSpec.scala
Outdated
Show resolved
Hide resolved
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverSpec.scala
Outdated
Show resolved
Hide resolved
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverEffectsSpec.scala
Outdated
Show resolved
Hide resolved
There is no shared state between test cases. |
project/CompilerSettings.scala
Outdated
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverEffectsSpec.scala
Outdated
Show resolved
Hide resolved
implicit val (bsImp, brImp, bdsImp) = (bs, br, bds) | ||
BlockReceiver(state, incomingBlockStream, validatedBlocksStream, shardId) | ||
} | ||
res <- f(incomingBlockQueue, validatedBlocksQueue, blockReceiver, bs, br, bds) |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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.
project/CompilerSettings.scala
Outdated
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" |
There was a problem hiding this comment.
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.
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverEffectsSpec.scala
Outdated
Show resolved
Hide resolved
} | ||
} | ||
|
||
it should "pass to output blocks with resolved dependencies" in withEnv[Task]("root") { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ???
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 🙂
casper/src/test/scala/coop/rchain/casper/batch2/BlockReceiverEffectsSpec.scala
Show resolved
Hide resolved
a3507fa
to
3d2ba33
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work! 🎉
} 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))) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private def addBlock[F[_]: Sync](bs: BlockStore[F]): F[BlockMessage] = | ||
for { | ||
block <- Sync[F].delay(makeBlock()) | ||
_ <- bs.put(Seq((block.blockHash, block))) | ||
} yield block |
There was a problem hiding this comment.
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)
}
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
bors merge |
Build succeeded: |
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.