Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
76 changes: 75 additions & 1 deletion stdlib/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package stdlib

import (
"fmt"
"strings"
"sync"

"github.com/onflow/cadence/ast"
Expand Down Expand Up @@ -328,12 +329,15 @@ func newErrorValue(context interpreter.InvocationContext, err error) interpreter
// Create a 'Error' by calling its constructor.
errorConstructor := getConstructor(context, testErrorTypeName)

// Format the error message in a more readable way
formattedMessage := formatErrorMessageForTest(err)

errorValue, invocationErr := interpreter.InvokeExternally(
context,
errorConstructor,
errorConstructor.Type,
[]interpreter.Value{
interpreter.NewUnmeteredStringValue(err.Error()),
interpreter.NewUnmeteredStringValue(formattedMessage),
},
)

Expand All @@ -344,6 +348,76 @@ func newErrorValue(context interpreter.InvocationContext, err error) interpreter
return errorValue
}

// formatErrorMessageForTest formats error messages to be more readable in tests
func formatErrorMessageForTest(err error) string {
message := err.Error()

// Look for common error patterns and extract key information
if strings.Contains(message, "Execution failed:") {
// Extract just the core error message, not the entire stack trace
lines := strings.Split(message, "\n")
var relevantLines []string
inCodeExcerpt := false

for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}

// Skip the "Execution failed:" prefix
if strings.HasPrefix(line, "Execution failed:") {
continue
}

// Mark code excerpt sections (starts with -->)
if strings.HasPrefix(line, "--> ") {
inCodeExcerpt = true
continue
}

// Skip line numbers and code content within excerpts
if inCodeExcerpt {
// Skip lines that are part of code excerpts
if strings.HasPrefix(line, "|") ||
strings.Contains(line, "^^^") ||
strings.Contains(line, "~~~") ||
(len(line) > 0 && (line[0] >= '0' && line[0] <= '9')) {
continue
}
// If we encounter another error line, reset the flag
if strings.HasPrefix(line, "error:") {
inCodeExcerpt = false
} else {
continue
}
}

// Include error descriptions but limit length
if strings.HasPrefix(line, "error:") ||
strings.Contains(line, "error occurred:") ||
strings.Contains(line, "cannot deploy") ||
strings.Contains(line, "does not conform") {
if len(line) > 200 {
line = line[:200] + "..."
}
relevantLines = append(relevantLines, line)
}
}

if len(relevantLines) > 0 {
return strings.Join(relevantLines, "\n")
}
}

// For other long error messages, truncate and provide context
if len(message) > 500 {
return message[:500] + "..."
}

return message
}

// TestFailedError

type TestFailedError struct {
Expand Down
76 changes: 76 additions & 0 deletions stdlib/test_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,28 @@ var testTypeFailFunctionType = &sema.FunctionType{
Arity: &sema.Arity{Min: 0, Max: 1},
}

// 'Test.assertNoError' function

const testTypeAssertNoErrorFunctionDocString = `
Fails the test-case if the given result has an error.
This function provides better formatted error messages for test failures.
`

const testTypeAssertNoErrorFunctionName = "assertNoError"

var testTypeAssertNoErrorFunctionType = &sema.FunctionType{
Parameters: []sema.Parameter{
{
Label: sema.ArgumentLabelNotRequired,
Identifier: "result",
TypeAnnotation: sema.NewTypeAnnotation(
sema.AnyStructType,
),
},
},
ReturnTypeAnnotation: sema.VoidTypeAnnotation,
}

func testTypeFailFunction(
inter *interpreter.Interpreter,
testContractValue *interpreter.CompositeValue,
Expand All @@ -240,6 +262,48 @@ func testTypeFailFunction(
)
}

func testTypeAssertNoErrorFunction(
inter *interpreter.Interpreter,
testContractValue *interpreter.CompositeValue,
) interpreter.BoundFunctionValue {
return interpreter.NewUnmeteredBoundHostFunctionValue(
inter,
testContractValue,
testTypeAssertNoErrorFunctionType,
func(invocation interpreter.Invocation) interpreter.Value {
result := invocation.Arguments[0]

// Get the error field from the result
if resultValue, ok := result.(interpreter.MemberAccessibleValue); ok {
errorFieldValue := resultValue.GetMember(invocation.InvocationContext, "error")

// Check if error is not nil
if errorFieldValue != interpreter.Nil {
// Extract error message
if errorValue, ok := errorFieldValue.(*interpreter.CompositeValue); ok {
messageValue := errorValue.GetMember(invocation.InvocationContext, "message")
if messageString, ok := messageValue.(*interpreter.StringValue); ok {
// Use formatted error message
formattedMessage := formatErrorMessageForTest(fmt.Errorf("%s", messageString.Str))
panic(&AssertionError{
Message: formattedMessage,
})
}
}

// Fallback if we can't extract the message - use string representation
errorString := errorFieldValue.String()
panic(&AssertionError{
Message: fmt.Sprintf("error: %s", errorString),
})
}
}

return interpreter.Void
},
)
}

// 'Test.expect' function

const testTypeExpectFunctionDocString = `
Expand Down Expand Up @@ -1067,6 +1131,17 @@ func newTestContractType() *TestContractType {
),
)

// Test.assertNoError()
compositeType.Members.Set(
testTypeAssertNoErrorFunctionName,
sema.NewUnmeteredPublicFunctionMember(
compositeType,
testTypeAssertNoErrorFunctionName,
testTypeAssertNoErrorFunctionType,
testTypeAssertNoErrorFunctionDocString,
),
)

// Test.readFile()
compositeType.Members.Set(
testTypeReadFileFunctionName,
Expand Down Expand Up @@ -1290,6 +1365,7 @@ func (t *TestContractType) NewTestContract(
compositeValue.Functions.Set(testTypeAssertFunctionName, testTypeAssertFunction(inter, compositeValue))
compositeValue.Functions.Set(testTypeAssertEqualFunctionName, testTypeAssertEqualFunction(inter, compositeValue))
compositeValue.Functions.Set(testTypeFailFunctionName, testTypeFailFunction(inter, compositeValue))
compositeValue.Functions.Set(testTypeAssertNoErrorFunctionName, testTypeAssertNoErrorFunction(inter, compositeValue))
compositeValue.Functions.Set(testTypeExpectFunctionName, t.expectFunction(inter, compositeValue))
compositeValue.Functions.Set(
testTypeReadFileFunctionName,
Expand Down
123 changes: 123 additions & 0 deletions stdlib/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package stdlib
import (
"errors"
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -2036,6 +2037,128 @@ func TestTestExpect(t *testing.T) {
})
}

func TestTestAssertNoError(t *testing.T) {

t.Parallel()

t.Run("successful result", func(t *testing.T) {
t.Parallel()

script := `
import Test

access(all)
fun test() {
let result = Test.ScriptResult(
status: Test.ResultStatus.succeeded,
returnValue: 42,
error: nil
)

Test.assertNoError(result)
}
`

inter, err := newTestContractInterpreter(t, script)
require.NoError(t, err)

_, err = inter.Invoke("test")
require.NoError(t, err)
})

t.Run("failed result with error", func(t *testing.T) {
t.Parallel()

script := `
import Test

access(all)
fun test() {
let result = Test.ScriptResult(
status: Test.ResultStatus.failed,
returnValue: nil,
error: Test.Error("Something went wrong")
)

Test.assertNoError(result)
}
`

inter, err := newTestContractInterpreter(t, script)
require.NoError(t, err)

_, err = inter.Invoke("test")
require.Error(t, err)
assert.ErrorContains(t, err, "Something went wrong")
})

t.Run("transaction result with error", func(t *testing.T) {
t.Parallel()

script := `
import Test

access(all)
fun test() {
let result = Test.TransactionResult(
status: Test.ResultStatus.failed,
error: Test.Error("Transaction execution failed")
)

Test.assertNoError(result)
}
`

inter, err := newTestContractInterpreter(t, script)
require.NoError(t, err)

_, err = inter.Invoke("test")
require.Error(t, err)
assert.ErrorContains(t, err, "Transaction execution failed")
})
}

func TestErrorMessageFormatting(t *testing.T) {
t.Parallel()

// Test the error message formatting function
t.Run("short error message", func(t *testing.T) {
err := errors.New("Simple error")
formatted := formatErrorMessageForTest(err)
assert.Equal(t, "Simple error", formatted)
})

t.Run("long error with execution failed", func(t *testing.T) {
longError := `Execution failed:
error: cannot deploy invalid contract
--> 751d5fec3fe39dfac9e27973430720b5fcf70588d93503e349d5f4b88f80e0e4:4:16
|
4 | signer.contracts.add(name: "FiatToken", code: "696d706f...")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: resource FiatToken.Admin does not conform to resource interface OnChainMultiSig.PublicSigner`

formatted := formatErrorMessageForTest(errors.New(longError))

// Debug: print the formatted output
t.Logf("Formatted error: %q", formatted)

// Should extract the key error messages
assert.Contains(t, formatted, "cannot deploy invalid contract")
assert.Contains(t, formatted, "does not conform")
// Should not contain code excerpts
assert.NotContains(t, formatted, "696d706f")
})

t.Run("very long error truncation", func(t *testing.T) {
veryLongError := strings.Repeat("This is a very long error message. ", 100)
formatted := formatErrorMessageForTest(errors.New(veryLongError))

// Should be truncated
assert.True(t, len(formatted) <= 503) // 500 + "..."
assert.Contains(t, formatted, "...")
})
}

func TestTestExpectFailure(t *testing.T) {

t.Parallel()
Expand Down