From ffaf58f0a98bd987bbe76e8669bb22c405dcd62a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 6 May 2016 12:40:23 +0300 Subject: cmd, console: split off the console into a reusable package --- console/console_test.go | 283 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 console/console_test.go (limited to 'console/console_test.go') diff --git a/console/console_test.go b/console/console_test.go new file mode 100644 index 000000000..5d38331e8 --- /dev/null +++ b/console/console_test.go @@ -0,0 +1,283 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package console + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "math/big" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/internal/jsre" + "github.com/ethereum/go-ethereum/node" +) + +const ( + testInstance = "console-tester" + testAddress = "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182" +) + +// hookedPrompter implements UserPrompter to simulate use input via channels. +type hookedPrompter struct { + scheduler chan string +} + +func (p *hookedPrompter) PromptInput(prompt string) (string, error) { + // Send the prompt to the tester + select { + case p.scheduler <- prompt: + case <-time.After(time.Second): + return "", errors.New("prompt timeout") + } + // Retrieve the response and feed to the console + select { + case input := <-p.scheduler: + return input, nil + case <-time.After(time.Second): + return "", errors.New("input timeout") + } +} + +func (p *hookedPrompter) PromptPassword(prompt string) (string, error) { + return "", errors.New("not implemented") +} +func (p *hookedPrompter) PromptConfirm(prompt string) (bool, error) { + return false, errors.New("not implemented") +} +func (p *hookedPrompter) SetScrollHistory(history []string) {} +func (p *hookedPrompter) SetWordCompleter(completer WordCompleter) {} + +// tester is a console test environment for the console tests to operate on. +type tester struct { + workspace string + stack *node.Node + ethereum *eth.Ethereum + console *Console + input *hookedPrompter + output *bytes.Buffer + + lastConfirm string +} + +// newTester creates a test environment based on which the console can operate. +// Please ensure you call Close() on the returned tester to avoid leaks. +func newTester(t *testing.T, confOverride func(*eth.Config)) *tester { + // Create a temporary storage for the node keys and initialize it + workspace, err := ioutil.TempDir("", "console-tester-") + if err != nil { + t.Fatalf("failed to create temporary keystore: %v", err) + } + accman := accounts.NewPlaintextManager(filepath.Join(workspace, "keystore")) + + // Create a networkless protocol stack and start an Ethereum service within + stack, err := node.New(&node.Config{DataDir: workspace, Name: testInstance, NoDiscovery: true}) + if err != nil { + t.Fatalf("failed to create node: %v", err) + } + ethConf := ð.Config{ + ChainConfig: &core.ChainConfig{HomesteadBlock: new(big.Int)}, + Etherbase: common.HexToAddress(testAddress), + AccountManager: accman, + PowTest: true, + } + if confOverride != nil { + confOverride(ethConf) + } + if err = stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { return eth.New(ctx, ethConf) }); err != nil { + t.Fatalf("failed to register Ethereum protocol: %v", err) + } + // Start the node and assemble the JavaScript console around it + if err = stack.Start(); err != nil { + t.Fatalf("failed to start test stack: %v", err) + } + client, err := stack.Attach() + if err != nil { + t.Fatalf("failed to attach to node: %v", err) + } + prompter := &hookedPrompter{scheduler: make(chan string)} + printer := new(bytes.Buffer) + + console, err := New(Config{ + DataDir: stack.DataDir(), + DocRoot: "testdata", + Client: client, + Prompter: prompter, + Printer: printer, + Preload: []string{"preload.js"}, + }) + if err != nil { + t.Fatalf("failed to create JavaScript console: %v", err) + } + // Create the final tester and return + var ethereum *eth.Ethereum + stack.Service(ðereum) + + return &tester{ + workspace: workspace, + stack: stack, + ethereum: ethereum, + console: console, + input: prompter, + output: printer, + } +} + +// Close cleans up any temporary data folders and held resources. +func (env *tester) Close(t *testing.T) { + if err := env.console.Stop(false); err != nil { + t.Errorf("failed to stop embedded console: %v", err) + } + if err := env.stack.Stop(); err != nil { + t.Errorf("failed to stop embedded node: %v", err) + } + os.RemoveAll(env.workspace) +} + +// Tests that the node lists the correct welcome message, notably that it contains +// the instance name, coinbase account, block number, data directory and supported +// console modules. +func TestWelcome(t *testing.T) { + tester := newTester(t, nil) + defer tester.Close(t) + + tester.console.Welcome() + + output := string(tester.output.Bytes()) + if want := "Welcome"; !strings.Contains(output, want) { + t.Fatalf("console output missing welcome message: have\n%s\nwant also %s", output, want) + } + if want := fmt.Sprintf("instance: %s", testInstance); !strings.Contains(output, want) { + t.Fatalf("console output missing instance: have\n%s\nwant also %s", output, want) + } + if want := fmt.Sprintf("coinbase: %s", testAddress); !strings.Contains(output, want) { + t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want) + } + if want := "at block: 0"; !strings.Contains(output, want) { + t.Fatalf("console output missing sync status: have\n%s\nwant also %s", output, want) + } + if want := fmt.Sprintf("datadir: %s", tester.workspace); !strings.Contains(output, want) { + t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want) + } +} + +// Tests that JavaScript statement evaluation works as intended. +func TestEvaluate(t *testing.T) { + tester := newTester(t, nil) + defer tester.Close(t) + + tester.console.Evaluate("2 + 2") + if output := string(tester.output.Bytes()); !strings.Contains(output, "4") { + t.Fatalf("statement evaluation failed: have %s, want %s", output, "4") + } +} + +// Tests that the console can be used in interactive mode. +func TestInteractive(t *testing.T) { + // Create a tester and run an interactive console in the background + tester := newTester(t, nil) + defer tester.Close(t) + + go tester.console.Interactive() + + // Wait for a promt and send a statement back + select { + case <-tester.input.scheduler: + case <-time.After(time.Second): + t.Fatalf("initial prompt timeout") + } + select { + case tester.input.scheduler <- "2+2": + case <-time.After(time.Second): + t.Fatalf("input feedback timeout") + } + // Wait for the second promt and ensure first statement was evaluated + select { + case <-tester.input.scheduler: + case <-time.After(time.Second): + t.Fatalf("secondary prompt timeout") + } + if output := string(tester.output.Bytes()); !strings.Contains(output, "4") { + t.Fatalf("statement evaluation failed: have %s, want %s", output, "4") + } +} + +// Tests that preloaded JavaScript files have been executed before user is given +// input. +func TestPreload(t *testing.T) { + tester := newTester(t, nil) + defer tester.Close(t) + + tester.console.Evaluate("preloaded") + if output := string(tester.output.Bytes()); !strings.Contains(output, "some-preloaded-string") { + t.Fatalf("preloaded variable missing: have %s, want %s", output, "some-preloaded-string") + } +} + +// Tests that JavaScript scripts can be executes from the configured asset path. +func TestExecute(t *testing.T) { + tester := newTester(t, nil) + defer tester.Close(t) + + tester.console.Execute("exec.js") + + tester.console.Evaluate("execed") + if output := string(tester.output.Bytes()); !strings.Contains(output, "some-executed-string") { + t.Fatalf("execed variable missing: have %s, want %s", output, "some-executed-string") + } +} + +// Tests that the JavaScript objects returned by statement executions are properly +// pretty printed instead of just displaing "[object]". +func TestPrettyPrint(t *testing.T) { + tester := newTester(t, nil) + defer tester.Close(t) + + tester.console.Evaluate("obj = {int: 1, string: 'two', list: [3, 3, 3], obj: {null: null, func: function(){}}}") + + // Define some specially formatted fields + var ( + one = jsre.NumberColor("1") + two = jsre.StringColor("\"two\"") + three = jsre.NumberColor("3") + null = jsre.SpecialColor("null") + fun = jsre.FunctionColor("function()") + ) + // Assemble the actual output we're after and verify + want := `{ + int: ` + one + `, + list: [` + three + `, ` + three + `, ` + three + `], + obj: { + null: ` + null + `, + func: ` + fun + ` + }, + string: ` + two + ` +} +` + if output := string(tester.output.Bytes()); output != want { + t.Fatalf("pretty print mismatch: have %s, want %s", output, want) + } +} -- cgit v1.2.3