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
173 changes: 173 additions & 0 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ import (
"context"
"os"
"path/filepath"
"strconv"
"testing"
)

const (
benchmarkParentDepth = 16
benchmarkBOMCount = 6
benchmarkManagedDepsPerBOM = 40
benchmarkPropertyCount = 128
benchmarkManagedDepCount = 256
benchmarkArtifactCount = 64
)

// memFetcher preloads every fixture POM into memory so benchmarks measure
// resolution work, not disk I/O.
type memFetcher map[GAV]*POM
Expand Down Expand Up @@ -137,6 +147,169 @@ func BenchmarkResolveCorpus(b *testing.B) {
}
}

func BenchmarkResolveDeepParents(b *testing.B) {
f, root := benchmarkDeepParents()
ctx := context.Background()
b.ResetTimer()
for b.Loop() {
if _, err := NewResolver(f).Resolve(ctx, root, Options{}); err != nil {
b.Fatal(err)
}
}
}

func BenchmarkResolveImportedBOMs(b *testing.B) {
f, root := benchmarkImportedBOMs()
ctx := context.Background()
b.ResetTimer()
for b.Loop() {
if _, err := NewResolver(f).Resolve(ctx, root, Options{}); err != nil {
b.Fatal(err)
}
}
}

func BenchmarkResolveRepeatedProperties(b *testing.B) {
f, root := benchmarkRepeatedProperties()
ctx := context.Background()
b.ResetTimer()
for b.Loop() {
if _, err := NewResolver(f).Resolve(ctx, root, Options{}); err != nil {
b.Fatal(err)
}
}
}

func BenchmarkResolveDependencyManagement(b *testing.B) {
f, root := benchmarkDependencyManagement()
ctx := context.Background()
b.ResetTimer()
for b.Loop() {
if _, err := NewResolver(f).Resolve(ctx, root, Options{}); err != nil {
b.Fatal(err)
}
}
}

func BenchmarkResolveManyArtifacts(b *testing.B) {
f, roots := benchmarkManyArtifacts()
ctx := context.Background()
b.ResetTimer()
for b.Loop() {
r := NewResolver(f)
for _, root := range roots {
if _, err := r.Resolve(ctx, root, Options{}); err != nil {
b.Fatal(err)
}
}
}
}

func benchmarkDeepParents() (memFetcher, GAV) {
f := memFetcher{}
var parent *Parent
for i := range benchmarkParentDepth {
id := "parent-" + strconv.Itoa(i)
gav := GAV{GroupID: "org.example", ArtifactID: id, Version: "1"}
p := &POM{
GroupID: gav.GroupID,
ArtifactID: gav.ArtifactID,
Version: gav.Version,
Parent: parent,
Properties: Properties{"shared.version": strconv.Itoa(i + 1)},
Dependencies: []Dep{{
GroupID: "org.example.lib", ArtifactID: "lib-" + strconv.Itoa(i),
}},
DependencyManagement: DepMgmt{Dependencies: []Dep{{
GroupID: "org.example.lib", ArtifactID: "lib-" + strconv.Itoa(i), Version: "${shared.version}",
}}},
}
f[gav] = p
parent = &Parent{GroupID: gav.GroupID, ArtifactID: gav.ArtifactID, Version: gav.Version}
}
root := GAV{GroupID: "org.example", ArtifactID: "deep-app", Version: "1"}
f[root] = &POM{GroupID: root.GroupID, ArtifactID: root.ArtifactID, Version: root.Version, Parent: parent}
return f, root
}

func benchmarkImportedBOMs() (memFetcher, GAV) {
f := memFetcher{}
imports := make([]Dep, 0, benchmarkBOMCount)
deps := make([]Dep, 0, benchmarkBOMCount*benchmarkManagedDepsPerBOM)
for i := range benchmarkBOMCount {
bom := GAV{GroupID: "org.example.bom", ArtifactID: "bom-" + strconv.Itoa(i), Version: "1"}
managed := make([]Dep, 0, benchmarkManagedDepsPerBOM)
for j := range benchmarkManagedDepsPerBOM {
artifact := "lib-" + strconv.Itoa(i) + "-" + strconv.Itoa(j)
managed = append(managed, Dep{GroupID: "org.example.lib", ArtifactID: artifact, Version: "1." + strconv.Itoa(j)})
deps = append(deps, Dep{GroupID: "org.example.lib", ArtifactID: artifact})
}
f[bom] = &POM{GroupID: bom.GroupID, ArtifactID: bom.ArtifactID, Version: bom.Version, DependencyManagement: DepMgmt{Dependencies: managed}}
imports = append(imports, Dep{GroupID: bom.GroupID, ArtifactID: bom.ArtifactID, Version: bom.Version, Type: "pom", Scope: scopeImport})
}
root := GAV{GroupID: "org.example", ArtifactID: "bom-app", Version: "1"}
f[root] = &POM{
GroupID: root.GroupID, ArtifactID: root.ArtifactID, Version: root.Version,
DependencyManagement: DepMgmt{Dependencies: imports}, Dependencies: deps,
}
return f, root
}

func benchmarkRepeatedProperties() (memFetcher, GAV) {
props := make(Properties, benchmarkPropertyCount)
props["property.0"] = "1.0.0"
for i := 1; i < benchmarkPropertyCount; i++ {
props["property."+strconv.Itoa(i)] = "${property." + strconv.Itoa(i-1) + "}"
}
deps := make([]Dep, benchmarkPropertyCount)
for i := range deps {
deps[i] = Dep{GroupID: "org.example.lib", ArtifactID: "lib-" + strconv.Itoa(i), Version: "${property.127}"}
}
root := GAV{GroupID: "org.example", ArtifactID: "property-app", Version: "1"}
f := memFetcher{root: {
GroupID: root.GroupID, ArtifactID: root.ArtifactID, Version: root.Version,
Properties: props, Dependencies: deps,
}}
return f, root
}

func benchmarkDependencyManagement() (memFetcher, GAV) {
managed := make([]Dep, benchmarkManagedDepCount)
deps := make([]Dep, benchmarkManagedDepCount)
for i := range benchmarkManagedDepCount {
artifact := "lib-" + strconv.Itoa(i)
managed[i] = Dep{GroupID: "org.example.lib", ArtifactID: artifact, Version: "${shared.version}", Scope: "runtime"}
deps[i] = Dep{GroupID: "org.example.lib", ArtifactID: artifact}
}
root := GAV{GroupID: "org.example", ArtifactID: "managed-app", Version: "1"}
f := memFetcher{root: {
GroupID: root.GroupID, ArtifactID: root.ArtifactID, Version: root.Version,
Properties: Properties{"shared.version": "2.0.0"},
DependencyManagement: DepMgmt{Dependencies: managed}, Dependencies: deps,
}}
return f, root
}

func benchmarkManyArtifacts() (memFetcher, []GAV) {
f, bomRoot := benchmarkImportedBOMs()
parent := GAV{GroupID: "org.example", ArtifactID: "shared-parent", Version: "1"}
f[parent] = &POM{
GroupID: parent.GroupID, ArtifactID: parent.ArtifactID, Version: parent.Version,
DependencyManagement: f[bomRoot].DependencyManagement,
}
roots := make([]GAV, benchmarkArtifactCount)
for i := range roots {
root := GAV{GroupID: "org.example", ArtifactID: "app-" + strconv.Itoa(i), Version: "1"}
roots[i] = root
f[root] = &POM{
GroupID: root.GroupID, ArtifactID: root.ArtifactID, Version: root.Version,
Parent: &Parent{GroupID: parent.GroupID, ArtifactID: parent.ArtifactID, Version: parent.Version},
Dependencies: []Dep{{GroupID: "org.example.lib", ArtifactID: "lib-0-0"}},
}
}
return f, roots
}

func splitFixtureName(s string) [3]string {
var out [3]string
first := -1
Expand Down
131 changes: 99 additions & 32 deletions interpolate.go
Original file line number Diff line number Diff line change
@@ -1,50 +1,111 @@
package pom

import (
"regexp"
"strings"
)

const (
maxInterpolationPasses = 10
maxInterpolatedLength = 1 << 20 // 1 MiB
maxInterpolationPasses = 10
maxInterpolatedLength = 1 << 20 // 1 MiB
expressionStart = "${"
)

var exprRE = regexp.MustCompile(`\$\{([^}]+)\}`)

// interpolate substitutes ${name} expressions in s using props. It iterates
// until no further substitutions occur or maxInterpolationPasses is reached,
// so chained references like ${a} -> ${b} -> value resolve correctly.
func interpolate(s string, props map[string]string) string {
if !strings.Contains(s, "${") {
if !strings.Contains(s, expressionStart) {
return s
}
for range maxInterpolationPasses {
changed := false
capped := false
growth := 0
baseLen := len(s)
s = exprRE.ReplaceAllStringFunc(s, func(m string) string {
if capped {
return m
}
name := m[2 : len(m)-1]
if v, ok := lookup(props, name); ok {
growth += len(v) - len(m)
if baseLen+growth > maxInterpolatedLength {
capped = true
return m
var changed, capped bool
s, changed, capped = interpolatePass(s, props)
if capped || !changed || !strings.Contains(s, expressionStart) {
break
}
}
return s
}

func interpolatePass(s string, props map[string]string) (string, bool, bool) {
first := strings.Index(s, expressionStart)
if first < 0 {
return s, false, false
}

// A property reference is commonly the whole value. Returning the map's
// string directly avoids building an identical intermediate string.
if v, ok := wholeExpression(s, props); ok {
if len(v) > maxInterpolatedLength {
return s, false, true
}
return v, v != s, false
}

baseLen := len(s)
growth := 0
search := 0
last := 0
changed := false
capped := false
var out strings.Builder
for {
open, close, ok := nextExpression(s, search)
if !ok {
break
}
if close == open+len(expressionStart) {
search = close + 1
continue
}

replacement, ok := lookup(props, s[open+len(expressionStart):close])
match := s[open : close+1]
if ok && !capped {
growth += len(replacement) - len(match)
if baseLen+growth > maxInterpolatedLength {
capped = true
} else if replacement != match {
if !changed {
out.Grow(baseLen)
}
out.WriteString(s[last:open])
out.WriteString(replacement)
last = close + 1
changed = true
return v
}
return m
})
if capped || !changed || !strings.Contains(s, "${") {
break
}
search = close + 1
}
return s
if !changed {
return s, false, capped
}
out.WriteString(s[last:])
return out.String(), true, capped
}

func wholeExpression(s string, props map[string]string) (string, bool) {
if !strings.HasPrefix(s, expressionStart) {
return "", false
}
close := strings.IndexByte(s[len(expressionStart):], '}')
if close < 0 || close != len(s)-len(expressionStart)-1 || close == 0 {
return "", false
}
return lookup(props, s[len(expressionStart):len(s)-1])
}

func nextExpression(s string, search int) (int, int, bool) {
relOpen := strings.Index(s[search:], expressionStart)
if relOpen < 0 {
return 0, 0, false
}
open := search + relOpen
relClose := strings.IndexByte(s[open+len(expressionStart):], '}')
if relClose < 0 {
return 0, 0, false
}
return open, open + len(expressionStart) + relClose, true
}

// lookup resolves a single property name, applying the alias rules Maven
Expand All @@ -59,7 +120,7 @@ func lookup(props map[string]string, name string) (string, bool) {
}
}
switch name {
case "version", "groupId", "artifactId":
case elementVersion, elementGroupID, elementArtifactID:
if v, ok := props["project."+name]; ok {
return v, true
}
Expand All @@ -69,14 +130,20 @@ func lookup(props map[string]string, name string) (string, bool) {

// containsExpr reports whether s still contains an unresolved ${...}.
func containsExpr(s string) bool {
return strings.Contains(s, "${")
return strings.Contains(s, expressionStart)
}

// firstExpr returns the first ${name} property name in s, or "" if none.
func firstExpr(s string) string {
m := exprRE.FindStringSubmatch(s)
if m == nil {
return ""
for search := 0; search < len(s); {
open, close, ok := nextExpression(s, search)
if !ok {
break
}
if close > open+len(expressionStart) {
return s[open+len(expressionStart) : close]
}
search = close + 1
}
return m[1]
return ""
}
4 changes: 4 additions & 0 deletions interpolate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ func TestInterpolate(t *testing.T) {
{"${b}", "1"},
{"${c}", "1.1"},
{"v${a}-final", "v1-final"},
{"${a}${b}", "11"},
{"${missing}", "${missing}"},
{"${a}.${missing}", "1.${missing}"},
{"before ${a", "before ${a"},
{"${", "${"},
{"${} ${a}", "${} 1"},
{"${pom.version}", "2.0"},
{"${version}", "2.0"},
{"${groupId}", "org.example"},
Expand Down
Loading