Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance virtual terminal and assertion handling features #71

Merged
merged 25 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
436b4bc
refactor: simplify shell start and prompt assertion logic
ryan-gang Dec 12, 2024
917e900
refactor: streamline shell start and prompt assertion process
ryan-gang Dec 12, 2024
520c789
refactor: consolidate shell start and prompt assertion into a single …
ryan-gang Dec 12, 2024
deb3269
refactor: encapsulate shell start and prompt assertion in a dedicated…
ryan-gang Dec 12, 2024
25364ee
refactor: extract shell start and prompt assertion into a helper func…
ryan-gang Dec 12, 2024
bc64778
refactor: unify shell start and prompt assertion into a single function
ryan-gang Dec 12, 2024
6405017
refactor: simplify shell start and prompt assertion handling
ryan-gang Dec 12, 2024
7a88955
refactor: streamline shell start and prompt assertion into a single f…
ryan-gang Dec 12, 2024
5a4787c
refactor: add logging and shell start assertion functions
ryan-gang Dec 12, 2024
7b0205b
refactor: move screen state validation to assertion collection
ryan-gang Dec 12, 2024
b7ad904
refactor: enhance error messaging in SingleLineAssertion
ryan-gang Dec 13, 2024
22d9c2f
refactor: improve error handling and logging in shell assertion tests
ryan-gang Dec 13, 2024
d256b44
refactor: add ErrorMessage method to AssertionError for improved erro…
ryan-gang Dec 13, 2024
8a72386
refactor: add GetLogger method to ShellExecutable for improved loggin…
ryan-gang Dec 13, 2024
4ca3b44
refactor: simplify error message construction in SingleLineAssertion
ryan-gang Dec 13, 2024
f9e08bd
feat: add fixture setup for no output from shell
ryan-gang Dec 13, 2024
1bce914
chore: remove unused files, and update comments
ryan-gang Dec 13, 2024
a678f78
feat: add escape codes scenario with shell and debug configuration
ryan-gang Dec 13, 2024
f2e956a
refactor: update how we clean our vt row before asserting on it
ryan-gang Dec 13, 2024
b267901
refactor: change error handling in assertion methods to return Assert…
ryan-gang Dec 13, 2024
7526dbc
refactor: update VT_SENTINEL_CHARACTER and adjust column size in NewS…
ryan-gang Dec 13, 2024
8dde85f
refactor: enhance error message in SingleLineAssertion for better cla…
ryan-gang Dec 13, 2024
212a62f
refactor: update VT_SENTINEL_CHARACTER to a new symbol for improved c…
ryan-gang Dec 13, 2024
34a61df
refactor: streamline logging of assertion errors by consolidating row…
ryan-gang Dec 13, 2024
58aa188
refactor: adjust row logging in LogRemainingOutput to handle initial …
ryan-gang Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions internal/assertion_collection/assertion_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (c *AssertionCollection) AddAssertion(assertion assertions.Assertion) {
c.Assertions = append(c.Assertions, assertion)
}

func (c *AssertionCollection) RunWithPromptAssertion(screenState [][]string) error {
func (c *AssertionCollection) RunWithPromptAssertion(screenState [][]string) *assertions.AssertionError {
return c.runWithExtraAssertions(screenState, []assertions.Assertion{
assertions.PromptAssertion{ExpectedPrompt: "$ "},
})
Expand All @@ -33,7 +33,8 @@ func (c *AssertionCollection) RunWithoutPromptAssertion(screenState [][]string)
return c.runWithExtraAssertions(screenState, nil)
}

func (c *AssertionCollection) runWithExtraAssertions(screenState [][]string, extraAssertions []assertions.Assertion) error {
// ToDo: Remove all debug logs
func (c *AssertionCollection) runWithExtraAssertions(screenState [][]string, extraAssertions []assertions.Assertion) *assertions.AssertionError {
allAssertions := append(c.Assertions, extraAssertions...)
currentRowIndex := 0

Expand All @@ -42,6 +43,14 @@ func (c *AssertionCollection) runWithExtraAssertions(screenState [][]string, ext
}

for _, assertion := range allAssertions {
if len(screenState) == 0 {
panic("CodeCrafters internal error: expected screen to have at least one row, but it was empty")
}

if currentRowIndex >= len(screenState) {
panic("CodeCrafters internal error: startRowIndex is larger than screenState rows")
}

processedRowCount, err := assertion.Run(screenState, currentRowIndex)
if err != nil {
if ShouldPrintDebugLogs {
Expand Down
7 changes: 6 additions & 1 deletion internal/assertions/assertion_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@

func (e AssertionError) Error() string {
return `CodeCrafters Internal Error: AssertionError#Error() should not be called`
}
}

// ToDo: Review and possibly remove

Check failure on line 13 in internal/assertions/assertion_error.go

View workflow job for this annotation

GitHub Actions / lint

comment on exported method ErrorMessage should be of the form "ErrorMessage ..." (ST1020)
func (e AssertionError) ErrorMessage() string {
return e.Message
}
9 changes: 0 additions & 9 deletions internal/assertions/prompt_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@ func (t PromptAssertion) Run(screenState [][]string, startRowIndex int) (process
// We don't want to count the processed prompt as a complete row
processedRowCount = 0

// TODO: Move these to assertion collection
if len(screenState) == 0 {
panic("CodeCrafters internal error: Expected screen state to have at least one row")
}

if startRowIndex >= len(screenState) {
panic("CodeCrafters internal error: startRowIndex is larger than screenState rows")
}

rawRow := screenState[startRowIndex] // Could be nil?
cleanedRow := utils.BuildCleanedRow(rawRow)

Expand Down
14 changes: 2 additions & 12 deletions internal/assertions/single_line_assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@ func (a SingleLineAssertion) Inspect() string {
}

func (a SingleLineAssertion) Run(screenState [][]string, startRowIndex int) (processedRowCount int, err *AssertionError) {
// TODO: Move these to assertion collection
if len(screenState) == 0 {
panic("CodeCrafters internal error: expected screen to have at least one row, but it was empty")
}

if startRowIndex >= len(screenState) {
panic("CodeCrafters internal error: startRowIndex is larger than screenState rows")
}

if a.ExpectedOutput == "" {
panic("CodeCrafters Internal Error: ExpectedOutput must be provided")
}
Expand All @@ -44,12 +35,11 @@ func (a SingleLineAssertion) Run(screenState [][]string, startRowIndex int) (pro
}

if cleanedRow != a.ExpectedOutput {
// TODO: Return colored error message when constructing AssertionError
// detailedErrorMessage := BuildColoredErrorMessage(t.expectedPatternExplanation, cleanedRow)
detailedErrorMessage := utils.BuildColoredErrorMessage(a.ExpectedOutput, cleanedRow)
return 0, &AssertionError{
StartRowIndex: startRowIndex,
ErrorRowIndex: startRowIndex,
Message: fmt.Sprintf("Expected %q, got %q", a.ExpectedOutput, cleanedRow),
Message: "Output does not match expected value.\n" + detailedErrorMessage,
}
} else {
return 1, nil
Expand Down
26 changes: 20 additions & 6 deletions internal/logged_shell_asserter/logged_shell_asserter.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ func (a *LoggedShellAsserter) AddAssertion(assertion assertions.Assertion) {
}

func (a *LoggedShellAsserter) Assert() error {
assertFn := func() error {
assertFn := func() *assertions.AssertionError {
return a.AssertionCollection.RunWithPromptAssertion(a.Shell.GetScreenState())
}

if readErr := a.Shell.ReadUntil(utils.AsBool(assertFn)); readErr != nil {
conditionFn := func() bool {
return assertFn() == nil
}

if readErr := a.Shell.ReadUntil(conditionFn); readErr != nil {
if assertionErr := assertFn(); assertionErr != nil {
a.logAssertionError(assertionErr)
a.logAssertionError(*assertionErr)
// TODO: Figure out remaining output in SUCCESS scenario
// asserter.LogRemainingOutput()

Expand Down Expand Up @@ -77,13 +81,23 @@ func (a *LoggedShellAsserter) onAssertionSuccess(startRowIndex int, processedRow
a.lastLoggedRowIndex += processedRowCount
}

func (a *LoggedShellAsserter) logAssertionError(err error) {
// TODO: Log all shell output remaining
func (a *LoggedShellAsserter) logAssertionError(err assertions.AssertionError) {
a.logRows(a.lastLoggedRowIndex, err.ErrorRowIndex)
l := a.Shell.GetLogger()
l.Errorf("%s", err.Message)
a.logRows(err.ErrorRowIndex, len(a.Shell.GetScreenState()))
}

func (a *LoggedShellAsserter) LogRemainingOutput() {
// if we ever call this, before logging anything
// a.lastLoggedRowIndex would be -1, we want it to be 0
startRowIndex := max(0, a.lastLoggedRowIndex)
endRowIndex := len(a.Shell.GetScreenState())
for i := a.lastLoggedRowIndex + 1; i < endRowIndex; i++ {
a.logRows(startRowIndex, endRowIndex)
}

func (a *LoggedShellAsserter) logRows(startRowIndex int, endRowIndex int) {
for i := startRowIndex; i < endRowIndex; i++ {
rawRow := a.Shell.GetScreenState()[i]
cleanedRow := utils.BuildCleanedRow(rawRow)
if len(cleanedRow) > 0 {
Expand Down
5 changes: 5 additions & 0 deletions internal/shell_executable/shell_executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,8 @@

return readerErr
}

// ToDo: Review and possibly remove

Check failure on line 189 in internal/shell_executable/shell_executable.go

View workflow job for this annotation

GitHub Actions / lint

comment on exported method GetLogger should be of the form "GetLogger ..." (ST1020)
func (b *ShellExecutable) GetLogger() *logger.Logger {
return b.logger
}
6 changes: 4 additions & 2 deletions internal/shell_executable/virtual_terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"github.com/gookit/color"
)

const VT_SENTINEL_CHARACTER = "."
const VT_SENTINEL_CHARACTER = ""

// TODO: See if we can remove this wrapper entirely, and only add helpers to fetch rows/data from the vterm instance directly

Check failure on line 10 in internal/shell_executable/virtual_terminal.go

View workflow job for this annotation

GitHub Actions / lint

comment on exported type VirtualTerminal should be of the form "VirtualTerminal ..." (with optional leading article) (ST1021)
type VirtualTerminal struct {
vt *vterm.VTerm
rows int
Expand All @@ -17,9 +17,11 @@
func NewStandardVT() *VirtualTerminal {
// TODO: Check if this affects performance
// This affects performance majorly, improve all functions operating on this
return NewCustomVT(100, 100)
return NewCustomVT(100, 120)
}

// ToDo: Add check if last line gets written to and panic then

func NewCustomVT(rows, cols int) *VirtualTerminal {
return &VirtualTerminal{
vt: vterm.New(rows, cols),
Expand All @@ -39,7 +41,7 @@
return vt.vt.Write(p)
}

// TODO: What if there are tabs, will we have SENTINEL_CHAR in the middle of a row?

Check failure on line 44 in internal/shell_executable/virtual_terminal.go

View workflow job for this annotation

GitHub Actions / lint

comment on exported method GetScreenState should be of the form "GetScreenState ..." (ST1020)
func (vt *VirtualTerminal) GetScreenState(retainColors bool) [][]string {
screenState := make([][]string, vt.rows)
for i := 0; i < vt.rows; i++ {
Expand Down
7 changes: 1 addition & 6 deletions internal/stage1.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,7 @@ func testPrompt(stageHarness *test_case_harness.TestCaseHarness) error {
// ensures that we never accept starter code that wouldn't work in those stages.
shell.Setenv("HOME", randomDir)

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand Down
10 changes: 2 additions & 8 deletions internal/stage2.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ func testMissingCommand(stageHarness *test_case_harness.TestCaseHarness) error {
shell := shell_executable.NewShellExecutable(stageHarness)
asserter := logged_shell_asserter.NewLoggedShellAsserter(shell)

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand All @@ -40,6 +35,5 @@ func testMissingCommand(stageHarness *test_case_harness.TestCaseHarness) error {
return err
}

asserter.LogRemainingOutput()
return nil
return logAndQuit(asserter, nil)
}
10 changes: 2 additions & 8 deletions internal/stage3.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ func testREPL(stageHarness *test_case_harness.TestCaseHarness) error {

numberOfCommands := random.RandomInt(3, 6)

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand All @@ -47,6 +42,5 @@ func testREPL(stageHarness *test_case_harness.TestCaseHarness) error {
}

// Assert() already makes sure that the prompt is present in the last row
asserter.LogRemainingOutput()
return nil
return logAndQuit(asserter, nil)
}
9 changes: 2 additions & 7 deletions internal/stage4.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ func testExit(stageHarness *test_case_harness.TestCaseHarness) error {
shell := shell_executable.NewShellExecutable(stageHarness)
asserter := logged_shell_asserter.NewLoggedShellAsserter(shell)

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand Down Expand Up @@ -83,5 +78,5 @@ func testExit(stageHarness *test_case_harness.TestCaseHarness) error {

logger.Successf("✓ No output after exit command")

return nil
return logAndQuit(asserter, nil)
}
10 changes: 2 additions & 8 deletions internal/stage5.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,7 @@ func testEcho(stageHarness *test_case_harness.TestCaseHarness) error {

numberOfCommands := random.RandomInt(2, 4)

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand All @@ -42,6 +37,5 @@ func testEcho(stageHarness *test_case_harness.TestCaseHarness) error {
}
}

asserter.LogRemainingOutput()
return nil
return logAndQuit(asserter, nil)
}
10 changes: 2 additions & 8 deletions internal/stage6.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@ func testType1(stageHarness *test_case_harness.TestCaseHarness) error {

builtIns := []string{"echo", "exit", "type"}

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand Down Expand Up @@ -56,6 +51,5 @@ func testType1(stageHarness *test_case_harness.TestCaseHarness) error {
}
}

asserter.LogRemainingOutput()
return nil
return logAndQuit(asserter, nil)
}
10 changes: 2 additions & 8 deletions internal/stage7.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,7 @@ func testType2(stageHarness *test_case_harness.TestCaseHarness) error {
return err
}

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand Down Expand Up @@ -86,6 +81,5 @@ func testType2(stageHarness *test_case_harness.TestCaseHarness) error {
}
}

asserter.LogRemainingOutput()
return nil
return logAndQuit(asserter, nil)
}
12 changes: 3 additions & 9 deletions internal/stage8.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ func testRun(stageHarness *test_case_harness.TestCaseHarness) error {
currentPath := os.Getenv("PATH")
shell.Setenv("PATH", fmt.Sprintf("%s:%s", randomDir, currentPath))

if err := shell.Start(); err != nil {
return err
}

// First prompt assertion
if err := asserter.Assert(); err != nil {
if err := startShellAndAssertPrompt(asserter, shell); err != nil {
return err
}

Expand All @@ -51,14 +46,13 @@ func testRun(stageHarness *test_case_harness.TestCaseHarness) error {

testCase := test_cases.CommandResponseTestCase{
Command: strings.Join(command, " "),
ExpectedOutput: fmt.Sprintf("Hello %s! The secret code is %s", randomName, randomCode),
ExpectedOutput: fmt.Sprintf("Hello %s! The secret code is %s.", randomName, randomCode),
FallbackPatterns: nil,
SuccessMessage: "Received expected response",
}
if err := testCase.Run(asserter, shell, logger); err != nil {
return err
}

asserter.LogRemainingOutput()
return nil
return logAndQuit(asserter, nil)
}
7 changes: 7 additions & 0 deletions internal/stages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ func TestStages(t *testing.T) {
StdoutFixturePath: "./test_helpers/fixtures/missing_command/wrong_output",
NormalizeOutputFunc: normalizeTesterOutput,
},
"no_command_fail": {
UntilStageSlug: "cz2",
CodePath: "./test_helpers/scenarios/missing_command/no_output",
ExpectedExitCode: 1,
StdoutFixturePath: "./test_helpers/fixtures/missing_command/no_output",
NormalizeOutputFunc: normalizeTesterOutput,
},
"quoting_pass": {
StageSlugs: []string{"ni6", "tg6", "yt5", "le5", "gu3", "qj0"},
CodePath: "./test_helpers/bash",
Expand Down
Loading
Loading