Commit a2b1a7e1 authored by smartcontracts's avatar smartcontracts Committed by GitHub

feat: common core for contracts Go check scripts (#13134)

Introduces a new common base framework for writing contracts
check scripts in Go. Many of the check scripts basically do the
exact same logic of somehow parsing either the interfaces or the
artifact files. Goal of this small project is to make the process
of writing new checks easier and more reliable.

We demonstrate this framework in action by porting the test-names
script to use this new framework.
parent 623609ae
......@@ -1382,6 +1382,7 @@ workflows:
op-e2e/interop
op-e2e/actions
op-e2e/faultproofs
packages/contracts-bedrock/scripts/checks
requires:
- contracts-bedrock-build
- cannon-prestate
......
......@@ -7,6 +7,7 @@ toolchain go1.22.7
require (
github.com/BurntSushi/toml v1.4.0
github.com/andybalholm/brotli v1.1.0
github.com/bmatcuk/doublestar/v4 v4.7.1
github.com/btcsuite/btcd v0.24.2
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/cockroachdb/pebble v1.1.2
......
......@@ -50,6 +50,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
......
package solc
import (
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
......@@ -129,5 +128,158 @@ type Ast struct {
Id uint `json:"id"`
License string `json:"license"`
NodeType string `json:"nodeType"`
Nodes json.RawMessage `json:"nodes"`
Nodes []AstNode `json:"nodes"`
Src string `json:"src"`
}
type AstNode struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
Nodes []AstNode `json:"nodes,omitempty"`
Abstract bool `json:"abstract,omitempty"`
BaseContracts []AstBaseContract `json:"baseContracts,omitempty"`
CanonicalName string `json:"canonicalName,omitempty"`
ContractDependencies []int `json:"contractDependencies,omitempty"`
ContractKind string `json:"contractKind,omitempty"`
Documentation interface{} `json:"documentation,omitempty"`
FullyImplemented bool `json:"fullyImplemented,omitempty"`
LinearizedBaseContracts []int `json:"linearizedBaseContracts,omitempty"`
Name string `json:"name,omitempty"`
NameLocation string `json:"nameLocation,omitempty"`
Scope int `json:"scope,omitempty"`
UsedErrors []int `json:"usedErrors,omitempty"`
UsedEvents []int `json:"usedEvents,omitempty"`
// Function specific
Body *AstBlock `json:"body,omitempty"`
Parameters *AstParameterList `json:"parameters,omitempty"`
ReturnParameters *AstParameterList `json:"returnParameters,omitempty"`
StateMutability string `json:"stateMutability,omitempty"`
Virtual bool `json:"virtual,omitempty"`
Visibility string `json:"visibility,omitempty"`
// Variable specific
Constant bool `json:"constant,omitempty"`
Mutability string `json:"mutability,omitempty"`
StateVariable bool `json:"stateVariable,omitempty"`
StorageLocation string `json:"storageLocation,omitempty"`
TypeDescriptions *AstTypeDescriptions `json:"typeDescriptions,omitempty"`
TypeName *AstTypeName `json:"typeName,omitempty"`
// Expression specific
Expression *Expression `json:"expression,omitempty"`
IsConstant bool `json:"isConstant,omitempty"`
IsLValue bool `json:"isLValue,omitempty"`
IsPure bool `json:"isPure,omitempty"`
LValueRequested bool `json:"lValueRequested,omitempty"`
// Literal specific
HexValue string `json:"hexValue,omitempty"`
Kind string `json:"kind,omitempty"`
Value interface{} `json:"value,omitempty"`
// Other fields
Arguments []Expression `json:"arguments,omitempty"`
Condition *Expression `json:"condition,omitempty"`
TrueBody *AstBlock `json:"trueBody,omitempty"`
FalseBody *AstBlock `json:"falseBody,omitempty"`
Operator string `json:"operator,omitempty"`
}
type AstBaseContract struct {
BaseName *AstTypeName `json:"baseName"`
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
}
type AstDocumentation struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
Text string `json:"text"`
}
type AstBlock struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
Statements []AstNode `json:"statements"`
}
type AstParameterList struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Parameters []AstNode `json:"parameters"`
Src string `json:"src"`
}
type AstTypeDescriptions struct {
TypeIdentifier string `json:"typeIdentifier"`
TypeString string `json:"typeString"`
}
type AstTypeName struct {
Id int `json:"id"`
Name string `json:"name"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
StateMutability string `json:"stateMutability,omitempty"`
TypeDescriptions *AstTypeDescriptions `json:"typeDescriptions,omitempty"`
}
type Expression struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
TypeDescriptions *AstTypeDescriptions `json:"typeDescriptions,omitempty"`
Name string `json:"name,omitempty"`
OverloadedDeclarations []int `json:"overloadedDeclarations,omitempty"`
ReferencedDeclaration int `json:"referencedDeclaration,omitempty"`
ArgumentTypes []AstTypeDescriptions `json:"argumentTypes,omitempty"`
}
type ForgeArtifact struct {
Abi abi.ABI `json:"abi"`
Bytecode CompilerOutputBytecode `json:"bytecode"`
DeployedBytecode CompilerOutputBytecode `json:"deployedBytecode"`
MethodIdentifiers map[string]string `json:"methodIdentifiers"`
RawMetadata string `json:"rawMetadata"`
Metadata ForgeCompilerMetadata `json:"metadata"`
StorageLayout *StorageLayout `json:"storageLayout,omitempty"`
Ast Ast `json:"ast"`
Id int `json:"id"`
}
type ForgeCompilerMetadata struct {
Compiler ForgeCompilerInfo `json:"compiler"`
Language string `json:"language"`
Output ForgeMetadataOutput `json:"output"`
Settings CompilerSettings `json:"settings"`
Sources map[string]ForgeSourceInfo `json:"sources"`
Version int `json:"version"`
}
type ForgeCompilerInfo struct {
Version string `json:"version"`
}
type ForgeMetadataOutput struct {
Abi abi.ABI `json:"abi"`
DevDoc ForgeDocObject `json:"devdoc"`
UserDoc ForgeDocObject `json:"userdoc"`
}
type ForgeSourceInfo struct {
Keccak256 string `json:"keccak256"`
License string `json:"license"`
Urls []string `json:"urls"`
}
type ForgeDocObject struct {
Kind string `json:"kind"`
Methods map[string]interface{} `json:"methods"`
Notice string `json:"notice,omitempty"`
Version int `json:"version"`
}
package common
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
"github.com/bmatcuk/doublestar/v4"
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"golang.org/x/sync/errgroup"
)
type ErrorReporter struct {
hasErr atomic.Bool
outMtx sync.Mutex
}
func NewErrorReporter() *ErrorReporter {
return &ErrorReporter{}
}
func (e *ErrorReporter) Fail(msg string, args ...any) {
e.outMtx.Lock()
// Useful for suppressing error reporting in tests
if os.Getenv("SUPPRESS_ERROR_REPORTER") == "" {
_, _ = fmt.Fprintf(os.Stderr, "❌ "+msg+"\n", args...)
}
e.outMtx.Unlock()
e.hasErr.Store(true)
}
func (e *ErrorReporter) HasError() bool {
return e.hasErr.Load()
}
type FileProcessor func(path string) []error
func ProcessFiles(files map[string]string, processor FileProcessor) error {
g := errgroup.Group{}
g.SetLimit(runtime.NumCPU())
reporter := NewErrorReporter()
for name, path := range files {
name, path := name, path // Capture loop variables
g.Go(func() error {
if errs := processor(path); len(errs) > 0 {
for _, err := range errs {
reporter.Fail("%s: %v", name, err)
}
}
return nil
})
}
err := g.Wait()
if err != nil {
return fmt.Errorf("processing failed: %w", err)
}
if reporter.HasError() {
return fmt.Errorf("processing failed")
}
return nil
}
func ProcessFilesGlob(includes, excludes []string, processor FileProcessor) error {
files, err := FindFiles(includes, excludes)
if err != nil {
return err
}
return ProcessFiles(files, processor)
}
func FindFiles(includes, excludes []string) (map[string]string, error) {
included := make(map[string]string)
excluded := make(map[string]struct{})
// Get all included files
for _, pattern := range includes {
matches, err := doublestar.Glob(os.DirFS("."), pattern)
if err != nil {
return nil, fmt.Errorf("glob pattern error: %w", err)
}
for _, match := range matches {
name := filepath.Base(match)
included[name] = match
}
}
// Get all excluded files
for _, pattern := range excludes {
matches, err := doublestar.Glob(os.DirFS("."), pattern)
if err != nil {
return nil, fmt.Errorf("glob pattern error: %w", err)
}
for _, match := range matches {
excluded[filepath.Base(match)] = struct{}{}
}
}
// Remove excluded files from result
for name := range excluded {
delete(included, name)
}
return included, nil
}
func ReadForgeArtifact(path string) (*solc.ForgeArtifact, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read artifact: %w", err)
}
var artifact solc.ForgeArtifact
if err := json.Unmarshal(data, &artifact); err != nil {
return nil, fmt.Errorf("failed to parse artifact: %w", err)
}
return &artifact, nil
}
package common
import (
"os"
"path/filepath"
"testing"
)
func TestErrorReporter(t *testing.T) {
os.Setenv("SUPPRESS_ERROR_REPORTER", "1")
defer os.Unsetenv("SUPPRESS_ERROR_REPORTER")
reporter := NewErrorReporter()
if reporter.HasError() {
t.Error("new reporter should not have errors")
}
reporter.Fail("test error")
if !reporter.HasError() {
t.Error("reporter should have error after Fail")
}
}
func TestProcessFiles(t *testing.T) {
os.Setenv("SUPPRESS_ERROR_REPORTER", "1")
defer os.Unsetenv("SUPPRESS_ERROR_REPORTER")
files := map[string]string{
"file1": "path1",
"file2": "path2",
}
// Test successful processing
err := ProcessFiles(files, func(path string) []error {
return nil
})
if err != nil {
t.Errorf("expected no error, got %v", err)
}
// Test error handling
err = ProcessFiles(files, func(path string) []error {
var errors []error
errors = append(errors, os.ErrNotExist)
return errors
})
if err == nil {
t.Error("expected error, got nil")
}
}
func TestProcessFilesGlob(t *testing.T) {
os.Setenv("SUPPRESS_ERROR_REPORTER", "1")
defer os.Unsetenv("SUPPRESS_ERROR_REPORTER")
// Create test directory structure
tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
// Create test files
files := map[string]string{
"test1.txt": "content1",
"test2.txt": "content2",
"skip.txt": "content3",
}
for name, content := range files {
if err := os.WriteFile(name, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
// Test processing with includes and excludes
includes := []string{"*.txt"}
excludes := []string{"skip.txt"}
processedFiles := make(map[string]bool)
err := ProcessFilesGlob(includes, excludes, func(path string) []error {
processedFiles[filepath.Base(path)] = true
return nil
})
if err != nil {
t.Errorf("ProcessFiles failed: %v", err)
}
// Verify results
if len(processedFiles) != 2 {
t.Errorf("expected 2 processed files, got %d", len(processedFiles))
}
if !processedFiles["test1.txt"] {
t.Error("expected to process test1.txt")
}
if !processedFiles["test2.txt"] {
t.Error("expected to process test2.txt")
}
if processedFiles["skip.txt"] {
t.Error("skip.txt should have been excluded")
}
}
func TestFindFiles(t *testing.T) {
// Create test directory structure
tmpDir := t.TempDir()
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
// Create test files
files := map[string]string{
"test1.txt": "content1",
"test2.txt": "content2",
"skip.txt": "content3",
}
for name, content := range files {
if err := os.WriteFile(name, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
// Test finding files
includes := []string{"*.txt"}
excludes := []string{"skip.txt"}
found, err := FindFiles(includes, excludes)
if err != nil {
t.Fatalf("FindFiles failed: %v", err)
}
// Verify results
if len(found) != 2 {
t.Errorf("expected 2 files, got %d", len(found))
}
if _, exists := found["test1.txt"]; !exists {
t.Error("expected to find test1.txt")
}
if _, exists := found["test2.txt"]; !exists {
t.Error("expected to find test2.txt")
}
if _, exists := found["skip.txt"]; exists {
t.Error("skip.txt should have been excluded")
}
}
func TestReadForgeArtifact(t *testing.T) {
// Create a temporary test artifact
tmpDir := t.TempDir()
artifactContent := `{
"abi": [],
"bytecode": {
"object": "0x123"
},
"deployedBytecode": {
"object": "0x456"
}
}`
tmpFile := filepath.Join(tmpDir, "Test.json")
if err := os.WriteFile(tmpFile, []byte(artifactContent), 0644); err != nil {
t.Fatal(err)
}
// Test processing
artifact, err := ReadForgeArtifact(tmpFile)
if err != nil {
t.Fatalf("ReadForgeArtifact failed: %v", err)
}
// Verify results
if artifact.Bytecode.Object != "0x123" {
t.Errorf("expected bytecode '0x123', got %q", artifact.Bytecode.Object)
}
if artifact.DeployedBytecode.Object != "0x456" {
t.Errorf("expected deployed bytecode '0x456', got %q", artifact.DeployedBytecode.Object)
}
}
package main
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"unicode"
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common"
)
type Check func(parts []string) bool
func main() {
if err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"},
[]string{},
processFile,
); err != nil {
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
}
func processFile(path string) []error {
artifact, err := common.ReadForgeArtifact(path)
if err != nil {
return []error{err}
}
var errors []error
names := extractTestNames(artifact)
for _, name := range names {
if err = checkTestName(name); err != nil {
errors = append(errors, err)
}
}
return errors
}
func extractTestNames(artifact *solc.ForgeArtifact) []string {
isTest := false
for _, entry := range artifact.Abi.Methods {
if entry.Name == "IS_TEST" {
isTest = true
break
}
}
if !isTest {
return nil
}
names := []string{}
for _, entry := range artifact.Abi.Methods {
if !strings.HasPrefix(entry.Name, "test") {
continue
}
names = append(names, entry.Name)
}
return names
}
type CheckFunc func(parts []string) bool
type CheckInfo struct {
check Check
error string
check CheckFunc
}
var excludes = map[string]bool{}
var checks = []CheckInfo{
{
var checks = map[string]CheckInfo{
"doubleUnderscores": {
error: "test names cannot have double underscores",
check: func(parts []string) bool {
for _, part := range parts {
if len(strings.TrimSpace(part)) == 0 {
return false
}
}
return true
},
},
"camelCase": {
error: "test name parts should be in camelCase",
check: func(parts []string) bool {
for _, part := range parts {
......@@ -32,21 +92,24 @@ var checks = []CheckInfo{
return true
},
},
{
"partsCount": {
error: "test names should have either 3 or 4 parts, each separated by underscores",
check: func(parts []string) bool {
return len(parts) == 3 || len(parts) == 4
},
},
{
error: "test names should begin with \"test\", \"testFuzz\", or \"testDiff\"",
"prefix": {
error: "test names should begin with 'test', 'testFuzz', or 'testDiff'",
check: func(parts []string) bool {
return parts[0] == "test" || parts[0] == "testFuzz" || parts[0] == "testDiff"
return len(parts) > 0 && (parts[0] == "test" || parts[0] == "testFuzz" || parts[0] == "testDiff")
},
},
{
error: "test names should end with either \"succeeds\", \"reverts\", \"fails\", \"works\" or \"benchmark[_num]\"",
"suffix": {
error: "test names should end with either 'succeeds', 'reverts', 'fails', 'works', or 'benchmark[_num]'",
check: func(parts []string) bool {
if len(parts) == 0 {
return false
}
last := parts[len(parts)-1]
if last == "succeeds" || last == "reverts" || last == "fails" || last == "works" {
return true
......@@ -58,113 +121,24 @@ var checks = []CheckInfo{
return last == "benchmark"
},
},
{
"failureParts": {
error: "failure tests should have 4 parts, third part should indicate the reason for failure",
check: func(parts []string) bool {
if len(parts) == 0 {
return false
}
last := parts[len(parts)-1]
return len(parts) == 4 || (last != "reverts" && last != "fails")
},
},
}
func main() {
cmd := exec.Command("forge", "config", "--json")
output, err := cmd.Output()
if err != nil {
fmt.Printf("Error executing forge config: %v\n", err)
os.Exit(1)
}
var config map[string]interface{}
err = json.Unmarshal(output, &config)
if err != nil {
fmt.Printf("Error parsing forge config: %v\n", err)
os.Exit(1)
}
outDir, ok := config["out"].(string)
if !ok {
outDir = "out"
}
fmt.Println("Success:")
var errors []string
err = filepath.Walk(outDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
if excludes[strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))] {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var artifact map[string]interface{}
err = json.Unmarshal(data, &artifact)
if err != nil {
return nil // Skip files that are not valid JSON
}
abi, ok := artifact["abi"].([]interface{})
if !ok {
return nil
func checkTestName(name string) error {
parts := strings.Split(name, "_")
for _, check := range checks {
if !check.check(parts) {
return fmt.Errorf("%s: %s", name, check.error)
}
isTest := false
for _, element := range abi {
if elem, ok := element.(map[string]interface{}); ok {
if elem["name"] == "IS_TEST" {
isTest = true
break
}
}
}
if isTest {
success := true
for _, element := range abi {
if elem, ok := element.(map[string]interface{}); ok {
if elem["type"] == "function" {
name, ok := elem["name"].(string)
if !ok || !strings.HasPrefix(name, "test") {
continue
}
parts := strings.Split(name, "_")
for _, check := range checks {
if !check.check(parts) {
errors = append(errors, fmt.Sprintf("%s#%s: %s", path, name, check.error))
success = false
}
}
}
}
}
if success {
fmt.Printf(" - %s\n", filepath.Base(path[:len(path)-len(filepath.Ext(path))]))
}
}
return nil
})
if err != nil {
fmt.Printf("Error walking the path %q: %v\n", outDir, err)
os.Exit(1)
}
if len(errors) > 0 {
fmt.Println(strings.Join(errors, "\n"))
os.Exit(1)
}
return nil
}
package main
import (
"reflect"
"testing"
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"github.com/ethereum/go-ethereum/accounts/abi"
)
func TestCamelCaseCheck(t *testing.T) {
tests := []struct {
name string
parts []string
expected bool
}{
{"valid single part", []string{"test"}, true},
{"valid multiple parts", []string{"test", "something", "succeeds"}, true},
{"invalid uppercase", []string{"Test"}, false},
{"invalid middle uppercase", []string{"test", "Something", "succeeds"}, false},
{"empty parts", []string{}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := checks["camelCase"].check(tt.parts); got != tt.expected {
t.Errorf("checkCamelCase error for %v = %v, want %v", tt.parts, got, tt.expected)
}
})
}
}
func TestPartsCountCheck(t *testing.T) {
tests := []struct {
name string
parts []string
expected bool
}{
{"three parts", []string{"test", "something", "succeeds"}, true},
{"four parts", []string{"test", "something", "reason", "fails"}, true},
{"too few parts", []string{"test", "fails"}, false},
{"too many parts", []string{"test", "a", "b", "c", "fails"}, false},
{"empty parts", []string{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := checks["partsCount"].check(tt.parts); got != tt.expected {
t.Errorf("checkPartsCount error for %v = %v, want %v", tt.parts, got, tt.expected)
}
})
}
}
func TestPrefixCheck(t *testing.T) {
tests := []struct {
name string
parts []string
expected bool
}{
{"valid test", []string{"test", "something", "succeeds"}, true},
{"valid testFuzz", []string{"testFuzz", "something", "succeeds"}, true},
{"valid testDiff", []string{"testDiff", "something", "succeeds"}, true},
{"invalid prefix", []string{"testing", "something", "succeeds"}, false},
{"empty parts", []string{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := checks["prefix"].check(tt.parts); got != tt.expected {
t.Errorf("checkPrefix error for %v = %v, want %v", tt.parts, got, tt.expected)
}
})
}
}
func TestSuffixCheck(t *testing.T) {
tests := []struct {
name string
parts []string
expected bool
}{
{"valid succeeds", []string{"test", "something", "succeeds"}, true},
{"valid reverts", []string{"test", "something", "reverts"}, true},
{"valid fails", []string{"test", "something", "fails"}, true},
{"valid works", []string{"test", "something", "works"}, true},
{"valid benchmark", []string{"test", "something", "benchmark"}, true},
{"valid benchmark_num", []string{"test", "something", "benchmark", "123"}, true},
{"invalid suffix", []string{"test", "something", "invalid"}, false},
{"invalid benchmark_text", []string{"test", "something", "benchmark", "abc"}, false},
{"empty parts", []string{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := checks["suffix"].check(tt.parts); got != tt.expected {
t.Errorf("checkSuffix error for %v = %v, want %v", tt.parts, got, tt.expected)
}
})
}
}
func TestFailurePartsCheck(t *testing.T) {
tests := []struct {
name string
parts []string
expected bool
}{
{"valid failure with reason", []string{"test", "something", "reason", "fails"}, true},
{"valid failure with reason", []string{"test", "something", "reason", "reverts"}, true},
{"invalid failure without reason", []string{"test", "something", "fails"}, false},
{"invalid failure without reason", []string{"test", "something", "reverts"}, false},
{"valid non-failure with three parts", []string{"test", "something", "succeeds"}, true},
{"empty parts", []string{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := checks["failureParts"].check(tt.parts); got != tt.expected {
t.Errorf("checkFailureParts error for %v = %v, want %v", tt.parts, got, tt.expected)
}
})
}
}
func TestCheckTestName(t *testing.T) {
tests := []struct {
name string
testName string
shouldSucceed bool
}{
// Valid test names - Basic patterns
{"valid basic test succeeds", "test_something_succeeds", true},
{"valid basic test fails with reason", "test_something_reason_fails", true},
{"valid basic test reverts with reason", "test_something_reason_reverts", true},
{"valid basic test works", "test_something_works", true},
// Valid test names - Fuzz variants
{"valid fuzz test succeeds", "testFuzz_something_succeeds", true},
{"valid fuzz test fails with reason", "testFuzz_something_reason_fails", true},
{"valid fuzz test reverts with reason", "testFuzz_something_reason_reverts", true},
{"valid fuzz test works", "testFuzz_something_works", true},
// Valid test names - Diff variants
{"valid diff test succeeds", "testDiff_something_succeeds", true},
{"valid diff test fails with reason", "testDiff_something_reason_fails", true},
{"valid diff test reverts with reason", "testDiff_something_reason_reverts", true},
{"valid diff test works", "testDiff_something_works", true},
// Valid test names - Benchmark variants
{"valid benchmark test", "test_something_benchmark", true},
{"valid benchmark with number", "test_something_benchmark_123", true},
{"valid benchmark with large number", "test_something_benchmark_999999", true},
{"valid benchmark with zero", "test_something_benchmark_0", true},
// Valid test names - Complex middle parts
{"valid complex middle part", "test_complexOperation_succeeds", true},
{"valid multiple word middle", "test_veryComplexOperation_succeeds", true},
{"valid numbers in middle", "test_operation123_succeeds", true},
{"valid special case", "test_specialCase_reason_fails", true},
// Invalid test names - Prefix issues
{"invalid empty string", "", false},
{"invalid prefix Test", "Test_something_succeeds", false},
{"invalid prefix testing", "testing_something_succeeds", false},
{"invalid prefix testfuzz", "testfuzz_something_succeeds", false},
{"invalid prefix testdiff", "testdiff_something_succeeds", false},
{"invalid prefix TEST", "TEST_something_succeeds", false},
// Invalid test names - Suffix issues
{"invalid suffix succeed", "test_something_succeed", false},
{"invalid suffix revert", "test_something_revert", false},
{"invalid suffix fail", "test_something_fail", false},
{"invalid suffix work", "test_something_work", false},
{"invalid suffix benchmarks", "test_something_benchmarks", false},
{"invalid benchmark suffix text", "test_something_benchmark_abc", false},
{"invalid benchmark suffix special", "test_something_benchmark_123abc", false},
// Invalid test names - Case issues
{"invalid uppercase middle", "test_Something_succeeds", false},
{"invalid multiple uppercase", "test_SomethingHere_succeeds", false},
{"invalid all caps middle", "test_SOMETHING_succeeds", false},
{"invalid mixed case suffix", "test_something_Succeeds", false},
// Invalid test names - Structure issues
{"invalid single part", "test", false},
{"invalid two parts", "test_succeeds", false},
{"invalid five parts", "test_this_that_those_succeeds", false},
{"invalid six parts", "test_this_that_those_these_succeeds", false},
{"invalid failure without reason", "test_something_fails", false},
{"invalid revert without reason", "test_something_reverts", false},
// Invalid test names - Special cases
{"invalid empty parts", "test__succeeds", false},
{"invalid multiple underscores", "test___succeeds", false},
{"invalid trailing underscore", "test_something_succeeds_", false},
{"invalid leading underscore", "_test_something_succeeds", false},
{"invalid benchmark no number", "test_something_benchmark_", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkTestName(tt.testName)
if (err != nil) == tt.shouldSucceed {
t.Errorf("checkTestName(%q) error = %v, shouldSucceed %v", tt.testName, err, tt.shouldSucceed)
}
})
}
}
func TestExtractTestNames(t *testing.T) {
tests := []struct {
name string
artifact *solc.ForgeArtifact
want []string
}{
{
name: "valid test contract",
artifact: &solc.ForgeArtifact{
Abi: abi.ABI{
Methods: map[string]abi.Method{
"IS_TEST": {Name: "IS_TEST"},
"test_something_succeeds": {Name: "test_something_succeeds"},
"test_other_fails": {Name: "test_other_fails"},
"not_a_test": {Name: "not_a_test"},
"testFuzz_something_works": {Name: "testFuzz_something_works"},
},
},
},
want: []string{
"test_something_succeeds",
"test_other_fails",
"testFuzz_something_works",
},
},
{
name: "non-test contract",
artifact: &solc.ForgeArtifact{
Abi: abi.ABI{
Methods: map[string]abi.Method{
"test_something_succeeds": {Name: "test_something_succeeds"},
"not_a_test": {Name: "not_a_test"},
},
},
},
want: nil,
},
{
name: "empty contract",
artifact: &solc.ForgeArtifact{
Abi: abi.ABI{
Methods: map[string]abi.Method{},
},
},
want: nil,
},
{
name: "test contract with no test methods",
artifact: &solc.ForgeArtifact{
Abi: abi.ABI{
Methods: map[string]abi.Method{
"IS_TEST": {Name: "IS_TEST"},
"not_a_test": {Name: "not_a_test"},
"another_method": {Name: "another_method"},
},
},
},
want: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractTestNames(tt.artifact)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("extractTestNames() = %v, want %v", got, tt.want)
}
})
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment