Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions embedding/parsing/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions embedding/parsing/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,33 @@ var _ = Describe("Parser states", func() {
"```",
}))
})

It("should report changed content when generated result is shorter than processed source", func() {
config := configuration.NewConfiguration()
context := newStateContext(
"<embed-code file=\"Example.java\"/>",
"```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{
"<embed-code file=\"Example.java\"/>",
"```java",
}))
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.
Expand Down
58 changes: 58 additions & 0 deletions fragmentation/fragmentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,64 @@ 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",
))
})

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() {
mainFragment := "main"
subMainFragment := "sub-main"
Expand Down
30 changes: 13 additions & 17 deletions fragmentation/partition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -69,8 +67,15 @@ func (p Partition) Select(lines []string) ([]string, error) {
return lines[startPosition:], nil
}

hasEndPosition := safeAccess(lines, endPosition)
if !hasEndPosition {
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",
endPosition,
Expand All @@ -80,16 +85,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)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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
Expand Down
3 changes: 1 addition & 2 deletions logging/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package logging

import (
"context"
"fmt"
"log/slog"
"net/url"
Expand All @@ -27,8 +28,6 @@ import (
"runtime/debug"
"strconv"
"strings"

"golang.org/x/net/context"
)

const fileScheme = "file"
Expand Down
171 changes: 171 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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 (
"bytes"
"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",
))
})

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 {
originalStdout := os.Stdout
outputFile, err := os.CreateTemp("", "embed-code-stdout-*.txt")
Expect(err).ShouldNot(HaveOccurred())
os.Stdout = outputFile
defer func() {
os.Stdout = originalStdout
_ = outputFile.Close()
_ = os.Remove(outputFile.Name())
}()

action()

_, err = outputFile.Seek(0, io.SeekStart)
Expect(err).ShouldNot(HaveOccurred())
output, err := io.ReadAll(outputFile)
Expect(err).ShouldNot(HaveOccurred())

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<embed-code file=\"Example.java\"/>\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
}
Loading