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
5 changes: 4 additions & 1 deletion embedding/orchestration.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,10 @@ func processRequiredDocs(
}

var processingErrors []error
resolver := fragmentation.NewResolver()
resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit)
if err != nil {
return requiredDocPaths, []error{err}
}
for _, doc := range requiredDocPaths {
processor := newProcessor(doc, config, parsing.Transitions, requiredDocPaths, resolver)
if err := handle(doc, processor); err != nil {
Expand Down
15 changes: 12 additions & 3 deletions embedding/parsing/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,27 @@ type EmbeddingContext struct {
// Context - initialized parsing context.
// error - when the documentation file cannot be read.
func NewContext(markdownFile string) (Context, error) {
return NewContextWithResolver(markdownFile, fragmentation.NewResolver())
resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit)
if err != nil {
return Context{}, err
}

return NewContextWithResolver(markdownFile, resolver)
}

// NewContextWithResolver creates a parsing context using the provided source resolver.
//
// If resolver is nil, it creates a default source resolver.
// If resolver is nil, it creates a resolver with the default cache limit.
func NewContextWithResolver(
markdownFile string,
resolver *fragmentation.Resolver,
) (Context, error) {
if resolver == nil {
resolver = fragmentation.NewResolver()
var err error
resolver, err = fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit)
if err != nil {
return Context{}, err
}
}

source, err := readLines(markdownFile)
Expand Down
6 changes: 5 additions & 1 deletion embedding/parsing/instruction.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,11 @@ func parseInstructionPattern(attribute string, value string) (Pattern, error) {
func (e Instruction) Content() ([]string, error) {
resolver := e.resolver
if resolver == nil {
resolver = fragmentation.NewResolver()
var err error
resolver, err = fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit)
if err != nil {
return nil, err
}
}

fileContent, err := resolver.ResolveContent(e.CodeFile, e.Fragment, e.Configuration)
Expand Down
34 changes: 34 additions & 0 deletions embedding/parsing/instruction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,40 @@ var _ = Describe("Instruction", func() {
),
))
})

It("should use shared resolver as default", func() {
sourceRoot := GinkgoT().TempDir()
markdownPath := filepath.Join(GinkgoT().TempDir(), "doc.md")
codePath := filepath.Join(sourceRoot, "Example.java")
config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}}
Expect(os.WriteFile(markdownPath, []byte(""), 0600)).To(Succeed())
Expect(os.WriteFile(
codePath,
[]byte("class Example { String version = \"first\"; }"),
0600,
)).To(Succeed())
context, err := parsing.NewContextWithResolver(markdownPath, nil)
Expect(err).ShouldNot(HaveOccurred())
context.StartEmbedding(parsing.Instruction{
CodeFile: "Example.java",
Configuration: config,
})

firstContent, err := context.EmbeddingInstruction.Content()
Expect(err).ShouldNot(HaveOccurred())
Expect(os.WriteFile(
codePath,
[]byte("class Example { String version = \"second\"; }"),
0600,
)).To(Succeed())
secondContent, err := context.EmbeddingInstruction.Content()
Expect(err).ShouldNot(HaveOccurred())

Expect(secondContent).Should(Equal(firstContent))
Expect(secondContent).Should(Equal([]string{
"class Example { String version = \"first\"; }",
}))
})
})

// getXMLExtractionContent returns source lines selected by an XML instruction fixture.
Expand Down
6 changes: 5 additions & 1 deletion embedding/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,17 @@ func NewProcessor(docFile string, config configuration.Configuration) (Processor
if err != nil {
return Processor{}, err
}
resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit)
if err != nil {
return Processor{}, err
}
Comment on lines +68 to +71

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cache limit is not intended to be configured by the user, for now.


return newProcessor(
docFile,
config,
parsing.Transitions,
requiredDocPaths,
fragmentation.NewResolver(),
resolver,
), nil
}

Expand Down
64 changes: 62 additions & 2 deletions fragmentation/fragmentation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ var _ = Describe("Fragmentation", func() {
var resolver *fragmentation.Resolver

BeforeEach(func() {
resolver = fragmentation.NewResolver()
resolver = newTestResolver(fragmentation.DefaultResolverCacheLimit)
config = configuration.NewConfiguration()
config.DocumentationRoot = "../test/resources/docs"
config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}}
Expand Down Expand Up @@ -149,7 +149,8 @@ var _ = Describe("Fragmentation", func() {
config,
)
Expect(err).ShouldNot(HaveOccurred())
freshContent, err := fragmentation.NewResolver().ResolveContent(
freshResolver := newTestResolver(fragmentation.DefaultResolverCacheLimit)
freshContent, err := freshResolver.ResolveContent(
fileName,
fragmentation.DefaultFragmentName,
config,
Expand All @@ -161,6 +162,36 @@ var _ = Describe("Fragmentation", func() {
Expect(freshContent).Should(Equal([]string{"class Second {}"}))
})

It("should evict the least recently used source from a limited resolver cache", func() {
sourceRoot := GinkgoT().TempDir()
config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}}
resolver = newTestResolver(2)
writeSourceFile(sourceRoot, "A.java", "class A { String version = \"first\"; }")
writeSourceFile(sourceRoot, "B.java", "class B { String version = \"first\"; }")
writeSourceFile(sourceRoot, "C.java", "class C { String version = \"first\"; }")

firstA := resolveSourceFile(resolver, "A.java", config)
_ = resolveSourceFile(resolver, "B.java", config)
cachedA := resolveSourceFile(resolver, "A.java", config)
writeSourceFile(sourceRoot, "A.java", "class A { String version = \"second\"; }")
writeSourceFile(sourceRoot, "B.java", "class B { String version = \"second\"; }")
_ = resolveSourceFile(resolver, "C.java", config)

stillCachedA := resolveSourceFile(resolver, "A.java", config)
reloadedB := resolveSourceFile(resolver, "B.java", config)

Expect(cachedA).Should(Equal(firstA))
Expect(stillCachedA).Should(Equal(firstA))
Expect(reloadedB).Should(Equal([]string{"class B { String version = \"second\"; }"}))
})

It("should reject cache limits below one", func() {
resolver, err := fragmentation.NewResolver(0)

Expect(resolver).Should(BeNil())
Expect(err).Should(HaveOccurred())
})

It("should fail on an unopened fragment", func() {
frag := buildTestFragmentation(unopenedFragmentFileName, config)

Expand Down Expand Up @@ -327,3 +358,32 @@ func resolveTestFragment(

return content
}

// newTestResolver creates a resolver with a test-provided cache limit.
func newTestResolver(cacheLimit int) *fragmentation.Resolver {
resolver, err := fragmentation.NewResolver(cacheLimit)
Expect(err).ShouldNot(HaveOccurred())

return resolver
}

// writeSourceFile writes one source fixture into a temporary source root.
func writeSourceFile(sourceRoot string, fileName string, content string) {
Expect(os.WriteFile(filepath.Join(sourceRoot, fileName), []byte(content), 0600)).To(Succeed())
}

// resolveSourceFile returns the whole-file content resolved from a temporary source root.
func resolveSourceFile(
resolver *fragmentation.Resolver,
fileName string,
config configuration.Configuration,
) []string {
content, err := resolver.ResolveContent(
fileName,
fragmentation.DefaultFragmentName,
config,
)
Expect(err).ShouldNot(HaveOccurred())

return content
}
22 changes: 17 additions & 5 deletions fragmentation/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ import (
_type "embed-code/embed-code-go/type"
)

// resolverCacheLimit is the maximum number of source files retained in the resolver cache.
const resolverCacheLimit = 100
// DefaultResolverCacheLimit is the default maximum number of source files
// retained in a resolver cache.
const DefaultResolverCacheLimit = 100

// fragmentedFile stores cleaned source lines and parsed fragments for one source file.
type fragmentedFile struct {
Expand All @@ -53,13 +54,24 @@ type Resolver struct {
}

// NewResolver creates a resolver with an independent source-fragment cache.
func NewResolver() *Resolver {
//
// Parameters:
// cacheLimit - maximum number of source files retained in the resolver cache.
//
// Returns:
// *Resolver - resolver with an independent cache.
// error - when cacheLimit is below one.
func NewResolver(cacheLimit int) (*Resolver, error) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the whole PR intention, to have it configurable.

if cacheLimit < 1 {
return nil, errors.New("resolver cache limit must be at least 1")
}

return &Resolver{
cache: newCache[absolutePath, fragmentedFile](
resolverCacheLimit,
cacheLimit,
loadSourceFragments,
),
}
}, nil
}

// ResolveContent returns source lines for the requested code file fragment.
Expand Down
Loading