From 240d01c8efac52edba597960aa72ae2110238802 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 11:51:54 +0800 Subject: [PATCH] feat: add tests for explaingen and bugfix gen --- errgoengine.go | 4 +- output_gen.go | 12 +- translation.go | 80 ++++--- translation_test.go | 545 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 607 insertions(+), 34 deletions(-) create mode 100644 translation_test.go diff --git a/errgoengine.go b/errgoengine.go index 8270a5f..7ced40b 100644 --- a/errgoengine.go +++ b/errgoengine.go @@ -111,7 +111,7 @@ func (e *ErrgoEngine) Analyze(workingPath, msg string) (*CompiledErrorTemplate, } func (e *ErrgoEngine) Translate(template *CompiledErrorTemplate, contextData *ContextData) (mainExp string, fullExp string) { - expGen := &ExplainGenerator{errorName: template.Name} + expGen := &ExplainGenerator{ErrorName: template.Name} fixGen := &BugFixGenerator{} if contextData.MainError != nil { fixGen.Document = contextData.MainError.Document @@ -128,7 +128,7 @@ func (e *ErrgoEngine) Translate(template *CompiledErrorTemplate, contextData *Co output := e.OutputGen.Generate(contextData, expGen, fixGen) defer e.OutputGen.Reset() - return expGen.mainExp.String(), output + return expGen.Builder.String(), output } func ParseFromStackTrace(contextData *ContextData, defaultLanguage *Language, files fs.ReadFileFS) error { diff --git a/output_gen.go b/output_gen.go index fbedefb..9481e9f 100644 --- a/output_gen.go +++ b/output_gen.go @@ -25,12 +25,12 @@ func (gen *OutputGenerator) _break() { } func (gen *OutputGenerator) generateFromExp(level int, explain *ExplainGenerator) { - if explain.mainExp != nil { - gen.write(explain.mainExp.String()) + if explain.Builder != nil { + gen.write(explain.Builder.String()) } - if explain.sections != nil { - for sectionName, exp := range explain.sections { + if explain.Sections != nil { + for sectionName, exp := range explain.Sections { gen._break() gen.heading(level+1, sectionName) gen.generateFromExp(level+1, exp) @@ -75,8 +75,8 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, gen.wr = &strings.Builder{} } - if len(explain.errorName) != 0 { - gen.heading(1, explain.errorName) + if len(explain.ErrorName) != 0 { + gen.heading(1, explain.ErrorName) } gen.generateFromExp(1, explain) diff --git a/translation.go b/translation.go index 7d8b398..9351e1e 100644 --- a/translation.go +++ b/translation.go @@ -9,45 +9,52 @@ import ( type GenExplainFn func(*ContextData, *ExplainGenerator) type ExplainGenerator struct { - errorName string - mainExp *strings.Builder - sections map[string]*ExplainGenerator + ErrorName string + Builder *strings.Builder + Sections map[string]*ExplainGenerator } func (gen *ExplainGenerator) Add(text string, data ...any) { - if gen.mainExp == nil { - gen.mainExp = &strings.Builder{} + if gen.Builder == nil { + gen.Builder = &strings.Builder{} } if len(data) != 0 { - gen.mainExp.WriteString(fmt.Sprintf(text, data...)) + gen.Builder.WriteString(fmt.Sprintf(text, data...)) } else { - gen.mainExp.WriteString(text) + gen.Builder.WriteString(text) } } func (gen *ExplainGenerator) CreateSection(name string) *ExplainGenerator { - if gen.sections == nil { - gen.sections = map[string]*ExplainGenerator{} + if len(name) == 0 { + return nil } - _, ok := gen.sections[name] + + if gen.Sections == nil { + gen.Sections = map[string]*ExplainGenerator{} + } + _, ok := gen.Sections[name] if !ok { - gen.sections[name] = &ExplainGenerator{} + gen.Sections[name] = &ExplainGenerator{} } - return gen.sections[name] + return gen.Sections[name] } type GenBugFixFn func(*ContextData, *BugFixGenerator) type BugFixSuggestion struct { Title string - Description string Steps []*BugFixStep diffPosition Position Doc *EditableDocument } -func (gen *BugFixSuggestion) addStep(doc *EditableDocument, content string, d ...any) *BugFixStep { +func (gen *BugFixSuggestion) addStep(isCopyable bool, content string, d ...any) (*BugFixStep, error) { + if len(content) == 0 { + return nil, fmt.Errorf("content cannot be empty") + } + if gen.Steps == nil { gen.Steps = []*BugFixStep{} } @@ -56,23 +63,34 @@ func (gen *BugFixSuggestion) addStep(doc *EditableDocument, content string, d .. content += "." } + doc := gen.Doc + if isCopyable { + if len(gen.Steps) == 0 { + doc = gen.Doc.Copy() + } else { + // use the last document as the base + doc = gen.Steps[len(gen.Steps)-1].Doc.Copy() + } + } + gen.Steps = append(gen.Steps, &BugFixStep{ suggestion: gen, Content: fmt.Sprintf(content, d...), - Doc: gen.Doc, + Doc: doc, + isCopyable: isCopyable, }) - return gen.Steps[len(gen.Steps)-1] + return gen.Steps[len(gen.Steps)-1], nil } func (gen *BugFixSuggestion) AddStep(content string, d ...any) *BugFixStep { - s := gen.addStep(gen.Doc.Copy(), content, d...) - s.isCopyable = true - return s -} - -func (gen *BugFixSuggestion) AddDescription(exp string, d ...any) { - gen.Description = fmt.Sprintf(exp, d...) + step, err := gen.addStep(true, content, d...) + // we cannot panic here because we need to return the step + // to get the error, use recover() to catch the panic + if err != nil { + panic(err) + } + return step } type BugFixStep struct { @@ -174,16 +192,26 @@ type BugFixGenerator struct { Suggestions []*BugFixSuggestion } -func (gen *BugFixGenerator) Add(title string, makerFn func(s *BugFixSuggestion)) { +func (gen *BugFixGenerator) Add(title string, makerFn func(s *BugFixSuggestion)) error { + if len(title) == 0 { + return fmt.Errorf("title cannot be empty") + } + + if makerFn == nil { + return fmt.Errorf("maker function cannot be nil") + } + if gen.Suggestions == nil { gen.Suggestions = []*BugFixSuggestion{} } suggestion := &BugFixSuggestion{ Title: title, - Doc: gen.Document.Editable(), + // Copy the document to avoid modifying the original document + Doc: gen.Document.Editable(), } - makerFn(suggestion) + makerFn(suggestion) gen.Suggestions = append(gen.Suggestions, suggestion) + return nil } diff --git a/translation_test.go b/translation_test.go new file mode 100644 index 0000000..717bd29 --- /dev/null +++ b/translation_test.go @@ -0,0 +1,545 @@ +package errgoengine_test + +import ( + "strings" + "testing" + + lib "github.com/nedpals/errgoengine" + sitter "github.com/smacker/go-tree-sitter" +) + +func TestExplainGenerator(t *testing.T) { + t.Run("errorName", func(t *testing.T) { + gen := &lib.ExplainGenerator{ErrorName: "TestError"} + + if gen.ErrorName != "TestError" { + t.Errorf("Expected 'TestError', got %s", gen.ErrorName) + } + }) + + t.Run("Add", func(t *testing.T) { + t.Run("Simple", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message.") + + if gen.Builder.String() != "This is a simple error message." { + t.Errorf("Expected 'This is a simple error message.', got %s", gen.Builder.String()) + } + }) + + t.Run("Simple with string data", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message with data: %s", "Hello") + + if gen.Builder.String() != "This is a simple error message with data: Hello" { + t.Errorf("Expected 'This is a simple error message with data: Hello', got %s", gen.Builder.String()) + } + }) + + t.Run("Simple with int data", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message with data: %d", 10) + + if gen.Builder.String() != "This is a simple error message with data: 10" { + t.Errorf("Expected 'This is a simple error message with data: 10', got %s", gen.Builder.String()) + } + }) + + t.Run("Simple with mixed data", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message with data: %s and %d", "Hello", 10) + + if gen.Builder.String() != "This is a simple error message with data: Hello and 10" { + t.Errorf("Expected 'This is a simple error message with data: Hello and 10', got %s", gen.Builder.String()) + } + }) + + t.Run("Append", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message.") + gen.Add("This is another error message.") + + if gen.Builder.String() != "This is a simple error message.This is another error message." { + t.Errorf("Expected 'This is a simple error message.This is another error message.', got %s", gen.Builder.String()) + } + }) + + t.Run("Append with newline", func(t *testing.T) { + gen := &lib.ExplainGenerator{ErrorName: "TestError"} + gen.Add("This is a simple error message.\n") + gen.Add("This is another error message.") + + if gen.Builder.String() != "This is a simple error message.\nThis is another error message." { + t.Errorf("Expected 'This is a simple error message.\nThis is another error message.', got %s", gen.Builder.String()) + } + }) + + t.Run("Append with string data", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message with data: %s", "Hello") + gen.Add("This is another error message with data: %s", "World") + + if gen.Builder.String() != "This is a simple error message with data: HelloThis is another error message with data: World" { + t.Errorf("Expected 'This is a simple error message with data: HelloThis is another error message with data: World', got %s", gen.Builder.String()) + } + }) + + t.Run("Append with int data", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message with data: %d", 10) + gen.Add("This is another error message with data: %d", 20) + + if gen.Builder.String() != "This is a simple error message with data: 10This is another error message with data: 20" { + t.Errorf("Expected 'This is a simple error message with data: 10This is another error message with data: 20', got %s", gen.Builder.String()) + } + }) + + t.Run("Append with mixed data", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("This is a simple error message with data: %s", "Hello") + gen.Add("This is another error message with data: %d", 20) + + if gen.Builder.String() != "This is a simple error message with data: HelloThis is another error message with data: 20" { + t.Errorf("Expected 'This is a simple error message with data: HelloThis is another error message with data: 20', got %s", gen.Builder.String()) + } + }) + + t.Run("Empty", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + gen.Add("") + + if gen.Builder.String() != "" { + t.Errorf("Expected '', got %s", gen.Builder.String()) + } + }) + }) + + t.Run("CreateSection", func(t *testing.T) { + t.Run("Simple", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + + section := gen.CreateSection("TestSection") + + if gen.Sections == nil { + t.Errorf("Expected sections to be created") + } + + if _, ok := gen.Sections["TestSection"]; !ok { + t.Errorf("Expected section to be added to sections") + } + + if section == nil { + t.Errorf("Expected section to be created") + } + }) + + t.Run("Empty", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + + section := gen.CreateSection("") + + if _, ok := gen.Sections[""]; ok { + t.Errorf("Expected section to not be added to sections") + } + + if section != nil { + t.Errorf("Expected section to not be created") + } + }) + + t.Run("Writable section", func(t *testing.T) { + gen := &lib.ExplainGenerator{} + + section := gen.CreateSection("TestSection") + section.Add("This is a simple error message.") + + if gen.Sections["TestSection"].Builder.String() != "This is a simple error message." { + t.Errorf("Expected 'This is a simple error message.', got %s", gen.Sections["TestSection"].Builder.String()) + } + }) + }) +} + +func TestBugFixGenerator(t *testing.T) { + parser := sitter.NewParser() + // create a parsed document + doc, err := lib.ParseDocument("hello.test", strings.NewReader("print('Hello, World!')"), parser, lib.TestLanguage, nil) + if err != nil { + t.Errorf("Error parsing document: %s", err) + } + + t.Run("Add", func(t *testing.T) { + t.Run("Simple", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + if s.Title != "This is a simple error message." { + t.Errorf("Expected 'This is a simple error message.', got %s", s.Title) + } + + if s.Doc == nil { + t.Errorf("Expected document to be set") + } + }) + }) + + t.Run("Empty title", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + err := gen.Add("", func(s *lib.BugFixSuggestion) {}) + if err == nil { + t.Errorf("Expected error, got nil") + } + + if err.Error() != "title cannot be empty" { + t.Errorf("Expected 'title cannot be empty', got %s", err.Error()) + } + }) + + t.Run("Empty function", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + err := gen.Add("This is a simple error message.", nil) + if err == nil { + t.Errorf("Expected error, got nil") + } + + if err.Error() != "maker function cannot be nil" { + t.Errorf("Expected 'maker function cannot be nil', got %s", err.Error()) + } + }) + + t.Run("Multiple", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) {}) + gen.Add("This is another error message.", func(s *lib.BugFixSuggestion) {}) + + if len(gen.Suggestions) != 2 { + t.Errorf("Expected 2 suggestions, got %d", len(gen.Suggestions)) + } + }) + }) + + t.Run("Suggestion/AddStep", func(t *testing.T) { + t.Run("Simple", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a simple step.", func(step *lib.BugFixStep) { + if step.Content != "This is a simple step." { + t.Errorf("Expected 'This is a simple step.', got %s", step.Content) + } + + if step.Doc == nil { + t.Errorf("Expected document to be set") + } + }) + }) + }) + + t.Run("Without period", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a simple step", func(step *lib.BugFixStep) { + if step.Content != "This is a simple step." { + t.Errorf("Expected 'This is a simple step.', got %s", step.Content) + } + }) + }) + }) + + t.Run("With string data", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a simple step with data: %s", "Hello", func(step *lib.BugFixStep) { + if step.Content != "This is a simple step with data: Hello." { + t.Errorf("Expected 'This is a simple step with data: Hello.', got %s", step.Content) + } + }) + }) + }) + + t.Run("With int data", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a simple step with data: %d", 10, func(step *lib.BugFixStep) { + if step.Content != "This is a simple step with data: 10." { + t.Errorf("Expected 'This is a simple step with data: 10.', got %s", step.Content) + } + }) + }) + }) + + t.Run("With mixed data", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a simple step with data: %s and %d", "Hello", 10, func(step *lib.BugFixStep) { + if step.Content != "This is a simple step with data: Hello and 10." { + t.Errorf("Expected 'This is a simple step with data: Hello and 10.', got %s", step.Content) + } + }) + }) + }) + + t.Run("Empty content", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + // recover + defer func() { + if r := recover(); r != nil { + err := r.(error) + if err.Error() != "content cannot be empty" { + t.Errorf("Expected 'content cannot be empty', got %s", err.Error()) + } + } + }() + + s.AddStep("", func(step *lib.BugFixStep) {}) + }) + }) + }) + + t.Run("Suggestion/AddFix/Add content", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + // add a = 1 fix + step := s.AddStep("This is a simple step."). + AddFix(lib.FixSuggestion{ + NewText: "\na = 1", + StartPosition: lib.Position{ + Line: 0, + Column: 22, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 22, + }, + }) + + if step.Doc.String() != "print('Hello, World!')\na = 1" { + t.Errorf("Expected 'print('Hello, World!')\na = 1', got %s", step.Doc.String()) + } + + if len(s.Steps[0].Fixes) != 1 { + t.Errorf("Expected 1 fix, got %d", len(s.Steps[0].Fixes)) + } + }) + + }) + + t.Run("Suggestion/AddFix/Update", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + step := s.AddStep("This is a simple step."). + AddFix(lib.FixSuggestion{ + NewText: "Welcome to the world", + StartPosition: lib.Position{ + Line: 0, + Column: 7, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 20, + }, + }) + + if step.Doc.String() != "print('Welcome to the world')" { + t.Errorf("Expected 'print('Welcome to the world')', got %s", step.Doc.String()) + } + + // add a = 1 fix + step.AddFix(lib.FixSuggestion{ + NewText: "\na = 1", + StartPosition: lib.Position{ + Line: 0, + Column: 22, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 22, + }, + }) + + if step.Doc.String() != "print('Welcome to the world')\na = 1" { + t.Errorf("Expected 'print('Welcome to the world')\na = 1', got %s", step.Doc.String()) + } + }) + }) + + t.Run("Suggestion/AddFix/Delete", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + // removes the content inside the print function + step := s.AddStep("This is a simple step."). + AddFix(lib.FixSuggestion{ + NewText: "", + StartPosition: lib.Position{ + Line: 0, + Column: 7, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 20, + }, + }) + + if step.Doc.String() != "print('')" { + t.Errorf("Expected '', got %s", step.Doc.String()) + } + }) + }) + + t.Run("Suggestion/AddFix/Mixed", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + // removes the content inside the print function + step := s.AddStep("This is a simple step."). + AddFix(lib.FixSuggestion{ + NewText: "", + StartPosition: lib.Position{ + Line: 0, + Column: 7, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 20, + }, + }) + + if step.Doc.String() != "print('')" { + t.Errorf("Expected 'print('')', got %s", step.Doc.String()) + } + + // add a text inside the print function + step.AddFix(lib.FixSuggestion{ + NewText: "Hello, World!", + StartPosition: lib.Position{ + Line: 0, + Column: 7, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + + if step.Doc.String() != "print('Hello, World!')" { + t.Errorf("Expected 'print('Hello, World!')', got %s", step.Doc.String()) + } + + // add a = 1 fix + step.AddFix(lib.FixSuggestion{ + NewText: "\na = 1", + StartPosition: lib.Position{ + Line: 0, + Column: 24, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 24, + }, + }) + + if step.Doc.String() != "print('Hello, World!')\na = 1" { + t.Errorf("Expected 'print('Hello, World!')\na = 1', got %s", step.Doc.String()) + } + }) + }) + + t.Run("Suggestion/MultipleSteps/AddFix", func(t *testing.T) { + gen := &lib.BugFixGenerator{ + Document: doc, + } + + gen.Add("Improve the print call", func(s *lib.BugFixSuggestion) { + step := s.AddStep("Remove the content inside the print function"). + AddFix(lib.FixSuggestion{ + NewText: "", + StartPosition: lib.Position{ + Line: 0, + Column: 7, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 20, + }, + }) + + if step.Doc.String() != "print('')" { + t.Errorf("Expected 'print('')', got %s", step.Doc.String()) + } + + // add a text inside the print function + step2 := s.AddStep("Add a custom text"). + AddFix(lib.FixSuggestion{ + NewText: "Foo bar?", + StartPosition: lib.Position{ + Line: 0, + Column: 7, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + + if step2.Doc.String() != "print('Foo bar?')" { + t.Errorf("Expected 'print('Foo bar?')', got %s", step2.Doc.String()) + } + + step3 := s.AddStep("Add an assignment below").AddFix(lib.FixSuggestion{ + NewText: "\nx = 2", + StartPosition: lib.Position{ + Line: 0, + Column: 24, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 24, + }, + }) + + if step3.Doc.String() != "print('Foo bar?')\nx = 2" { + t.Errorf("Expected 'print('Foo bar?')\nx = 2', got %s", step3.Doc.String()) + } + }) + }) +}