diff --git a/internal/assertions/multi_line_assertion.go b/internal/assertions/multi_line_assertion.go index f5a979b..654e418 100644 --- a/internal/assertions/multi_line_assertion.go +++ b/internal/assertions/multi_line_assertion.go @@ -3,54 +3,57 @@ package assertions import ( "fmt" "regexp" - "strings" - - "github.com/codecrafters-io/shell-tester/internal/utils" - virtual_terminal "github.com/codecrafters-io/shell-tester/internal/vt" ) // MultiLineAssertion asserts that multiple lines of output matches against a given array of strings // Or a multi-line regex pattern(s) type MultiLineAssertion struct { - // ExpectedOutput is the array of expected output strings to match against - ExpectedOutput []string - - // FallbackPatterns is a list of regex patterns to match against. This is useful to handle shell-specific variable behaviour - FallbackPatterns []*regexp.Regexp -} - -func (a MultiLineAssertion) Inspect() string { - return fmt.Sprintf("MultiLineAssertion (%q)", a.ExpectedOutput) + SingleLineAssertions []SingleLineAssertion } -func (a MultiLineAssertion) Run(screenState [][]string, startRowIndex int) (processedRowCount int, err *AssertionError) { - if len(a.ExpectedOutput) == 0 { - panic("CodeCrafters Internal Error: ExpectedOutput must be provided") +func NewMultiLineAssertion(expectedOutput []string) MultiLineAssertion { + // No way to add fallbackPatterns through this constructor + singleLineAssertions := []SingleLineAssertion{} + for _, expectedLine := range expectedOutput { + singleLineAssertions = append(singleLineAssertions, SingleLineAssertion{ + ExpectedOutput: expectedLine, + }) } - totalRows := len(a.ExpectedOutput) - rawRows := screenState[startRowIndex : startRowIndex+totalRows] - cleanedRows := []string{} - for _, rawRow := range rawRows { - cleanedRows = append(cleanedRows, virtual_terminal.BuildCleanedRow(rawRow)) + return MultiLineAssertion{ + SingleLineAssertions: singleLineAssertions, } - cleanedRowsString := strings.Join(cleanedRows, "\n") - expectedOutputString := strings.Join(a.ExpectedOutput, "\n") +} - for _, pattern := range a.FallbackPatterns { - if pattern.Match([]byte(cleanedRowsString)) { - return len(a.ExpectedOutput), nil - } +func NewEmptyMultiLineAssertion() MultiLineAssertion { + return MultiLineAssertion{ + SingleLineAssertions: []SingleLineAssertion{}, } +} + +// AddSingleLineAssertion is the recommended way to add single line assertions +// When they contain fallbackPatterns +func (a *MultiLineAssertion) AddSingleLineAssertion(expectedOutput string, fallbackPatterns []*regexp.Regexp) *MultiLineAssertion { + a.SingleLineAssertions = append(a.SingleLineAssertions, SingleLineAssertion{ + ExpectedOutput: expectedOutput, + FallbackPatterns: fallbackPatterns, + }) + return a +} + +func (a *MultiLineAssertion) Inspect() string { + return fmt.Sprintf("MultiLineAssertion (%q)", a.SingleLineAssertions) +} + +func (a *MultiLineAssertion) Run(screenState [][]string, startRowIndex int) (processedRowCount int, err *AssertionError) { + totalProcessedRowCount := 0 - if cleanedRowsString != expectedOutputString { - detailedErrorMessage := utils.BuildColoredErrorMessage(expectedOutputString, cleanedRowsString) - return 0, &AssertionError{ - StartRowIndex: startRowIndex, - ErrorRowIndex: startRowIndex, - Message: "Output does not match expected value.\n" + detailedErrorMessage, + for _, singleLineAssertion := range a.SingleLineAssertions { + processedRowCount, err = singleLineAssertion.Run(screenState, startRowIndex+totalProcessedRowCount) + if err != nil { + return totalProcessedRowCount, err } - } else { - return len(a.ExpectedOutput), nil + totalProcessedRowCount += processedRowCount } + return totalProcessedRowCount, nil } diff --git a/internal/stage_r1.go b/internal/stage_r1.go index 2f3028d..ad385c2 100644 --- a/internal/stage_r1.go +++ b/internal/stage_r1.go @@ -7,6 +7,7 @@ import ( "regexp" "slices" + "github.com/codecrafters-io/shell-tester/internal/assertions" "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" "github.com/codecrafters-io/shell-tester/internal/shell_executable" "github.com/codecrafters-io/shell-tester/internal/test_cases" @@ -66,10 +67,9 @@ func testR1(stageHarness *test_case_harness.TestCaseHarness) error { } multiLineTestCase := test_cases.CommandWithMultilineResponseTestCase{ - Command: command2, - ExpectedOutput: randomWords, - FallbackPatterns: nil, - SuccessMessage: "✓ Received redirected file content", + Command: command2, + MultiLineAssertion: assertions.NewMultiLineAssertion(randomWords), + SuccessMessage: "✓ Received redirected file content", } if err := multiLineTestCase.Run(asserter, shell, logger); err != nil { return err diff --git a/internal/stage_r3.go b/internal/stage_r3.go index f8599b9..8f4356c 100644 --- a/internal/stage_r3.go +++ b/internal/stage_r3.go @@ -5,6 +5,7 @@ import ( "path" "slices" + "github.com/codecrafters-io/shell-tester/internal/assertions" "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" "github.com/codecrafters-io/shell-tester/internal/shell_executable" "github.com/codecrafters-io/shell-tester/internal/test_cases" @@ -64,10 +65,9 @@ func testR3(stageHarness *test_case_harness.TestCaseHarness) error { } responseTestCase := test_cases.CommandWithMultilineResponseTestCase{ - Command: command2, - ExpectedOutput: randomWords, - FallbackPatterns: nil, - SuccessMessage: "✓ Received redirected file content", + Command: command2, + MultiLineAssertion: assertions.NewMultiLineAssertion(randomWords), + SuccessMessage: "✓ Received redirected file content", } if err := responseTestCase.Run(asserter, shell, logger); err != nil { @@ -98,10 +98,9 @@ func testR3(stageHarness *test_case_harness.TestCaseHarness) error { } responseTestCase = test_cases.CommandWithMultilineResponseTestCase{ - Command: command6, - ExpectedOutput: []string{message1, message2}, - FallbackPatterns: nil, - SuccessMessage: "✓ Received redirected file content", + Command: command6, + MultiLineAssertion: assertions.NewMultiLineAssertion([]string{message1, message2}), + SuccessMessage: "✓ Received redirected file content", } if err := responseTestCase.Run(asserter, shell, logger); err != nil { return err @@ -129,10 +128,9 @@ func testR3(stageHarness *test_case_harness.TestCaseHarness) error { } responseTestCase = test_cases.CommandWithMultilineResponseTestCase{ - Command: command9, - ExpectedOutput: append([]string{"List of files:"}, randomWords...), - FallbackPatterns: nil, - SuccessMessage: "✓ Received redirected file content", + Command: command9, + MultiLineAssertion: assertions.NewMultiLineAssertion(append([]string{"List of files:"}, randomWords...)), + SuccessMessage: "✓ Received redirected file content", } if err := responseTestCase.Run(asserter, shell, logger); err != nil { return err diff --git a/internal/stage_r4.go b/internal/stage_r4.go index 59c1634..944a2a2 100644 --- a/internal/stage_r4.go +++ b/internal/stage_r4.go @@ -131,17 +131,20 @@ func testR4(stageHarness *test_case_harness.TestCaseHarness) error { "cat: nonexistent: No such file or directory", "ls: nonexistent: No such file or directory", } + linuxLSErrorMessage := "ls: cannot access 'nonexistent': No such file or directory" + linuxLSErrorMessageRegex := []*regexp.Regexp{regexp.MustCompile(fmt.Sprintf("^%s$", linuxLSErrorMessage))} alpineCatErrorMessage := "cat: can't open 'nonexistent': No such file or directory" - essorMessagesInFileRegex := []*regexp.Regexp{ - regexp.MustCompile(fmt.Sprintf("^%s\n%s$", errorMessagesInFile[0], linuxLSErrorMessage)), - regexp.MustCompile(fmt.Sprintf("^%s\n%s$", alpineCatErrorMessage, errorMessagesInFile[1])), - } + alpineCatErrorMessageRegex := []*regexp.Regexp{regexp.MustCompile(fmt.Sprintf("^%s$", alpineCatErrorMessage))} + + multiLineAssertion := assertions.NewEmptyMultiLineAssertion() + multiLineAssertion.AddSingleLineAssertion(errorMessagesInFile[0], alpineCatErrorMessageRegex) + multiLineAssertion.AddSingleLineAssertion(errorMessagesInFile[1], linuxLSErrorMessageRegex) + multiLineResponseTestCase := test_cases.CommandWithMultilineResponseTestCase{ - Command: command7, - ExpectedOutput: errorMessagesInFile, - FallbackPatterns: essorMessagesInFileRegex, - SuccessMessage: "✓ Received redirected file content", + Command: command7, + MultiLineAssertion: multiLineAssertion, + SuccessMessage: "✓ Received redirected file content", } if err := multiLineResponseTestCase.Run(asserter, shell, logger); err != nil { return err diff --git a/internal/test_cases/command_reflection_test_case.go b/internal/test_cases/command_reflection_test_case.go index 8465299..1cb5ff9 100644 --- a/internal/test_cases/command_reflection_test_case.go +++ b/internal/test_cases/command_reflection_test_case.go @@ -46,7 +46,7 @@ func (t CommandReflectionTestCase) Run(asserter *logged_shell_asserter.LoggedShe } if !skipSuccessMessage { - logger.Successf(t.SuccessMessage) + logger.Successf("%s", t.SuccessMessage) } return nil } diff --git a/internal/test_cases/command_response_test_case.go b/internal/test_cases/command_response_test_case.go index 1b0e06b..667a33a 100644 --- a/internal/test_cases/command_response_test_case.go +++ b/internal/test_cases/command_response_test_case.go @@ -48,6 +48,6 @@ func (t CommandResponseTestCase) Run(asserter *logged_shell_asserter.LoggedShell return err } - logger.Successf(t.SuccessMessage) + logger.Successf("%s", t.SuccessMessage) return nil } diff --git a/internal/test_cases/command_with_multiline_response_test_case.go b/internal/test_cases/command_with_multiline_response_test_case.go index d4a12aa..b03ba1d 100644 --- a/internal/test_cases/command_with_multiline_response_test_case.go +++ b/internal/test_cases/command_with_multiline_response_test_case.go @@ -2,7 +2,6 @@ package test_cases import ( "fmt" - "regexp" "github.com/codecrafters-io/shell-tester/internal/assertions" "github.com/codecrafters-io/shell-tester/internal/logged_shell_asserter" @@ -14,11 +13,8 @@ type CommandWithMultilineResponseTestCase struct { // Command is the command to send to the shell Command string - // ExpectedOutput is the expected output string to match against - ExpectedOutput []string - - // FallbackPatterns is a list of regex patterns to match against - FallbackPatterns []*regexp.Regexp + // MultiLineAssertion is the assertion to run + MultiLineAssertion assertions.MultiLineAssertion // SuccessMessage is the message to log in case of success SuccessMessage string @@ -34,15 +30,12 @@ func (t CommandWithMultilineResponseTestCase) Run(asserter *logged_shell_asserte ExpectedOutput: commandReflection, }) - asserter.AddAssertion(assertions.MultiLineAssertion{ - ExpectedOutput: t.ExpectedOutput, - FallbackPatterns: t.FallbackPatterns, - }) + asserter.AddAssertion(&t.MultiLineAssertion) if err := asserter.AssertWithPrompt(); err != nil { return err } - logger.Successf(t.SuccessMessage) + logger.Successf("%s", t.SuccessMessage) return nil }