From 1dd2999e47bbf902999c0ff9bf4973589fc139fa Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Wed, 11 Feb 2026 20:58:59 +0400 Subject: [PATCH 1/5] Add empty folder export, folder comments, and / escaping in folder names - Add AddFolder(folder, comment) API for creating empty folders and folder comments - Add "Folder Comment" CSV column - Support / in folder names by escaping as \/ in the joined path - Reject folder segments ending with \ to prevent ambiguity with \/ escape - Replace json.Marshal with jsonMarshal (SetEscapeHTML=false) to avoid mangling &, <, > in JSON fields - Preserve folder insertion order instead of sorting alphabetically - Add functional test examples for manual import verification - Add *.csv to .gitignore --- .gitignore | 3 + examples/functional/main.go | 114 +++++++++++++++++++++++ qacsv_test.go | 174 ++++++++++++++++++++++++++++++++---- qascsv.go | 110 +++++++++++++++++++---- 4 files changed, 367 insertions(+), 34 deletions(-) create mode 100644 examples/functional/main.go diff --git a/.gitignore b/.gitignore index 6ecd0fc..e7bea99 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # .env files .env + +# Generated CSV files +*.csv diff --git a/examples/functional/main.go b/examples/functional/main.go new file mode 100644 index 0000000..2d56ac0 --- /dev/null +++ b/examples/functional/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "log" + + qascsv "github.com/hypersequent/qasphere-csv" +) + +func main() { + generateFolderComments() + generateEmptyFolders() + generateEscaping() +} + +func generateFolderComments() { + q := qascsv.NewQASphereCSV() + + if err := q.AddFolder([]string{"Commented Folder"}, "This folder has a comment but also contains test cases"); err != nil { + log.Fatal(err) + } + if err := q.AddTestCase(qascsv.TestCase{ + Title: "Test inside commented folder", + Folder: []string{"Commented Folder"}, + Priority: qascsv.PriorityHigh, + }); err != nil { + log.Fatal(err) + } + + if err := q.AddFolder([]string{"Another Folder"}, "Standalone comment on a folder with no test cases"); err != nil { + log.Fatal(err) + } + + if err := q.AddFolder([]string{"Parent", "Child With Comment"}, "Nested folder comment"); err != nil { + log.Fatal(err) + } + if err := q.AddTestCase(qascsv.TestCase{ + Title: "Test in parent folder", + Folder: []string{"Parent"}, + Priority: qascsv.PriorityMedium, + }); err != nil { + log.Fatal(err) + } + + if err := q.WriteCSVToFile("folder_comments.csv"); err != nil { + log.Fatal(err) + } + log.Println("wrote folder_comments.csv") +} + +func generateEmptyFolders() { + q := qascsv.NewQASphereCSV() + + if err := q.AddFolder([]string{"Empty Root Folder"}, ""); err != nil { + log.Fatal(err) + } + if err := q.AddFolder([]string{"Parent", "Empty Child"}, ""); err != nil { + log.Fatal(err) + } + if err := q.AddFolder([]string{"Parent", "Another Empty Child"}, ""); err != nil { + log.Fatal(err) + } + if err := q.AddTestCase(qascsv.TestCase{ + Title: "Test in parent alongside empty children", + Folder: []string{"Parent"}, + Priority: qascsv.PriorityLow, + }); err != nil { + log.Fatal(err) + } + + if err := q.AddFolder([]string{"Deep", "Nested", "Empty"}, ""); err != nil { + log.Fatal(err) + } + + if err := q.WriteCSVToFile("empty_folders.csv"); err != nil { + log.Fatal(err) + } + log.Println("wrote empty_folders.csv") +} + +func generateEscaping() { + q := qascsv.NewQASphereCSV() + + if err := q.AddTestCase(qascsv.TestCase{ + Title: "Test in folder with slash", + Folder: []string{"Features/Bugs", "Login"}, + Priority: qascsv.PriorityHigh, + }); err != nil { + log.Fatal(err) + } + if err := q.AddTestCase(qascsv.TestCase{ + Title: "Test in folder with multiple slashes", + Folder: []string{"A/B/C", "D/E"}, + Priority: qascsv.PriorityMedium, + }); err != nil { + log.Fatal(err) + } + + if err := q.AddFolder([]string{"Empty/Slash/Folder"}, "This empty folder name contains slashes"); err != nil { + log.Fatal(err) + } + + if err := q.AddTestCase(qascsv.TestCase{ + Title: "Test in normal folder for comparison", + Folder: []string{"Normal Folder", "Subfolder"}, + Priority: qascsv.PriorityLow, + }); err != nil { + log.Fatal(err) + } + + if err := q.WriteCSVToFile("escaping.csv"); err != nil { + log.Fatal(err) + } + log.Println("wrote escaping.csv") +} diff --git a/qacsv_test.go b/qacsv_test.go index 3764c34..abb1749 100644 --- a/qacsv_test.go +++ b/qacsv_test.go @@ -125,11 +125,11 @@ var successTestCases = []TestCase{ }, } -const successTestCasesCSV = `Folder,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,Step 2,Expected 2 -root,standalone,tc-with-minimal-fields,,false,high,,,,,,,,,,, -root,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",,,,action-1,,,expected-2 -root/child,standalone,tc-with-all-fields,legacy-id,false,high,"tag1,tag2",[req1](http://req1),"[link-1](http://link1),[link-2](http://link2)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",preconditions,,,action-1,expected-1,action-2,expected-2 -root/child,standalone,"tc-with-special-chars.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",legacy-id,false,high,"tag1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","[req.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;]()","[link-1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;](http://link1)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]","preconditions.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,,"action.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","expected.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",, +const successTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,Step 2,Expected 2 +root/child,,standalone,tc-with-all-fields,legacy-id,false,high,"tag1,tag2",[req1](http://req1),"[link-1](http://link1),[link-2](http://link2)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",preconditions,,,action-1,expected-1,action-2,expected-2 +root/child,,standalone,"tc-with-special-chars.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",legacy-id,false,high,"tag1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","[req.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;]()","[link-1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;](http://link1)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]","preconditions.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,,"action.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","expected.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",, +root,,standalone,tc-with-minimal-fields,,false,high,,,,,,,,,,, +root,,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",,,,action-1,,,expected-2 ` var failureTestCases = []TestCase{ @@ -146,12 +146,12 @@ var failureTestCases = []TestCase{ Folder: []string{}, Priority: "high", }, { - Title: "folder with empty title", - Folder: []string{"root/child"}, + Title: "folder with empty segment", + Folder: []string{"root", ""}, Priority: "high", }, { - Title: "folder title with slash", - Folder: []string{"root/child"}, + Title: "folder segment ending with backslash", + Folder: []string{"root\\"}, Priority: "high", }, { Title: "wrong priority", @@ -353,12 +353,12 @@ var customFieldSuccessTestCases = []TestCase{ }, } -const customFieldSuccessTestCasesCSV = `Folder,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,custom_field_dropdown_test_env,custom_field_dropdown_automation,custom_field_text_notes -custom-fields,standalone,tc-with-single-custom-field,,false,medium,,,,,,,,,,"{""value"":""staging"",""isDefault"":false}",, -custom-fields,standalone,tc-with-multiple-custom-fields,,false,high,"regression,smoke",,,,,,,Execute test,Test passes,"{""value"":""production"",""isDefault"":false}","{""value"":""Automated"",""isDefault"":false}","{""value"":""This is a test note with special chars: !@#$%^\u0026*()"",""isDefault"":false}" -custom-fields,standalone,tc-with-empty-custom-field-value,,false,low,,,,,,,,,,,,"{""value"":"""",""isDefault"":false}" -custom-fields,standalone,tc-with-default-custom-field,,false,medium,,,,,,,,,,,"{""value"":"""",""isDefault"":false}", -custom-fields/comprehensive,standalone,tc-with-all-fields-and-custom-fields,CF-001,false,high,"custom,comprehensive",[CF Requirements](http://cf-req),[CF Link](http://cf-link),"[{""fileName"":""cf-test.txt"",""id"":""cf-file-id"",""url"":""http://cf-file"",""mimeType"":""text/plain"",""size"":100}]",Custom field test setup,,,Step 1,Result 1,"{""value"":""development"",""isDefault"":false}","{""value"":""In Progress"",""isDefault"":false}", +const customFieldSuccessTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,custom_field_dropdown_test_env,custom_field_dropdown_automation,custom_field_text_notes +custom-fields,,standalone,tc-with-single-custom-field,,false,medium,,,,,,,,,,"{""value"":""staging"",""isDefault"":false}",, +custom-fields,,standalone,tc-with-multiple-custom-fields,,false,high,"regression,smoke",,,,,,,Execute test,Test passes,"{""value"":""production"",""isDefault"":false}","{""value"":""Automated"",""isDefault"":false}","{""value"":""This is a test note with special chars: !@#$%^&*()"",""isDefault"":false}" +custom-fields,,standalone,tc-with-empty-custom-field-value,,false,low,,,,,,,,,,,,"{""value"":"""",""isDefault"":false}" +custom-fields,,standalone,tc-with-default-custom-field,,false,medium,,,,,,,,,,,"{""value"":"""",""isDefault"":false}", +custom-fields/comprehensive,,standalone,tc-with-all-fields-and-custom-fields,CF-001,false,high,"custom,comprehensive",[CF Requirements](http://cf-req),[CF Link](http://cf-link),"[{""fileName"":""cf-test.txt"",""id"":""cf-file-id"",""url"":""http://cf-file"",""mimeType"":""text/plain"",""size"":100}]",Custom field test setup,,,Step 1,Result 1,"{""value"":""development"",""isDefault"":false}","{""value"":""In Progress"",""isDefault"":false}", ` var customFieldFailureTestCases = []TestCase{ @@ -456,3 +456,147 @@ func TestCustomFieldFailureTestCases(t *testing.T) { }) } } + +func TestFolderSlashEscaping(t *testing.T) { + qasCSV := NewQASphereCSV() + + err := qasCSV.AddTestCase(TestCase{ + Title: "tc-in-slash-folder", + Folder: []string{"root/parent", "child/leaf"}, + Priority: "high", + }) + require.NoError(t, err) + + csv, err := qasCSV.GenerateCSV() + require.NoError(t, err) + + expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params +root\/parent/child\/leaf,,standalone,tc-in-slash-folder,,false,high,,,,,,, +` + require.Equal(t, expected, csv) +} + +func TestFolderSegmentEndingWithBackslash(t *testing.T) { + qasCSV := NewQASphereCSV() + + err := qasCSV.AddTestCase(TestCase{ + Title: "tc-bad-backslash", + Folder: []string{"root\\"}, + Priority: "high", + }) + require.Error(t, err) + require.Contains(t, err.Error(), "must not end with '\\'") +} + +func TestAddFolderEmpty(t *testing.T) { + qasCSV := NewQASphereCSV() + + err := qasCSV.AddFolder([]string{"empty-folder"}, "") + require.NoError(t, err) + + csv, err := qasCSV.GenerateCSV() + require.NoError(t, err) + + expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params +empty-folder,,,,,,,,,,,,, +` + require.Equal(t, expected, csv) +} + +func TestAddFolderWithComment(t *testing.T) { + qasCSV := NewQASphereCSV() + + err := qasCSV.AddFolder([]string{"commented-folder"}, "This is a folder comment") + require.NoError(t, err) + + csv, err := qasCSV.GenerateCSV() + require.NoError(t, err) + + expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params +commented-folder,This is a folder comment,,,,,,,,,,,, +` + require.Equal(t, expected, csv) +} + +func TestAddFolderWithCommentAndTestCases(t *testing.T) { + qasCSV := NewQASphereCSV() + + err := qasCSV.AddFolder([]string{"my-folder"}, "Folder description") + require.NoError(t, err) + + err = qasCSV.AddTestCase(TestCase{ + Title: "tc-in-commented-folder", + Folder: []string{"my-folder"}, + Priority: "high", + }) + require.NoError(t, err) + + csv, err := qasCSV.GenerateCSV() + require.NoError(t, err) + + expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params +my-folder,Folder description,,,,,,,,,,,, +my-folder,,standalone,tc-in-commented-folder,,false,high,,,,,,, +` + require.Equal(t, expected, csv) +} + +func TestAddFolderValidation(t *testing.T) { + t.Run("empty folder path", func(t *testing.T) { + qasCSV := NewQASphereCSV() + err := qasCSV.AddFolder([]string{}, "comment") + require.Error(t, err) + require.Contains(t, err.Error(), "folder path must not be empty") + }) + + t.Run("empty folder segment", func(t *testing.T) { + qasCSV := NewQASphereCSV() + err := qasCSV.AddFolder([]string{"root", ""}, "comment") + require.Error(t, err) + require.Contains(t, err.Error(), "folder segment must not be empty") + }) + + t.Run("folder segment ending with backslash", func(t *testing.T) { + qasCSV := NewQASphereCSV() + err := qasCSV.AddFolder([]string{"root\\"}, "comment") + require.Error(t, err) + require.Contains(t, err.Error(), "must not end with '\\'") + }) + + t.Run("duplicate folder", func(t *testing.T) { + qasCSV := NewQASphereCSV() + err := qasCSV.AddFolder([]string{"root"}, "comment") + require.NoError(t, err) + err = qasCSV.AddFolder([]string{"root"}, "another comment") + require.Error(t, err) + require.Contains(t, err.Error(), "already exists") + }) + + t.Run("folder already has test cases", func(t *testing.T) { + qasCSV := NewQASphereCSV() + err := qasCSV.AddTestCase(TestCase{ + Title: "tc", + Folder: []string{"root"}, + Priority: "high", + }) + require.NoError(t, err) + err = qasCSV.AddFolder([]string{"root"}, "comment") + require.Error(t, err) + require.Contains(t, err.Error(), "already exists") + }) +} + +func TestAddFolderWithSlashInName(t *testing.T) { + qasCSV := NewQASphereCSV() + + err := qasCSV.AddFolder([]string{"folder/with/slashes", "child"}, "slash comment") + require.NoError(t, err) + + csv, err := qasCSV.GenerateCSV() + require.NoError(t, err) + + expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params +folder\/with\/slashes/child,slash comment,,,,,,,,,,,, +` + require.Equal(t, expected, csv) +} diff --git a/qascsv.go b/qascsv.go index 553fb8c..733700f 100644 --- a/qascsv.go +++ b/qascsv.go @@ -3,12 +3,12 @@ package qascsv import ( + "bytes" "encoding/csv" "encoding/json" "fmt" "io" "os" - "slices" "strconv" "strings" @@ -20,7 +20,7 @@ import ( // staticColumns will always be present in the CSV file // but there can be additional columns for steps and custom fields. var staticColumns = []string{ - "Folder", "Type", "Name", "Legacy ID", "Draft", "Priority", "Tags", "Requirements", + "Folder", "Folder Comment", "Type", "Name", "Legacy ID", "Draft", "Priority", "Tags", "Requirements", "Links", "Files", "Preconditions", "Parameter Values", "Template Suffix Params", } @@ -113,7 +113,7 @@ type TestCase struct { // for reference. (optional) LegacyID string `validate:"max=255"` // The complete folder path to the test case. (required) - Folder []string `validate:"min=1,dive,required,max=255,excludesall=/"` + Folder []string `validate:"min=1,dive,required,max=255"` // The priority of the test case. (required) Priority Priority `validate:"required,oneof=low medium high"` // The tags to assign to the test cases. This can be used to group, @@ -155,9 +155,11 @@ type TestCase struct { // QASphereCSV provides APIs to generate CSV that can be used to import // test cases in a project on QA Sphere. type QASphereCSV struct { - folderTCaseMap map[string][]TestCase - validate *validator.Validate - customFields []CustomField + folderTCaseMap map[string][]TestCase + folderCommentMap map[string]string + folderOrder []string + validate *validator.Validate + customFields []CustomField numTCases int maxSteps int @@ -165,8 +167,9 @@ type QASphereCSV struct { func NewQASphereCSV() *QASphereCSV { return &QASphereCSV{ - folderTCaseMap: make(map[string][]TestCase), - validate: validator.New(), + folderTCaseMap: make(map[string][]TestCase), + folderCommentMap: make(map[string]string), + validate: validator.New(), } } @@ -235,6 +238,35 @@ func (q *QASphereCSV) AddTestCases(tcs []TestCase) error { return nil } +func (q *QASphereCSV) AddFolder(folder []string, comment string) error { + if len(folder) == 0 { + return errors.New("folder path must not be empty") + } + for _, seg := range folder { + if seg == "" { + return errors.New("folder segment must not be empty") + } + if len(seg) > 255 { + return errors.Errorf("folder segment %q exceeds 255 character limit", seg) + } + } + if err := validateFolderSegments(folder); err != nil { + return err + } + + folderPath := escapeFolderPath(folder) + if _, exists := q.folderTCaseMap[folderPath]; exists { + return errors.Errorf("folder %q already exists", folderPath) + } + + q.folderOrder = append(q.folderOrder, folderPath) + q.folderTCaseMap[folderPath] = nil + if comment != "" { + q.folderCommentMap[folderPath] = comment + } + return nil +} + func (q *QASphereCSV) GenerateCSV() (string, error) { w := &strings.Builder{} if err := q.writeCSV(w); err != nil { @@ -257,7 +289,28 @@ func (q *QASphereCSV) WriteCSVToFile(file string) error { return nil } +func validateFolderSegments(segments []string) error { + for _, seg := range segments { + if strings.HasSuffix(seg, `\`) { + return errors.Errorf("folder segment %q must not end with '\\'", seg) + } + } + return nil +} + +func escapeFolderPath(segments []string) string { + escaped := make([]string, len(segments)) + for i, seg := range segments { + escaped[i] = strings.ReplaceAll(seg, "/", `\/`) + } + return strings.Join(escaped, "/") +} + func (q *QASphereCSV) validateTestCase(tc TestCase) error { + if err := validateFolderSegments(tc.Folder); err != nil { + return err + } + if tc.CustomFields != nil { for systemName := range tc.CustomFields { var found bool @@ -277,7 +330,10 @@ func (q *QASphereCSV) validateTestCase(tc TestCase) error { } func (q *QASphereCSV) addTCase(tc TestCase) { - folderPath := strings.Join(tc.Folder, "/") + folderPath := escapeFolderPath(tc.Folder) + if _, exists := q.folderTCaseMap[folderPath]; !exists { + q.folderOrder = append(q.folderOrder, folderPath) + } q.folderTCaseMap[folderPath] = append(q.folderTCaseMap[folderPath], tc) q.numTCases++ @@ -287,12 +343,17 @@ func (q *QASphereCSV) addTCase(tc TestCase) { } func (q *QASphereCSV) getFolders() []string { - var folders []string - for folder := range q.folderTCaseMap { - folders = append(folders, folder) + return q.folderOrder +} + +func jsonMarshal(v any) ([]byte, error) { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return nil, err } - slices.Sort(folders) - return folders + return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil } func (q *QASphereCSV) getCSVRows() ([][]string, error) { @@ -313,7 +374,18 @@ func (q *QASphereCSV) getCSVRows() ([][]string, error) { folders := q.getFolders() for _, f := range folders { - for _, tc := range q.folderTCaseMap[f] { + tcs := q.folderTCaseMap[f] + comment := q.folderCommentMap[f] + + // Write a folder-only row if the folder is empty or has a comment + if len(tcs) == 0 || comment != "" { + row := make([]string, numCols) + row[0] = f + row[1] = comment + rows = append(rows, row) + } + + for _, tc := range tcs { var requirements []string for _, req := range tc.Requirements { if req.Title == "" && req.URL == "" { @@ -330,7 +402,7 @@ func (q *QASphereCSV) getCSVRows() ([][]string, error) { var files string if len(tc.Files) > 0 { - filesb, err := json.Marshal(tc.Files) + filesb, err := jsonMarshal(tc.Files) if err != nil { return nil, errors.Wrap(err, "json marshal files") } @@ -339,7 +411,7 @@ func (q *QASphereCSV) getCSVRows() ([][]string, error) { var parameterValues string if len(tc.ParameterValues) > 0 { - parameterValuesb, err := json.Marshal(tc.ParameterValues) + parameterValuesb, err := jsonMarshal(tc.ParameterValues) if err != nil { return nil, errors.Wrap(err, "json marshal parameter values") } @@ -347,7 +419,7 @@ func (q *QASphereCSV) getCSVRows() ([][]string, error) { } row := make([]string, 0, numCols) - row = append(row, f, string(tc.Type), tc.Title, tc.LegacyID, strconv.FormatBool(tc.Draft), + row = append(row, f, "", string(tc.Type), tc.Title, tc.LegacyID, strconv.FormatBool(tc.Draft), string(tc.Priority), strings.Join(tc.Tags, ","), strings.Join(requirements, ","), strings.Join(links, ","), files, tc.Preconditions, parameterValues, strings.Join(tc.FilledTCaseTitleSuffixParams, ",")) @@ -363,7 +435,7 @@ func (q *QASphereCSV) getCSVRows() ([][]string, error) { customFieldCols := make([]string, len(customFieldsMap)) for systemName, cfValue := range tc.CustomFields { - cfValueJSON, err := json.Marshal(cfValue) + cfValueJSON, err := jsonMarshal(cfValue) if err != nil { return nil, errors.Wrap(err, "json marshal custom field value") } From 838cf8f653cfebb7d84c2faaf2031074430d7a91 Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Fri, 13 Feb 2026 18:22:44 +0400 Subject: [PATCH 2/5] Address PR review feedback from satvik007 - Introduce Folder struct with validation tags for AddFolder API - Use go-validator consistently for folder validation (same as TestCase) - Refactor examples/functional to return errors instead of log.Fatal --- examples/functional/main.go | 91 ++++++++++++++++++++++++------------- qacsv_test.go | 31 ++++++++----- qascsv.go | 30 ++++++------ 3 files changed, 94 insertions(+), 58 deletions(-) diff --git a/examples/functional/main.go b/examples/functional/main.go index 2d56ac0..64ec5fc 100644 --- a/examples/functional/main.go +++ b/examples/functional/main.go @@ -7,77 +7,102 @@ import ( ) func main() { - generateFolderComments() - generateEmptyFolders() - generateEscaping() + if err := generateFolderComments(); err != nil { + log.Fatal(err) + } + if err := generateEmptyFolders(); err != nil { + log.Fatal(err) + } + if err := generateEscaping(); err != nil { + log.Fatal(err) + } } -func generateFolderComments() { +func generateFolderComments() error { q := qascsv.NewQASphereCSV() - if err := q.AddFolder([]string{"Commented Folder"}, "This folder has a comment but also contains test cases"); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Commented Folder"}, + Comment: "This folder has a comment but also contains test cases", + }); err != nil { + return err } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test inside commented folder", Folder: []string{"Commented Folder"}, Priority: qascsv.PriorityHigh, }); err != nil { - log.Fatal(err) + return err } - if err := q.AddFolder([]string{"Another Folder"}, "Standalone comment on a folder with no test cases"); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Another Folder"}, + Comment: "Standalone comment on a folder with no test cases", + }); err != nil { + return err } - if err := q.AddFolder([]string{"Parent", "Child With Comment"}, "Nested folder comment"); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Parent", "Child With Comment"}, + Comment: "Nested folder comment", + }); err != nil { + return err } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in parent folder", Folder: []string{"Parent"}, Priority: qascsv.PriorityMedium, }); err != nil { - log.Fatal(err) + return err } if err := q.WriteCSVToFile("folder_comments.csv"); err != nil { - log.Fatal(err) + return err } log.Println("wrote folder_comments.csv") + return nil } -func generateEmptyFolders() { +func generateEmptyFolders() error { q := qascsv.NewQASphereCSV() - if err := q.AddFolder([]string{"Empty Root Folder"}, ""); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Empty Root Folder"}, + }); err != nil { + return err } - if err := q.AddFolder([]string{"Parent", "Empty Child"}, ""); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Parent", "Empty Child"}, + }); err != nil { + return err } - if err := q.AddFolder([]string{"Parent", "Another Empty Child"}, ""); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Parent", "Another Empty Child"}, + }); err != nil { + return err } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in parent alongside empty children", Folder: []string{"Parent"}, Priority: qascsv.PriorityLow, }); err != nil { - log.Fatal(err) + return err } - if err := q.AddFolder([]string{"Deep", "Nested", "Empty"}, ""); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Deep", "Nested", "Empty"}, + }); err != nil { + return err } if err := q.WriteCSVToFile("empty_folders.csv"); err != nil { - log.Fatal(err) + return err } log.Println("wrote empty_folders.csv") + return nil } -func generateEscaping() { +func generateEscaping() error { q := qascsv.NewQASphereCSV() if err := q.AddTestCase(qascsv.TestCase{ @@ -85,18 +110,21 @@ func generateEscaping() { Folder: []string{"Features/Bugs", "Login"}, Priority: qascsv.PriorityHigh, }); err != nil { - log.Fatal(err) + return err } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in folder with multiple slashes", Folder: []string{"A/B/C", "D/E"}, Priority: qascsv.PriorityMedium, }); err != nil { - log.Fatal(err) + return err } - if err := q.AddFolder([]string{"Empty/Slash/Folder"}, "This empty folder name contains slashes"); err != nil { - log.Fatal(err) + if err := q.AddFolder(qascsv.Folder{ + FolderPath: []string{"Empty/Slash/Folder"}, + Comment: "This empty folder name contains slashes", + }); err != nil { + return err } if err := q.AddTestCase(qascsv.TestCase{ @@ -104,11 +132,12 @@ func generateEscaping() { Folder: []string{"Normal Folder", "Subfolder"}, Priority: qascsv.PriorityLow, }); err != nil { - log.Fatal(err) + return err } if err := q.WriteCSVToFile("escaping.csv"); err != nil { - log.Fatal(err) + return err } log.Println("wrote escaping.csv") + return nil } diff --git a/qacsv_test.go b/qacsv_test.go index abb1749..f4b5b05 100644 --- a/qacsv_test.go +++ b/qacsv_test.go @@ -491,7 +491,7 @@ func TestFolderSegmentEndingWithBackslash(t *testing.T) { func TestAddFolderEmpty(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{"empty-folder"}, "") + err := qasCSV.AddFolder(Folder{FolderPath: []string{"empty-folder"}}) require.NoError(t, err) csv, err := qasCSV.GenerateCSV() @@ -506,7 +506,10 @@ empty-folder,,,,,,,,,,,,, func TestAddFolderWithComment(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{"commented-folder"}, "This is a folder comment") + err := qasCSV.AddFolder(Folder{ + FolderPath: []string{"commented-folder"}, + Comment: "This is a folder comment", + }) require.NoError(t, err) csv, err := qasCSV.GenerateCSV() @@ -521,7 +524,10 @@ commented-folder,This is a folder comment,,,,,,,,,,,, func TestAddFolderWithCommentAndTestCases(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{"my-folder"}, "Folder description") + err := qasCSV.AddFolder(Folder{ + FolderPath: []string{"my-folder"}, + Comment: "Folder description", + }) require.NoError(t, err) err = qasCSV.AddTestCase(TestCase{ @@ -544,30 +550,28 @@ my-folder,,standalone,tc-in-commented-folder,,false,high,,,,,,, func TestAddFolderValidation(t *testing.T) { t.Run("empty folder path", func(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{}, "comment") + err := qasCSV.AddFolder(Folder{Comment: "comment"}) require.Error(t, err) - require.Contains(t, err.Error(), "folder path must not be empty") }) t.Run("empty folder segment", func(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{"root", ""}, "comment") + err := qasCSV.AddFolder(Folder{FolderPath: []string{"root", ""}, Comment: "comment"}) require.Error(t, err) - require.Contains(t, err.Error(), "folder segment must not be empty") }) t.Run("folder segment ending with backslash", func(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{"root\\"}, "comment") + err := qasCSV.AddFolder(Folder{FolderPath: []string{"root\\"}, Comment: "comment"}) require.Error(t, err) require.Contains(t, err.Error(), "must not end with '\\'") }) t.Run("duplicate folder", func(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{"root"}, "comment") + err := qasCSV.AddFolder(Folder{FolderPath: []string{"root"}, Comment: "comment"}) require.NoError(t, err) - err = qasCSV.AddFolder([]string{"root"}, "another comment") + err = qasCSV.AddFolder(Folder{FolderPath: []string{"root"}, Comment: "another comment"}) require.Error(t, err) require.Contains(t, err.Error(), "already exists") }) @@ -580,7 +584,7 @@ func TestAddFolderValidation(t *testing.T) { Priority: "high", }) require.NoError(t, err) - err = qasCSV.AddFolder([]string{"root"}, "comment") + err = qasCSV.AddFolder(Folder{FolderPath: []string{"root"}, Comment: "comment"}) require.Error(t, err) require.Contains(t, err.Error(), "already exists") }) @@ -589,7 +593,10 @@ func TestAddFolderValidation(t *testing.T) { func TestAddFolderWithSlashInName(t *testing.T) { qasCSV := NewQASphereCSV() - err := qasCSV.AddFolder([]string{"folder/with/slashes", "child"}, "slash comment") + err := qasCSV.AddFolder(Folder{ + FolderPath: []string{"folder/with/slashes", "child"}, + Comment: "slash comment", + }) require.NoError(t, err) csv, err := qasCSV.GenerateCSV() diff --git a/qascsv.go b/qascsv.go index 733700f..a6cc52d 100644 --- a/qascsv.go +++ b/qascsv.go @@ -101,6 +101,14 @@ type CustomFieldValue struct { IsDefault bool `json:"isDefault" validate:"omitempty"` } +// Folder represents a folder to be created in QA Sphere. +type Folder struct { + // The folder path segments. (required) + FolderPath []string `validate:"min=1,dive,required,max=255"` + // An optional comment for the folder. + Comment string +} + // TestCase represents a test case in QA Sphere. type TestCase struct { // The title of the test case. (required) @@ -238,31 +246,23 @@ func (q *QASphereCSV) AddTestCases(tcs []TestCase) error { return nil } -func (q *QASphereCSV) AddFolder(folder []string, comment string) error { - if len(folder) == 0 { - return errors.New("folder path must not be empty") - } - for _, seg := range folder { - if seg == "" { - return errors.New("folder segment must not be empty") - } - if len(seg) > 255 { - return errors.Errorf("folder segment %q exceeds 255 character limit", seg) - } +func (q *QASphereCSV) AddFolder(f Folder) error { + if err := q.validate.Struct(f); err != nil { + return errors.Wrap(err, "folder validation") } - if err := validateFolderSegments(folder); err != nil { + if err := validateFolderSegments(f.FolderPath); err != nil { return err } - folderPath := escapeFolderPath(folder) + folderPath := escapeFolderPath(f.FolderPath) if _, exists := q.folderTCaseMap[folderPath]; exists { return errors.Errorf("folder %q already exists", folderPath) } q.folderOrder = append(q.folderOrder, folderPath) q.folderTCaseMap[folderPath] = nil - if comment != "" { - q.folderCommentMap[folderPath] = comment + if f.Comment != "" { + q.folderCommentMap[folderPath] = f.Comment } return nil } From 3fb795844373b6c7b8b467fa6d6075cf3631e904 Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Fri, 13 Feb 2026 20:09:40 +0400 Subject: [PATCH 3/5] Rename TestCase.Folder to FolderPath for clarity --- examples/basic/main.go | 6 ++-- examples/functional/main.go | 12 +++---- qacsv_test.go | 64 ++++++++++++++++++------------------- qascsv.go | 6 ++-- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/examples/basic/main.go b/examples/basic/main.go index 8448993..0f876c6 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -14,7 +14,7 @@ func main() { // Add a single test case if err := qasCSV.AddTestCase(qascsv.TestCase{ Title: "Changing to corresponding cursor after hovering the element", - Folder: []string{"Bistro Delivery", "About Us"}, + FolderPath: []string{"Bistro Delivery", "About Us"}, Priority: "low", Tags: []string{"About Us", "Checklist", "REQ-4", "UI"}, Preconditions: "The \"About Us\" page is opened", @@ -28,7 +28,7 @@ func main() { // Add multiple test cases if err := qasCSV.AddTestCases([]qascsv.TestCase{{ Title: "Cart should be cleared after making the checkout", - Folder: []string{"Bistro Delivery", "Cart", "Checkout"}, + FolderPath: []string{"Bistro Delivery", "Cart", "Checkout"}, Priority: "medium", Tags: []string{"Cart", "checkout", "REQ-6", "Functional"}, Preconditions: "1. Order is placed\n2. Successful message is shown", @@ -41,7 +41,7 @@ func main() { }}, }, { Title: "Changing to corresponding cursor after hovering the element", - Folder: []string{"Bistro Delivery", "Cart", "Checkout"}, + FolderPath: []string{"Bistro Delivery", "Cart", "Checkout"}, Priority: "low", Tags: []string{"Checklist", "REQ-6", "UI", "checkout"}, Preconditions: "The \"Checkout\" page is opened", diff --git a/examples/functional/main.go b/examples/functional/main.go index 64ec5fc..7e001e4 100644 --- a/examples/functional/main.go +++ b/examples/functional/main.go @@ -29,7 +29,7 @@ func generateFolderComments() error { } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test inside commented folder", - Folder: []string{"Commented Folder"}, + FolderPath: []string{"Commented Folder"}, Priority: qascsv.PriorityHigh, }); err != nil { return err @@ -50,7 +50,7 @@ func generateFolderComments() error { } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in parent folder", - Folder: []string{"Parent"}, + FolderPath: []string{"Parent"}, Priority: qascsv.PriorityMedium, }); err != nil { return err @@ -83,7 +83,7 @@ func generateEmptyFolders() error { } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in parent alongside empty children", - Folder: []string{"Parent"}, + FolderPath: []string{"Parent"}, Priority: qascsv.PriorityLow, }); err != nil { return err @@ -107,14 +107,14 @@ func generateEscaping() error { if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in folder with slash", - Folder: []string{"Features/Bugs", "Login"}, + FolderPath: []string{"Features/Bugs", "Login"}, Priority: qascsv.PriorityHigh, }); err != nil { return err } if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in folder with multiple slashes", - Folder: []string{"A/B/C", "D/E"}, + FolderPath: []string{"A/B/C", "D/E"}, Priority: qascsv.PriorityMedium, }); err != nil { return err @@ -129,7 +129,7 @@ func generateEscaping() error { if err := q.AddTestCase(qascsv.TestCase{ Title: "Test in normal folder for comparison", - Folder: []string{"Normal Folder", "Subfolder"}, + FolderPath: []string{"Normal Folder", "Subfolder"}, Priority: qascsv.PriorityLow, }); err != nil { return err diff --git a/qacsv_test.go b/qacsv_test.go index f4b5b05..7da935c 100644 --- a/qacsv_test.go +++ b/qacsv_test.go @@ -13,7 +13,7 @@ var successTestCases = []TestCase{ { Title: "tc-with-all-fields", LegacyID: "legacy-id", - Folder: []string{"root", "child"}, + FolderPath: []string{"root", "child"}, Priority: "high", Tags: []string{"tag1", "tag2"}, Preconditions: "preconditions", @@ -56,13 +56,13 @@ var successTestCases = []TestCase{ }, { Title: "tc-with-minimal-fields", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", }, { Title: "tc-with-special-chars.,<>/@$%\"\"''*&()[]{}+-`!~;", LegacyID: "legacy-id", - Folder: []string{"root", "child"}, + FolderPath: []string{"root", "child"}, Priority: "high", Tags: []string{"tag1.,<>/@$%\"\"''*&()[]{}+-`!~;"}, Preconditions: "preconditions.,<>/@$%\"\"''*&()[]{}+-`!~;", @@ -92,7 +92,7 @@ var successTestCases = []TestCase{ }, { Title: "tc-with-partial-fields", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "low", Tags: []string{}, Preconditions: "", @@ -135,71 +135,71 @@ root,,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileNam var failureTestCases = []TestCase{ { Title: "", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", }, { Title: strings.Repeat("a", 512), // Exceeds 511 char limit - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", }, { Title: "no folder", - Folder: []string{}, + FolderPath: []string{}, Priority: "high", }, { Title: "folder with empty segment", - Folder: []string{"root", ""}, + FolderPath: []string{"root", ""}, Priority: "high", }, { Title: "folder segment ending with backslash", - Folder: []string{"root\\"}, + FolderPath: []string{"root\\"}, Priority: "high", }, { Title: "wrong priority", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "very high", }, { Title: "empty tag", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Tags: []string{""}, }, { Title: "long tag", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Tags: []string{strings.Repeat("a", 256)}, // Exceeds 255 char limit }, { Title: "requirement without title and url", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Requirements: []Requirement{{}}, }, { Title: "requirement with invalid url", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Requirements: []Requirement{{URL: "ftp://req1"}}, }, { Title: "link without title and url", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Links: []Link{{}}, }, { Title: "link with no url", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Links: []Link{{Title: "link-1"}}, }, { Title: "link with no title", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Links: []Link{{URL: "http://link1"}}, }, { Title: "link with invalid url", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Links: []Link{{Title: "link-1", URL: "ftp://link1"}}, }, { Title: "file without name", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Files: []File{ { @@ -210,7 +210,7 @@ var failureTestCases = []TestCase{ }, }, { Title: "file without id and url", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Files: []File{ { @@ -221,7 +221,7 @@ var failureTestCases = []TestCase{ }, }, { Title: "file with invalid url", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", Files: []File{ { @@ -253,7 +253,7 @@ var customFields = []CustomField{ var customFieldSuccessTestCases = []TestCase{ { Title: "tc-with-single-custom-field", - Folder: []string{"custom-fields"}, + FolderPath: []string{"custom-fields"}, Priority: "medium", CustomFields: map[string]CustomFieldValue{ "test_env": { @@ -264,7 +264,7 @@ var customFieldSuccessTestCases = []TestCase{ }, { Title: "tc-with-multiple-custom-fields", - Folder: []string{"custom-fields"}, + FolderPath: []string{"custom-fields"}, Priority: "high", Tags: []string{"regression", "smoke"}, CustomFields: map[string]CustomFieldValue{ @@ -290,7 +290,7 @@ var customFieldSuccessTestCases = []TestCase{ }, { Title: "tc-with-empty-custom-field-value", - Folder: []string{"custom-fields"}, + FolderPath: []string{"custom-fields"}, Priority: "low", CustomFields: map[string]CustomFieldValue{ "notes": { @@ -301,7 +301,7 @@ var customFieldSuccessTestCases = []TestCase{ }, { Title: "tc-with-default-custom-field", - Folder: []string{"custom-fields"}, + FolderPath: []string{"custom-fields"}, Priority: "medium", CustomFields: map[string]CustomFieldValue{ "automation": { @@ -313,7 +313,7 @@ var customFieldSuccessTestCases = []TestCase{ { Title: "tc-with-all-fields-and-custom-fields", LegacyID: "CF-001", - Folder: []string{"custom-fields", "comprehensive"}, + FolderPath: []string{"custom-fields", "comprehensive"}, Priority: "high", Tags: []string{"custom", "comprehensive"}, Preconditions: "Custom field test setup", @@ -364,7 +364,7 @@ custom-fields/comprehensive,,standalone,tc-with-all-fields-and-custom-fields,CF- var customFieldFailureTestCases = []TestCase{ { Title: "tc-with-undefined-custom-field", - Folder: []string{"custom-fields-errors"}, + FolderPath: []string{"custom-fields-errors"}, Priority: "high", CustomFields: map[string]CustomFieldValue{ "undefined_field": { @@ -374,7 +374,7 @@ var customFieldFailureTestCases = []TestCase{ }, { Title: "tc-with-very-long-custom-field-value", - Folder: []string{"custom-fields-errors"}, + FolderPath: []string{"custom-fields-errors"}, Priority: "medium", CustomFields: map[string]CustomFieldValue{ "notes": { @@ -462,7 +462,7 @@ func TestFolderSlashEscaping(t *testing.T) { err := qasCSV.AddTestCase(TestCase{ Title: "tc-in-slash-folder", - Folder: []string{"root/parent", "child/leaf"}, + FolderPath: []string{"root/parent", "child/leaf"}, Priority: "high", }) require.NoError(t, err) @@ -481,7 +481,7 @@ func TestFolderSegmentEndingWithBackslash(t *testing.T) { err := qasCSV.AddTestCase(TestCase{ Title: "tc-bad-backslash", - Folder: []string{"root\\"}, + FolderPath: []string{"root\\"}, Priority: "high", }) require.Error(t, err) @@ -532,7 +532,7 @@ func TestAddFolderWithCommentAndTestCases(t *testing.T) { err = qasCSV.AddTestCase(TestCase{ Title: "tc-in-commented-folder", - Folder: []string{"my-folder"}, + FolderPath: []string{"my-folder"}, Priority: "high", }) require.NoError(t, err) @@ -580,7 +580,7 @@ func TestAddFolderValidation(t *testing.T) { qasCSV := NewQASphereCSV() err := qasCSV.AddTestCase(TestCase{ Title: "tc", - Folder: []string{"root"}, + FolderPath: []string{"root"}, Priority: "high", }) require.NoError(t, err) diff --git a/qascsv.go b/qascsv.go index a6cc52d..4dd68fc 100644 --- a/qascsv.go +++ b/qascsv.go @@ -121,7 +121,7 @@ type TestCase struct { // for reference. (optional) LegacyID string `validate:"max=255"` // The complete folder path to the test case. (required) - Folder []string `validate:"min=1,dive,required,max=255"` + FolderPath []string `validate:"min=1,dive,required,max=255"` // The priority of the test case. (required) Priority Priority `validate:"required,oneof=low medium high"` // The tags to assign to the test cases. This can be used to group, @@ -307,7 +307,7 @@ func escapeFolderPath(segments []string) string { } func (q *QASphereCSV) validateTestCase(tc TestCase) error { - if err := validateFolderSegments(tc.Folder); err != nil { + if err := validateFolderSegments(tc.FolderPath); err != nil { return err } @@ -330,7 +330,7 @@ func (q *QASphereCSV) validateTestCase(tc TestCase) error { } func (q *QASphereCSV) addTCase(tc TestCase) { - folderPath := escapeFolderPath(tc.Folder) + folderPath := escapeFolderPath(tc.FolderPath) if _, exists := q.folderTCaseMap[folderPath]; !exists { q.folderOrder = append(q.folderOrder, folderPath) } From 154675b86ca5dcf9aae6cc81d783860785192849 Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Fri, 13 Feb 2026 20:13:02 +0400 Subject: [PATCH 4/5] Fix gofumpt formatting --- examples/functional/main.go | 24 ++++---- qacsv_test.go | 118 ++++++++++++++++++------------------ 2 files changed, 71 insertions(+), 71 deletions(-) diff --git a/examples/functional/main.go b/examples/functional/main.go index 7e001e4..b19d413 100644 --- a/examples/functional/main.go +++ b/examples/functional/main.go @@ -28,9 +28,9 @@ func generateFolderComments() error { return err } if err := q.AddTestCase(qascsv.TestCase{ - Title: "Test inside commented folder", + Title: "Test inside commented folder", FolderPath: []string{"Commented Folder"}, - Priority: qascsv.PriorityHigh, + Priority: qascsv.PriorityHigh, }); err != nil { return err } @@ -49,9 +49,9 @@ func generateFolderComments() error { return err } if err := q.AddTestCase(qascsv.TestCase{ - Title: "Test in parent folder", + Title: "Test in parent folder", FolderPath: []string{"Parent"}, - Priority: qascsv.PriorityMedium, + Priority: qascsv.PriorityMedium, }); err != nil { return err } @@ -82,9 +82,9 @@ func generateEmptyFolders() error { return err } if err := q.AddTestCase(qascsv.TestCase{ - Title: "Test in parent alongside empty children", + Title: "Test in parent alongside empty children", FolderPath: []string{"Parent"}, - Priority: qascsv.PriorityLow, + Priority: qascsv.PriorityLow, }); err != nil { return err } @@ -106,16 +106,16 @@ func generateEscaping() error { q := qascsv.NewQASphereCSV() if err := q.AddTestCase(qascsv.TestCase{ - Title: "Test in folder with slash", + Title: "Test in folder with slash", FolderPath: []string{"Features/Bugs", "Login"}, - Priority: qascsv.PriorityHigh, + Priority: qascsv.PriorityHigh, }); err != nil { return err } if err := q.AddTestCase(qascsv.TestCase{ - Title: "Test in folder with multiple slashes", + Title: "Test in folder with multiple slashes", FolderPath: []string{"A/B/C", "D/E"}, - Priority: qascsv.PriorityMedium, + Priority: qascsv.PriorityMedium, }); err != nil { return err } @@ -128,9 +128,9 @@ func generateEscaping() error { } if err := q.AddTestCase(qascsv.TestCase{ - Title: "Test in normal folder for comparison", + Title: "Test in normal folder for comparison", FolderPath: []string{"Normal Folder", "Subfolder"}, - Priority: qascsv.PriorityLow, + Priority: qascsv.PriorityLow, }); err != nil { return err } diff --git a/qacsv_test.go b/qacsv_test.go index 7da935c..5b80714 100644 --- a/qacsv_test.go +++ b/qacsv_test.go @@ -55,9 +55,9 @@ var successTestCases = []TestCase{ Draft: false, }, { - Title: "tc-with-minimal-fields", + Title: "tc-with-minimal-fields", FolderPath: []string{"root"}, - Priority: "high", + Priority: "high", }, { Title: "tc-with-special-chars.,<>/@$%\"\"''*&()[]{}+-`!~;", @@ -134,39 +134,39 @@ root,,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileNam var failureTestCases = []TestCase{ { - Title: "", + Title: "", FolderPath: []string{"root"}, - Priority: "high", + Priority: "high", }, { - Title: strings.Repeat("a", 512), // Exceeds 511 char limit + Title: strings.Repeat("a", 512), // Exceeds 511 char limit FolderPath: []string{"root"}, - Priority: "high", + Priority: "high", }, { - Title: "no folder", + Title: "no folder", FolderPath: []string{}, - Priority: "high", + Priority: "high", }, { - Title: "folder with empty segment", + Title: "folder with empty segment", FolderPath: []string{"root", ""}, - Priority: "high", + Priority: "high", }, { - Title: "folder segment ending with backslash", + Title: "folder segment ending with backslash", FolderPath: []string{"root\\"}, - Priority: "high", + Priority: "high", }, { - Title: "wrong priority", + Title: "wrong priority", FolderPath: []string{"root"}, - Priority: "very high", + Priority: "very high", }, { - Title: "empty tag", + Title: "empty tag", FolderPath: []string{"root"}, - Priority: "high", - Tags: []string{""}, + Priority: "high", + Tags: []string{""}, }, { - Title: "long tag", + Title: "long tag", FolderPath: []string{"root"}, - Priority: "high", - Tags: []string{strings.Repeat("a", 256)}, // Exceeds 255 char limit + Priority: "high", + Tags: []string{strings.Repeat("a", 256)}, // Exceeds 255 char limit }, { Title: "requirement without title and url", FolderPath: []string{"root"}, @@ -178,29 +178,29 @@ var failureTestCases = []TestCase{ Priority: "high", Requirements: []Requirement{{URL: "ftp://req1"}}, }, { - Title: "link without title and url", + Title: "link without title and url", FolderPath: []string{"root"}, - Priority: "high", - Links: []Link{{}}, + Priority: "high", + Links: []Link{{}}, }, { - Title: "link with no url", + Title: "link with no url", FolderPath: []string{"root"}, - Priority: "high", - Links: []Link{{Title: "link-1"}}, + Priority: "high", + Links: []Link{{Title: "link-1"}}, }, { - Title: "link with no title", + Title: "link with no title", FolderPath: []string{"root"}, - Priority: "high", - Links: []Link{{URL: "http://link1"}}, + Priority: "high", + Links: []Link{{URL: "http://link1"}}, }, { - Title: "link with invalid url", + Title: "link with invalid url", FolderPath: []string{"root"}, - Priority: "high", - Links: []Link{{Title: "link-1", URL: "ftp://link1"}}, + Priority: "high", + Links: []Link{{Title: "link-1", URL: "ftp://link1"}}, }, { - Title: "file without name", + Title: "file without name", FolderPath: []string{"root"}, - Priority: "high", + Priority: "high", Files: []File{ { MimeType: "text/csv", @@ -209,9 +209,9 @@ var failureTestCases = []TestCase{ }, }, }, { - Title: "file without id and url", + Title: "file without id and url", FolderPath: []string{"root"}, - Priority: "high", + Priority: "high", Files: []File{ { Name: "file-1.csv", @@ -220,9 +220,9 @@ var failureTestCases = []TestCase{ }, }, }, { - Title: "file with invalid url", + Title: "file with invalid url", FolderPath: []string{"root"}, - Priority: "high", + Priority: "high", Files: []File{ { Name: "file-1.csv", @@ -252,9 +252,9 @@ var customFields = []CustomField{ var customFieldSuccessTestCases = []TestCase{ { - Title: "tc-with-single-custom-field", + Title: "tc-with-single-custom-field", FolderPath: []string{"custom-fields"}, - Priority: "medium", + Priority: "medium", CustomFields: map[string]CustomFieldValue{ "test_env": { Value: "staging", @@ -263,10 +263,10 @@ var customFieldSuccessTestCases = []TestCase{ }, }, { - Title: "tc-with-multiple-custom-fields", + Title: "tc-with-multiple-custom-fields", FolderPath: []string{"custom-fields"}, - Priority: "high", - Tags: []string{"regression", "smoke"}, + Priority: "high", + Tags: []string{"regression", "smoke"}, CustomFields: map[string]CustomFieldValue{ "test_env": { Value: "production", @@ -289,9 +289,9 @@ var customFieldSuccessTestCases = []TestCase{ }, }, { - Title: "tc-with-empty-custom-field-value", + Title: "tc-with-empty-custom-field-value", FolderPath: []string{"custom-fields"}, - Priority: "low", + Priority: "low", CustomFields: map[string]CustomFieldValue{ "notes": { Value: "", @@ -300,9 +300,9 @@ var customFieldSuccessTestCases = []TestCase{ }, }, { - Title: "tc-with-default-custom-field", + Title: "tc-with-default-custom-field", FolderPath: []string{"custom-fields"}, - Priority: "medium", + Priority: "medium", CustomFields: map[string]CustomFieldValue{ "automation": { Value: "", @@ -363,9 +363,9 @@ custom-fields/comprehensive,,standalone,tc-with-all-fields-and-custom-fields,CF- var customFieldFailureTestCases = []TestCase{ { - Title: "tc-with-undefined-custom-field", + Title: "tc-with-undefined-custom-field", FolderPath: []string{"custom-fields-errors"}, - Priority: "high", + Priority: "high", CustomFields: map[string]CustomFieldValue{ "undefined_field": { Value: "some value", @@ -373,9 +373,9 @@ var customFieldFailureTestCases = []TestCase{ }, }, { - Title: "tc-with-very-long-custom-field-value", + Title: "tc-with-very-long-custom-field-value", FolderPath: []string{"custom-fields-errors"}, - Priority: "medium", + Priority: "medium", CustomFields: map[string]CustomFieldValue{ "notes": { Value: strings.Repeat("a", 256), // Exceeds 255 char limit @@ -461,9 +461,9 @@ func TestFolderSlashEscaping(t *testing.T) { qasCSV := NewQASphereCSV() err := qasCSV.AddTestCase(TestCase{ - Title: "tc-in-slash-folder", + Title: "tc-in-slash-folder", FolderPath: []string{"root/parent", "child/leaf"}, - Priority: "high", + Priority: "high", }) require.NoError(t, err) @@ -480,9 +480,9 @@ func TestFolderSegmentEndingWithBackslash(t *testing.T) { qasCSV := NewQASphereCSV() err := qasCSV.AddTestCase(TestCase{ - Title: "tc-bad-backslash", + Title: "tc-bad-backslash", FolderPath: []string{"root\\"}, - Priority: "high", + Priority: "high", }) require.Error(t, err) require.Contains(t, err.Error(), "must not end with '\\'") @@ -531,9 +531,9 @@ func TestAddFolderWithCommentAndTestCases(t *testing.T) { require.NoError(t, err) err = qasCSV.AddTestCase(TestCase{ - Title: "tc-in-commented-folder", + Title: "tc-in-commented-folder", FolderPath: []string{"my-folder"}, - Priority: "high", + Priority: "high", }) require.NoError(t, err) @@ -579,9 +579,9 @@ func TestAddFolderValidation(t *testing.T) { t.Run("folder already has test cases", func(t *testing.T) { qasCSV := NewQASphereCSV() err := qasCSV.AddTestCase(TestCase{ - Title: "tc", + Title: "tc", FolderPath: []string{"root"}, - Priority: "high", + Priority: "high", }) require.NoError(t, err) err = qasCSV.AddFolder(Folder{FolderPath: []string{"root"}, Comment: "comment"}) From 517bf598c8183213cc7500b15c2bde0220d3082d Mon Sep 17 00:00:00 2001 From: Andrian Budantsov Date: Fri, 13 Feb 2026 20:20:15 +0400 Subject: [PATCH 5/5] Add build-examples step to Makefile and CI --- .github/workflows/ci.yml | 3 +++ Makefile | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b220f4b..ec9eac5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,5 +41,8 @@ jobs: version: v1.62.2 args: --verbose --timeout=3m + - name: Build examples + run: make build-examples + - name: Test run: make test diff --git a/Makefile b/Makefile index 1555ea4..b9e1ee1 100644 --- a/Makefile +++ b/Makefile @@ -12,4 +12,7 @@ lint: linters-install test: $(GOCMD) test -v -cover -race ./... -.PHONY: test lint linters-install +build-examples: + $(GOCMD) build ./examples/... + +.PHONY: test lint linters-install build-examples