1// Copyright 2015 The go-ethereum Authors
2// This file is part of the go-ethereum library.
3//
4// The go-ethereum library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Lesser General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// The go-ethereum library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Lesser General Public License for more details.
13//
14// You should have received a copy of the GNU Lesser General Public License
15// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16
17package console
18
19import (
20	"bytes"
21	"errors"
22	"fmt"
23	"io/ioutil"
24	"os"
25	"strings"
26	"testing"
27	"time"
28
29	"github.com/ethereum/go-ethereum/common"
30	"github.com/ethereum/go-ethereum/consensus/ethash"
31	"github.com/ethereum/go-ethereum/console/prompt"
32	"github.com/ethereum/go-ethereum/core"
33	"github.com/ethereum/go-ethereum/eth"
34	"github.com/ethereum/go-ethereum/eth/ethconfig"
35	"github.com/ethereum/go-ethereum/internal/jsre"
36	"github.com/ethereum/go-ethereum/miner"
37	"github.com/ethereum/go-ethereum/node"
38)
39
40const (
41	testInstance = "console-tester"
42	testAddress  = "0x8605cdbbdb6d264aa742e77020dcbc58fcdce182"
43)
44
45// hookedPrompter implements UserPrompter to simulate use input via channels.
46type hookedPrompter struct {
47	scheduler chan string
48}
49
50func (p *hookedPrompter) PromptInput(prompt string) (string, error) {
51	// Send the prompt to the tester
52	select {
53	case p.scheduler <- prompt:
54	case <-time.After(time.Second):
55		return "", errors.New("prompt timeout")
56	}
57	// Retrieve the response and feed to the console
58	select {
59	case input := <-p.scheduler:
60		return input, nil
61	case <-time.After(time.Second):
62		return "", errors.New("input timeout")
63	}
64}
65
66func (p *hookedPrompter) PromptPassword(prompt string) (string, error) {
67	return "", errors.New("not implemented")
68}
69func (p *hookedPrompter) PromptConfirm(prompt string) (bool, error) {
70	return false, errors.New("not implemented")
71}
72func (p *hookedPrompter) SetHistory(history []string)                     {}
73func (p *hookedPrompter) AppendHistory(command string)                    {}
74func (p *hookedPrompter) ClearHistory()                                   {}
75func (p *hookedPrompter) SetWordCompleter(completer prompt.WordCompleter) {}
76
77// tester is a console test environment for the console tests to operate on.
78type tester struct {
79	workspace string
80	stack     *node.Node
81	ethereum  *eth.Ethereum
82	console   *Console
83	input     *hookedPrompter
84	output    *bytes.Buffer
85}
86
87// newTester creates a test environment based on which the console can operate.
88// Please ensure you call Close() on the returned tester to avoid leaks.
89func newTester(t *testing.T, confOverride func(*ethconfig.Config)) *tester {
90	// Create a temporary storage for the node keys and initialize it
91	workspace, err := ioutil.TempDir("", "console-tester-")
92	if err != nil {
93		t.Fatalf("failed to create temporary keystore: %v", err)
94	}
95
96	// Create a networkless protocol stack and start an Ethereum service within
97	stack, err := node.New(&node.Config{DataDir: workspace, UseLightweightKDF: true, Name: testInstance})
98	if err != nil {
99		t.Fatalf("failed to create node: %v", err)
100	}
101	ethConf := &ethconfig.Config{
102		Genesis: core.DeveloperGenesisBlock(15, 11_500_000, common.Address{}),
103		Miner: miner.Config{
104			Etherbase: common.HexToAddress(testAddress),
105		},
106		Ethash: ethash.Config{
107			PowMode: ethash.ModeTest,
108		},
109	}
110	if confOverride != nil {
111		confOverride(ethConf)
112	}
113	ethBackend, err := eth.New(stack, ethConf)
114	if err != nil {
115		t.Fatalf("failed to register Ethereum protocol: %v", err)
116	}
117	// Start the node and assemble the JavaScript console around it
118	if err = stack.Start(); err != nil {
119		t.Fatalf("failed to start test stack: %v", err)
120	}
121	client, err := stack.Attach()
122	if err != nil {
123		t.Fatalf("failed to attach to node: %v", err)
124	}
125	prompter := &hookedPrompter{scheduler: make(chan string)}
126	printer := new(bytes.Buffer)
127
128	console, err := New(Config{
129		DataDir:  stack.DataDir(),
130		DocRoot:  "testdata",
131		Client:   client,
132		Prompter: prompter,
133		Printer:  printer,
134		Preload:  []string{"preload.js"},
135	})
136	if err != nil {
137		t.Fatalf("failed to create JavaScript console: %v", err)
138	}
139	// Create the final tester and return
140	return &tester{
141		workspace: workspace,
142		stack:     stack,
143		ethereum:  ethBackend,
144		console:   console,
145		input:     prompter,
146		output:    printer,
147	}
148}
149
150// Close cleans up any temporary data folders and held resources.
151func (env *tester) Close(t *testing.T) {
152	if err := env.console.Stop(false); err != nil {
153		t.Errorf("failed to stop embedded console: %v", err)
154	}
155	if err := env.stack.Close(); err != nil {
156		t.Errorf("failed to tear down embedded node: %v", err)
157	}
158	os.RemoveAll(env.workspace)
159}
160
161// Tests that the node lists the correct welcome message, notably that it contains
162// the instance name, coinbase account, block number, data directory and supported
163// console modules.
164func TestWelcome(t *testing.T) {
165	tester := newTester(t, nil)
166	defer tester.Close(t)
167
168	tester.console.Welcome()
169
170	output := tester.output.String()
171	if want := "Welcome"; !strings.Contains(output, want) {
172		t.Fatalf("console output missing welcome message: have\n%s\nwant also %s", output, want)
173	}
174	if want := fmt.Sprintf("instance: %s", testInstance); !strings.Contains(output, want) {
175		t.Fatalf("console output missing instance: have\n%s\nwant also %s", output, want)
176	}
177	if want := fmt.Sprintf("coinbase: %s", testAddress); !strings.Contains(output, want) {
178		t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want)
179	}
180	if want := "at block: 0"; !strings.Contains(output, want) {
181		t.Fatalf("console output missing sync status: have\n%s\nwant also %s", output, want)
182	}
183	if want := fmt.Sprintf("datadir: %s", tester.workspace); !strings.Contains(output, want) {
184		t.Fatalf("console output missing coinbase: have\n%s\nwant also %s", output, want)
185	}
186}
187
188// Tests that JavaScript statement evaluation works as intended.
189func TestEvaluate(t *testing.T) {
190	tester := newTester(t, nil)
191	defer tester.Close(t)
192
193	tester.console.Evaluate("2 + 2")
194	if output := tester.output.String(); !strings.Contains(output, "4") {
195		t.Fatalf("statement evaluation failed: have %s, want %s", output, "4")
196	}
197}
198
199// Tests that the console can be used in interactive mode.
200func TestInteractive(t *testing.T) {
201	// Create a tester and run an interactive console in the background
202	tester := newTester(t, nil)
203	defer tester.Close(t)
204
205	go tester.console.Interactive()
206
207	// Wait for a prompt and send a statement back
208	select {
209	case <-tester.input.scheduler:
210	case <-time.After(time.Second):
211		t.Fatalf("initial prompt timeout")
212	}
213	select {
214	case tester.input.scheduler <- "2+2":
215	case <-time.After(time.Second):
216		t.Fatalf("input feedback timeout")
217	}
218	// Wait for the second prompt and ensure first statement was evaluated
219	select {
220	case <-tester.input.scheduler:
221	case <-time.After(time.Second):
222		t.Fatalf("secondary prompt timeout")
223	}
224	if output := tester.output.String(); !strings.Contains(output, "4") {
225		t.Fatalf("statement evaluation failed: have %s, want %s", output, "4")
226	}
227}
228
229// Tests that preloaded JavaScript files have been executed before user is given
230// input.
231func TestPreload(t *testing.T) {
232	tester := newTester(t, nil)
233	defer tester.Close(t)
234
235	tester.console.Evaluate("preloaded")
236	if output := tester.output.String(); !strings.Contains(output, "some-preloaded-string") {
237		t.Fatalf("preloaded variable missing: have %s, want %s", output, "some-preloaded-string")
238	}
239}
240
241// Tests that JavaScript scripts can be executes from the configured asset path.
242func TestExecute(t *testing.T) {
243	tester := newTester(t, nil)
244	defer tester.Close(t)
245
246	tester.console.Execute("exec.js")
247
248	tester.console.Evaluate("execed")
249	if output := tester.output.String(); !strings.Contains(output, "some-executed-string") {
250		t.Fatalf("execed variable missing: have %s, want %s", output, "some-executed-string")
251	}
252}
253
254// Tests that the JavaScript objects returned by statement executions are properly
255// pretty printed instead of just displaying "[object]".
256func TestPrettyPrint(t *testing.T) {
257	tester := newTester(t, nil)
258	defer tester.Close(t)
259
260	tester.console.Evaluate("obj = {int: 1, string: 'two', list: [3, 3, 3], obj: {null: null, func: function(){}}}")
261
262	// Define some specially formatted fields
263	var (
264		one   = jsre.NumberColor("1")
265		two   = jsre.StringColor("\"two\"")
266		three = jsre.NumberColor("3")
267		null  = jsre.SpecialColor("null")
268		fun   = jsre.FunctionColor("function()")
269	)
270	// Assemble the actual output we're after and verify
271	want := `{
272  int: ` + one + `,
273  list: [` + three + `, ` + three + `, ` + three + `],
274  obj: {
275    null: ` + null + `,
276    func: ` + fun + `
277  },
278  string: ` + two + `
279}
280`
281	if output := tester.output.String(); output != want {
282		t.Fatalf("pretty print mismatch: have %s, want %s", output, want)
283	}
284}
285
286// Tests that the JavaScript exceptions are properly formatted and colored.
287func TestPrettyError(t *testing.T) {
288	tester := newTester(t, nil)
289	defer tester.Close(t)
290	tester.console.Evaluate("throw 'hello'")
291
292	want := jsre.ErrorColor("hello") + "\n\tat <eval>:1:7(1)\n\n"
293	if output := tester.output.String(); output != want {
294		t.Fatalf("pretty error mismatch: have %s, want %s", output, want)
295	}
296}
297
298// Tests that tests if the number of indents for JS input is calculated correct.
299func TestIndenting(t *testing.T) {
300	testCases := []struct {
301		input               string
302		expectedIndentCount int
303	}{
304		{`var a = 1;`, 0},
305		{`"some string"`, 0},
306		{`"some string with (parenthesis`, 0},
307		{`"some string with newline
308		("`, 0},
309		{`function v(a,b) {}`, 0},
310		{`function f(a,b) { var str = "asd("; };`, 0},
311		{`function f(a) {`, 1},
312		{`function f(a, function(b) {`, 2},
313		{`function f(a, function(b) {
314		     var str = "a)}";
315		  });`, 0},
316		{`function f(a,b) {
317		   var str = "a{b(" + a, ", " + b;
318		   }`, 0},
319		{`var str = "\"{"`, 0},
320		{`var str = "'("`, 0},
321		{`var str = "\\{"`, 0},
322		{`var str = "\\\\{"`, 0},
323		{`var str = 'a"{`, 0},
324		{`var obj = {`, 1},
325		{`var obj = { {a:1`, 2},
326		{`var obj = { {a:1}`, 1},
327		{`var obj = { {a:1}, b:2}`, 0},
328		{`var obj = {}`, 0},
329		{`var obj = {
330			a: 1, b: 2
331		}`, 0},
332		{`var test = }`, -1},
333		{`var str = "a\""; var obj = {`, 1},
334	}
335
336	for i, tt := range testCases {
337		counted := countIndents(tt.input)
338		if counted != tt.expectedIndentCount {
339			t.Errorf("test %d: invalid indenting: have %d, want %d", i, counted, tt.expectedIndentCount)
340		}
341	}
342}
343