From 0362b1f4bde52456fb7efb5b426bfc9951281779 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 24 Jun 2026 11:13:40 +0200 Subject: [PATCH 1/7] Provide per-operation resolvers. --- embedding/orchestration.go | 4 ++- embedding/parsing/context.go | 14 ++++++++++ embedding/parsing/instruction.go | 11 ++++++-- embedding/processor.go | 14 ++++++++-- fragmentation/cache.go | 10 ------- fragmentation/resolver.go | 48 +++++++++++++++++++------------- 6 files changed, 67 insertions(+), 34 deletions(-) diff --git a/embedding/orchestration.go b/embedding/orchestration.go index fd591358..9d74549e 100644 --- a/embedding/orchestration.go +++ b/embedding/orchestration.go @@ -27,6 +27,7 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding/parsing" + "embed-code/embed-code-go/fragmentation" "embed-code/embed-code-go/logging" "github.com/bmatcuk/doublestar/v4" @@ -143,8 +144,9 @@ func processRequiredDocs( } var processingErrors []error + resolver := fragmentation.NewResolver() for _, doc := range requiredDocPaths { - processor := newProcessor(doc, config, parsing.Transitions, requiredDocPaths) + processor := newProcessor(doc, config, parsing.Transitions, requiredDocPaths, resolver) if err := handle(processor); err != nil { processingErrors = append(processingErrors, err) } diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index 95f6f6c6..ce98bfb1 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -22,6 +22,8 @@ import ( "fmt" "os" "regexp" + + "embed-code/embed-code-go/fragmentation" ) // Context represents the context for parsing a file containing code embeddings. @@ -59,6 +61,8 @@ type Context struct { fileContainsEmbedding bool // embeddings - a list of embedding instructions found in the markdown file. embeddings []EmbeddingContext + // resolver owns source fragmentation cache state for this processing operation. + resolver *fragmentation.Resolver } // EmbeddingsCount returns number of found embeddings. @@ -82,6 +86,14 @@ type EmbeddingContext struct { // NewContext Creates and returns a new Context struct with initial values for markdownFile, source, // lineIndex, and result. func NewContext(markdownFile string) (Context, error) { + return NewContextWithResolver(markdownFile, fragmentation.NewResolver()) +} + +// NewContextWithResolver creates a parsing context using the provided source resolver. +func NewContextWithResolver( + markdownFile string, + resolver *fragmentation.Resolver, +) (Context, error) { source, err := readLines(markdownFile) if err != nil { return Context{}, err @@ -92,6 +104,7 @@ func NewContext(markdownFile string) (Context, error) { Result: make([]string, 0), source: source, lineIndex: 0, + resolver: resolver, }, nil } @@ -163,6 +176,7 @@ func (c *Context) ResolveUnacceptedEmbedding() { // StartEmbedding records an instruction as the current embedding. func (c *Context) StartEmbedding(instruction Instruction) { c.fileContainsEmbedding = true + instruction.resolver = c.resolver embeddingContext := EmbeddingContext{ embeddingInstruction: instruction, } diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index 7f98dbc4..b9a8136b 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -64,6 +64,7 @@ type Instruction struct { DocumentationFile string DocumentationLine int Configuration configuration.Configuration + resolver *fragmentation.Resolver } // PatternNotFoundError reports that an instruction pattern did not match the code file. @@ -133,6 +134,7 @@ func NewInstruction( LinePattern: patterns.line, CommentMode: commentMode, Configuration: config, + resolver: fragmentation.NewResolver(), }, nil } @@ -201,11 +203,16 @@ func parseInstructionPattern(attribute string, value string) (Pattern, error) { // // Returns an error if there was an error during reading the content. func (e Instruction) Content() ([]string, error) { - fileContent, err := fragmentation.ResolveContent(e.CodeFile, e.Fragment, e.Configuration) + resolver := e.resolver + if resolver == nil { + resolver = fragmentation.NewResolver() + } + + fileContent, err := resolver.ResolveContent(e.CodeFile, e.Fragment, e.Configuration) if err != nil { return nil, err } - codeFileReference, referenceErr := fragmentation.ResolveCodeFileReference( + codeFileReference, referenceErr := resolver.ResolveCodeFileReference( e.CodeFile, e.Configuration, ) diff --git a/embedding/processor.go b/embedding/processor.go index 21271f83..a9a2e11c 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -29,6 +29,7 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding/parsing" "embed-code/embed-code-go/files" + "embed-code/embed-code-go/fragmentation" "embed-code/embed-code-go/logging" ) @@ -38,6 +39,7 @@ type Processor struct { config configuration.Configuration transitionsMap parsing.TransitionMap requiredDocPaths []string + resolver *fragmentation.Resolver } // NewProcessor creates and returns new Processor with given docFile and config. @@ -47,7 +49,13 @@ func NewProcessor(docFile string, config configuration.Configuration) (Processor return Processor{}, err } - return newProcessor(docFile, config, parsing.Transitions, requiredDocPaths), nil + return newProcessor( + docFile, + config, + parsing.Transitions, + requiredDocPaths, + fragmentation.NewResolver(), + ), nil } // newProcessor creates a Processor with a precomputed documentation file list. @@ -56,12 +64,14 @@ func newProcessor( config configuration.Configuration, transitions parsing.TransitionMap, requiredDocPaths []string, + resolver *fragmentation.Resolver, ) Processor { return Processor{ docFilePath: docFile, config: config, transitionsMap: transitions, requiredDocPaths: requiredDocPaths, + resolver: resolver, } } @@ -143,7 +153,7 @@ func (p Processor) isUpToDate() (bool, error) { // // Returns a parsing.Context and an error if any occurs. func (p Processor) fillEmbeddingContext() (parsing.Context, error) { - context, err := parsing.NewContext(p.docFilePath) + context, err := parsing.NewContextWithResolver(p.docFilePath, p.resolver) if err != nil { return context, err } diff --git a/fragmentation/cache.go b/fragmentation/cache.go index bc5a3feb..77c4c6a5 100644 --- a/fragmentation/cache.go +++ b/fragmentation/cache.go @@ -70,16 +70,6 @@ func (c *cache[K, V]) get(key K) (V, error) { return value, nil } -// clear removes all cached values. -func (c *cache[K, V]) clear() { - c.Lock() - defer c.Unlock() - - c.values = make(map[K]V) - c.entries = make(map[K]*list.Element) - c.order.Init() -} - // storeLoaded stores a loaded value and evicts the least recently used value when needed. func (c *cache[K, V]) storeLoaded(key K, value V) { c.values[key] = value diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index 6b21f43a..e665a057 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -43,16 +43,25 @@ type fragmentedFile struct { // absolutePath is a resolved absolute filesystem path. type absolutePath string -// resolverCache stores source fragmentations already resolved during the current run. -var resolverCache = newCache[absolutePath, fragmentedFile]( - resolverCacheLimit, - loadSourceFragments, -) +// Resolver resolves source files and caches fragmentations for one processing operation. +type Resolver struct { + cache *cache[absolutePath, fragmentedFile] +} + +// NewResolver creates a resolver with an independent source-fragment cache. +func NewResolver() *Resolver { + return &Resolver{ + cache: newCache[absolutePath, fragmentedFile]( + resolverCacheLimit, + loadSourceFragments, + ), + } +} // ResolveContent returns source lines for the requested code file fragment. // // Named fragments are extracted directly from the source file on demand and cached by source file. -func ResolveContent( +func (r *Resolver) ResolveContent( codePath string, fragmentName string, config config.Configuration, @@ -61,7 +70,7 @@ func ResolveContent( fragmentName = DefaultFragmentName } - source, found, err := resolveSource(codePath, config) + source, found, err := r.resolveSource(codePath, config) if err != nil { return nil, err } @@ -74,7 +83,7 @@ func ResolveContent( return nil, unresolvedSourceError(codePath, fragmentName, config) } - content, err := cachedSourceFragments(source) + content, err := r.cachedSourceFragments(source) if err != nil { return nil, err } @@ -102,8 +111,11 @@ func missingFragmentLogMessage(fragmentName string, sourcePath absolutePath) str } // ResolveCodeFileReference returns a user-facing reference to the source file. -func ResolveCodeFileReference(codePath string, config config.Configuration) (string, error) { - source, found, err := resolveSource(codePath, config) +func (r *Resolver) ResolveCodeFileReference( + codePath string, + config config.Configuration, +) (string, error) { + source, found, err := r.resolveSource(codePath, config) if err != nil { return "", err } @@ -114,13 +126,11 @@ func ResolveCodeFileReference(codePath string, config config.Configuration) (str return codeFileReference(codePath, config) } -// ClearResolverCache removes cached source fragmentations. -func ClearResolverCache() { - resolverCache.clear() -} - // resolveSource resolves the user-facing code path to the source file. -func resolveSource(codePath string, config config.Configuration) (absolutePath, bool, error) { +func (r *Resolver) resolveSource( + codePath string, + config config.Configuration, +) (absolutePath, bool, error) { codeRootName, relativePath, named := splitNamedPath(codePath) for _, root := range config.CodeRoots { if named && strings.TrimSpace(root.Name) != codeRootName { @@ -139,7 +149,7 @@ func resolveSource(codePath string, config config.Configuration) (absolutePath, continue } - _, err = cachedSourceFragments(source) + _, err = r.cachedSourceFragments(source) var encodingError *unsupportedEncodingError if errors.As(err, &encodingError) { continue @@ -178,8 +188,8 @@ func sourceFromRoot(root _type.NamedPath, relativePath string) (absolutePath, er } // cachedSourceFragments returns cached source fragmentation for an absolute source path. -func cachedSourceFragments(source absolutePath) (fragmentedFile, error) { - return resolverCache.get(source) +func (r *Resolver) cachedSourceFragments(source absolutePath) (fragmentedFile, error) { + return r.cache.get(source) } // loadSourceFragments reads and fragments the source file when it is not already cached. From 5c08012e1df724640d8f3235c6b2bcea6fab1bfe Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 24 Jun 2026 11:14:05 +0200 Subject: [PATCH 2/7] Update tests. --- embedding/embedding_test.go | 141 +++++++--------------------- fragmentation/fragmentation_test.go | 60 ++++++++++-- 2 files changed, 82 insertions(+), 119 deletions(-) diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index c8adbdb1..dbafcaa0 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -20,8 +20,6 @@ package embedding_test import ( "errors" - "fmt" - "io" "os" "path/filepath" "strings" @@ -30,15 +28,12 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding" "embed-code/embed-code-go/embedding/parsing" - "embed-code/embed-code-go/files" _type "embed-code/embed-code-go/type" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -const temporaryTestDir = "../test/docs" - func TestEmbedding(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Data Suite") @@ -48,28 +43,15 @@ var _ = Describe("Embedding", func() { var config configuration.Configuration BeforeEach(func() { - currentDir, err := os.Getwd() - if err != nil { - Fail("unexpected error during the test setup: " + err.Error()) - } - err = os.Chdir(currentDir) - if err != nil { - Fail("unexpected error during the test setup: " + err.Error()) - } - config = buildConfigWithSourceFiles() - - // Copying files not to edit them directly during the test run. - copyDirRecursive("../test/resources/docs", config.DocumentationRoot) - }) - - AfterEach(func() { - if err := os.RemoveAll(temporaryTestDir); err != nil { - Fail(err.Error()) - } + config = buildConfigWithSourceFiles(GinkgoT().TempDir()) + Expect(os.CopyFS( + config.DocumentationRoot, + os.DirFS("../test/resources/docs"), + )).To(Succeed()) }) It("should be up to date", func() { - docPath := fmt.Sprintf("%s/whole-file-fragment.md", config.DocumentationRoot) + docPath := testDocPath(config, "whole-file-fragment.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -77,7 +59,7 @@ var _ = Describe("Embedding", func() { }) It("should be up to date as there is nothing to update", func() { - docPath := fmt.Sprintf("%s/no-embedding-doc.md", config.DocumentationRoot) + docPath := testDocPath(config, "no-embedding-doc.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -85,7 +67,7 @@ var _ = Describe("Embedding", func() { }) It("should successfully embed with multi lined tag", func() { - docPath := fmt.Sprintf("%s/multi-lined-tag.md", config.DocumentationRoot) + docPath := testDocPath(config, "multi-lined-tag.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -93,7 +75,7 @@ var _ = Describe("Embedding", func() { }) It("should embed directly from source", func() { - docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) + docPath := testDocPath(config, "doc.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -103,7 +85,7 @@ var _ = Describe("Embedding", func() { It("should report files that are not up to date", func() { config.DocIncludes = []string{"doc.md"} - docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) + docPath := testDocPath(config, "doc.md") outdatedFiles, err := embedding.CheckUpToDate(config) @@ -112,7 +94,7 @@ var _ = Describe("Embedding", func() { }) It("should ignore embed-code samples inside markdown code fences", func() { - docPath := fmt.Sprintf("%s/embed-code-sample-in-fence.md", config.DocumentationRoot) + docPath := testDocPath(config, "embed-code-sample-in-fence.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -120,7 +102,7 @@ var _ = Describe("Embedding", func() { }) It("should detect markdown fences by triple-or-more backticks only", func() { - docPath := fmt.Sprintf("%s/triple-backticks-only-fence.md", config.DocumentationRoot) + docPath := testDocPath(config, "triple-backticks-only-fence.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -178,7 +160,7 @@ var _ = Describe("Embedding", func() { }) It("should embed with multi lined tag attributes", func() { - docPath := fmt.Sprintf("%s/multi-lined-valid-tag-attributes.md", config.DocumentationRoot) + docPath := testDocPath(config, "multi-lined-valid-tag-attributes.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -187,7 +169,7 @@ var _ = Describe("Embedding", func() { It("should embed a method with escaped newline patterns", func() { config.DocIncludes = []string{"escaped-newline-pattern.md"} - docPath := fmt.Sprintf("%s/escaped-newline-pattern.md", config.DocumentationRoot) + docPath := testDocPath(config, "escaped-newline-pattern.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -202,7 +184,7 @@ var _ = Describe("Embedding", func() { It("should embed a method with exact escaped newline patterns", func() { config.DocIncludes = []string{"escaped-newline-exact-pattern.md"} - docPath := fmt.Sprintf("%s/escaped-newline-exact-pattern.md", config.DocumentationRoot) + docPath := testDocPath(config, "escaped-newline-exact-pattern.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -217,7 +199,7 @@ var _ = Describe("Embedding", func() { It("should embed matching lines with an escaped newline line pattern", func() { config.DocIncludes = []string{"escaped-newline-line-pattern.md"} - docPath := fmt.Sprintf("%s/escaped-newline-line-pattern.md", config.DocumentationRoot) + docPath := testDocPath(config, "escaped-newline-line-pattern.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -232,7 +214,7 @@ var _ = Describe("Embedding", func() { It("should embed a line with an escaped newline literal pattern", func() { config.DocIncludes = []string{"escaped-newline-literal-pattern.md"} - docPath := fmt.Sprintf("%s/escaped-newline-literal-pattern.md", config.DocumentationRoot) + docPath := testDocPath(config, "escaped-newline-literal-pattern.md") processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -245,7 +227,7 @@ var _ = Describe("Embedding", func() { }) It("should report a missing closing tag", func() { - docPath := fmt.Sprintf("%s/missing-closing-tag.md", config.DocumentationRoot) + docPath := testDocPath(config, "missing-closing-tag.md") processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -259,7 +241,7 @@ var _ = Describe("Embedding", func() { }) It("should preserve typed parser errors after adding document context", func() { - docPath := fmt.Sprintf("%s/missing-closing-tag.md", config.DocumentationRoot) + docPath := testDocPath(config, "missing-closing-tag.md") processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -277,7 +259,7 @@ var _ = Describe("Embedding", func() { }) It("should report the XML parser error", func() { - docPath := fmt.Sprintf("%s/unclosed-nested-tag.md", config.DocumentationRoot) + docPath := testDocPath(config, "unclosed-nested-tag.md") processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -291,7 +273,7 @@ var _ = Describe("Embedding", func() { }) It("should report a missing code fence after the instruction", func() { - docPath := fmt.Sprintf("%s/missing-code-fence.md", config.DocumentationRoot) + docPath := testDocPath(config, "missing-code-fence.md") processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -304,7 +286,7 @@ var _ = Describe("Embedding", func() { }) It("should report an unclosed code fence after the instruction", func() { - docPath := fmt.Sprintf("%s/unclosed-code-fence.md", config.DocumentationRoot) + docPath := testDocPath(config, "unclosed-code-fence.md") processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -319,8 +301,7 @@ var _ = Describe("Embedding", func() { It("should successfully embed to a file in a nested dir", func() { config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/kotlin"}} config.DocIncludes = []string{"nested-dir-1/nested-dir-2/nested-dir-doc.md"} - docPath := fmt.Sprintf("%s/nested-dir-1/nested-dir-2/nested-dir-doc.md", - config.DocumentationRoot) + docPath := testDocPath(config, "nested-dir-1/nested-dir-2/nested-dir-doc.md") processor := newProcessor(docPath, config) _, err := embedding.EmbedAll(config) @@ -332,7 +313,7 @@ var _ = Describe("Embedding", func() { It("should not embed to a file matched the `doc-excludes` pattern", func() { config.DocExcludes = []string{"**/excluded-doc.*"} - docPath := fmt.Sprintf("%s/excluded-doc.md", config.DocumentationRoot) + docPath := testDocPath(config, "excluded-doc.md") processor := newProcessor(docPath, config) context, err := processor.Embed() @@ -345,14 +326,20 @@ var _ = Describe("Embedding", func() { }) }) -func buildConfigWithSourceFiles() configuration.Configuration { +// buildConfigWithSourceFiles builds an embedding config with an isolated documentation root. +func buildConfigWithSourceFiles(documentationRoot string) configuration.Configuration { var config = configuration.NewConfiguration() - config.DocumentationRoot = temporaryTestDir + config.DocumentationRoot = documentationRoot config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} return config } +// testDocPath returns the normalized path to a copied documentation fixture. +func testDocPath(config configuration.Configuration, name string) string { + return filepath.ToSlash(filepath.Join(config.DocumentationRoot, name)) +} + func newProcessor( docPath string, config configuration.Configuration, @@ -363,67 +350,3 @@ func newProcessor( return processor } - -func copyDirRecursive(sourceDirPath string, targetDirPath string) { - info, err := os.Stat(sourceDirPath) - if err != nil { - panic(err) - } - - err = os.MkdirAll(targetDirPath, info.Mode()) - if err != nil { - panic(err) - } - - entries, err := os.ReadDir(sourceDirPath) - if err != nil { - panic(err) - } - - for _, entry := range entries { - sourcePath := filepath.Join(sourceDirPath, entry.Name()) - targetPath := filepath.Join(targetDirPath, entry.Name()) - - if entry.IsDir() { - copyDirRecursive(sourcePath, targetPath) - } else { - err = copyFile(sourcePath, targetPath) - if err != nil { - panic(err) - } - } - } -} - -func copyFile(sourceFilePath string, targetFilePath string) (err error) { - sourceFile, err := os.Open(sourceFilePath) - if err != nil { - Fail(err.Error()) - } - - defer func(sourceFile *os.File) { - err = sourceFile.Close() - if err != nil { - Fail(err.Error()) - } - }(sourceFile) - - targetFile, err := os.Create(targetFilePath) - if err != nil { - return - } - defer func() { - err = targetFile.Close() - if err != nil { - Fail(err.Error()) - } - }() - - if _, err = io.Copy(targetFile, sourceFile); err != nil { - return - } - - err = os.Chmod(targetFilePath, os.FileMode(files.WritePermission)) - - return -} diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index 24d7a6e5..41929cde 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -49,9 +49,10 @@ func TestFragmentation(t *testing.T) { var _ = Describe("Fragmentation", func() { var config configuration.Configuration + var resolver *fragmentation.Resolver BeforeEach(func() { - fragmentation.ClearResolverCache() + resolver = fragmentation.NewResolver() config = configuration.NewConfiguration() config.DocumentationRoot = "../test/resources/docs" config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} @@ -69,7 +70,7 @@ var _ = Describe("Fragmentation", func() { }) It("should resolve named fragments", func() { - content := resolveTestFragment(correctFragmentsFileName, "main()", config) + content := resolveTestFragment(resolver, correctFragmentsFileName, "main()", config) Expect(content).Should(Equal([]string{ "public static void main(String[] args) {", @@ -79,7 +80,12 @@ var _ = Describe("Fragmentation", func() { }) It("should resolve fragments without an end marker through the end of the file", func() { - content := resolveTestFragment(unclosedFragmentFileName, "Fragment that never ends", config) + content := resolveTestFragment( + resolver, + unclosedFragmentFileName, + "Fragment that never ends", + config, + ) Expect(content).Should(Equal([]string{ indent + indent + "System.out.println(\"Hello world\");", @@ -111,7 +117,7 @@ var _ = Describe("Fragmentation", func() { _type.NamedPath{Path: validRoot}, } - content, err := fragmentation.ResolveContent( + content, err := resolver.ResolveContent( fileName, fragmentation.DefaultFragmentName, config, @@ -121,6 +127,39 @@ var _ = Describe("Fragmentation", func() { Expect(content).Should(Equal([]string{"class Example {}"})) }) + It("should isolate cached source content between resolvers", func() { + sourceRoot := GinkgoT().TempDir() + fileName := "Example.java" + sourcePath := filepath.Join(sourceRoot, fileName) + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}} + Expect(os.WriteFile(sourcePath, []byte("class First {}"), 0600)).To(Succeed()) + + firstContent, err := resolver.ResolveContent( + fileName, + fragmentation.DefaultFragmentName, + config, + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(os.WriteFile(sourcePath, []byte("class Second {}"), 0600)).To(Succeed()) + + cachedContent, err := resolver.ResolveContent( + fileName, + fragmentation.DefaultFragmentName, + config, + ) + Expect(err).ShouldNot(HaveOccurred()) + freshContent, err := fragmentation.NewResolver().ResolveContent( + fileName, + fragmentation.DefaultFragmentName, + config, + ) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(firstContent).Should(Equal([]string{"class First {}"})) + Expect(cachedContent).Should(Equal(firstContent)) + Expect(freshContent).Should(Equal([]string{"class Second {}"})) + }) + It("should fail on an unopened fragment", func() { frag := buildTestFragmentation(unopenedFragmentFileName, config) @@ -171,7 +210,7 @@ var _ = Describe("Fragmentation", func() { }) It("should correctly parse file into many partitions", func() { - content := resolveTestFragment(complexFragmentsFileName, "Main", config) + content := resolveTestFragment(resolver, complexFragmentsFileName, "Main", config) expected := []string{ "public class Main {", @@ -188,8 +227,8 @@ var _ = Describe("Fragmentation", func() { }) It("should correctly parse file with several different fragments", func() { - mainContent := resolveTestFragment(twoFragmentsFileName, "Main", config) - helloContent := resolveTestFragment(twoFragmentsFileName, "Hello", config) + mainContent := resolveTestFragment(resolver, twoFragmentsFileName, "Main", config) + helloContent := resolveTestFragment(resolver, twoFragmentsFileName, "Hello", config) Expect([][]string{mainContent, helloContent}).Should(ConsistOf([][]string{ { @@ -214,8 +253,8 @@ var _ = Describe("Fragmentation", func() { }) It("should correctly parse file with several overlapping fragments", func() { - mainContent := resolveTestFragment(overlappingFragmentsFileName, "Main", config) - helloContent := resolveTestFragment(overlappingFragmentsFileName, "Hello", config) + mainContent := resolveTestFragment(resolver, overlappingFragmentsFileName, "Main", config) + helloContent := resolveTestFragment(resolver, overlappingFragmentsFileName, "Hello", config) Expect([][]string{mainContent, helloContent}).Should(ConsistOf([][]string{ { @@ -269,11 +308,12 @@ func doTestFragmentation( } func resolveTestFragment( + resolver *fragmentation.Resolver, testFileName string, fragmentName string, config configuration.Configuration, ) []string { - content, err := fragmentation.ResolveContent( + content, err := resolver.ResolveContent( fmt.Sprintf("org/example/%s", testFileName), fragmentName, config, From 1bac543ea83b614114b01ea2eef8f2470a1b94ab Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 24 Jun 2026 11:14:23 +0200 Subject: [PATCH 3/7] Update GitHub workflow. --- .github/workflows/check.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 8320c098..e432c07d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -28,9 +28,7 @@ jobs: args: ./... - name: Run Tests - # Tests must be run sequentially because they create temporary files that can cause issues. - # Therefore, the "-p 1" argument is required. - run: go test -v ./... -p 1 + run: go test -v ./... - name: Run E2E Tests - run: go test -v -tags showcase ./showcase -p 1 + run: go test -v -tags showcase ./showcase From d123dbba3bc9bebb00c5a451b337b86a760db15a Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 25 Jun 2026 19:15:17 +0200 Subject: [PATCH 4/7] Provide docs. --- embedding/parsing/instruction.go | 4 +++- embedding/processor.go | 6 ++++-- fragmentation/resolver.go | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index c42acdb3..e2a0a751 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -60,7 +60,9 @@ type Instruction struct { // Configuration contains the embedding settings. Configuration configuration.Configuration - resolver *fragmentation.Resolver + + // resolver caches source fragmentations for this processing operation. + resolver *fragmentation.Resolver } // PatternNotFoundError reports that an instruction pattern did not match the code file. diff --git a/embedding/processor.go b/embedding/processor.go index faf69ab6..73e7e7df 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -46,7 +46,9 @@ type Processor struct { // requiredDocPaths contains documentation files included by the configuration. requiredDocPaths []string - resolver *fragmentation.Resolver + + // resolver caches source fragmentations for this processing operation. + resolver *fragmentation.Resolver } // NewProcessor creates and returns a new Processor with the given docFile and config. @@ -171,7 +173,7 @@ func (p Processor) isUpToDate() (bool, error) { // By the transition process, fills the parsing.Context accordingly, so it is ready to retrieve // the result. func (p Processor) fillEmbeddingContext() (parsing.Context, error) { - context, err := parsing.NewContextWithResolver(p.docFilePath, p.resolver) + context, err := parsing.NewContextWithResolver(p.DocFilePath, p.resolver) if err != nil { return context, err } diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index c9136f09..9f9729c8 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -48,6 +48,7 @@ type absolutePath string // Resolver resolves source files and caches fragmentations for one processing operation. type Resolver struct { + // cache stores source fragmentations for this resolver instance. cache *cache[absolutePath, fragmentedFile] } From 02f6ce3f2ab40d4dd90f5c36c338f1b74a8b424d Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 26 Jun 2026 10:03:25 +0200 Subject: [PATCH 5/7] Support `nil` resolver. --- embedding/parsing/context.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index 7ed365ee..54b7dd2c 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -72,6 +72,7 @@ type Context struct { // embeddings contains accepted embedding instructions and their source positions. embeddings []EmbeddingContext + // resolver owns source fragmentation cache state for this processing operation. resolver *fragmentation.Resolver } @@ -110,10 +111,16 @@ func NewContext(markdownFile string) (Context, error) { } // NewContextWithResolver creates a parsing context using the provided source resolver. +// +// If resolver is nil, it creates a default source resolver. func NewContextWithResolver( markdownFile string, resolver *fragmentation.Resolver, ) (Context, error) { + if resolver == nil { + resolver = fragmentation.NewResolver() + } + source, err := readLines(markdownFile) if err != nil { return Context{}, err From 341ea296e0a4a32038e1d73a94b5551715b309f9 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 26 Jun 2026 10:18:23 +0200 Subject: [PATCH 6/7] Add orchestration test. --- embedding/orchestration_test.go | 86 ++++++++++++++++++++++++++++++++ embedding/parsing/instruction.go | 1 - 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 embedding/orchestration_test.go diff --git a/embedding/orchestration_test.go b/embedding/orchestration_test.go new file mode 100644 index 00000000..2ed920b2 --- /dev/null +++ b/embedding/orchestration_test.go @@ -0,0 +1,86 @@ +// 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 embedding + +import ( + "os" + "path/filepath" + + "embed-code/embed-code-go/configuration" + _type "embed-code/embed-code-go/type" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Orchestration", func() { + It("should share resolver cache across documentation files in one operation", func() { + documentationRoot := GinkgoT().TempDir() + sourceRoot := GinkgoT().TempDir() + config := configuration.NewConfiguration() + config.DocumentationRoot = documentationRoot + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}} + config.DocIncludes = []string{"first.md", "second.md"} + sourcePath := filepath.Join(sourceRoot, "Example.java") + firstDoc := filepath.ToSlash(filepath.Join(documentationRoot, "first.md")) + secondDoc := filepath.ToSlash(filepath.Join(documentationRoot, "second.md")) + writeEmbeddingDoc(firstDoc) + writeEmbeddingDoc(secondDoc) + writeSource(sourcePath, "class Example { String version = \"first\"; }") + + _, processingErrors := processRequiredDocs(config, func( + docFilePath string, + processor Processor, + ) error { + _, err := processor.Embed() + if err != nil { + return err + } + if docFilePath == firstDoc { + writeSource(sourcePath, "class Example { String version = \"second\"; }") + } + + return nil + }) + + Expect(processingErrors).Should(BeEmpty()) + secondDocContent, err := os.ReadFile(secondDoc) + Expect(err).ShouldNot(HaveOccurred()) + Expect(string(secondDocContent)).Should(ContainSubstring( + "class Example { String version = \"first\"; }", + )) + Expect(string(secondDocContent)).ShouldNot(ContainSubstring( + "class Example { String version = \"second\"; }", + )) + }) +}) + +// writeEmbeddingDoc writes a target documentation file with one whole-file embedding. +func writeEmbeddingDoc(path string) { + Expect(os.WriteFile( + path, + []byte("\n```java\n```\n"), + 0600, + )).To(Succeed()) +} + +// writeSource writes source content used by the embedding resolver. +func writeSource(path string, content string) { + Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed()) +} diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index e2a0a751..59c1e2d3 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -142,7 +142,6 @@ func NewInstruction( LinePattern: patterns.line, CommentMode: commentMode, Configuration: config, - resolver: fragmentation.NewResolver(), }, nil } From fd3ab8c049d05726213011422b049b4472ba1445 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 26 Jun 2026 10:24:14 +0200 Subject: [PATCH 7/7] Fix test packge. --- embedding/orchestration_test.go | 56 +++++++++++++-------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/embedding/orchestration_test.go b/embedding/orchestration_test.go index 2ed920b2..bd32e1e6 100644 --- a/embedding/orchestration_test.go +++ b/embedding/orchestration_test.go @@ -16,13 +16,15 @@ // (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 embedding +package embedding_test import ( "os" "path/filepath" + "strings" "embed-code/embed-code-go/configuration" + "embed-code/embed-code-go/embedding" _type "embed-code/embed-code-go/type" . "github.com/onsi/ginkgo/v2" @@ -32,55 +34,39 @@ import ( var _ = Describe("Orchestration", func() { It("should share resolver cache across documentation files in one operation", func() { documentationRoot := GinkgoT().TempDir() - sourceRoot := GinkgoT().TempDir() config := configuration.NewConfiguration() config.DocumentationRoot = documentationRoot - config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}} - config.DocIncludes = []string{"first.md", "second.md"} - sourcePath := filepath.Join(sourceRoot, "Example.java") - firstDoc := filepath.ToSlash(filepath.Join(documentationRoot, "first.md")) + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: documentationRoot}} + config.DocIncludes = []string{"source.md", "second.md"} + sourceDoc := filepath.ToSlash(filepath.Join(documentationRoot, "source.md")) secondDoc := filepath.ToSlash(filepath.Join(documentationRoot, "second.md")) - writeEmbeddingDoc(firstDoc) + writeSourceEmbeddingDoc(sourceDoc) writeEmbeddingDoc(secondDoc) - writeSource(sourcePath, "class Example { String version = \"first\"; }") - _, processingErrors := processRequiredDocs(config, func( - docFilePath string, - processor Processor, - ) error { - _, err := processor.Embed() - if err != nil { - return err - } - if docFilePath == firstDoc { - writeSource(sourcePath, "class Example { String version = \"second\"; }") - } + _, err := embedding.EmbedAll(config) - return nil - }) - - Expect(processingErrors).Should(BeEmpty()) + Expect(err).ShouldNot(HaveOccurred()) secondDocContent, err := os.ReadFile(secondDoc) Expect(err).ShouldNot(HaveOccurred()) - Expect(string(secondDocContent)).Should(ContainSubstring( - "class Example { String version = \"first\"; }", - )) - Expect(string(secondDocContent)).ShouldNot(ContainSubstring( - "class Example { String version = \"second\"; }", - )) + Expect(strings.Count(string(secondDocContent), "original source line")). + Should(Equal(1)) }) }) -// writeEmbeddingDoc writes a target documentation file with one whole-file embedding. -func writeEmbeddingDoc(path string) { +// writeSourceEmbeddingDoc writes a source file that also acts as a target document. +func writeSourceEmbeddingDoc(path string) { Expect(os.WriteFile( path, - []byte("\n```java\n```\n"), + []byte("# Source\n\noriginal source line\n\n\n```md\n```\n"), 0600, )).To(Succeed()) } -// writeSource writes source content used by the embedding resolver. -func writeSource(path string, content string) { - Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed()) +// writeEmbeddingDoc writes a target documentation file with one whole-file embedding. +func writeEmbeddingDoc(path string) { + Expect(os.WriteFile( + path, + []byte("# Second\n\n\n```md\n```\n"), + 0600, + )).To(Succeed()) }