This is a tracking issue for individual refactorings of unit tests.
Rationale: Primitive types like CTxOut currently provide read and write access to their data members like scriptPubKey. Consensus validation does not require write access. Hence, it is forced to depend on functionality that it does not use, which is a violation of the Interface Segregation Principle (ISP). A whole category of bugs could be eliminated if this functionality was removed.
One case where write access to those data members is extensively used, is the imperative building of test data in unit tests like this:
The imperative building could be replaced with declarative construction:
const CTransaction tx_1{
std::array{
CTxIn{COutPoint{dummyTransactions[0].GetHash(), 1}, CScript{} << std::vector<unsigned char>(65, 0)},
CTxIn{COutPoint{dummyTransactions[1].GetHash(), 0}, CScript{} << std::vector<unsigned char>(65, 0) << std::vector<unsigned char>(33, 4)},
CTxIn{COutPoint{dummyTransactions[1].GetHash(), 1}, CScript{} << std::vector<unsigned char>(65, 0) << std::vector<unsigned char>(33, 4)},
},
std::array{
CTxOut{90 * COIN, CScript{} << OP_1},
CTxOut{},
},
};
This example shows several advantages:
- It reduces the amount of necessary boilerplate.
- It is necessarily honest: The fact that the outputs contain an empty element is plain to see rather than hidden in a
resize(2)call. - It removes the need for a dedicated "builder" type, like
CMutableTransactionin this case.
Another case where write access is currently used, is the reuse of test data, like in this example:
The test data txFrom is constructed in line 1287 and later modified in lines 1315 and 1331, through the scriptPubKey reference defined in line 1289. Notably, each block of code that uses a different state of the test data is introduced by a comment containing the word "case" ("Single signature case", "P2SH, single-signature case", "Hardest case: Multisig 2-of-3").
Separating the individual "cases" into actually independent test cases would remove the need for write access in addition to providing additional advantages:
- Easier to reason about: The test input data is visible in a single place. It does not depend on modifications elsewhere or side effects of other tests.
- Easier to parallelize: Individual test cases can run concurrently.
Refactoring the imperative code in unit tests is a precondition for solving the ISP issues of the primitive types. The two examples above show that such a refactoring is self-contained (it only affects test code, and in most cases only individual tests, so it can be done one test at a time). It also shows that it provides benefits for each individual test on its own.