Files
Jan KoronaandGitHub 8e931f3d53 [chore][cmd/mdatagen] Use metadata.yaml as a schema source for references (#15272)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

Previously, the schemagen loader used `config.schema.yaml` as the
standalone file format for schema references. This PR changes the source
of truth for cross-component schema references to `metadata.yaml` — the
same file that already exists in every component directory.

Instead of parsing a bare ConfigMetadata JSON-schema object, the loader
now reads the config: section from a component's `metadata.yaml`,
matching the structure that mdatagen already generates for every
component. The generated output is likewise renamed to metadata.yaml
(placed under internal/metadata/) and wrapped with the standard
`metadata.yaml` envelope (type, status, config), so it is usable as a
reference target by other components in the same way.

<!-- Issue number if applicable -->
#### Link to tracking issue
Fixes #15259

<!--Describe what testing was performed and which tests were added.-->
#### Testing

Existing unit tests in `internal/schemagen` and `cmd/mdatagen/internal`
were updated to reflect the new file name and Metadata wrapper format.
The persistToFile test was strengthened to verify a full round-trip
through loadFromFile. All tests pass.
2026-05-12 17:54:56 +00:00

418 lines
12 KiB
Go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
package schemagen
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNewRef(t *testing.T) {
tests := []struct {
name string
refPath string
expected *Ref
}{
{
name: "empty ref",
refPath: "",
expected: &Ref{
namespace: "",
schemaID: "",
defName: "",
kind: Internal,
},
},
{
name: "internal ref",
refPath: "target_type",
expected: &Ref{
namespace: "",
schemaID: "",
defName: "target_type",
kind: Internal,
},
},
{
name: "local ref with absolute path",
refPath: "/config/configauth.config",
expected: &Ref{
namespace: "",
schemaID: "/config/configauth",
defName: "config",
kind: Local,
},
},
{
name: "local ref with relative path (./)",
refPath: "./internal/metadata.config",
expected: &Ref{
namespace: "",
schemaID: "./internal/metadata",
defName: "config",
kind: Local,
},
},
{
name: "local ref with relative path (../)",
refPath: "../other.config",
expected: &Ref{
namespace: "",
schemaID: "../other",
defName: "config",
kind: Local,
},
},
{
name: "local ref without def name",
refPath: "/config/configauth",
expected: &Ref{
namespace: "",
schemaID: "/config/configauth",
defName: "",
kind: Local,
},
},
{
name: "local ref empty",
refPath: "../",
expected: &Ref{
namespace: "",
schemaID: "../",
defName: "",
kind: Local,
},
},
{
name: "local ref empty short schemaId",
refPath: "../.test",
expected: &Ref{
namespace: "",
schemaID: "../",
defName: "test",
kind: Local,
},
},
{
name: "external ref without version",
refPath: "go.opentelemetry.io/collector/config/confighttp.client_config",
expected: &Ref{
namespace: "go.opentelemetry.io/collector",
schemaID: "config/confighttp",
defName: "client_config",
kind: External,
},
},
{
name: "external ref without def name",
refPath: "go.opentelemetry.io/collector/config/confighttp",
expected: &Ref{
namespace: "go.opentelemetry.io/collector",
schemaID: "config/confighttp",
defName: "",
kind: External,
},
},
{
name: "external ref without schema ID",
refPath: "go.opentelemetry.io/collector",
expected: &Ref{
namespace: "go.opentelemetry.io/collector",
schemaID: "",
defName: "",
kind: External,
},
},
{
name: "unknown namespace",
refPath: "com.github.example/custom.xyz",
expected: &Ref{
namespace: "com.github.example",
schemaID: "custom",
defName: "xyz",
kind: External,
},
},
{
name: "ref with wrong format",
refPath: "some/path.with.dots",
expected: &Ref{
namespace: "some",
schemaID: "path",
defName: "with.dots",
kind: External,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := NewRef(tt.refPath)
require.Equal(t, tt.expected, result)
})
}
}
func TestWithOrigin(t *testing.T) {
tests := []struct {
name string
refPath string
origin string
expected string
}{
{
name: "absolute ref resolves against namespace",
refPath: "/config/configauth.config",
origin: "go.opentelemetry.io/collector/config/confighttp",
expected: "go.opentelemetry.io/collector/config/configauth.config",
},
{
name: "absolute ref with empty origin unchanged",
refPath: "/config/configauth.config",
origin: "",
expected: "/config/configauth.config",
},
{
name: "relative ref resolves against module path",
refPath: "./internal/metadata.config",
origin: "go.opentelemetry.io/collector/config/confighttp",
expected: "go.opentelemetry.io/collector/config/confighttp/internal/metadata.config",
},
{
name: "parent relative ref resolves against parent module",
refPath: "../configtls.tls_config",
origin: "go.opentelemetry.io/collector/config/confighttp",
expected: "go.opentelemetry.io/collector/config/configtls.tls_config",
},
{
name: "external ref with same-namespace origin joins schema IDs",
refPath: "go.opentelemetry.io/collector/config/confighttp.client_config",
origin: "go.opentelemetry.io/collector/config/confighttp",
expected: "go.opentelemetry.io/collector/config/confighttp/config/confighttp.client_config",
},
{
name: "internal ref with origin unchanged",
refPath: "target_type",
origin: "go.opentelemetry.io/collector/config/confighttp",
expected: "go.opentelemetry.io/collector/config/confighttp.target_type",
},
{
name: "local relative ref with local absolute resolves to local absolute schema ID",
refPath: "./internal.timeout_config",
origin: "/config/confighttp",
expected: "/config/confighttp/internal.timeout_config",
},
{
name: "local relative ref with external origin resolves to external schema ID",
refPath: "./internal.timeout_config",
origin: "go.opentelemetry.io/collector/config/confighttp",
expected: "go.opentelemetry.io/collector/config/confighttp/internal.timeout_config",
},
{
name: "local absolute ref with local absolute origin keeps original schema ID",
refPath: "/config/confighttp.timeout_config",
origin: "/config/confignet.connection_config",
expected: "/config/confighttp.timeout_config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref := WithOrigin(tt.refPath, NewRef(tt.origin))
require.Equal(t, tt.expected, ref.CacheKey())
})
}
}
func TestRef_Validate(t *testing.T) {
tests := []struct {
name string
refPath string
wantErr bool
}{
{
name: "valid collector reference",
refPath: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
wantErr: false,
},
{
name: "valid contrib reference",
refPath: "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/mysqlreceiver.config",
wantErr: false,
},
{
name: "valid internal reference",
refPath: "target_type",
wantErr: false,
},
{
name: "valid local absolute reference",
refPath: "/config/configauth.config",
wantErr: false,
},
{
name: "empty path",
refPath: "",
wantErr: true,
},
{
name: "missing def name",
refPath: "/config/configauth",
wantErr: true,
},
{
name: "missing schema ID with external ref",
refPath: "go.opentelemetry.io/collector",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref := NewRef(tt.refPath)
err := ref.Validate()
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestRef_URL(t *testing.T) {
tests := []struct {
name string
refPath string
version string
expected string
}{
{
name: "collector reference",
refPath: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
version: "v1.0.0",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector/v1.0.0/scraper/scraperhelper/metadata.yaml",
},
{
name: "contrib reference",
refPath: "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/mysqlreceiver.config",
version: "v0.95.0",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector-contrib/v0.95.0/receiver/mysqlreceiver/metadata.yaml",
},
{
name: "main version",
refPath: "go.opentelemetry.io/collector/processor/batchprocessor.config",
version: "main",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector/main/processor/batchprocessor/metadata.yaml",
},
{
name: "collector pseudo-version uses commit hash",
refPath: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
version: "v0.147.1-0.20260317033252-665ab5d0143d",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector/665ab5d0143d/scraper/scraperhelper/metadata.yaml",
},
{
name: "pseudo-version with incompatible suffix uses commit hash",
refPath: "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/mysqlreceiver.config",
version: "v0.95.1-0.20260317033252-665ab5d0143d+incompatible",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector-contrib/665ab5d0143d/receiver/mysqlreceiver/metadata.yaml",
},
{
name: "dirty version uses trimmed version string",
refPath: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
version: "v1.0.0+dirty",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector/v1.0.0/scraper/scraperhelper/metadata.yaml",
},
{
name: "unrecognized suffix uses raw version string",
refPath: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
version: "v1.0.0-xxx-yyy+dirty",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector/v1.0.0-xxx-yyy/scraper/scraperhelper/metadata.yaml",
},
{
name: "pre-release version passes through unchanged",
refPath: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
version: "v1.0.0-rc.1",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector/v1.0.0-rc.1/scraper/scraperhelper/metadata.yaml",
},
{
name: "pre-release tag that looks like a pseudo-version passes through unchanged",
refPath: "go.opentelemetry.io/collector/scraper/scraperhelper.controller_config",
version: "v1.0.0-beta-abc123abcdef",
expected: "https://raw.githubusercontent.com/open-telemetry/opentelemetry-collector/v1.0.0-beta-abc123abcdef/scraper/scraperhelper/metadata.yaml",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ref := NewRef(tt.refPath)
result, err := ref.URL(tt.version)
require.NoError(t, err)
require.Equal(t, tt.expected, result)
})
}
}
func TestRef_String(t *testing.T) {
ref := NewRef("go.opentelemetry.io/collector/config/confighttp.client_config")
require.Equal(t, "go.opentelemetry.io/collector/config/confighttp.client_config", ref.String())
}
func TestRef_Module_EmptyNamespace(t *testing.T) {
ref := NewRef("target_type")
require.Empty(t, ref.Module())
}
func TestRef_URL_UnsupportedNamespace(t *testing.T) {
ref := NewRef("unsupported.example.com/pkg/sub.config")
_, err := ref.URL("v1.0.0")
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported namespace")
}
func TestRef_Validate_LocalRefMissingSchemaID(t *testing.T) {
ref := &Ref{schemaID: "", defName: "config", kind: Local}
err := ref.Validate()
require.Error(t, err)
require.Contains(t, err.Error(), "missing schema ID in local reference")
}
func TestNamespaceOf_FallbackLastSlash(t *testing.T) {
result := namespaceOf("com.example/some/path.type")
require.Equal(t, "com.example/some", result)
}
func TestNamespaceOf_NoSlash(t *testing.T) {
result := namespaceOf("noslash")
require.Empty(t, result)
}
func TestLocalizeRef(t *testing.T) {
tests := []struct {
name string
refPath string
importRootPath string
expected string
}{
{
name: "same root collector ref becomes local absolute",
refPath: "go.opentelemetry.io/collector/filter.config",
importRootPath: "go.opentelemetry.io/collector",
expected: "/filter.config",
},
{
name: "different root ref stays external",
refPath: "go.opentelemetry.io/collector/filter.config",
importRootPath: "github.com/open-telemetry/opentelemetry-collector-contrib",
expected: "go.opentelemetry.io/collector/filter.config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, LocalizeRef(tt.refPath, tt.importRootPath))
})
}
}