github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/config/config_test.go (about)

     1  package config
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/stretchr/testify/assert"
     7  	"github.com/stretchr/testify/require"
     8  )
     9  
    10  func TestParsePyroscopeConfig(t *testing.T) {
    11  	t.Run("valid go config", func(t *testing.T) {
    12  		yaml := `source_code:
    13    mappings:
    14      - language: go
    15        path:
    16          - prefix: GOROOT
    17        source:
    18          github:
    19            owner: golang
    20            repo: go
    21            ref: go1.24.8
    22            path: src
    23  `
    24  		config, err := ParsePyroscopeConfig([]byte(yaml))
    25  		require.NoError(t, err)
    26  		require.NotNil(t, config)
    27  
    28  		require.Len(t, config.SourceCode.Mappings, 1)
    29  
    30  		mapping := config.SourceCode.Mappings[0]
    31  		assert.Equal(t, "go", mapping.Language)
    32  		require.Len(t, mapping.Path, 1)
    33  		assert.Equal(t, "GOROOT", mapping.Path[0].Prefix)
    34  		require.NotNil(t, mapping.Source.GitHub)
    35  		assert.Equal(t, "golang", mapping.Source.GitHub.Owner)
    36  		assert.Equal(t, "go", mapping.Source.GitHub.Repo)
    37  		assert.Equal(t, "go1.24.8", mapping.Source.GitHub.Ref)
    38  		assert.Equal(t, "src", mapping.Source.GitHub.Path)
    39  	})
    40  
    41  	t.Run("valid java config with multiple mappings", func(t *testing.T) {
    42  		yaml := `source_code:
    43    mappings:
    44      - language: java
    45        path:
    46          - prefix: org/example/rideshare
    47        source:
    48          local:
    49            path: src/main/java/org/example/rideshare
    50      - language: java
    51        path:
    52          - prefix: java
    53        source:
    54          github:
    55            owner: openjdk
    56            repo: jdk
    57            ref: jdk-17+0
    58            path: src/java.base/share/classes/java
    59      - language: java
    60        path:
    61          - prefix: org/springframework/http
    62        source:
    63          github:
    64            owner: spring-projects
    65            repo: spring-framework
    66            ref: v5.3.20
    67            path: spring-web/src/main/java/org/springframework/http
    68  `
    69  		config, err := ParsePyroscopeConfig([]byte(yaml))
    70  		require.NoError(t, err)
    71  		require.NotNil(t, config)
    72  
    73  		require.Len(t, config.SourceCode.Mappings, 3)
    74  
    75  		// Check first mapping (local)
    76  		mapping1 := config.SourceCode.Mappings[0]
    77  		assert.Equal(t, "java", mapping1.Language)
    78  		require.Len(t, mapping1.Path, 1)
    79  		assert.Equal(t, "org/example/rideshare", mapping1.Path[0].Prefix)
    80  		require.NotNil(t, mapping1.Source.Local)
    81  		assert.Equal(t, "src/main/java/org/example/rideshare", mapping1.Source.Local.Path)
    82  
    83  		// Check second mapping (github)
    84  		mapping2 := config.SourceCode.Mappings[1]
    85  		assert.Equal(t, "java", mapping2.Language)
    86  		require.Len(t, mapping2.Path, 1)
    87  		assert.Equal(t, "java", mapping2.Path[0].Prefix)
    88  		require.NotNil(t, mapping2.Source.GitHub)
    89  		assert.Equal(t, "openjdk", mapping2.Source.GitHub.Owner)
    90  
    91  		// Check third mapping (github)
    92  		mapping3 := config.SourceCode.Mappings[2]
    93  		assert.Equal(t, "java", mapping3.Language)
    94  		require.Len(t, mapping3.Path, 1)
    95  		assert.Equal(t, "org/springframework/http", mapping3.Path[0].Prefix)
    96  		require.NotNil(t, mapping3.Source.GitHub)
    97  		assert.Equal(t, "spring-projects", mapping3.Source.GitHub.Owner)
    98  	})
    99  
   100  	t.Run("invalid - missing language", func(t *testing.T) {
   101  		yaml := `source_code:
   102    mappings:
   103      - path:
   104          - prefix: GOROOT
   105        source:
   106          github:
   107            owner: golang
   108            repo: go
   109            ref: go1.24.8
   110            path: src
   111  `
   112  		_, err := ParsePyroscopeConfig([]byte(yaml))
   113  		require.Error(t, err)
   114  		assert.Contains(t, err.Error(), "language")
   115  	})
   116  
   117  	t.Run("invalid - missing source config", func(t *testing.T) {
   118  		yaml := `source_code:
   119    mappings:
   120      - language: go
   121        path:
   122          - prefix: GOROOT
   123  `
   124  		_, err := ParsePyroscopeConfig([]byte(yaml))
   125  		require.Error(t, err)
   126  		assert.Contains(t, err.Error(), "no source type supplied")
   127  	})
   128  
   129  	t.Run("invalid - missing path and function_name", func(t *testing.T) {
   130  		yaml := `source_code:
   131    mappings:
   132      - language: java
   133        source:
   134          local:
   135            path: src/main/java
   136  `
   137  		_, err := ParsePyroscopeConfig([]byte(yaml))
   138  		require.Error(t, err)
   139  		assert.Contains(t, err.Error(), "at least one path or a function_name match is required")
   140  	})
   141  
   142  	t.Run("invalid - unsupported language", func(t *testing.T) {
   143  		yaml := `source_code:
   144    mappings:
   145      - language: rust
   146        path:
   147          - prefix: src
   148        source:
   149          github:
   150            owner: rust-lang
   151            repo: rust
   152            ref: 1.75.0
   153            path: src
   154  `
   155  		_, err := ParsePyroscopeConfig([]byte(yaml))
   156  		require.Error(t, err)
   157  		assert.Contains(t, err.Error(), "unsupported")
   158  	})
   159  
   160  	t.Run("invalid yaml syntax", func(t *testing.T) {
   161  		yaml := `source_code:
   162    mappings:
   163      - language: go
   164        path:
   165          - prefix: GOROOT
   166        source:
   167          github:
   168            owner: golang
   169            repo: go
   170            ref: go1.24.8
   171            path: src
   172          github:
   173            owner: duplicate
   174  `
   175  		_, err := ParsePyroscopeConfig([]byte(yaml))
   176  		require.Error(t, err)
   177  	})
   178  
   179  	t.Run("wrong version", func(t *testing.T) {
   180  		yaml := `version: v1alpha1
   181  `
   182  		_, err := ParsePyroscopeConfig([]byte(yaml))
   183  		require.Error(t, err)
   184  	})
   185  }
   186  
   187  func TestFindMapping(t *testing.T) {
   188  	config := &PyroscopeConfig{
   189  		SourceCode: SourceCodeConfig{
   190  			Mappings: []MappingConfig{
   191  				{
   192  					Language: "java",
   193  					Path: []Match{
   194  						{Prefix: "org/example/rideshare"},
   195  					},
   196  					Source: Source{
   197  						Local: &LocalMappingConfig{
   198  							Path: "src/main/java/org/example/rideshare",
   199  						},
   200  					},
   201  				},
   202  				{
   203  					Language: "java",
   204  					Path: []Match{
   205  						{Prefix: "java"},
   206  					},
   207  					Source: Source{
   208  						GitHub: &GitHubMappingConfig{
   209  							Owner: "openjdk",
   210  							Repo:  "jdk",
   211  							Ref:   "jdk-17+0",
   212  							Path:  "src/java.base/share/classes/java",
   213  						},
   214  					},
   215  				},
   216  				{
   217  					Language: "java",
   218  					Path: []Match{
   219  						{Prefix: "org/springframework/http"},
   220  					},
   221  					Source: Source{
   222  						GitHub: &GitHubMappingConfig{
   223  							Owner: "spring-projects",
   224  							Repo:  "spring-framework",
   225  							Ref:   "v5.3.20",
   226  							Path:  "spring-web/src/main/java/org/springframework/http",
   227  						},
   228  					},
   229  				},
   230  			},
   231  		},
   232  	}
   233  
   234  	tests := []struct {
   235  		name           string
   236  		fileSpec       FileSpec
   237  		expectedPrefix string
   238  		expectedSource string // "local" or "github"
   239  		shouldBeNil    bool
   240  	}{
   241  		{
   242  			name:           "exact match for local",
   243  			fileSpec:       FileSpec{Path: "org/example/rideshare"},
   244  			expectedPrefix: "org/example/rideshare",
   245  			expectedSource: "local",
   246  		},
   247  		{
   248  			name:           "subdirectory of local",
   249  			fileSpec:       FileSpec{Path: "org/example/rideshare/App.java"},
   250  			expectedPrefix: "org/example/rideshare",
   251  			expectedSource: "local",
   252  		},
   253  		{
   254  			name:           "exact match for java",
   255  			fileSpec:       FileSpec{Path: "java"},
   256  			expectedPrefix: "java",
   257  			expectedSource: "github",
   258  		},
   259  		{
   260  			name:           "subdirectory of java",
   261  			fileSpec:       FileSpec{Path: "java/util/ArrayList.java"},
   262  			expectedPrefix: "java",
   263  			expectedSource: "github",
   264  		},
   265  		{
   266  			name:           "exact match for springframework",
   267  			fileSpec:       FileSpec{Path: "org/springframework/http"},
   268  			expectedPrefix: "org/springframework/http",
   269  			expectedSource: "github",
   270  		},
   271  		{
   272  			name:           "subdirectory of springframework",
   273  			fileSpec:       FileSpec{Path: "org/springframework/http/HttpStatus.java"},
   274  			expectedPrefix: "org/springframework/http",
   275  			expectedSource: "github",
   276  		},
   277  		{
   278  			name:           "longest prefix match",
   279  			fileSpec:       FileSpec{Path: "org/springframework/http/converter/HttpMessageConverter.java"},
   280  			expectedPrefix: "org/springframework/http",
   281  			expectedSource: "github",
   282  		},
   283  		{
   284  			name:        "no match",
   285  			fileSpec:    FileSpec{Path: "com/google/common/collect/Lists.java"},
   286  			shouldBeNil: true,
   287  		},
   288  		{
   289  			name:        "partial prefix should not match",
   290  			fileSpec:    FileSpec{Path: "organization/test/File.java"},
   291  			shouldBeNil: true,
   292  		},
   293  	}
   294  
   295  	for _, tt := range tests {
   296  		t.Run(tt.name, func(t *testing.T) {
   297  			result := config.FindMapping(tt.fileSpec)
   298  			if tt.shouldBeNil {
   299  				assert.Nil(t, result)
   300  			} else {
   301  				require.NotNil(t, result)
   302  				require.Len(t, result.Path, 1)
   303  				assert.Equal(t, tt.expectedPrefix, result.Path[0].Prefix)
   304  				switch tt.expectedSource {
   305  				case "local":
   306  					assert.NotNil(t, result.Source.Local)
   307  				case "github":
   308  					assert.NotNil(t, result.Source.GitHub)
   309  				}
   310  			}
   311  		})
   312  	}
   313  }