From c500fbc42380eba902a1945a6fa034480a52a086 Mon Sep 17 00:00:00 2001 From: Ivan Tomilov Date: Mon, 23 Oct 2017 14:05:52 +0300 Subject: [PATCH] Made all tests run on StatusChain instead of Rinkeby or Ropsten (#426) The reason is that we don't currently have a possibility to run tests on public testnets without compromising accounts and funds on them. --- Makefile | 1 + cmd/statusd/utils.go | 19 +++-- e2e/accounts/accounts_rpc_test.go | 5 +- e2e/api/api_test.go | 6 +- e2e/api/backend_test.go | 2 +- e2e/jail/jail_rpc_test.go | 10 +-- e2e/node/manager_test.go | 5 +- e2e/rpc/rpc_test.go | 19 ++--- e2e/transactions/transactions_test.go | 112 ++++---------------------- e2e/whisper/whisper_jail_test.go | 4 +- testing/testing.go | 11 ++- 11 files changed, 67 insertions(+), 127 deletions(-) diff --git a/Makefile b/Makefile index 3060b9ebe..ae3cf1e31 100644 --- a/Makefile +++ b/Makefile @@ -144,6 +144,7 @@ test-coverage: ##@tests Run unit and integration tests with covevare test-e2e: ##@tests Run e2e tests # order: reliability then alphabetical + # TODO(tiabc): make a single command out of them adding `-p 1` flag. build/env.sh go test -timeout 5m ./e2e/accounts/... build/env.sh go test -timeout 5m ./e2e/api/... build/env.sh go test -timeout 5m ./e2e/node/... diff --git a/cmd/statusd/utils.go b/cmd/statusd/utils.go index b328cd3b3..caaa880bb 100644 --- a/cmd/statusd/utils.go +++ b/cmd/statusd/utils.go @@ -4,6 +4,7 @@ import "C" import ( "context" "encoding/json" + "fmt" "io/ioutil" "math/big" "os" @@ -18,10 +19,6 @@ import ( "github.com/ethereum/go-ethereum/core" gethparams "github.com/ethereum/go-ethereum/params" - "fmt" - - "context" - "github.com/status-im/status-go/geth/account" "github.com/status-im/status-go/geth/common" "github.com/status-im/status-go/geth/params" @@ -35,7 +32,7 @@ import ( const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000" var nodeConfigJSON = `{ - "NetworkId": ` + strconv.Itoa(params.RopstenNetworkID) + `, + "NetworkId": ` + strconv.Itoa(params.StatusChainNetworkID) + `, "DataDir": "` + TestDataDir + `", "HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `, "WSPort": ` + strconv.Itoa(TestConfig.Node.WSPort) + `, @@ -176,6 +173,7 @@ func testGetDefaultConfig(t *testing.T) bool { {params.MainNetworkID, gethparams.MainnetChainConfig}, {params.RopstenNetworkID, gethparams.TestnetChainConfig}, {params.RinkebyNetworkID, gethparams.RinkebyChainConfig}, + // TODO(tiabc): The same for params.StatusChainNetworkID } for i := range networks { network := networks[i] @@ -228,7 +226,7 @@ func testResetChainData(t *testing.T) bool { } func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo - // to make sure that we start with empty account (which might get populated during previous tests) + // to make sure that we start with empty account (which might have gotten populated during previous tests) if err := statusAPI.Logout(); err != nil { t.Fatal(err) } @@ -272,6 +270,8 @@ func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo // nolint: dupl stopNodeFn := func() bool { response := common.APIResponse{} + // FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid + // 9-sec timeout below after stopping the node. rawResponse = StopNode() if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil { @@ -289,6 +289,8 @@ func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo // nolint: dupl resumeNodeFn := func() bool { response := common.APIResponse{} + // FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid + // 10-sec timeout below after resuming the node. rawResponse = StartNode(C.CString(nodeConfigJSON)) if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil { @@ -306,11 +308,14 @@ func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo if !stopNodeFn() { return false } + + time.Sleep(9 * time.Second) // allow to stop + if !resumeNodeFn() { return false } - time.Sleep(5 * time.Second) // allow to start (instead of using blocking version of start, of filter event) + time.Sleep(10 * time.Second) // allow to start (instead of using blocking version of start, of filter event) // now, verify that we still have account logged in whisperService, err = statusAPI.NodeManager().WhisperService() diff --git a/e2e/accounts/accounts_rpc_test.go b/e2e/accounts/accounts_rpc_test.go index a12b16bf4..755128d5c 100644 --- a/e2e/accounts/accounts_rpc_test.go +++ b/e2e/accounts/accounts_rpc_test.go @@ -19,7 +19,7 @@ type AccountsRPCTestSuite struct { } func (s *AccountsTestSuite) TestRPCEthAccounts() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() // log into test account @@ -40,6 +40,9 @@ func (s *AccountsTestSuite) TestRPCEthAccounts() { } func (s *AccountsTestSuite) TestRPCEthAccountsWithUpstream() { + // FIXME(tiabc): Stop skipping after https://github.com/status-im/status-go/issues/424 + s.T().Skip() + s.StartTestBackend( params.RopstenNetworkID, e2e.WithUpstream("https://ropsten.infura.io/z6GCTmjdP3FETEJmMBI4"), diff --git a/e2e/api/api_test.go b/e2e/api/api_test.go index 026be6120..d94f9e081 100644 --- a/e2e/api/api_test.go +++ b/e2e/api/api_test.go @@ -56,7 +56,7 @@ func (s *APITestSuite) TestRaceConditions() { progress := make(chan struct{}, cnt) rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - nodeConfig1, err := e2e.MakeTestNodeConfig(params.RopstenNetworkID) + nodeConfig1, err := e2e.MakeTestNodeConfig(params.StatusChainNetworkID) s.NoError(err) nodeConfig2, err := e2e.MakeTestNodeConfig(params.RinkebyNetworkID) @@ -129,7 +129,7 @@ func (s *APITestSuite) TestCellsRemovedAfterSwitchAccount() { } ) - config, err := e2e.MakeTestNodeConfig(params.RopstenNetworkID) + config, err := e2e.MakeTestNodeConfig(params.StatusChainNetworkID) require.NoError(err) err = s.api.StartNode(config) require.NoError(err) @@ -166,7 +166,7 @@ func (s *APITestSuite) TestLogoutRemovesCells() { require = s.Require() ) - config, err := e2e.MakeTestNodeConfig(params.RopstenNetworkID) + config, err := e2e.MakeTestNodeConfig(params.StatusChainNetworkID) require.NoError(err) err = s.api.StartNode(config) require.NoError(err) diff --git a/e2e/api/backend_test.go b/e2e/api/backend_test.go index 087189234..24d584f1b 100644 --- a/e2e/api/backend_test.go +++ b/e2e/api/backend_test.go @@ -34,7 +34,7 @@ func (s *APIBackendTestSuite) TestRaceConditions() { progress := make(chan struct{}, cnt) rnd := rand.New(rand.NewSource(time.Now().UnixNano())) - nodeConfig1, err := e2e.MakeTestNodeConfig(params.RopstenNetworkID) + nodeConfig1, err := e2e.MakeTestNodeConfig(params.StatusChainNetworkID) require.NoError(err) nodeConfig2, err := e2e.MakeTestNodeConfig(params.RinkebyNetworkID) diff --git a/e2e/jail/jail_rpc_test.go b/e2e/jail/jail_rpc_test.go index ba492af6d..2b2408f76 100644 --- a/e2e/jail/jail_rpc_test.go +++ b/e2e/jail/jail_rpc_test.go @@ -36,7 +36,7 @@ func (s *JailRPCTestSuite) SetupTest() { } func (s *JailRPCTestSuite) TestJailRPCSend() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -69,7 +69,7 @@ func (s *JailRPCTestSuite) TestJailRPCSend() { } func (s *JailRPCTestSuite) TestIsConnected() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.jail.Parse(testChatID, "") @@ -96,7 +96,7 @@ func (s *JailRPCTestSuite) TestIsConnected() { // regression test: eth_getTransactionReceipt with invalid transaction hash should return null func (s *JailRPCTestSuite) TestRegressionGetTransactionReceipt() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() rpcClient := s.Backend.NodeManager().RPCClient() @@ -109,7 +109,7 @@ func (s *JailRPCTestSuite) TestRegressionGetTransactionReceipt() { } func (s *JailRPCTestSuite) TestContractDeployment() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -195,7 +195,7 @@ func (s *JailRPCTestSuite) TestContractDeployment() { } func (s *JailRPCTestSuite) TestJailVMPersistence() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") diff --git a/e2e/node/manager_test.go b/e2e/node/manager_test.go index 743e450c4..5a7e4223d 100644 --- a/e2e/node/manager_test.go +++ b/e2e/node/manager_test.go @@ -194,7 +194,7 @@ func (s *ManagerTestSuite) TestReferencesWithStartedNode() { } func (s *ManagerTestSuite) TestNodeStartStop() { - nodeConfig, err := e2e.MakeTestNodeConfig(params.RopstenNetworkID) + nodeConfig, err := e2e.MakeTestNodeConfig(params.StatusChainNetworkID) s.NoError(err) // try stopping non-started node @@ -446,8 +446,7 @@ func (s *ManagerTestSuite) TestRaceConditions() { } time.Sleep(2 * time.Second) // so that we see some logs - nodeStopped, e := s.NodeManager.StopNode() // just in case we have a node running - s.NoError(e) + nodeStopped, _ := s.NodeManager.StopNode() // just in case we have a node running if nodeStopped != nil { <-nodeStopped diff --git a/e2e/rpc/rpc_test.go b/e2e/rpc/rpc_test.go index 69872ece7..3b9a45155 100644 --- a/e2e/rpc/rpc_test.go +++ b/e2e/rpc/rpc_test.go @@ -10,7 +10,9 @@ import ( "github.com/status-im/status-go/e2e" "github.com/status-im/status-go/geth/node" "github.com/status-im/status-go/geth/params" + . "github.com/status-im/status-go/testing" "github.com/stretchr/testify/suite" + "math/big" ) func TestRPCTestSuite(t *testing.T) { @@ -119,7 +121,7 @@ func (s *RPCTestSuite) TestCallRPC() { // TestCallRawResult checks if returned response is a valid JSON-RPC response. func (s *RPCTestSuite) TestCallRawResult() { - nodeConfig, err := e2e.MakeTestNodeConfig(params.RopstenNetworkID) + nodeConfig, err := e2e.MakeTestNodeConfig(params.StatusChainNetworkID) s.NoError(err) nodeStarted, err := s.NodeManager.StartNode(nodeConfig) @@ -138,20 +140,19 @@ func (s *RPCTestSuite) TestCallRawResult() { // TestCallContextResult checks if result passed to CallContext // is set accordingly to its underlying memory layout. func (s *RPCTestSuite) TestCallContextResult() { - s.StartTestNode( - params.RopstenNetworkID, - e2e.WithUpstream("https://ropsten.infura.io/nKmXgiFgc2KqtoQ8BCGJ"), - ) + s.StartTestNode(params.StatusChainNetworkID) defer s.StopTestNode() + s.Require().NoError(EnsureNodeSync(s.NodeManager)) + client := s.NodeManager.RPCClient() s.NotNil(client) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() - var blockNumber hexutil.Uint - err := client.CallContext(ctx, &blockNumber, "eth_blockNumber") + var balance hexutil.Big + err := client.CallContext(ctx, &balance, "eth_getBalance", "0xAdAf150b905Cf5E6A778E553E15A139B6618BbB7", "latest") s.NoError(err) - s.True(blockNumber > 0, "blockNumber should be higher than 0") + s.True(balance.ToInt().Cmp(big.NewInt(0)) > 0, "balance should be higher than 0") } diff --git a/e2e/transactions/transactions_test.go b/e2e/transactions/transactions_test.go index 7c9db19b5..31a1b27c0 100644 --- a/e2e/transactions/transactions_test.go +++ b/e2e/transactions/transactions_test.go @@ -9,8 +9,6 @@ import ( "testing" "time" - "context" - "github.com/ethereum/go-ethereum/accounts/keystore" gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" @@ -34,7 +32,7 @@ type TransactionsTestSuite struct { } func (s *TransactionsTestSuite) TestCallRPCSendTransaction() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -82,6 +80,9 @@ func (s *TransactionsTestSuite) TestCallRPCSendTransaction() { } func (s *TransactionsTestSuite) TestCallRPCSendTransactionUpstream() { + // FIXME(tiabc): Stop skipping after https://github.com/status-im/status-go/issues/424 + s.T().Skip() + s.StartTestBackend( params.RopstenNetworkID, e2e.WithUpstream("https://ropsten.infura.io/nKmXgiFgc2KqtoQ8BCGJ"), @@ -138,7 +139,7 @@ func (s *TransactionsTestSuite) TestCallRPCSendTransactionUpstream() { // FIXME(tiabc): Sometimes it fails due to "no suitable peers found". func (s *TransactionsTestSuite) TestSendContractTx() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -224,94 +225,12 @@ func (s *TransactionsTestSuite) TestSendContractTx() { s.Zero(s.TxQueueManager().TransactionQueue().Count(), "tx queue must be empty at this point") } -func (s *TransactionsTestSuite) TestSendEtherTx() { - s.StartTestBackend(params.RopstenNetworkID) - defer s.StopTestBackend() - - s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") - - backend := s.LightEthereumService().StatusBackend - s.NotNil(backend) - - // create an account - sampleAddress, _, _, err := s.Backend.AccountManager().CreateAccount(TestConfig.Account1.Password) - s.NoError(err) - - completeQueuedTransaction := make(chan struct{}) - - // replace transaction notification handler - var txHash = gethcommon.Hash{} - signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) { // nolint: dupl - var envelope signal.Envelope - err := json.Unmarshal([]byte(jsonEvent), &envelope) - s.NoError(err, fmt.Sprintf("cannot unmarshal JSON: %s", jsonEvent)) - - if envelope.Type == txqueue.EventTransactionQueued { - event := envelope.Event.(map[string]interface{}) - log.Info("transaction queued (will be completed shortly)", "id", event["id"].(string)) - - // the first call will fail (we are not logged in, but trying to complete tx) - log.Info("trying to complete with no user logged in") - txHash, err = s.Backend.CompleteTransaction( - common.QueuedTxID(event["id"].(string)), - TestConfig.Account1.Password, - ) - s.EqualError( - err, - account.ErrNoAccountSelected.Error(), - fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]), - ) - - // the second call will also fail (we are logged in as different user) - log.Info("trying to complete with invalid user") - err = s.Backend.AccountManager().SelectAccount(sampleAddress, TestConfig.Account1.Password) - s.NoError(err) - txHash, err = s.Backend.CompleteTransaction( - common.QueuedTxID(event["id"].(string)), TestConfig.Account1.Password) - s.EqualError( - err, - txqueue.ErrInvalidCompleteTxSender.Error(), - fmt.Sprintf("expected error on queued transaction[%v] not thrown", event["id"]), - ) - - // the third call will work as expected (as we are logged in with correct credentials) - log.Info("trying to complete with correct user, this should succeed") - s.NoError(s.Backend.AccountManager().SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password)) - txHash, err = s.Backend.CompleteTransaction( - common.QueuedTxID(event["id"].(string)), - TestConfig.Account1.Password, - ) - s.NoError(err, fmt.Sprintf("cannot complete queued transaction[%v]", event["id"])) - - log.Info("contract transaction complete", "URL", "https://ropsten.etherscan.io/tx/"+txHash.Hex()) - close(completeQueuedTransaction) - return - } - }) - - // this call blocks, up until Complete Transaction is called - txHashCheck, err := s.Backend.SendTransaction(context.TODO(), common.SendTxArgs{ - From: common.FromAddress(TestConfig.Account1.Address), - To: common.ToAddress(TestConfig.Account2.Address), - Value: (*hexutil.Big)(big.NewInt(1000000000000)), - }) - s.NoError(err, "cannot send transaction") - - select { - case <-completeQueuedTransaction: - case <-time.After(2 * time.Minute): - s.FailNow("completing transaction timed out") - } - - s.Equal(txHashCheck.Hex(), txHash.Hex(), "transaction hash returned from SendTransaction is invalid") - s.False(reflect.DeepEqual(txHashCheck, gethcommon.Hash{}), "transaction was never queued or completed") - s.Zero(s.Backend.TxQueueManager().TransactionQueue().Count(), "tx queue must be empty at this point") -} - -func (s *TransactionsTestSuite) TestSendEtherOnStatusChainTx() { +func (s *TransactionsTestSuite) TestSendEther() { s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() + s.NoError(EnsureNodeSync(s.Backend.NodeManager())) + backend := s.LightEthereumService().StatusBackend s.NotNil(backend) @@ -390,6 +309,9 @@ func (s *TransactionsTestSuite) TestSendEtherOnStatusChainTx() { } func (s *TransactionsTestSuite) TestSendEtherTxUpstream() { + // FIXME(tiabc): Stop skipping after https://github.com/status-im/status-go/issues/424 + s.T().Skip() + s.StartTestBackend( params.RopstenNetworkID, e2e.WithUpstream("https://ropsten.infura.io/z6GCTmjdP3FETEJmMBI4"), @@ -444,7 +366,7 @@ func (s *TransactionsTestSuite) TestSendEtherTxUpstream() { } func (s *TransactionsTestSuite) TestDoubleCompleteQueuedTransactions() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -521,7 +443,7 @@ func (s *TransactionsTestSuite) TestDoubleCompleteQueuedTransactions() { } func (s *TransactionsTestSuite) TestDiscardQueuedTransaction() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -601,7 +523,7 @@ func (s *TransactionsTestSuite) TestDiscardQueuedTransaction() { } func (s *TransactionsTestSuite) TestCompleteMultipleQueuedTransactions() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -695,7 +617,7 @@ func (s *TransactionsTestSuite) TestCompleteMultipleQueuedTransactions() { } func (s *TransactionsTestSuite) TestDiscardMultipleQueuedTransactions() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() s.NoError(EnsureNodeSync(s.Backend.NodeManager()), "cannot ensure node synchronization") @@ -811,7 +733,7 @@ func (s *TransactionsTestSuite) TestDiscardMultipleQueuedTransactions() { } func (s *TransactionsTestSuite) TestNonExistentQueuedTransactions() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() backend := s.LightEthereumService().StatusBackend @@ -830,7 +752,7 @@ func (s *TransactionsTestSuite) TestNonExistentQueuedTransactions() { } func (s *TransactionsTestSuite) TestEvictionOfQueuedTransactions() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() backend := s.LightEthereumService().StatusBackend diff --git a/e2e/whisper/whisper_jail_test.go b/e2e/whisper/whisper_jail_test.go index b0150afc3..ea0209e6d 100644 --- a/e2e/whisper/whisper_jail_test.go +++ b/e2e/whisper/whisper_jail_test.go @@ -81,7 +81,7 @@ func (s *WhisperJailTestSuite) GetAccountKey(account struct { // TODO(adamb) Uncomment when issue #336 is fixed. /* func (s *WhisperJailTestSuite) DontTestJailWhisper() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() _, accountKey1Hex, err := s.GetAccountKey(TestConfig.Account1) @@ -375,7 +375,7 @@ func (s *WhisperJailTestSuite) DontTestJailWhisper() { */ func (s *WhisperJailTestSuite) TestEncryptedAnonymousMessage() { - s.StartTestBackend(params.RopstenNetworkID) + s.StartTestBackend(params.StatusChainNetworkID) defer s.StopTestBackend() accountKey2, accountKey2Hex, err := s.GetAccountKey(TestConfig.Account2) diff --git a/testing/testing.go b/testing/testing.go index 374b55a9a..53dfbaabc 100644 --- a/testing/testing.go +++ b/testing/testing.go @@ -71,6 +71,15 @@ func LoadFromFile(filename string) string { // EnsureNodeSync waits until node synchronzation is done to continue // with tests afterwards. Returns an error in case of a timeout. func EnsureNodeSync(nodeManager common.NodeManager) error { + nc, err := nodeManager.NodeConfig() + if err != nil { + return errors.New("can't retrieve NodeConfig") + } + // Don't wait for any blockchain sync for the local private chain as blocks are never mined. + if nc.NetworkID == params.StatusChainNetworkID { + return nil + } + les, err := nodeManager.LightEthereumService() if err != nil { return err @@ -81,7 +90,7 @@ func EnsureNodeSync(nodeManager common.NodeManager) error { timeouter := time.NewTimer(20 * time.Minute) defer timeouter.Stop() - ticker := time.NewTicker(time.Second) + ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for {