From 4105eac1b1fb2fe647d60e9065c19e569a60817a Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 10 Jul 2026 16:33:56 +0200 Subject: [PATCH 1/3] Improve `cli` test coverage. --- cli/cli_test.go | 249 ++++++++++++++++++ .../config_files/optional_root_config.yml | 10 + 2 files changed, 259 insertions(+) create mode 100644 test/resources/config_files/optional_root_config.yml diff --git a/cli/cli_test.go b/cli/cli_test.go index 56191808..5173c02e 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -28,7 +28,10 @@ package cli_test import ( "embed-code/embed-code-go/cli" + "embed-code/embed-code-go/configuration" _type "embed-code/embed-code-go/type" + "flag" + "io" "os" "path/filepath" "testing" @@ -68,10 +71,20 @@ var _ = Describe("CLI validation", func() { ConfigPath: configFilePath(), } + Expect(cli.IsUsingConfigFile(config)).To(BeTrue()) Expect(cli.ValidateConfig(config)).Error().ShouldNot(HaveOccurred()) Expect(cli.ValidateConfigFile(config)).Error().ShouldNot(HaveOccurred()) }) + It("should pass validation when no roots are set", func() { + config := cli.Config{ + Mode: cli.ModeCheck, + } + + Expect(cli.IsUsingConfigFile(config)).To(BeFalse()) + Expect(cli.ValidateConfig(config)).Error().ShouldNot(HaveOccurred()) + }) + It("should pass validation when embeddings are set", func() { config := cli.Config{ Mode: cli.ModeCheck, @@ -168,6 +181,31 @@ var _ = Describe("CLI validation", func() { "embedding #1: `name` must be set")) }) + It("should fail validation when embedding name contains illegal folder characters", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeCheck, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + } + invalidConfig.Embeddings[0].Name = "bad/name" + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "embedding `bad/name`: `name` `bad/name` is not valid, " + + "those characters are not allowed `/\\ *?:\"<>|`")) + }) + + It("should fail validation when embedding roots are incomplete", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeCheck, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + } + invalidConfig.Embeddings[0].DocsPath = "" + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "embedding `docs`: `code-path` and `docs-path` must both be set")) + }) + It("should fail validation when embedding names are duplicated", func() { embedding := baseEmbeddingConfig() duplicateEmbedding := baseEmbeddingConfig() @@ -193,6 +231,18 @@ var _ = Describe("CLI validation", func() { "duplicate source code path names detected:\n- samples")) }) + It("should fail validation when code path name contains illegal folder characters", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "bad/name", Path: codeResourcePath("java")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "the given code path name `bad/name` is not a valid name for the folder, " + + "those characters are not allowed `/\\ *?:\"<>|`")) + }) + It("should fail validation when multiple unnamed sources code paths are configured", func() { invalidConfig := baseCliConfig() invalidConfig.BaseCodePaths = _type.NamedPathList{ @@ -217,6 +267,29 @@ var _ = Describe("CLI validation", func() { "named and unnamed source code paths cannot be mixed")) }) + It("should pass validation and warn when duplicate docs paths are configured", func() { + first := baseEmbeddingConfig() + first.Name = "first" + second := baseEmbeddingConfig() + second.Name = "second" + config := cli.Config{ + Mode: cli.ModeCheck, + Embeddings: []cli.EmbeddingConfig{first, second}, + } + + Expect(cli.ValidateConfig(config)).Error().ShouldNot(HaveOccurred()) + }) + + It("should pass validation and warn when duplicate named code paths are configured", func() { + config := baseCliConfig() + config.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "first", Path: codeResourcePath("java")}, + _type.NamedPath{Name: "second", Path: codeResourcePath("java")}, + } + + Expect(cli.ValidateConfig(config)).Error().ShouldNot(HaveOccurred()) + }) + It("should correctly convert embeddings to a few configs", func() { config := cli.Config{ Mode: cli.ModeCheck, @@ -254,6 +327,127 @@ var _ = Describe("CLI validation", func() { }) +var _ = Describe("CLI arguments", func() { + + It("should read command-line arguments", func() { + config := readArgs( + "-mode=embed", + "-code-path=/code", + "-docs-path=/docs", + "-doc-includes=**/*.md, guides/*.html, ,", + "-doc-excludes=archive/**/*, drafts/**/*.md", + "-separator=---", + "-config-path=config.yml", + "-info=true", + "-stacktrace=true", + ) + + Expect(config.Mode).To(Equal(cli.ModeEmbed)) + Expect(config.BaseCodePaths).To(Equal(_type.NamedPathList{ + _type.NamedPath{Path: "/code"}, + })) + Expect(config.BaseDocsPath).To(Equal("/docs")) + Expect(config.DocIncludes).To(Equal(_type.StringList{"**/*.md", "guides/*.html"})) + Expect(config.DocExcludes).To(Equal(_type.StringList{"archive/**/*", "drafts/**/*.md"})) + Expect(config.Separator).To(Equal("---")) + Expect(config.ConfigPath).To(Equal("config.yml")) + Expect(config.Info).To(BeTrue()) + Expect(config.Stacktrace).To(BeTrue()) + }) + +}) + +var _ = Describe("CLI configuration building", func() { + + It("should fill args from a config file with optional root settings", func() { + config := cli.Config{ + Mode: cli.ModeCheck, + ConfigPath: "../test/resources/config_files/optional_root_config.yml", + } + + fileConfig, err := cli.FillArgsFromConfigFile(config) + + Expect(err).ToNot(HaveOccurred()) + Expect(fileConfig.BaseCodePaths).To(Equal(_type.NamedPathList{ + _type.NamedPath{Name: "java", Path: "test/resources/code/java"}, + })) + Expect(fileConfig.BaseDocsPath).To(Equal("test/resources/docs")) + Expect(fileConfig.DocIncludes).To(Equal(_type.StringList{"**/*.md"})) + Expect(fileConfig.DocExcludes).To(Equal(_type.StringList{"archive/**/*", "drafts/**/*.md"})) + Expect(fileConfig.Separator).To(Equal("---")) + Expect(fileConfig.Info).To(BeTrue()) + Expect(fileConfig.Stacktrace).To(BeTrue()) + }) + + It("should return an error when config file YAML is invalid", func() { + configPath := writeTempConfigFile("doc-includes: [") + config := cli.Config{ + Mode: cli.ModeCheck, + ConfigPath: configPath, + } + + _, err := cli.FillArgsFromConfigFile(config) + + Expect(err).To(HaveOccurred()) + }) + + It("should build default command-line configuration without roots", func() { + configs := cli.BuildEmbedCodeConfiguration(cli.Config{}) + + Expect(configs).To(HaveLen(1)) + Expect(configs[0]).To(Equal(configuration.NewConfiguration())) + }) + + It("should build embedding configuration with optional settings", func() { + embedding := baseEmbeddingConfig() + embedding.CodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "java", Path: codeResourcePath("java")}, + _type.NamedPath{Name: "kotlin", Path: codeResourcePath("kotlin")}, + } + embedding.DocIncludes = []string{"guides/**/*.md"} + embedding.DocExcludes = []string{"archive/**/*"} + embedding.Separator = "---" + config := cli.Config{ + Mode: cli.ModeCheck, + Embeddings: []cli.EmbeddingConfig{embedding}, + } + + configs := cli.BuildEmbedCodeConfiguration(config) + + Expect(configs).To(HaveLen(1)) + Expect(configs[0].Name).To(Equal("docs")) + Expect(configs[0].CodeRoots).To(Equal(embedding.CodePaths)) + Expect(configs[0].DocumentationRoot).To(Equal(embedding.DocsPath)) + Expect(configs[0].DocIncludes).To(Equal([]string{"guides/**/*.md"})) + Expect(configs[0].DocExcludes).To(Equal([]string{"archive/**/*"})) + Expect(configs[0].Separator).To(Equal("---")) + }) + +}) + +var _ = Describe("CLI processing wrappers", func() { + + It("should check selected docs through the public wrapper", func() { + config := noEmbeddingInstructionsConfig() + + staleDocs, err := cli.CheckCodeSamples(config) + + Expect(err).ToNot(HaveOccurred()) + Expect(staleDocs).To(BeEmpty()) + }) + + It("should embed selected docs through the public wrapper", func() { + config := noEmbeddingInstructionsConfig() + + result, err := cli.EmbedCodeSamples(config) + + Expect(err).ToNot(HaveOccurred()) + Expect(result.TotalEmbeddings).To(Equal(0)) + Expect(result.UpdatedTargetFiles).To(BeEmpty()) + }) + +}) + // baseCliConfig returns the default valid CLI config used by validation specs. func baseCliConfig() cli.Config { currentDir, err := os.Getwd() @@ -301,3 +495,58 @@ func codeResourcePath(name string) string { return filepath.Join(parentDir, "test/resources/code", name) } + +// docsResourcePath builds an absolute path to a test documentation fixture path. +func docsResourcePath(name string) string { + currentDir, err := os.Getwd() + if err != nil { + panic(err) + } + parentDir := filepath.Dir(currentDir) + + return filepath.Join(parentDir, "test/resources/docs", name) +} + +// readArgs runs CLI argument parsing with isolated global flag state. +func readArgs(args ...string) cli.Config { + oldArgs := os.Args + oldCommandLine := flag.CommandLine + defer func() { + os.Args = oldArgs + flag.CommandLine = oldCommandLine + }() + + os.Args = append([]string{"embed-code"}, args...) + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flag.CommandLine.SetOutput(io.Discard) + + return cli.ReadArgs() +} + +// writeTempConfigFile writes a YAML config fixture and returns its path. +func writeTempConfigFile(content string) string { + configFile, err := os.CreateTemp("", "embed-code-cli-*.yml") + if err != nil { + panic(err) + } + defer func() { + if err = configFile.Close(); err != nil { + panic(err) + } + }() + if _, err = configFile.WriteString(content); err != nil { + panic(err) + } + + return configFile.Name() +} + +// noEmbeddingInstructionsConfig builds a config selecting a document without embed-code tags. +func noEmbeddingInstructionsConfig() configuration.Configuration { + config := configuration.NewConfiguration() + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: codeResourcePath("java")}} + config.DocumentationRoot = docsResourcePath("") + config.DocIncludes = []string{"no-embedding-doc.md"} + + return config +} diff --git a/test/resources/config_files/optional_root_config.yml b/test/resources/config_files/optional_root_config.yml new file mode 100644 index 00000000..abd710c3 --- /dev/null +++ b/test/resources/config_files/optional_root_config.yml @@ -0,0 +1,10 @@ +code-path: + - name: java + path: test/resources/code/java +docs-path: test/resources/docs +doc-includes: + - "**/*.md" +doc-excludes: "archive/**/*, drafts/**/*.md" +separator: "---" +info: true +stacktrace: true From a51297d92c04f41fb4517608aa37d6aa87294db3 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 10 Jul 2026 16:42:00 +0200 Subject: [PATCH 2/3] Provide more negative tests. --- cli/cli_test.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/cli/cli_test.go b/cli/cli_test.go index 5173c02e..3224a8e5 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -122,6 +122,17 @@ var _ = Describe("CLI validation", func() { Equal("expected to use config file, but it does not exist")) }) + It("should fail validation when config path points to a directory", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeEmbed, + ConfigPath: docsResourcePath(""), + } + + Expect(cli.ValidateConfigFile(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfigFile(invalidConfig).Error()).Should(Equal( + docsResourcePath("") + " is a directory, the file was expected")) + }) + It("should fail validation when mode is not set", func() { invalidConfig := baseCliConfig() invalidConfig.Mode = "" @@ -139,6 +150,48 @@ var _ = Describe("CLI validation", func() { "`code-path` and `docs-path` must both be set")) }) + It("should fail validation when docs path does not exist", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseDocsPath = missingPath("docs") + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "the given path `" + missingPath("docs") + "` does not exist")) + }) + + It("should fail validation when docs path points to a file", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseDocsPath = configFilePath() + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + configFilePath() + " is a file, the directory was expected")) + }) + + It("should fail validation when code path does not exist", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Path: missingPath("code")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(ContainSubstring( + "the given path `")) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(ContainSubstring( + "does not exist")) + }) + + It("should fail validation when code path entry is empty", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Path: " "}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "`code-path` and `docs-path` must both be set")) + }) + It("should fail validation when config, code and docs paths are set at the same time", func() { invalidConfig := baseCliConfig() invalidConfig.ConfigPath = configFilePath() @@ -157,6 +210,34 @@ var _ = Describe("CLI validation", func() { "`code-path` and `docs-path` cannot be set when `embeddings` are set")) }) + It("should reject embeddings when root code path points to a file", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeCheck, + BaseCodePaths: _type.NamedPathList{ + _type.NamedPath{Path: configFilePath()}, + }, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(ContainSubstring( + "the given path `")) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(ContainSubstring( + "does not exist")) + }) + + It("should reject embeddings when root docs path points to a file", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeCheck, + BaseDocsPath: configFilePath(), + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + configFilePath() + " is a file, the directory was expected")) + }) + It("should reject embeddings with root optional params", func() { invalidConfig := cli.Config{ Mode: cli.ModeCheck, @@ -206,6 +287,49 @@ var _ = Describe("CLI validation", func() { "embedding `docs`: `code-path` and `docs-path` must both be set")) }) + It("should fail validation when embedding code path points to a file", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeCheck, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + } + invalidConfig.Embeddings[0].CodePaths = _type.NamedPathList{ + _type.NamedPath{Path: configFilePath()}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(ContainSubstring( + "embedding `docs`: the given path `")) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(ContainSubstring( + "does not exist")) + }) + + It("should fail validation when embedding source code path names are duplicated", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeCheck, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + } + invalidConfig.Embeddings[0].CodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "samples", Path: codeResourcePath("java")}, + _type.NamedPath{Name: "samples", Path: codeResourcePath("kotlin")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "embedding `docs`: duplicate source code path names detected:\n- samples")) + }) + + It("should fail validation when embedding docs path points to a file", func() { + invalidConfig := cli.Config{ + Mode: cli.ModeCheck, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + } + invalidConfig.Embeddings[0].DocsPath = configFilePath() + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "embedding `docs`: " + configFilePath() + " is a file, the directory was expected")) + }) + It("should fail validation when embedding names are duplicated", func() { embedding := baseEmbeddingConfig() duplicateEmbedding := baseEmbeddingConfig() @@ -507,6 +631,11 @@ func docsResourcePath(name string) string { return filepath.Join(parentDir, "test/resources/docs", name) } +// missingPath builds a path that should not exist in the test environment. +func missingPath(name string) string { + return filepath.Join(os.TempDir(), "embed-code-go-cli-missing-"+name) +} + // readArgs runs CLI argument parsing with isolated global flag state. func readArgs(args ...string) cli.Config { oldArgs := os.Args From dc183afad40bd47d07a25ef14d751c53ed3adc0e Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 10 Jul 2026 16:49:20 +0200 Subject: [PATCH 3/3] Add more negative tests. --- cli/cli_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cli/cli_test.go b/cli/cli_test.go index 3224a8e5..2bb9b32e 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -439,12 +439,16 @@ var _ = Describe("CLI validation", func() { It("should copy command line doc excludes to the runtime config", func() { config := baseCliConfig() + config.DocIncludes = []string{"guides/**/*.md"} config.DocExcludes = []string{"old-docs/**/*.md", "drafts/**/*"} + config.Separator = "---" embedConfigs := cli.BuildEmbedCodeConfiguration(config) Expect(embedConfigs).To(HaveLen(1)) + Expect(embedConfigs[0].DocIncludes).To(Equal([]string(config.DocIncludes))) Expect(embedConfigs[0].DocExcludes).To(Equal([]string(config.DocExcludes))) + Expect(embedConfigs[0].Separator).To(Equal("---")) }) }) @@ -515,6 +519,17 @@ var _ = Describe("CLI configuration building", func() { Expect(err).To(HaveOccurred()) }) + It("should return an error when config file cannot be read", func() { + config := cli.Config{ + Mode: cli.ModeCheck, + ConfigPath: missingPath("config.yml"), + } + + _, err := cli.FillArgsFromConfigFile(config) + + Expect(err).To(HaveOccurred()) + }) + It("should build default command-line configuration without roots", func() { configs := cli.BuildEmbedCodeConfiguration(cli.Config{}) @@ -570,6 +585,17 @@ var _ = Describe("CLI processing wrappers", func() { Expect(result.UpdatedTargetFiles).To(BeEmpty()) }) + It("should return embedding errors through the public wrapper", func() { + config := noEmbeddingInstructionsConfig() + config.DocIncludes = []string{"missing-code-fence.md"} + + result, err := cli.EmbedCodeSamples(config) + + Expect(err).To(HaveOccurred()) + Expect(result).To(Equal(cli.EmbedCodeSamplesResult{})) + Expect(err.Error()).To(ContainSubstring("missing-code-fence.md")) + }) + }) // baseCliConfig returns the default valid CLI config used by validation specs.