From da4633a4213b697e40c997733ef5035aac92a371 Mon Sep 17 00:00:00 2001 From: GrindStone Date: Thu, 14 Nov 2024 16:42:18 +0700 Subject: [PATCH] Localisation support (#665) * Add dialect options to support localisation * Add test for ParseFeatures support for localisation --- internal/flags/options.go | 3 + internal/parser/parser.go | 34 ++++---- internal/parser/parser_test.go | 137 ++++++++++++++++++++++++++++++++- run.go | 6 +- suite_context_test.go | 5 +- 5 files changed, 162 insertions(+), 23 deletions(-) diff --git a/internal/flags/options.go b/internal/flags/options.go index 5ca40f91..40acea65 100644 --- a/internal/flags/options.go +++ b/internal/flags/options.go @@ -48,6 +48,9 @@ type Options struct { // from feature files Tags string + // Dialect to be used to parse feature files. If not set, default to "en". + Dialect string + // The formatter name Format string diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 3db1b526..f607000a 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -33,7 +33,7 @@ func ExtractFeaturePathLine(p string) (string, int) { return retPath, line } -func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models.Feature, error) { +func parseFeatureFile(fsys fs.FS, path, dialect string, newIDFunc func() string) (*models.Feature, error) { reader, err := fsys.Open(path) if err != nil { return nil, err @@ -42,7 +42,7 @@ func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models defer reader.Close() var buf bytes.Buffer - gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc) + gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc) if err != nil { return nil, fmt.Errorf("%s - %v", path, err) } @@ -54,11 +54,11 @@ func parseFeatureFile(fsys fs.FS, path string, newIDFunc func() string) (*models return &f, nil } -func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.Feature, error) { +func parseBytes(path string, feature []byte, dialect string, newIDFunc func() string) (*models.Feature, error) { reader := bytes.NewReader(feature) var buf bytes.Buffer - gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc) + gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc) if err != nil { return nil, fmt.Errorf("%s - %v", path, err) } @@ -70,7 +70,7 @@ func parseBytes(path string, feature []byte, newIDFunc func() string) (*models.F return &f, nil } -func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models.Feature, error) { +func parseFeatureDir(fsys fs.FS, dir, dialect string, newIDFunc func() string) ([]*models.Feature, error) { var features []*models.Feature return features, fs.WalkDir(fsys, dir, func(p string, f fs.DirEntry, err error) error { if err != nil { @@ -85,7 +85,7 @@ func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models return nil } - feat, err := parseFeatureFile(fsys, p, newIDFunc) + feat, err := parseFeatureFile(fsys, p, dialect, newIDFunc) if err != nil { return err } @@ -95,7 +95,7 @@ func parseFeatureDir(fsys fs.FS, dir string, newIDFunc func() string) ([]*models }) } -func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feature, error) { +func parsePath(fsys fs.FS, path, dialect string, newIDFunc func() string) ([]*models.Feature, error) { var features []*models.Feature path, line := ExtractFeaturePathLine(path) @@ -114,10 +114,10 @@ func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feat } if fi.IsDir() { - return parseFeatureDir(fsys, path, newIDFunc) + return parseFeatureDir(fsys, path, dialect, newIDFunc) } - ft, err := parseFeatureFile(fsys, path, newIDFunc) + ft, err := parseFeatureFile(fsys, path, dialect, newIDFunc) if err != nil { return features, err } @@ -146,14 +146,18 @@ func parsePath(fsys fs.FS, path string, newIDFunc func() string) ([]*models.Feat } // ParseFeatures ... -func ParseFeatures(fsys fs.FS, filter string, paths []string) ([]*models.Feature, error) { +func ParseFeatures(fsys fs.FS, filter, dialect string, paths []string) ([]*models.Feature, error) { var order int + if dialect == "" { + dialect = gherkin.DefaultDialect + } + featureIdxs := make(map[string]int) uniqueFeatureURI := make(map[string]*models.Feature) newIDFunc := (&messages.Incrementing{}).NewId for _, path := range paths { - feats, err := parsePath(fsys, path, newIDFunc) + feats, err := parsePath(fsys, path, dialect, newIDFunc) switch { case os.IsNotExist(err): @@ -189,14 +193,18 @@ func ParseFeatures(fsys fs.FS, filter string, paths []string) ([]*models.Feature type FeatureContent = flags.Feature -func ParseFromBytes(filter string, featuresInputs []FeatureContent) ([]*models.Feature, error) { +func ParseFromBytes(filter, dialect string, featuresInputs []FeatureContent) ([]*models.Feature, error) { var order int + if dialect == "" { + dialect = gherkin.DefaultDialect + } + featureIdxs := make(map[string]int) uniqueFeatureURI := make(map[string]*models.Feature) newIDFunc := (&messages.Incrementing{}).NewId for _, f := range featuresInputs { - ft, err := parseBytes(f.Name, f.Contents, newIDFunc) + ft, err := parseBytes(f.Name, f.Contents, dialect, newIDFunc) if err != nil { return nil, err } diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 222f3175..265d3230 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -54,7 +54,7 @@ Feature: eat godogs {Name: "MyCoolDuplicatedFeature", Contents: []byte(eatGodogContents)}, } - featureFromBytes, err := parser.ParseFromBytes("", input) + featureFromBytes, err := parser.ParseFromBytes("", "", input) require.NoError(t, err) require.Len(t, featureFromBytes, 1) } @@ -80,7 +80,7 @@ Feature: eat godogs }, } - featureFromFile, err := parser.ParseFeatures(fsys, "", []string{baseDir}) + featureFromFile, err := parser.ParseFeatures(fsys, "", "", []string{baseDir}) require.NoError(t, err) require.Len(t, featureFromFile, 1) @@ -88,7 +88,7 @@ Feature: eat godogs {Name: filepath.Join(baseDir, featureFileName), Contents: []byte(eatGodogContents)}, } - featureFromBytes, err := parser.ParseFromBytes("", input) + featureFromBytes, err := parser.ParseFromBytes("", "", input) require.NoError(t, err) require.Len(t, featureFromBytes, 1) @@ -155,7 +155,7 @@ func Test_ParseFeatures_FromMultiplePaths(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - features, err := parser.ParseFeatures(test.fsys, "", test.paths) + features, err := parser.ParseFeatures(test.fsys, "", "", test.paths) if test.expError != nil { require.Error(t, err) require.EqualError(t, err, test.expError.Error()) @@ -178,3 +178,132 @@ func Test_ParseFeatures_FromMultiplePaths(t *testing.T) { }) } } + +func Test_ParseFeatures_Localisation(t *testing.T) { + tests := map[string]struct { + dialect string + contents string + }{ + "english": { + dialect: "en", + contents: ` +Feature: dummy + Rule: dummy + Background: dummy + Given dummy + When dummy + Then dummy + Scenario: dummy + Given dummy + When dummy + Then dummy + And dummy + But dummy + Example: dummy + Given dummy + When dummy + Then dummy + Scenario Outline: dummy + Given dummy + When dummy + Then dummy + `, + }, + "afrikaans": { + dialect: "af", + contents: ` +Funksie: dummy + Regel: dummy + Agtergrond: dummy + Gegewe dummy + Wanneer dummy + Dan dummy + Voorbeeld: dummy + Gegewe dummy + Wanneer dummy + Dan dummy + En dummy + Maar dummy + Voorbeelde: dummy + Gegewe dummy + Wanneer dummy + Dan dummy + Situasie Uiteensetting: dummy + Gegewe dummy + Wanneer dummy + Dan dummy + `, + }, + "arabic": { + dialect: "ar", + contents: ` +خاصية: dummy + Rule: dummy + الخلفية: dummy + بفرض dummy + متى dummy + اذاً dummy + مثال: dummy + بفرض dummy + متى dummy + اذاً dummy + و dummy + لكن dummy + امثلة: dummy + بفرض dummy + متى dummy + اذاً dummy + سيناريو مخطط: dummy + بفرض dummy + متى dummy + اذاً dummy + `, + }, + "chinese simplified": { + dialect: "zh-CN", + contents: ` +功能: dummy + 规则: dummy + 背景: dummy + 假如 dummy + 当 dummy + 那么 dummy + 场景: dummy + 假如 dummy + 当 dummy + 那么 dummy + 而且 dummy + 但是 dummy + 例子: dummy + 假如 dummy + 当 dummy + 那么 dummy + 场景大纲: dummy + 假如 dummy + 当 dummy + 那么 dummy + `, + }, + } + + featureFileName := "godogs.feature" + baseDir := "base" + + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + t.Parallel() + + fsys := fstest.MapFS{ + filepath.Join(baseDir, featureFileName): { + Data: []byte(test.contents), + Mode: fs.FileMode(0o644), + }, + } + + featureTestDialect, err := parser.ParseFeatures(fsys, "", test.dialect, []string{baseDir}) + require.NoError(t, err) + require.Len(t, featureTestDialect, 1) + }) + } +} diff --git a/run.go b/run.go index 7378b2fe..1231d028 100644 --- a/run.go +++ b/run.go @@ -247,7 +247,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int { opt.FS = storage.FS{FS: opt.FS} if len(opt.FeatureContents) > 0 { - features, err := parser.ParseFromBytes(opt.Tags, opt.FeatureContents) + features, err := parser.ParseFromBytes(opt.Tags, opt.Dialect, opt.FeatureContents) if err != nil { fmt.Fprintln(os.Stderr, err) return exitOptionError @@ -256,7 +256,7 @@ func runWithOptions(suiteName string, runner runner, opt Options) int { } if len(opt.Paths) > 0 { - features, err := parser.ParseFeatures(opt.FS, opt.Tags, opt.Paths) + features, err := parser.ParseFeatures(opt.FS, opt.Tags, opt.Dialect, opt.Paths) if err != nil { fmt.Fprintln(os.Stderr, err) return exitOptionError @@ -389,7 +389,7 @@ func (ts TestSuite) RetrieveFeatures() ([]*models.Feature, error) { } } - return parser.ParseFeatures(opt.FS, opt.Tags, opt.Paths) + return parser.ParseFeatures(opt.FS, opt.Tags, opt.Dialect, opt.Paths) } func getDefaultOptions() (*Options, error) { diff --git a/suite_context_test.go b/suite_context_test.go index b7b5e9de..9b042d82 100644 --- a/suite_context_test.go +++ b/suite_context_test.go @@ -494,7 +494,7 @@ func (tc *godogFeaturesScenario) theLoggedMessagesShouldInclude(ctx context.Cont } func (tc *godogFeaturesScenario) followingStepsShouldHave(status string, steps *DocString) error { - var expected = strings.Split(steps.Content, "\n") + expected := strings.Split(steps.Content, "\n") var actual, unmatched, matched []string storage := tc.testedSuite.storage @@ -673,7 +673,7 @@ func (tc *godogFeaturesScenario) featurePath(path string) { } func (tc *godogFeaturesScenario) parseFeatures() error { - fts, err := parser.ParseFeatures(storage.FS{}, "", tc.paths) + fts, err := parser.ParseFeatures(storage.FS{}, "", "", tc.paths) if err != nil { return err } @@ -1226,7 +1226,6 @@ func TestTestSuite_Run(t *testing.T) { s.Step("^multistep has ambiguous$", func() Steps { return Steps{"step is ambiguous"} }) - }, Options: &Options{ Format: "pretty",