turbo-editors/turbo-corepublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo Core f3ade8d · on v1.0.1 · k33g · 9h ago
parameters_test.go · 284 lines · 9.2 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
package app

import (
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/gdamore/tcell/v2"

	"rickub.com/turbo-editors/turbo-core/tools"
	"rickub.com/turbo-editors/turbo-core/ui"
)

// parametersBox returns the parameters dialog on screen, failing the test when
// there is none.
func parametersBox(t *testing.T, a *App) *ui.Dialog {
	t.Helper()

	dialog := a.TopModal()
	if dialog == nil {
		t.Fatal("no dialog opened; the command must not run before its values are given")
	}
	return dialog
}

// typeInto sends a run of characters to the dialog in front.
func typeInto(a *App, text string) {
	for _, r := range text {
		a.handle(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))
	}
}

// answer types a value into the first field and presses Enter on OK.
func answer(t *testing.T, a *App, value string) {
	t.Helper()

	parametersBox(t, a)
	typeInto(a, value)
	a.handle(tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone))
}

func TestAToolWithAPlaceholderAsksBeforeItRuns(t *testing.T) {
	// The whole point: nothing is started until the value is given, so a
	// half-formed command never reaches the shell.
	a, _ := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "~I~nit", Command: "touch {{file name}}", Output: tools.OutputPopup})

	dialog := parametersBox(t, a)
	if got := dialog.Title(); got != "Init" {
		t.Errorf("the box is titled %q, want the tool's name without its hot-key markers", got)
	}
	if a.running != nil {
		t.Error("the command started before the box was answered")
	}
}

func TestAToolWithNoPlaceholderRunsStraightAway(t *testing.T) {
	a, _ := newToolsApp(t, "")

	runAndDrain(t, a, tools.Tool{Name: "Echo", Command: "echo plain", Output: tools.OutputPopup})

	if a.running == nil {
		t.Fatal("an ordinary tool did not run")
	}
	if lines := a.running.run.Lines(); len(lines) == 0 || lines[0] != "plain" {
		t.Errorf("the command printed %v, want plain", lines)
	}
}

func TestAnsweringTheBoxRunsTheFilledCommand(t *testing.T) {
	a, _ := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "Init", Command: "echo {{message}}", Output: tools.OutputPopup})
	answer(t, a, "hello there")

	waitForLoopTurn(t, a, "the filled command to finish", func() bool {
		return a.running != nil && a.runningFinished()
	})

	lines := a.running.run.Lines()
	if len(lines) == 0 || lines[0] != "hello there" {
		t.Fatalf("the command printed %v, want %q — the value did not reach it", lines, "hello there")
	}
	// Shell-quoted, so the space stayed inside one argument rather than
	// becoming two.
	if got := a.running.run.Command(); !strings.Contains(got, "'hello there'") {
		t.Errorf("the command was %q, want the value quoted", got)
	}
}

func TestCancellingTheBoxRunsNothing(t *testing.T) {
	a, _ := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "Init", Command: "touch {{name}}", Output: tools.OutputPopup})

	parametersBox(t, a)
	typeInto(a, "cancelled.txt")
	a.handle(tcell.NewEventKey(tcell.KeyEscape, 0, tcell.ModNone))

	if a.Modals() != 0 {
		t.Fatal("Escape left the box open")
	}
	if a.running != nil {
		t.Error("the command ran although the box was cancelled")
	}
	if _, err := os.Stat("cancelled.txt"); err == nil {
		t.Error("the cancelled command created its file")
	}
}

func TestTheValuesAreRememberedForTheNextRun(t *testing.T) {
	// Running the same parameterised command twice should not mean typing the
	// same thing twice.
	a, _ := newToolsApp(t, "")
	tool := tools.Tool{Name: "Init", Command: "echo {{message}}", Output: tools.OutputPopup}

	a.RunTool(tool)
	answer(t, a, "remembered")
	waitForLoopTurn(t, a, "the first run to finish", func() bool {
		return a.running != nil && a.runningFinished()
	})
	a.finishRun()

	a.RunTool(tool)

	parametersBox(t, a)
	if got := a.toolValues[tool.Name]["message"]; got != "remembered" {
		t.Fatalf("the remembered value is %q, want %q", got, "remembered")
	}
	// Enter alone, with nothing typed, must re-run what was typed before.
	a.handle(tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone))
	waitForLoopTurn(t, a, "the second run to finish", func() bool {
		return a.running != nil && a.runningFinished()
	})
	if lines := a.running.run.Lines(); len(lines) == 0 || lines[0] != "remembered" {
		t.Errorf("the second run printed %v, want the remembered value", lines)
	}
}

func TestValuesAreRememberedPerToolAndNotShared(t *testing.T) {
	a, _ := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "First", Command: "echo {{message}}", Output: tools.OutputPopup})
	answer(t, a, "one")
	waitForLoopTurn(t, a, "the first tool to finish", func() bool { return a.running != nil && a.runningFinished() })
	a.finishRun()

	a.RunTool(tools.Tool{Name: "Second", Command: "echo {{message}}", Output: tools.OutputPopup})

	parametersBox(t, a)
	if got := a.toolValues["Second"]["message"]; got != "" {
		t.Errorf("the second tool starts with %q; a value belongs to the tool it was typed for", got)
	}
}

func TestNothingIsWrittenToTheProjectDirectory(t *testing.T) {
	// The values live for the session only. A value somebody typed this
	// afternoon is not a decision the project made.
	a, root := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "Init", Command: "echo {{message}}", Output: tools.OutputPopup})
	answer(t, a, "not persisted")
	waitForLoopTurn(t, a, "the command to finish", func() bool { return a.running != nil && a.runningFinished() })

	if entries, err := os.ReadDir(filepath.Join(root, testProfile().ProjectDir())); err == nil {
		for _, entry := range entries {
			if entry.Name() != tools.FileName {
				t.Errorf("running a parameterised tool left %q behind", entry.Name())
			}
		}
	}
}

func TestSeveralPlaceholdersGetAFieldEach(t *testing.T) {
	a, _ := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "Copy", Command: "echo {{from}} {{to}}", Output: tools.OutputPopup})

	parametersBox(t, a)
	// Tab moves between the fields; type into each in turn.
	typeInto(a, "here")
	a.handle(tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone))
	typeInto(a, "there")
	a.handle(tcell.NewEventKey(tcell.KeyEnter, 0, tcell.ModNone))

	waitForLoopTurn(t, a, "the command to finish", func() bool { return a.running != nil && a.runningFinished() })

	if lines := a.running.run.Lines(); len(lines) == 0 || lines[0] != "here there" {
		t.Errorf("the command printed %v, want both values", lines)
	}
}

func TestARawPlaceholderReachesTheShellUnquoted(t *testing.T) {
	// The escape hatch, end to end: one field standing for several arguments.
	a, _ := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "Echo", Command: "echo {{words...}}", Output: tools.OutputPopup})
	answer(t, a, "a b c")

	waitForLoopTurn(t, a, "the command to finish", func() bool { return a.running != nil && a.runningFinished() })

	if got := a.running.run.Command(); got != "echo a b c" {
		t.Errorf("the command was %q, want the value verbatim", got)
	}
}

func TestATerminalToolIsAlsoAskedFirst(t *testing.T) {
	// The other output destination takes the same path, and the window's title
	// is the filled command rather than the one with braces in it.
	a, _ := newToolsApp(t, "")

	a.RunTool(tools.Tool{Name: "Run", Command: "echo {{message}}", Output: tools.OutputTerminal})
	parametersBox(t, a)
	answer(t, a, "in a terminal")

	window := a.Desktop().Active()
	if window == nil {
		t.Fatal("no terminal window opened")
	}
	if got := window.Title(); !strings.Contains(got, "'in a terminal'") {
		t.Errorf("the window is titled %q, want the filled command", got)
	}
}

func TestATallDemandIsRefusedRatherThanShownOffScreen(t *testing.T) {
	// A dialog whose OK button is below the bottom of the screen cannot be
	// answered, which is worse than being told it will not fit.
	a, _ := newToolsApp(t, "")
	command := "echo"
	for i := range MaxParameterFields(24) + 1 {
		command += " {{value " + string(rune('a'+i)) + "}}"
	}

	a.RunTool(tools.Tool{Name: "Too many", Command: command, Output: tools.OutputPopup})

	dialog := a.TopModal()
	if dialog == nil {
		t.Fatal("nothing was shown at all")
	}
	if got := dialog.Title(); got != a.toolsMenuName() {
		t.Errorf("the box is titled %q, want a message from the toolchain menu", got)
	}
	if a.running != nil {
		t.Error("the command ran although its values were never given")
	}
}

func TestMaxParameterFieldsGrowsWithTheScreen(t *testing.T) {
	if got := MaxParameterFields(24); got != 9 {
		t.Errorf("MaxParameterFields(24) = %d, want 9", got)
	}
	if MaxParameterFields(50) <= MaxParameterFields(24) {
		t.Error("a taller screen must fit more fields")
	}
	// A screen too small for even one still offers one, because refusing every
	// parameterised tool on a short terminal is worse than a clipped box.
	if got := MaxParameterFields(4); got != 1 {
		t.Errorf("MaxParameterFields(4) = %d, want 1", got)
	}
}

func TestTheBoxStartsFromWhatWasRemembered(t *testing.T) {
	box := NewParametersDialog("Init", []string{"module"}, map[string]string{"module": "example.com/mine"}, ui.Rect{W: 80, H: 24})

	if got := box.Values()["module"]; got != "example.com/mine" {
		t.Errorf("the field holds %q, want the remembered value", got)
	}
}

func TestTheBoxAsksForEveryLabelItWasGiven(t *testing.T) {
	box := NewParametersDialog("Copy", []string{"from", "to"}, nil, ui.Rect{W: 80, H: 24})

	values := box.Values()
	if len(values) != 2 {
		t.Fatalf("Values() = %v, want one entry per label", values)
	}
	for _, label := range []string{"from", "to"} {
		if _, asked := values[label]; !asked {
			t.Errorf("Values() has no entry for %q", label)
		}
	}
}