From 9ed2e32fb9b43f2ff4a7bdf26ca5b5735bc01769 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 11:24:55 +0200 Subject: [PATCH 1/7] Replace panic-based partition bounds checks. --- fragmentation/fragmentation_test.go | 44 +++++++++++++++++++++++++++++ fragmentation/partition.go | 22 ++++----------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index 6159143..0a9bf14 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -200,6 +200,50 @@ var _ = Describe("Fragmentation", func() { Expect(err).Should(HaveOccurred()) }) + Describe("Partition.Select", func() { + It("should reject a start position before source lines", func() { + partition := fragmentation.Partition{ + StartPosition: -1, + EndPosition: 0, + } + + lines, err := partition.Select([]string{"first line"}) + + Expect(lines).Should(BeNil()) + Expect(err).Should(MatchError( + "fragment partition start position -1 is outside source lines", + )) + }) + + It("should reject a start position after source lines", func() { + partition := fragmentation.Partition{ + StartPosition: 1, + EndPosition: 1, + } + + lines, err := partition.Select([]string{"first line"}) + + Expect(lines).Should(BeNil()) + Expect(err).Should(MatchError( + "fragment partition start position 1 is outside source lines", + )) + }) + + It("should reject an end position after source lines", func() { + partition := fragmentation.Partition{ + StartPosition: 0, + EndPosition: 1, + } + + lines, err := partition.Select([]string{"first line"}) + + Expect(lines).Should(BeNil()) + Expect(err).Should(MatchError( + "fragment partition end position 1 is outside source lines", + )) + }) + }) + Context("fragments parsing", func() { mainFragment := "main" subMainFragment := "sub-main" diff --git a/fragmentation/partition.go b/fragmentation/partition.go index e0d14e2..3438f0d 100644 --- a/fragmentation/partition.go +++ b/fragmentation/partition.go @@ -56,9 +56,7 @@ func (p Partition) Select(lines []string) ([]string, error) { startPosition := p.StartPosition endPosition := p.EndPosition - // Verify source lines actually contain configured partition indexes. - hasStartPosition := safeAccess(lines, startPosition) - if !hasStartPosition { + if !hasLineIndex(lines, startPosition) { return nil, fmt.Errorf( "fragment partition start position %d is outside source lines", startPosition, @@ -69,8 +67,7 @@ func (p Partition) Select(lines []string) ([]string, error) { return lines[startPosition:], nil } - hasEndPosition := safeAccess(lines, endPosition) - if !hasEndPosition { + if !hasLineIndex(lines, endPosition) { return nil, fmt.Errorf( "fragment partition end position %d is outside source lines", endPosition, @@ -80,16 +77,7 @@ func (p Partition) Select(lines []string) ([]string, error) { return lines[startPosition : endPosition+1], nil } -// safeAccess reports whether slice contains index. -func safeAccess(slice []string, index int) bool { - var hasIndex bool - defer func() { - if r := recover(); r != nil { - hasIndex = false - } - }() - _ = slice[index] - hasIndex = true - - return hasIndex +// hasLineIndex reports whether lines contain index. +func hasLineIndex(lines []string, index int) bool { + return index >= 0 && index < len(lines) } From 13aa25466adf11cc7612fb15ba2c700001b23231 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 12:26:47 +0200 Subject: [PATCH 2/7] Guard IsContentChanged against short generated results. --- embedding/parsing/context.go | 4 ++++ embedding/parsing/state_test.go | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index a75229a..1242584 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -190,6 +190,10 @@ func (c *Context) ReachedEOF() bool { // // Returns true when generated lines differ from original source lines. func (c *Context) IsContentChanged() bool { + if len(c.Result) < c.lineIndex { + return true + } + for i := 0; i < c.lineIndex; i++ { if c.source[i] != c.Result[i] { return true diff --git a/embedding/parsing/state_test.go b/embedding/parsing/state_test.go index fff4377..3ad0fe6 100644 --- a/embedding/parsing/state_test.go +++ b/embedding/parsing/state_test.go @@ -144,6 +144,24 @@ var _ = Describe("Parser states", func() { "```", })) }) + + It("should report changed content when generated result is shorter than processed source", func() { + config := configuration.NewConfiguration() + context := newStateContext( + "", + "```java", + "old source", + ) + Expect(parsing.EmbedInstruction.Accept(&context, config)).Should(Succeed()) + Expect(parsing.CodeFenceStart.Accept(&context, config)).Should(Succeed()) + Expect(parsing.CodeSampleLine.Accept(&context, config)).Should(Succeed()) + + Expect(context.GetResult()).Should(Equal([]string{ + "", + "```java", + })) + Expect(context.IsContentChanged()).Should(BeTrue()) + }) }) // newStateContext builds a parser context from in-memory source lines. From 37553282d1ebb600db29822679302a0c0bfcb50d Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 12:46:46 +0200 Subject: [PATCH 3/7] Remove legacy dependency. --- go.mod | 2 +- logging/logger.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7b2cf4a..6bf71b2 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,6 @@ require ( github.com/gobwas/glob v0.2.3 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 - golang.org/x/net v0.28.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -35,6 +34,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/stretchr/testify v1.9.0 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect golang.org/x/tools v0.24.0 // indirect diff --git a/logging/logger.go b/logging/logger.go index 4917284..48bee8a 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -19,6 +19,7 @@ package logging import ( + "context" "fmt" "log/slog" "net/url" @@ -27,8 +28,6 @@ import ( "runtime/debug" "strconv" "strings" - - "golang.org/x/net/context" ) const fileScheme = "file" From 1ffadcc4e21dfd5adc855639af5994f57251c80e Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 12:54:21 +0200 Subject: [PATCH 4/7] Add tests for main. --- main_test.go | 157 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 main_test.go diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..9cab28a --- /dev/null +++ b/main_test.go @@ -0,0 +1,157 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package main + +import ( + "io" + "os" + "path/filepath" + "testing" + + "embed-code/embed-code-go/configuration" + "embed-code/embed-code-go/logging" + _type "embed-code/embed-code-go/type" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// TestMainOrchestrator runs the main package specs. +func TestMainOrchestrator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Main Orchestrator Suite") +} + +var _ = Describe("Main orchestrator", func() { + It("should aggregate check errors while printing stale files", func() { + staleConfig, staleDocPath := writeMainModeFixture("stale documentation") + errorConfig, _ := writeMainModeFixture("documentation with a missing source") + errorConfig.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: GinkgoT().TempDir()}} + + output := captureStdout(func() { + err := checkByConfigs([]configuration.Configuration{staleConfig, errorConfig}) + + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(And( + ContainSubstring("the documentation files are not up-to-date with code files"), + ContainSubstring("code file `file://"), + ContainSubstring("Example.java` not found"), + )) + }) + + Expect(output).Should(ContainSubstring("File to update:\n")) + Expect(output).Should(ContainSubstring("- " + logging.FileReference(staleDocPath) + ".\n")) + }) + + It("should print updated files after embedding", func() { + config, docPath := writeMainModeFixture("outdated documentation") + + output := captureStdout(func() { + Expect(embedByConfigs([]configuration.Configuration{config})).Should(Succeed()) + }) + + Expect(output).Should(ContainSubstring("File updated:\n")) + Expect(output).Should(ContainSubstring("- " + logging.FileReference(docPath) + ".\n")) + content, err := os.ReadFile(docPath) + Expect(err).ShouldNot(HaveOccurred()) + Expect(string(content)).Should(ContainSubstring("class Example {}")) + }) + + It("should aggregate embed errors from multiple configs", func() { + firstConfig, firstDocPath := writeMainModeFixture("first documentation") + secondConfig, secondDocPath := writeMainModeFixture("second documentation") + firstConfig.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: GinkgoT().TempDir()}} + secondConfig.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: GinkgoT().TempDir()}} + + output := captureStdout(func() { + err := embedByConfigs([]configuration.Configuration{firstConfig, secondConfig}) + + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(And( + ContainSubstring(logging.FileReferenceWithLine(firstDocPath, 3)), + ContainSubstring(logging.FileReferenceWithLine(secondDocPath, 3)), + ContainSubstring("Example.java` not found"), + )) + }) + + Expect(output).Should(BeEmpty()) + }) + + It("should print plural headings for multiple files", func() { + firstPath := filepath.ToSlash(filepath.Join(GinkgoT().TempDir(), "first.md")) + secondPath := filepath.ToSlash(filepath.Join(GinkgoT().TempDir(), "second.md")) + + output := captureStdout(func() { + printFiles("One file:", "Many files:", []string{firstPath, secondPath}) + }) + + Expect(output).Should(Equal( + "Many files:\n" + + "- " + logging.FileReference(firstPath) + ".\n" + + "- " + logging.FileReference(secondPath) + ".\n", + )) + }) +}) + +// captureStdout runs action and returns text written to standard output. +func captureStdout(action func()) string { + originalStdout := os.Stdout + reader, writer, err := os.Pipe() + Expect(err).ShouldNot(HaveOccurred()) + os.Stdout = writer + defer func() { + os.Stdout = originalStdout + }() + + action() + + Expect(writer.Close()).Should(Succeed()) + output, err := io.ReadAll(reader) + Expect(err).ShouldNot(HaveOccurred()) + Expect(reader.Close()).Should(Succeed()) + + return string(output) +} + +// writeMainModeFixture creates one source file and one stale documentation file. +func writeMainModeFixture(docTitle string) (configuration.Configuration, string) { + root := GinkgoT().TempDir() + codeRoot := filepath.Join(root, "code") + docsRoot := filepath.Join(root, "docs") + Expect(os.MkdirAll(codeRoot, 0700)).To(Succeed()) + Expect(os.MkdirAll(docsRoot, 0700)).To(Succeed()) + Expect(os.WriteFile( + filepath.Join(codeRoot, "Example.java"), + []byte("class Example {}\n"), + 0600, + )).To(Succeed()) + docPath := filepath.ToSlash(filepath.Join(docsRoot, "doc.md")) + Expect(os.WriteFile( + docPath, + []byte("# "+docTitle+"\n\n\n```java\nold source\n```\n"), + 0600, + )).To(Succeed()) + + config := configuration.NewConfiguration() + config.DocumentationRoot = docsRoot + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: codeRoot}} + config.DocIncludes = []string{"doc.md"} + + return config, docPath +} From 04682c98b89be15431542c449ef85e5d42229b7b Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 13:22:00 +0200 Subject: [PATCH 5/7] Improve test behavior. --- fragmentation/fragmentation_test.go | 14 ++++++++++++ fragmentation/partition.go | 8 +++++++ main_test.go | 33 +++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index 0a9bf14..80be780 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -242,6 +242,20 @@ var _ = Describe("Fragmentation", func() { "fragment partition end position 1 is outside source lines", )) }) + + It("should reject an end position before the start position", func() { + partition := fragmentation.Partition{ + StartPosition: 2, + EndPosition: 0, + } + + lines, err := partition.Select([]string{"first line", "second line", "third line"}) + + Expect(lines).Should(BeNil()) + Expect(err).Should(MatchError( + "fragment partition end position 0 is before start position 2", + )) + }) }) Context("fragments parsing", func() { diff --git a/fragmentation/partition.go b/fragmentation/partition.go index 3438f0d..ff1da99 100644 --- a/fragmentation/partition.go +++ b/fragmentation/partition.go @@ -67,6 +67,14 @@ func (p Partition) Select(lines []string) ([]string, error) { return lines[startPosition:], nil } + if endPosition < startPosition-1 { + return nil, fmt.Errorf( + "fragment partition end position %d is before start position %d", + endPosition, + startPosition, + ) + } + if !hasLineIndex(lines, endPosition) { return nil, fmt.Errorf( "fragment partition end position %d is outside source lines", diff --git a/main_test.go b/main_test.go index 9cab28a..47e35bc 100644 --- a/main_test.go +++ b/main_test.go @@ -19,6 +19,7 @@ package main import ( + "bytes" "io" "os" "path/filepath" @@ -107,26 +108,44 @@ var _ = Describe("Main orchestrator", func() { "- " + logging.FileReference(secondPath) + ".\n", )) }) + + It("should capture output larger than the pipe buffer", func() { + largeOutput := bytes.Repeat([]byte("x"), 128*1024) + + output := captureStdout(func() { + _, err := os.Stdout.Write(largeOutput) + Expect(err).ShouldNot(HaveOccurred()) + }) + + Expect(output).Should(Equal(string(largeOutput))) + }) }) // captureStdout runs action and returns text written to standard output. -func captureStdout(action func()) string { +func captureStdout(action func()) (output string) { originalStdout := os.Stdout reader, writer, err := os.Pipe() Expect(err).ShouldNot(HaveOccurred()) os.Stdout = writer + var buffer bytes.Buffer + readDone := make(chan error, 1) + go func() { + _, err := io.Copy(&buffer, reader) + readDone <- err + }() defer func() { os.Stdout = originalStdout }() + defer func() { + Expect(writer.Close()).Should(Succeed()) + Expect(<-readDone).ShouldNot(HaveOccurred()) + Expect(reader.Close()).Should(Succeed()) + output = buffer.String() + }() action() - Expect(writer.Close()).Should(Succeed()) - output, err := io.ReadAll(reader) - Expect(err).ShouldNot(HaveOccurred()) - Expect(reader.Close()).Should(Succeed()) - - return string(output) + return output } // writeMainModeFixture creates one source file and one stale documentation file. From 01427e06563210b77d8363f6ecab526ef319d315 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 13:25:05 +0200 Subject: [PATCH 6/7] Fix linter issues. --- main_test.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/main_test.go b/main_test.go index 47e35bc..173ee7e 100644 --- a/main_test.go +++ b/main_test.go @@ -122,30 +122,35 @@ var _ = Describe("Main orchestrator", func() { }) // captureStdout runs action and returns text written to standard output. -func captureStdout(action func()) (output string) { +func captureStdout(action func()) string { originalStdout := os.Stdout reader, writer, err := os.Pipe() Expect(err).ShouldNot(HaveOccurred()) os.Stdout = writer var buffer bytes.Buffer readDone := make(chan error, 1) + closed := false go func() { _, err := io.Copy(&buffer, reader) readDone <- err }() defer func() { os.Stdout = originalStdout - }() - defer func() { - Expect(writer.Close()).Should(Succeed()) - Expect(<-readDone).ShouldNot(HaveOccurred()) - Expect(reader.Close()).Should(Succeed()) - output = buffer.String() + if !closed { + _ = writer.Close() + <-readDone + _ = reader.Close() + } }() action() - return output + Expect(writer.Close()).Should(Succeed()) + closed = true + Expect(<-readDone).ShouldNot(HaveOccurred()) + Expect(reader.Close()).Should(Succeed()) + + return buffer.String() } // writeMainModeFixture creates one source file and one stale documentation file. From a40bdf1a8f30255876c2b4532ffa23d68932dd26 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 6 Jul 2026 14:45:48 +0200 Subject: [PATCH 7/7] Improve processing. --- embedding/parsing/context.go | 2 +- embedding/parsing/state_test.go | 9 +++++++++ main_test.go | 28 +++++++++------------------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index 1242584..91a01a0 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -190,7 +190,7 @@ func (c *Context) ReachedEOF() bool { // // Returns true when generated lines differ from original source lines. func (c *Context) IsContentChanged() bool { - if len(c.Result) < c.lineIndex { + if len(c.Result) != c.lineIndex { return true } diff --git a/embedding/parsing/state_test.go b/embedding/parsing/state_test.go index 3ad0fe6..909604a 100644 --- a/embedding/parsing/state_test.go +++ b/embedding/parsing/state_test.go @@ -162,6 +162,15 @@ var _ = Describe("Parser states", func() { })) Expect(context.IsContentChanged()).Should(BeTrue()) }) + + It("should report changed content when generated result is longer than processed source", func() { + config := configuration.NewConfiguration() + context := newStateContext("original source") + Expect(parsing.RegularLine.Accept(&context, config)).Should(Succeed()) + context.Result = append(context.Result, "extra generated line") + + Expect(context.IsContentChanged()).Should(BeTrue()) + }) }) // newStateContext builds a parser context from in-memory source lines. diff --git a/main_test.go b/main_test.go index 173ee7e..fc788d4 100644 --- a/main_test.go +++ b/main_test.go @@ -124,33 +124,23 @@ var _ = Describe("Main orchestrator", func() { // captureStdout runs action and returns text written to standard output. func captureStdout(action func()) string { originalStdout := os.Stdout - reader, writer, err := os.Pipe() + outputFile, err := os.CreateTemp("", "embed-code-stdout-*.txt") Expect(err).ShouldNot(HaveOccurred()) - os.Stdout = writer - var buffer bytes.Buffer - readDone := make(chan error, 1) - closed := false - go func() { - _, err := io.Copy(&buffer, reader) - readDone <- err - }() + os.Stdout = outputFile defer func() { os.Stdout = originalStdout - if !closed { - _ = writer.Close() - <-readDone - _ = reader.Close() - } + _ = outputFile.Close() + _ = os.Remove(outputFile.Name()) }() action() - Expect(writer.Close()).Should(Succeed()) - closed = true - Expect(<-readDone).ShouldNot(HaveOccurred()) - Expect(reader.Close()).Should(Succeed()) + _, err = outputFile.Seek(0, io.SeekStart) + Expect(err).ShouldNot(HaveOccurred()) + output, err := io.ReadAll(outputFile) + Expect(err).ShouldNot(HaveOccurred()) - return buffer.String() + return string(output) } // writeMainModeFixture creates one source file and one stale documentation file.