diff --git a/scripts/contracts-migration/coverage-baseline.json b/scripts/contracts-migration/coverage-baseline.json index c8e3de6dd..240212f17 100644 --- a/scripts/contracts-migration/coverage-baseline.json +++ b/scripts/contracts-migration/coverage-baseline.json @@ -2,261 +2,734 @@ "contracts/base/Multicall.sol": { "linesFound": 8, "linesHit": 6, - "pct": 75 + "pct": 75, + "functionsHit": ["multicall"] }, "contracts/curation/Curation.sol": { "linesFound": 86, "linesHit": 86, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_setCurationTaxPercentage", + "_setCurationTokenMaster", + "_setDefaultReserveRatio", + "_setMinimumCurationDeposit", + "_tokensToSignal", + "_updateRewards", + "burn", + "collect", + "getCurationPoolSignal", + "getCurationPoolTokens", + "getCuratorSignal", + "initialize", + "isCurated", + "mint", + "setCurationTaxPercentage", + "setCurationTokenMaster", + "setDefaultReserveRatio", + "setMinimumCurationDeposit", + "signalToTokens", + "tokensToSignal" + ] }, "contracts/curation/CurationStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/curation/GraphCurationToken.sol": { "linesFound": 4, "linesHit": 4, - "pct": 100 + "pct": 100, + "functionsHit": ["burnFrom", "initialize", "mint"] }, "contracts/discovery/GNS.sol": { "linesFound": 159, "linesHit": 109, - "pct": 68.55 + "pct": 68.55, + "functionsHit": [ + "_buildSubgraphID", + "_burnNFT", + "_chargeOwnerTax", + "_getSubgraphOrRevert", + "_isPublished", + "_mintNFT", + "_nextAccountSeqID", + "_nextSubgraphID", + "_setOwnerTaxPercentage", + "_setSubgraphMetadata", + "_setSubgraphNFT", + "approveAll", + "burnSignal", + "deprecateSubgraph", + "getCuratorSignal", + "initialize", + "mintSignal", + "nSignalToTokens", + "nSignalToVSignal", + "onlySubgraphAuth", + "ownerOf", + "publishNewSubgraph", + "publishNewVersion", + "setCounterpartGNSAddress", + "setOwnerTaxPercentage", + "tokensToNSignal", + "vSignalToNSignal" + ] }, "contracts/discovery/GNSStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/discovery/L1GNS.sol": { "linesFound": 40, "linesHit": 0, - "pct": 0 + "pct": 0, + "functionsHit": [] }, "contracts/discovery/L1GNSStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/discovery/ServiceRegistry.sol": { "linesFound": 15, "linesHit": 15, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_isAuth", + "_register", + "_unregister", + "initialize", + "isRegistered", + "register", + "registerFor", + "unregister", + "unregisterFor" + ] }, "contracts/discovery/ServiceRegistryStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/discovery/SubgraphNFT.sol": { "linesFound": 27, "linesHit": 15, - "pct": 55.56 + "pct": 55.56, + "functionsHit": [ + "_setMinter", + "_setTokenDescriptor", + "burn", + "constructor", + "mint", + "onlyMinter", + "setMinter", + "setSubgraphMetadata", + "setTokenDescriptor" + ] }, "contracts/discovery/SubgraphNFTDescriptor.sol": { "linesFound": 4, "linesHit": 0, - "pct": 0 + "pct": 0, + "functionsHit": [] }, "contracts/discovery/erc1056/EthereumDIDRegistry.sol": { "linesFound": 40, "linesHit": 0, - "pct": 0 + "pct": 0, + "functionsHit": [] }, "contracts/disputes/DisputeManager.sol": { "linesFound": 124, "linesHit": 123, - "pct": 99.19 + "pct": 99.19, + "functionsHit": [ + "_createIndexingDisputeWithAllocation", + "_createQueryDisputeWithAttestation", + "_drawDisputeInConflict", + "_getChainID", + "_getSlashingPercentageForDisputeType", + "_isDisputeInConflict", + "_onlyArbitrator", + "_parseAttestation", + "_pullSubmitterDeposit", + "_recoverAttestationSigner", + "_setArbitrator", + "_setFishermanRewardPercentage", + "_setMinimumDeposit", + "_setSlashingPercentage", + "_slashIndexer", + "_toBytes32", + "_toUint8", + "acceptDispute", + "areConflictingAttestations", + "createIndexingDispute", + "createQueryDispute", + "createQueryDisputeConflict", + "drawDispute", + "encodeHashReceipt", + "getAttestationIndexer", + "initialize", + "isDisputeCreated", + "onlyArbitrator", + "onlyPendingDispute", + "rejectDispute", + "setArbitrator", + "setFishermanRewardPercentage", + "setMinimumDeposit", + "setSlashingPercentage" + ] }, "contracts/disputes/DisputeManagerStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/epochs/EpochManager.sol": { "linesFound": 27, "linesHit": 17, - "pct": 62.96 + "pct": 62.96, + "functionsHit": [ + "blockNum", + "currentEpoch", + "currentEpochBlock", + "currentEpochBlockSinceStart", + "epochsSinceUpdate", + "initialize", + "setEpochLength" + ] }, "contracts/epochs/EpochManagerStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/gateway/BridgeEscrow.sol": { "linesFound": 3, "linesHit": 3, - "pct": 100 + "pct": 100, + "functionsHit": ["approveAll", "initialize", "revokeAll"] }, "contracts/gateway/GraphTokenGateway.sol": { "linesFound": 9, "linesHit": 9, - "pct": 100 + "pct": 100, + "functionsHit": ["_notPaused", "onlyGovernorOrGuardian", "paused", "setPauseGuardian", "setPaused"] }, "contracts/gateway/L1GraphTokenGateway.sol": { "linesFound": 102, "linesHit": 102, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_checksBeforeUnpause", + "_l2MintAmountAllowed", + "_mintFromL2", + "_parseOutboundData", + "accumulatedL2MintAllowanceAtBlock", + "addToCallhookAllowlist", + "calculateL2TokenAddress", + "counterpartGateway", + "finalizeInboundTransfer", + "getOutboundCalldata", + "initialize", + "onlyL2Counterpart", + "outboundTransfer", + "removeFromCallhookAllowlist", + "setArbitrumAddresses", + "setEscrowAddress", + "setL2CounterpartAddress", + "setL2MintAllowanceParametersManual", + "setL2TokenAddress", + "updateL2MintAllowance" + ] }, "contracts/governance/Controller.sol": { "linesFound": 19, "linesHit": 19, - "pct": 100 + "pct": 100, + "functionsHit": [ + "constructor", + "getContractProxy", + "getGovernor", + "onlyGovernorOrGuardian", + "partialPaused", + "paused", + "setContractProxy", + "setPartialPaused", + "setPauseGuardian", + "setPaused", + "unsetContractProxy", + "updateController" + ] }, "contracts/governance/Governed.sol": { "linesFound": 14, "linesHit": 14, - "pct": 100 + "pct": 100, + "functionsHit": ["_initialize", "acceptOwnership", "onlyGovernor", "transferOwnership"] }, "contracts/governance/Managed.sol": { "linesFound": 40, "linesHit": 40, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_initialize", + "_notPartialPaused", + "_notPaused", + "_onlyController", + "_onlyGovernor", + "_resolveContract", + "_setController", + "_syncContract", + "curation", + "epochManager", + "gns", + "graphToken", + "graphTokenGateway", + "notPartialPaused", + "notPaused", + "onlyController", + "onlyGovernor", + "rewardsManager", + "setController", + "staking", + "syncAllContracts" + ] }, "contracts/governance/Pausable.sol": { "linesFound": 15, "linesHit": 13, - "pct": 86.67 + "pct": 86.67, + "functionsHit": ["_setPartialPaused", "_setPauseGuardian", "_setPaused"] }, "contracts/l2/curation/L2Curation.sol": { "linesFound": 107, "linesHit": 107, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_setCurationTaxPercentage", + "_setCurationTokenMaster", + "_setMinimumCurationDeposit", + "_tokensToSignal", + "_updateRewards", + "burn", + "collect", + "getCurationPoolSignal", + "getCurationPoolTokens", + "getCuratorSignal", + "initialize", + "isCurated", + "mint", + "mintTaxFree", + "onlyGNS", + "setCurationTaxPercentage", + "setCurationTokenMaster", + "setDefaultReserveRatio", + "setMinimumCurationDeposit", + "setSubgraphService", + "signalToTokens", + "tokensToSignal", + "tokensToSignalNoTax", + "tokensToSignalToTokensNoTax" + ] }, "contracts/l2/discovery/L2GNS.sol": { "linesFound": 81, "linesHit": 77, - "pct": 95.06 + "pct": 95.06, + "functionsHit": [ + "_getSubgraphData", + "_mintSignalFromL1", + "_receiveSubgraphFromL1", + "finishSubgraphTransferFromL1", + "getAliasedL2SubgraphID", + "onTokenTransfer", + "onlyL2Gateway", + "publishNewVersion" + ] }, "contracts/l2/discovery/L2GNSStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/l2/gateway/L2GraphTokenGateway.sol": { "linesFound": 46, "linesHit": 41, - "pct": 89.13 + "pct": 89.13, + "functionsHit": [ + "_checksBeforeUnpause", + "_parseOutboundData", + "calculateL2TokenAddress", + "finalizeInboundTransfer", + "initialize", + "onlyL1Counterpart", + "outboundTransfer", + "setL1CounterpartAddress", + "setL1TokenAddress", + "setL2Router" + ] }, "contracts/l2/staking/L2Staking.sol": { "linesFound": 26, "linesHit": 26, - "pct": 100 + "pct": 100, + "functionsHit": ["_receiveDelegation", "_receiveIndexerStake", "onTokenTransfer", "onlyL2Gateway"] }, "contracts/l2/token/GraphTokenUpgradeable.sol": { "linesFound": 28, "linesHit": 28, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_addMinter", + "_getChainID", + "_initialize", + "_removeMinter", + "addMinter", + "isMinter", + "mint", + "onlyMinter", + "permit", + "removeMinter", + "renounceMinter" + ] }, "contracts/l2/token/L2GraphToken.sol": { "linesFound": 14, "linesHit": 14, - "pct": 100 + "pct": 100, + "functionsHit": ["bridgeBurn", "bridgeMint", "initialize", "onlyGateway", "setGateway", "setL1Address"] }, "contracts/libraries/Base58Encoder.sol": { "linesFound": 27, "linesHit": 0, - "pct": 0 + "pct": 0, + "functionsHit": [] }, "contracts/libraries/HexStrings.sol": { "linesFound": 16, "linesHit": 0, - "pct": 0 + "pct": 0, + "functionsHit": [] }, "contracts/payments/AllocationExchange.sol": { "linesFound": 27, "linesHit": 25, - "pct": 92.59 + "pct": 92.59, + "functionsHit": ["_redeem", "_setAuthority", "approveAll", "constructor", "redeem", "setAuthority", "withdraw"] }, "contracts/rewards/RewardsManager.sol": { "linesFound": 165, "linesHit": 165, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_calcAllocationRewards", + "_calcRewards", + "_deniedRewards", + "_getNewRewardsPerSignal", + "_getSubgraphAllocatedTokens", + "_getSubgraphRewardsState", + "_reclaimRewards", + "_setDenied", + "_setIssuancePerBlock", + "_updateSubgraphRewards", + "beforeIssuanceAllocationChange", + "calcRewards", + "getAccRewardsForSubgraph", + "getAccRewardsPerAllocatedToken", + "getAccRewardsPerSignal", + "getAllocatedIssuancePerBlock", + "getDefaultReclaimAddress", + "getIssuanceAllocator", + "getNewRewardsPerSignal", + "getProviderEligibilityOracle", + "getRawIssuancePerBlock", + "getReclaimAddress", + "getRevertOnIneligible", + "getRewards", + "initialize", + "isDenied", + "onSubgraphAllocationUpdate", + "onSubgraphSignalUpdate", + "onlySubgraphAvailabilityOracle", + "reclaimRewards", + "setDefaultReclaimAddress", + "setDenied", + "setIssuanceAllocator", + "setIssuancePerBlock", + "setMinimumSubgraphSignal", + "setProviderEligibilityOracle", + "setReclaimAddress", + "setRevertOnIneligible", + "setSubgraphAvailabilityOracle", + "setSubgraphService", + "supportsInterface", + "takeRewards", + "updateAccRewardsPerSignal" + ] }, "contracts/rewards/RewardsManagerStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/rewards/SubgraphAvailabilityManager.sol": { "linesFound": 46, "linesHit": 13, - "pct": 28.26 + "pct": 28.26, + "functionsHit": ["constructor"] }, "contracts/staking/L1Staking.sol": { "linesFound": 69, "linesHit": 69, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_sendTokensAndMessageToL2Staking", + "_transferDelegationToL2", + "_transferStakeToL2", + "setL1GraphTokenLockTransferTool", + "transferDelegationToL2", + "transferLockedDelegationToL2", + "transferLockedStakeToL2", + "transferStakeToL2", + "unlockDelegationToTransferredIndexer" + ] }, "contracts/staking/L1StakingStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/staking/Staking.sol": { "linesFound": 220, "linesHit": 215, - "pct": 97.73 + "pct": 97.73, + "functionsHit": [ + "_allocate", + "_closeAllocation", + "_collectCurationFees", + "_collectDelegationIndexingRewards", + "_collectDelegationQueryRewards", + "_collectTax", + "_distributeRewards", + "_getAllocationState", + "_isAuth", + "_sendRewards", + "_setCurationPercentage", + "_setDelegationParameters", + "_setMaxAllocationEpochs", + "_setMinimumIndexerStake", + "_setProtocolPercentage", + "_setRebateParameters", + "_setThawingPeriod", + "_stake", + "_updateRewards", + "_withdraw", + "allocate", + "allocateFrom", + "closeAllocation", + "collect", + "getAllocation", + "getAllocationData", + "getAllocationState", + "getIndexerCapacity", + "getIndexerStakedTokens", + "getSubgraphAllocatedTokens", + "hasStake", + "initialize", + "isOperator", + "setCounterpartStakingAddress", + "setCurationPercentage", + "setDelegationParameters", + "setExtensionImpl", + "setMaxAllocationEpochs", + "setMinimumIndexerStake", + "setOperator", + "setProtocolPercentage", + "setRebateParameters", + "setRewardsDestination", + "setThawingPeriod", + "stake", + "stakeTo", + "unstake", + "withdraw" + ] }, "contracts/staking/StakingExtension.sol": { "linesFound": 114, "linesHit": 113, - "pct": 99.12 + "pct": 99.12, + "functionsHit": [ + "_collectTax", + "_delegate", + "_setDelegationRatio", + "_setDelegationTaxPercentage", + "_setDelegationUnbondingPeriod", + "_undelegate", + "_withdrawDelegated", + "allocations", + "alphaDenominator", + "alphaNumerator", + "curationPercentage", + "delegate", + "delegationPools", + "delegationRatio", + "delegationTaxPercentage", + "delegationUnbondingPeriod", + "getDelegation", + "getWithdraweableDelegatedTokens", + "initialize", + "isDelegator", + "lambdaDenominator", + "lambdaNumerator", + "maxAllocationEpochs", + "minimumIndexerStake", + "onlySlasher", + "operatorAuth", + "protocolPercentage", + "rewardsDestination", + "setDelegationRatio", + "setDelegationTaxPercentage", + "setDelegationUnbondingPeriod", + "setSlasher", + "slash", + "slashers", + "stakes", + "thawingPeriod", + "undelegate", + "withdrawDelegated" + ] }, "contracts/staking/StakingStorage.sol": { "linesFound": 0, "linesHit": 0, - "pct": null + "pct": null, + "functionsHit": [] }, "contracts/staking/libs/Exponential.sol": { "linesFound": 11, "linesHit": 11, - "pct": 100 + "pct": 100, + "functionsHit": ["exponentialRebates"] }, "contracts/staking/libs/LibFixedMath.sol": { "linesFound": 166, "linesHit": 83, - "pct": 50 + "pct": 50, + "functionsHit": ["_add", "_div", "_mul", "exp", "mul", "mulDiv", "one", "sub", "toFixed", "toInteger", "uintMul"] }, "contracts/staking/libs/MathUtils.sol": { "linesFound": 3, "linesHit": 3, - "pct": 100 + "pct": 100, + "functionsHit": ["diffOrZero", "min", "weightedAverageRoundingUp"] }, "contracts/staking/libs/Stakes.sol": { "linesFound": 28, "linesHit": 28, - "pct": 100 + "pct": 100, + "functionsHit": [ + "allocate", + "deposit", + "lockTokens", + "release", + "tokensAvailable", + "tokensAvailableWithDelegation", + "tokensSecureStake", + "tokensUsed", + "tokensWithdrawable", + "unallocate", + "unlockTokens", + "withdrawTokens" + ] }, "contracts/token/GraphToken.sol": { "linesFound": 24, "linesHit": 24, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_addMinter", + "_getChainID", + "_removeMinter", + "addMinter", + "constructor", + "isMinter", + "mint", + "onlyMinter", + "permit", + "removeMinter", + "renounceMinter" + ] }, "contracts/upgrades/GraphProxy.sol": { "linesFound": 30, "linesHit": 30, - "pct": 100 + "pct": 100, + "functionsHit": [ + "_acceptUpgrade", + "_fallback", + "acceptUpgrade", + "acceptUpgradeAndCall", + "admin", + "constructor", + "ifAdmin", + "ifAdminOrPendingImpl", + "implementation", + "pendingImplementation", + "setAdmin", + "upgradeTo" + ] }, "contracts/upgrades/GraphProxyAdmin.sol": { "linesFound": 14, "linesHit": 14, - "pct": 100 + "pct": 100, + "functionsHit": [ + "acceptProxy", + "acceptProxyAndCall", + "changeProxyAdmin", + "constructor", + "getProxyAdmin", + "getProxyImplementation", + "getProxyPendingImplementation", + "upgrade" + ] }, "contracts/upgrades/GraphProxyStorage.sol": { "linesFound": 20, "linesHit": 18, - "pct": 90 + "pct": 90, + "functionsHit": [ + "_getAdmin", + "_getImplementation", + "_getPendingImplementation", + "_setAdmin", + "_setImplementation", + "_setPendingImplementation" + ] }, "contracts/upgrades/GraphUpgradeable.sol": { "linesFound": 8, "linesHit": 8, - "pct": 100 + "pct": 100, + "functionsHit": ["_implementation", "acceptProxy", "acceptProxyAndCall", "onlyImpl", "onlyProxyAdmin"] }, "contracts/utils/TokenUtils.sol": { "linesFound": 6, "linesHit": 6, - "pct": 100 + "pct": 100, + "functionsHit": ["burnTokens", "pullTokens", "pushTokens"] } } diff --git a/scripts/contracts-migration/summarize-coverage.mjs b/scripts/contracts-migration/summarize-coverage.mjs index afd854c26..afd67973f 100644 --- a/scripts/contracts-migration/summarize-coverage.mjs +++ b/scripts/contracts-migration/summarize-coverage.mjs @@ -10,24 +10,33 @@ function normalizePath(sf) { } export function parseLcov(text) { - const result = {} + const files = {} let current = null for (const line of text.split('\n')) { if (line.startsWith('SF:')) { - current = { key: normalizePath(line.slice(3).trim()), linesFound: 0, linesHit: 0 } + const key = normalizePath(line.slice(3).trim()) + current = files[key] ??= { lines: new Map(), functions: new Set() } } else if (!current) { continue - } else if (line.startsWith('LF:')) { - current.linesFound = Number(line.slice(3)) - } else if (line.startsWith('LH:')) { - current.linesHit = Number(line.slice(3)) + } else if (line.startsWith('DA:')) { + const [num, hits] = line.slice(3).split(',') + current.lines.set(num, (current.lines.get(num) ?? false) || Number(hits) > 0) + } else if (line.startsWith('FNDA:')) { + const rest = line.slice(5) + const comma = rest.indexOf(',') + const name = rest.slice(comma + 1).trim() + if (Number(rest.slice(0, comma)) > 0 && name) current.functions.add(name) } else if (line.startsWith('end_of_record')) { - const { key, linesFound, linesHit } = current - const pct = linesFound === 0 ? null : Math.round((linesHit / linesFound) * 10000) / 100 - result[key] = { linesFound, linesHit, pct } current = null } } + const result = {} + for (const [key, { lines, functions }] of Object.entries(files)) { + const linesFound = lines.size + const linesHit = [...lines.values()].filter(Boolean).length + const pct = linesFound === 0 ? null : Math.round((linesHit / linesFound) * 10000) / 100 + result[key] = { linesFound, linesHit, pct, functionsHit: [...functions].sort() } + } return result } diff --git a/scripts/contracts-migration/summarize-coverage.test.mjs b/scripts/contracts-migration/summarize-coverage.test.mjs index 981b4637d..665eda894 100644 --- a/scripts/contracts-migration/summarize-coverage.test.mjs +++ b/scripts/contracts-migration/summarize-coverage.test.mjs @@ -3,32 +3,33 @@ import { test } from 'node:test' import { parseLcov } from './summarize-coverage.mjs' +function da(found, hit) { + return Array.from({ length: found }, (_, i) => `DA:${i + 1},${i < hit ? 1 : 0}`) +} + test('parses one record with an absolute CI path', () => { const text = [ 'SF:/home/runner/work/contracts/contracts/packages/contracts-test/contracts/rewards/RewardsManager.sol', - 'LF:200', - 'LH:150', + ...da(200, 150), 'end_of_record', ].join('\n') assert.deepEqual(parseLcov(text), { - 'contracts/rewards/RewardsManager.sol': { linesFound: 200, linesHit: 150, pct: 75 }, + 'contracts/rewards/RewardsManager.sol': { linesFound: 200, linesHit: 150, pct: 75, functionsHit: [] }, }) }) test('keeps relative contracts paths', () => { - const text = 'SF:contracts/upgrades/GraphProxy.sol\nLF:1\nLH:1\nend_of_record\n' + const text = 'SF:contracts/upgrades/GraphProxy.sol\nDA:1,1\nend_of_record\n' assert.deepEqual(Object.keys(parseLcov(text)), ['contracts/upgrades/GraphProxy.sol']) }) test('keeps records separate', () => { const text = [ 'SF:contracts/A.sol', - 'LF:10', - 'LH:10', + ...da(10, 10), 'end_of_record', 'SF:contracts/B.sol', - 'LF:4', - 'LH:1', + ...da(4, 1), 'end_of_record', ].join('\n') const result = parseLcov(text) @@ -37,6 +38,51 @@ test('keeps records separate', () => { }) test('zero lines yields null pct', () => { - const result = parseLcov('SF:contracts/Z.sol\nLF:0\nLH:0\nend_of_record\n') + const result = parseLcov('SF:contracts/Z.sol\nend_of_record\n') assert.equal(result['contracts/Z.sol'].pct, null) }) + +test('computes lines from DA records', () => { + const text = 'SF:contracts/A.sol\nDA:1,3\nDA:2,0\nDA:3,1\nLF:99\nLH:99\nend_of_record\n' + assert.deepEqual(parseLcov(text)['contracts/A.sol'], { linesFound: 3, linesHit: 2, pct: 66.67, functionsHit: [] }) +}) + +test('merges duplicate SF records', () => { + const text = [ + 'SF:contracts/A.sol', + 'DA:1,0', + 'DA:2,1', + 'end_of_record', + 'SF:contracts/A.sol', + 'DA:1,2', + 'DA:3,0', + 'end_of_record', + ].join('\n') + const result = parseLcov(text)['contracts/A.sol'] + assert.equal(result.linesFound, 3) + assert.equal(result.linesHit, 2) +}) + +test('lists hit functions sorted and unique', () => { + const text = [ + 'SF:contracts/A.sol', + 'FNDA:2,b', + 'FNDA:0,c', + 'FNDA:1,a', + 'end_of_record', + 'SF:contracts/A.sol', + 'FNDA:1,b', + 'end_of_record', + ].join('\n') + assert.deepEqual(parseLcov(text)['contracts/A.sol'].functionsHit, ['a', 'b']) +}) + +test('drops empty function names', () => { + const result = parseLcov('SF:contracts/A.sol\nFNDA:1,\nend_of_record\n') + assert.deepEqual(result['contracts/A.sol'].functionsHit, []) +}) + +test('file without functions has empty functionsHit', () => { + const result = parseLcov('SF:contracts/A.sol\nDA:1,1\nend_of_record\n') + assert.deepEqual(result['contracts/A.sol'].functionsHit, []) +})