// TestReorganization loads a set of test blocks which force a chain // reorganization to test the block chain handling code. // The test blocks were originally from a post on the bitcoin talk forums: // https://bitcointalk.org/index.php?topic=46370.msg577556#msg577556 func TestReorganization(t *testing.T) { // Intentionally load the side chain blocks out of order to ensure // orphans are handled properly along with chain reorganization. testFiles := []string{ "blk_0_to_4.dat.bz2", "blk_4A.dat.bz2", "blk_5A.dat.bz2", "blk_3A.dat.bz2", } var blocks []*btcutil.Block for _, file := range testFiles { blockTmp, err := loadBlocks(file) if err != nil { t.Errorf("Error loading file: %v\n", err) } for _, block := range blockTmp { blocks = append(blocks, block) } } t.Logf("Number of blocks: %v\n", len(blocks)) // Create a new database and chain instance to run tests against. chain, teardownFunc, err := chainSetup("reorg") if err != nil { t.Errorf("Failed to setup chain instance: %v", err) return } defer teardownFunc() // Since we're not dealing with the real block chain, disable // checkpoints and set the coinbase maturity to 1. chain.DisableCheckpoints(true) blockchain.TstSetCoinbaseMaturity(1) timeSource := blockchain.NewMedianTime() expectedOrphans := map[int]struct{}{5: struct{}{}, 6: struct{}{}} for i := 1; i < len(blocks); i++ { isOrphan, err := chain.ProcessBlock(blocks[i], timeSource, blockchain.BFNone) if err != nil { t.Errorf("ProcessBlock fail on block %v: %v\n", i, err) return } if _, ok := expectedOrphans[i]; !ok && isOrphan { t.Errorf("ProcessBlock incorrectly returned block %v "+ "is an orphan\n", i) } } return }
// newBlockImporter returns a new importer for the provided file reader seeker // and database. func newBlockImporter(db database.DB, r io.ReadSeeker) (*blockImporter, error) { // Create the transaction and address indexes if needed. // // CAUTION: the txindex needs to be first in the indexes array because // the addrindex uses data from the txindex during catchup. If the // addrindex is run first, it may not have the transactions from the // current block indexed. var indexes []indexers.Indexer if cfg.TxIndex || cfg.AddrIndex { // Enable transaction index if address index is enabled since it // requires it. if !cfg.TxIndex { log.Infof("Transaction index enabled because it is " + "required by the address index") cfg.TxIndex = true } else { log.Info("Transaction index is enabled") } indexes = append(indexes, indexers.NewTxIndex(db)) } if cfg.AddrIndex { log.Info("Address index is enabled") indexes = append(indexes, indexers.NewAddrIndex(db, activeNetParams)) } // Create an index manager if any of the optional indexes are enabled. var indexManager blockchain.IndexManager if len(indexes) > 0 { indexManager = indexers.NewManager(db, indexes) } chain, err := blockchain.New(&blockchain.Config{ DB: db, ChainParams: activeNetParams, TimeSource: blockchain.NewMedianTime(), IndexManager: indexManager, }) if err != nil { return nil, err } return &blockImporter{ db: db, r: r, processQueue: make(chan []byte, 2), doneChan: make(chan bool), errChan: make(chan error), quit: make(chan struct{}), chain: chain, lastLogTime: time.Now(), }, nil }
// newBlockImporter returns a new importer for the provided file reader seeker // and database. func newBlockImporter(db database.Db, r io.ReadSeeker) *blockImporter { return &blockImporter{ db: db, r: r, processQueue: make(chan []byte, 2), doneChan: make(chan bool), errChan: make(chan error), quit: make(chan struct{}), chain: blockchain.New(db, activeNetParams, nil), medianTime: blockchain.NewMedianTime(), lastLogTime: time.Now(), } }
// This example demonstrates how to create a new chain instance and use // ProcessBlock to attempt to attempt add a block to the chain. As the package // overview documentation describes, this includes all of the Bitcoin consensus // rules. This example intentionally attempts to insert a duplicate genesis // block to illustrate how an invalid block is handled. func ExampleBlockChain_ProcessBlock() { // Create a new database to store the accepted blocks into. Typically // this would be opening an existing database and would not be deleting // and creating a new database like this, but it is done here so this is // a complete working example and does not leave temporary files laying // around. dbPath := filepath.Join(os.TempDir(), "exampleprocessblock") _ = os.RemoveAll(dbPath) db, err := database.Create("ffldb", dbPath, chaincfg.MainNetParams.Net) if err != nil { fmt.Printf("Failed to create database: %v\n", err) return } defer os.RemoveAll(dbPath) defer db.Close() // Create a new BlockChain instance using the underlying database for // the main bitcoin network. This example does not demonstrate some // of the other available configuration options such as specifying a // notification callback and signature cache. chain, err := blockchain.New(&blockchain.Config{ DB: db, ChainParams: &chaincfg.MainNetParams, }) if err != nil { fmt.Printf("Failed to create chain instance: %v\n", err) return } // Create a new median time source that is required by the upcoming // call to ProcessBlock. Ordinarily this would also add time values // obtained from other peers on the network so the local time is // adjusted to be in agreement with other peers. timeSource := blockchain.NewMedianTime() // Process a block. For this example, we are going to intentionally // cause an error by trying to process the genesis block which already // exists. genesisBlock := btcutil.NewBlock(chaincfg.MainNetParams.GenesisBlock) isOrphan, err := chain.ProcessBlock(genesisBlock, timeSource, blockchain.BFNone) if err != nil { fmt.Printf("Failed to process block: %v\n", err) return } fmt.Printf("Block accepted. Is it an orphan?: %v", isOrphan) // Output: // Failed to process block: already have block 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f }
// This example demonstrates how to create a new chain instance and use // ProcessBlock to attempt to attempt add a block to the chain. As the package // overview documentation describes, this includes all of the Bitcoin consensus // rules. This example intentionally attempts to insert a duplicate genesis // block to illustrate how an invalid block is handled. func ExampleBlockChain_ProcessBlock() { // Create a new database to store the accepted blocks into. Typically // this would be opening an existing database and would not use memdb // which is a memory-only database backend, but we create a new db // here so this is a complete working example. db, err := database.CreateDB("memdb") if err != nil { fmt.Printf("Failed to create database: %v\n", err) return } defer db.Close() // Insert the main network genesis block. This is part of the initial // database setup. Like above, this typically would not be needed when // opening an existing database. genesisBlock := btcutil.NewBlock(chaincfg.MainNetParams.GenesisBlock) _, err = db.InsertBlock(genesisBlock) if err != nil { fmt.Printf("Failed to insert genesis block: %v\n", err) return } // Create a new BlockChain instance without an initialized signature // verification cache, using the underlying database for the main // bitcoin network and ignore notifications. chain := blockchain.New(db, &chaincfg.MainNetParams, nil, nil) // Create a new median time source that is required by the upcoming // call to ProcessBlock. Ordinarily this would also add time values // obtained from other peers on the network so the local time is // adjusted to be in agreement with other peers. timeSource := blockchain.NewMedianTime() // Process a block. For this example, we are going to intentionally // cause an error by trying to process the genesis block which already // exists. isOrphan, err := chain.ProcessBlock(genesisBlock, timeSource, blockchain.BFNone) if err != nil { fmt.Printf("Failed to process block: %v\n", err) return } fmt.Printf("Block accepted. Is it an orphan?: %v", isOrphan) // Output: // Failed to process block: already have block 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f }
// TestCheckBlockSanity tests the CheckBlockSanity function to ensure it works // as expected. func TestCheckBlockSanity(t *testing.T) { powLimit := chaincfg.MainNetParams.PowLimit block := btcutil.NewBlock(&Block100000) timeSource := blockchain.NewMedianTime() err := blockchain.CheckBlockSanity(block, powLimit, timeSource) if err != nil { t.Errorf("CheckBlockSanity: %v", err) } // Ensure a block that has a timestamp with a precision higher than one // second fails. timestamp := block.MsgBlock().Header.Timestamp block.MsgBlock().Header.Timestamp = timestamp.Add(time.Nanosecond) err = blockchain.CheckBlockSanity(block, powLimit, timeSource) if err == nil { t.Errorf("CheckBlockSanity: error is nil when it shouldn't be") } }
// newBlockImporter returns a new importer for the provided file reader seeker // and database. func newBlockImporter(db database.DB, r io.ReadSeeker) (*blockImporter, error) { chain, err := blockchain.New(&blockchain.Config{ DB: db, ChainParams: activeNetParams, }) if err != nil { return nil, err } return &blockImporter{ db: db, r: r, processQueue: make(chan []byte, 2), doneChan: make(chan bool), errChan: make(chan error), quit: make(chan struct{}), chain: chain, medianTime: blockchain.NewMedianTime(), lastLogTime: time.Now(), }, nil }
// newServer returns a new btcd server configured to listen on addr for the // bitcoin network type specified by chainParams. Use start to begin accepting // connections from peers. func newServer(listenAddrs []string, db database.Db, chainParams *chaincfg.Params) (*server, error) { nonce, err := wire.RandomUint64() if err != nil { return nil, err } amgr := addrmgr.New(cfg.DataDir, btcdLookup) var listeners []net.Listener var nat NAT if !cfg.DisableListen { ipv4Addrs, ipv6Addrs, wildcard, err := parseListeners(listenAddrs) if err != nil { return nil, err } listeners = make([]net.Listener, 0, len(ipv4Addrs)+len(ipv6Addrs)) discover := true if len(cfg.ExternalIPs) != 0 { discover = false // if this fails we have real issues. port, _ := strconv.ParseUint( activeNetParams.DefaultPort, 10, 16) for _, sip := range cfg.ExternalIPs { eport := uint16(port) host, portstr, err := net.SplitHostPort(sip) if err != nil { // no port, use default. host = sip } else { port, err := strconv.ParseUint( portstr, 10, 16) if err != nil { srvrLog.Warnf("Can not parse "+ "port from %s for "+ "externalip: %v", sip, err) continue } eport = uint16(port) } na, err := amgr.HostToNetAddress(host, eport, wire.SFNodeNetwork) if err != nil { srvrLog.Warnf("Not adding %s as "+ "externalip: %v", sip, err) continue } err = amgr.AddLocalAddress(na, addrmgr.ManualPrio) if err != nil { amgrLog.Warnf("Skipping specified external IP: %v", err) } } } else if discover && cfg.Upnp { nat, err = Discover() if err != nil { srvrLog.Warnf("Can't discover upnp: %v", err) } // nil nat here is fine, just means no upnp on network. } // TODO(oga) nonstandard port... if wildcard { port, err := strconv.ParseUint(activeNetParams.DefaultPort, 10, 16) if err != nil { // I can't think of a cleaner way to do this... goto nowc } addrs, err := net.InterfaceAddrs() for _, a := range addrs { ip, _, err := net.ParseCIDR(a.String()) if err != nil { continue } na := wire.NewNetAddressIPPort(ip, uint16(port), wire.SFNodeNetwork) if discover { err = amgr.AddLocalAddress(na, addrmgr.InterfacePrio) if err != nil { amgrLog.Debugf("Skipping local address: %v", err) } } } } nowc: for _, addr := range ipv4Addrs { listener, err := net.Listen("tcp4", addr) if err != nil { srvrLog.Warnf("Can't listen on %s: %v", addr, err) continue } listeners = append(listeners, listener) if discover { if na, err := amgr.DeserializeNetAddress(addr); err == nil { err = amgr.AddLocalAddress(na, addrmgr.BoundPrio) if err != nil { amgrLog.Warnf("Skipping bound address: %v", err) } } } } for _, addr := range ipv6Addrs { listener, err := net.Listen("tcp6", addr) if err != nil { srvrLog.Warnf("Can't listen on %s: %v", addr, err) continue } listeners = append(listeners, listener) if discover { if na, err := amgr.DeserializeNetAddress(addr); err == nil { err = amgr.AddLocalAddress(na, addrmgr.BoundPrio) if err != nil { amgrLog.Debugf("Skipping bound address: %v", err) } } } } if len(listeners) == 0 { return nil, errors.New("no valid listen address") } } s := server{ nonce: nonce, listeners: listeners, chainParams: chainParams, addrManager: amgr, newPeers: make(chan *peer, cfg.MaxPeers), donePeers: make(chan *peer, cfg.MaxPeers), banPeers: make(chan *peer, cfg.MaxPeers), wakeup: make(chan struct{}), query: make(chan interface{}), relayInv: make(chan relayMsg, cfg.MaxPeers), broadcast: make(chan broadcastMsg, cfg.MaxPeers), quit: make(chan struct{}), modifyRebroadcastInv: make(chan interface{}), peerHeightsUpdate: make(chan updatePeerHeightsMsg), nat: nat, db: db, timeSource: blockchain.NewMedianTime(), } bm, err := newBlockManager(&s) if err != nil { return nil, err } s.blockManager = bm s.txMemPool = newTxMemPool(&s) s.cpuMiner = newCPUMiner(&s) if cfg.AddrIndex { ai, err := newAddrIndexer(&s) if err != nil { return nil, err } s.addrIndexer = ai } if !cfg.DisableRPC { s.rpcServer, err = newRPCServer(cfg.RPCListeners, &s) if err != nil { return nil, err } } return &s, nil }
// TestHaveBlock tests the HaveBlock API to ensure proper functionality. func TestHaveBlock(t *testing.T) { // Load up blocks such that there is a side chain. // (genesis block) -> 1 -> 2 -> 3 -> 4 // \-> 3a testFiles := []string{ "blk_0_to_4.dat.bz2", "blk_3A.dat.bz2", } var blocks []*btcutil.Block for _, file := range testFiles { blockTmp, err := loadBlocks(file) if err != nil { t.Errorf("Error loading file: %v\n", err) return } for _, block := range blockTmp { blocks = append(blocks, block) } } // Create a new database and chain instance to run tests against. chain, teardownFunc, err := chainSetup("haveblock") if err != nil { t.Errorf("Failed to setup chain instance: %v", err) return } defer teardownFunc() // Since we're not dealing with the real block chain, disable // checkpoints and set the coinbase maturity to 1. chain.DisableCheckpoints(true) blockchain.TstSetCoinbaseMaturity(1) timeSource := blockchain.NewMedianTime() for i := 1; i < len(blocks); i++ { isOrphan, err := chain.ProcessBlock(blocks[i], timeSource, blockchain.BFNone) if err != nil { t.Errorf("ProcessBlock fail on block %v: %v\n", i, err) return } if isOrphan { t.Errorf("ProcessBlock incorrectly returned block %v "+ "is an orphan\n", i) return } } // Insert an orphan block. isOrphan, err := chain.ProcessBlock(btcutil.NewBlock(&Block100000), timeSource, blockchain.BFNone) if err != nil { t.Errorf("Unable to process block: %v", err) return } if !isOrphan { t.Errorf("ProcessBlock indicated block is an not orphan when " + "it should be\n") return } tests := []struct { hash string want bool }{ // Genesis block should be present (in the main chain). {hash: chaincfg.MainNetParams.GenesisHash.String(), want: true}, // Block 3a should be present (on a side chain). {hash: "00000000474284d20067a4d33f6a02284e6ef70764a3a26d6a5b9df52ef663dd", want: true}, // Block 100000 should be present (as an orphan). {hash: "000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506", want: true}, // Random hashes should not be availble. {hash: "123", want: false}, } for i, test := range tests { hash, err := wire.NewShaHashFromStr(test.hash) if err != nil { t.Errorf("NewShaHashFromStr: %v", err) continue } result, err := chain.HaveBlock(hash) if err != nil { t.Errorf("HaveBlock #%d unexpected error: %v", i, err) return } if result != test.want { t.Errorf("HaveBlock #%d got %v want %v", i, result, test.want) continue } } }
// TestCheckTransactionStandard tests the checkTransactionStandard API. func TestCheckTransactionStandard(t *testing.T) { // Create some dummy, but otherwise standard, data for transactions. prevOutHash, err := wire.NewShaHashFromStr("01") if err != nil { t.Fatalf("NewShaHashFromStr: unexpected error: %v", err) } dummyPrevOut := wire.OutPoint{Hash: *prevOutHash, Index: 1} dummySigScript := bytes.Repeat([]byte{0x00}, 65) dummyTxIn := wire.TxIn{ PreviousOutPoint: dummyPrevOut, SignatureScript: dummySigScript, Sequence: wire.MaxTxInSequenceNum, } addrHash := [20]byte{0x01} addr, err := btcutil.NewAddressPubKeyHash(addrHash[:], &chaincfg.TestNet3Params) if err != nil { t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err) } dummyPkScript, err := txscript.PayToAddrScript(addr) if err != nil { t.Fatalf("PayToAddrScript: unexpected error: %v", err) } dummyTxOut := wire.TxOut{ Value: 100000000, // 1 BTC PkScript: dummyPkScript, } tests := []struct { name string tx wire.MsgTx height int32 isStandard bool code wire.RejectCode }{ { name: "Typical pay-to-pubkey-hash transaction", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{&dummyTxIn}, TxOut: []*wire.TxOut{&dummyTxOut}, LockTime: 0, }, height: 300000, isStandard: true, }, { name: "Transaction version too high", tx: wire.MsgTx{ Version: wire.TxVersion + 1, TxIn: []*wire.TxIn{&dummyTxIn}, TxOut: []*wire.TxOut{&dummyTxOut}, LockTime: 0, }, height: 300000, isStandard: false, code: wire.RejectNonstandard, }, { name: "Transaction is not finalized", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: dummyPrevOut, SignatureScript: dummySigScript, Sequence: 0, }}, TxOut: []*wire.TxOut{&dummyTxOut}, LockTime: 300001, }, height: 300000, isStandard: false, code: wire.RejectNonstandard, }, { name: "Transaction size is too large", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{&dummyTxIn}, TxOut: []*wire.TxOut{{ Value: 0, PkScript: bytes.Repeat([]byte{0x00}, maxStandardTxSize+1), }}, LockTime: 0, }, height: 300000, isStandard: false, code: wire.RejectNonstandard, }, { name: "Signature script size is too large", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: dummyPrevOut, SignatureScript: bytes.Repeat([]byte{0x00}, maxStandardSigScriptSize+1), Sequence: wire.MaxTxInSequenceNum, }}, TxOut: []*wire.TxOut{&dummyTxOut}, LockTime: 0, }, height: 300000, isStandard: false, code: wire.RejectNonstandard, }, { name: "Signature script that does more than push data", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{{ PreviousOutPoint: dummyPrevOut, SignatureScript: []byte{ txscript.OP_CHECKSIGVERIFY}, Sequence: wire.MaxTxInSequenceNum, }}, TxOut: []*wire.TxOut{&dummyTxOut}, LockTime: 0, }, height: 300000, isStandard: false, code: wire.RejectNonstandard, }, { name: "Valid but non standard public key script", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{&dummyTxIn}, TxOut: []*wire.TxOut{{ Value: 100000000, PkScript: []byte{txscript.OP_TRUE}, }}, LockTime: 0, }, height: 300000, isStandard: false, code: wire.RejectNonstandard, }, { name: "More than one nulldata output", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{&dummyTxIn}, TxOut: []*wire.TxOut{{ Value: 0, PkScript: []byte{txscript.OP_RETURN}, }, { Value: 0, PkScript: []byte{txscript.OP_RETURN}, }}, LockTime: 0, }, height: 300000, isStandard: false, code: wire.RejectNonstandard, }, { name: "Dust output", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{&dummyTxIn}, TxOut: []*wire.TxOut{{ Value: 0, PkScript: dummyPkScript, }}, LockTime: 0, }, height: 300000, isStandard: false, code: wire.RejectDust, }, { name: "One nulldata output with 0 amount (standard)", tx: wire.MsgTx{ Version: 1, TxIn: []*wire.TxIn{&dummyTxIn}, TxOut: []*wire.TxOut{{ Value: 0, PkScript: []byte{txscript.OP_RETURN}, }}, LockTime: 0, }, height: 300000, isStandard: true, }, } timeSource := blockchain.NewMedianTime() for _, test := range tests { // Ensure standardness is as expected. err := checkTransactionStandard(btcutil.NewTx(&test.tx), test.height, timeSource, defaultMinRelayTxFee) if err == nil && test.isStandard { // Test passes since function returned standard for a // transaction which is intended to be standard. continue } if err == nil && !test.isStandard { t.Errorf("checkTransactionStandard (%s): standard when "+ "it should not be", test.name) continue } if err != nil && test.isStandard { t.Errorf("checkTransactionStandard (%s): nonstandard "+ "when it should not be: %v", test.name, err) continue } // Ensure error type is a TxRuleError inside of a RuleError. rerr, ok := err.(RuleError) if !ok { t.Errorf("checkTransactionStandard (%s): unexpected "+ "error type - got %T", test.name, err) continue } txrerr, ok := rerr.Err.(TxRuleError) if !ok { t.Errorf("checkTransactionStandard (%s): unexpected "+ "error type - got %T", test.name, rerr.Err) continue } // Ensure the reject code is the expected one. if txrerr.RejectCode != test.code { t.Errorf("checkTransactionStandard (%s): unexpected "+ "error code - got %v, want %v", test.name, txrerr.RejectCode, test.code) continue } } }
// TestMedianTime tests the medianTime implementation. func TestMedianTime(t *testing.T) { tests := []struct { in []int64 wantOffset int64 useDupID bool }{ // Not enough samples must result in an offset of 0. {in: []int64{1}, wantOffset: 0}, {in: []int64{1, 2}, wantOffset: 0}, {in: []int64{1, 2, 3}, wantOffset: 0}, {in: []int64{1, 2, 3, 4}, wantOffset: 0}, // Various number of entries. The expected offset is only // updated on odd number of elements. {in: []int64{-13, 57, -4, -23, -12}, wantOffset: -12}, {in: []int64{55, -13, 61, -52, 39, 55}, wantOffset: 39}, {in: []int64{-62, -58, -30, -62, 51, -30, 15}, wantOffset: -30}, {in: []int64{29, -47, 39, 54, 42, 41, 8, -33}, wantOffset: 39}, {in: []int64{37, 54, 9, -21, -56, -36, 5, -11, -39}, wantOffset: -11}, {in: []int64{57, -28, 25, -39, 9, 63, -16, 19, -60, 25}, wantOffset: 9}, {in: []int64{-5, -4, -3, -2, -1}, wantOffset: -3, useDupID: true}, // The offset stops being updated once the max number of entries // has been reached. This is actually a bug from Bitcoin Core, // but since the time is ultimately used as a part of the // consensus rules, it must be mirrored. {in: []int64{-67, 67, -50, 24, 63, 17, 58, -14, 5, -32, -52}, wantOffset: 17}, {in: []int64{-67, 67, -50, 24, 63, 17, 58, -14, 5, -32, -52, 45}, wantOffset: 17}, {in: []int64{-67, 67, -50, 24, 63, 17, 58, -14, 5, -32, -52, 45, 4}, wantOffset: 17}, // Offsets that are too far away from the local time should // be ignored. {in: []int64{-4201, 4202, -4203, 4204, -4205}, wantOffset: 0}, // Excerise the condition where the median offset is greater // than the max allowed adjustment, but there is at least one // sample that is close enough to the current time to avoid // triggering a warning about an invalid local clock. {in: []int64{4201, 4202, 4203, 4204, -299}, wantOffset: 0}, } // Modify the max number of allowed median time entries for these tests. blockchain.TstSetMaxMedianTimeEntries(10) defer blockchain.TstSetMaxMedianTimeEntries(200) for i, test := range tests { filter := blockchain.NewMedianTime() for j, offset := range test.in { id := strconv.Itoa(j) now := time.Unix(time.Now().Unix(), 0) tOffset := now.Add(time.Duration(offset) * time.Second) filter.AddTimeSample(id, tOffset) // Ensure the duplicate IDs are ignored. if test.useDupID { // Modify the offsets to ensure the final median // would be different if the duplicate is added. tOffset = tOffset.Add(time.Duration(offset) * time.Second) filter.AddTimeSample(id, tOffset) } } // Since it is possible that the time.Now call in AddTimeSample // and the time.Now calls here in the tests will be off by one // second, allow a fudge factor to compensate. gotOffset := filter.Offset() wantOffset := time.Duration(test.wantOffset) * time.Second wantOffset2 := time.Duration(test.wantOffset-1) * time.Second if gotOffset != wantOffset && gotOffset != wantOffset2 { t.Errorf("Offset #%d: unexpected offset -- got %v, "+ "want %v or %v", i, gotOffset, wantOffset, wantOffset2) continue } // Since it is possible that the time.Now call in AdjustedTime // and the time.Now call here in the tests will be off by one // second, allow a fudge factor to compensate. adjustedTime := filter.AdjustedTime() now := time.Unix(time.Now().Unix(), 0) wantTime := now.Add(filter.Offset()) wantTime2 := now.Add(filter.Offset() - time.Second) if !adjustedTime.Equal(wantTime) && !adjustedTime.Equal(wantTime2) { t.Errorf("AdjustedTime #%d: unexpected result -- got %v, "+ "want %v or %v", i, adjustedTime, wantTime, wantTime2) continue } } }