How is test input passed to shell program in testing environment?

I’m stuck on Stage #GM9. I use Go for the challenge.

I just refactored my code to use the terminal raw mode to capture every keystroke on stdin. Upon ‘\r’ or ‘\n’ or on receiving the error io.EOF, I consider the input provided by the user as “entered” and restore the normal terminal mode. When I submit my program to move on to the next stage, the tests fail with the behavior I cannot reproduce when I run the same commands locally. Compare:

Local interactive mode

❯ ./your_program.sh
$ echo "world script"world script
world scriptworld script
$

Remote test output on submission

[your-program] $ echo "world script"world script
[tester::#TG6] Output does not match expected value.
[tester::#TG6] Expected: "$ echo "world script""
[tester::#TG6] Received: "$ echo "world script"world script"

Another example of test output:

[tester::#GP4] Running tests for Stage #GP4 (Navigation - The cd builtin: Home directory)
[tester::#GP4] Running ./your_program.sh
[your-program] $ cd /tmp/raspberry/orange/pear
[your-program] $ pwd
[your-program] /tmp/raspberry/orange/pear
[tester::#GP4] Received current working directory response
[your-program] $ cd ~
[your-program] $ pwd
[tester::#GP4] Output does not match expected value.
[tester::#GP4] Expected: "$ pwd"
[tester::#GP4] Received: "$ pwd/tmp/strawberry/mango/mango"
[your-program]                                 $
[tester::#GP4] Assertion failed.
[tester::#GP4] Test failed

It appears as if the normal terminal mode is not restored.

The discrepancy in the behavior makes me think the test input is injected into the tested shell program. My question is: how? Is test input passed to my shell program by redirecting/piping it?

My input reading function:

func readInput(inputCh chan string) {
	var err error
	var oldState *term.State
	success := false

	oldState, err = term.MakeRaw(int(os.Stdin.Fd()))
	if err != nil {
		panic(err)
	}

	buf := make([]byte, 1)
	input := []byte{}

	defer func() {
		input = append(input, '\n')
		if success {
			inputCh <- string(input)
		}
		fmt.Printf("\r\n")
		err := term.Restore(int(os.Stdin.Fd()), oldState)
		if err != nil {
			panic(err)
		}
		close(inputCh)
	}()

	for {
		_, err = os.Stdin.Read(buf)
		if err != nil {
			if errors.Is(err, io.EOF) {
				success = true
				return
			} else {
				panic(err)
			}
		}
		b := buf[0]

		switch b {
		case sigint:
			fmt.Printf("^C")
			return
		case cariageReturn, newLine:
			success = true
			return
		case tab:
			success = true
			if cmpl, ok := autocomplete(input); ok {
				clearPrompt()

				input = cmpl
				input = append(input, ' ')

				for i := range input {
					fmt.Printf("%c", input[i])
				}
			}
		case del:
			if len(input) > 0 {
				fmt.Print("\x1b[D \x1b[D")
				input = input[:len(input)-1]
			}
			continue
		default:
			fmt.Printf("%c", b)
			input = append(input, b)
		}
	}
}

Source code: go-shell/cmd/myshell/main.go at master · Kristina-Pianykh/go-shell · GitHub

I’ll take a look and get back to you by the end of the week.

1 Like

Hi @Kristina-Pianykh I tried running your code locally, but it seems that it’s not handling \n or \r properly:

My question is: how? Is test input passed to my shell program by redirecting/piping it?

To be honest, I’m not entirely sure about the details here. Just for context, our tester has been validated against several real-world shells, so it’s highly unlikely that the specific input method is causing issues.

Here’s the tester code in case you want to dig deeper:

Thank you so much for having a look, I really appreciate it! I’ll have a look at the testing repo a little later :slight_smile:

What terminal/shell did you use to launch from though?
I could reproduce the behavior on the native Linux console but not in Ghostty/kitty when launched from zsh or bash. After fixing a few things, the output on the native Linux console is as expected but the tests keep failing nonetheless (see the screenshot attached). I still cannot reproduce it locally.

Solved it, the problem was super hard to catch and reproduce and had to do with racing gorutines. Sometimes the REPL would go into the next iteration on fast exit (typically on command not found) before the terminal is transferred back into normal mode in a separate goroutine. Thanks again for your time @andy1li !

1 Like

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.