api/types/plugin: replace rapid with standard Go fuzzing

Replace the rapid-based property test for plugin capability IDs
with a table-driven test and Go's built-in fuzzing support.

Preserve the comparison against the historical JSON parser to
catch compatibility regressions, and add explicit test cases for
the supported serialization formats and invalid inputs.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn committed 2026-10-19 22:33:32 +02:00
1 parent c5c09961ba
commit d3c51c66da
7 files changed
+145 -90

No files matched your search

-1
View File
@@ -7,5 +7,4 @@ require (
github.com/moby/docker-image-spec v1.3.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
pgregory.net/rapid v1.2.0
)
-2
View File
@@ -6,5 +6,3 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
-1
View File
@@ -1 +0,0 @@
testdata/rapid/**
+34
View File
@@ -0,0 +1,34 @@
package plugin_test
import (
"testing"
"github.com/moby/moby/api/types/plugin"
)
func FuzzCapabilityID(f *testing.F) {
f.Add("docker", "networkdriver", "1.0")
f.Add("foo.bar", "baz", "1.0")
f.Add("", "bar", "1.0")
f.Fuzz(func(t *testing.T, prefix, capability, version string) {
original := plugin.CapabilityID{
Prefix: prefix,
Capability: capability,
Version: version,
}
b, err := original.MarshalText()
if err != nil {
return // Invalid combination.
}
var got plugin.CapabilityID
if err := got.UnmarshalText(b); err != nil {
t.Fatal(err)
}
if got != original {
t.Fatalf("roundtrip = %+v, want %+v", got, original)
}
})
}
+111 -83
View File
@@ -2,102 +2,130 @@ package plugin_test
import (
"encoding/json"
"fmt"
"testing"
"github.com/moby/moby/api/types/plugin"
"pgregory.net/rapid"
)
type pluginCapabilityID plugin.CapabilityID
// unmarshalJSON is a copy of the original PluginInterfaceType.UnmarshalJSON
// parser, used to test that the new parser produces the same results for
// well-formed inputs.
func (t *pluginCapabilityID) unmarshalJSON(p []byte) error {
versionIndex := len(p)
prefixIndex := 0
if len(p) < 2 || p[0] != '"' || p[len(p)-1] != '"' {
return fmt.Errorf("%q is not a plugin interface type", p)
}
p = p[1 : len(p)-1]
loop:
for i, b := range p {
switch b {
case '.':
prefixIndex = i
case '/':
versionIndex = i
break loop
}
}
t.Prefix = string(p[:prefixIndex])
t.Capability = string(p[prefixIndex+1 : versionIndex])
if versionIndex < len(p) {
t.Version = string(p[versionIndex+1:])
}
return nil
}
func TestCapabilityID_MarshalUnmarshal(t *testing.T) {
stringgen := rapid.StringMatching(`[a-z0-9-./]*`)
rapid.Check(t, func(t *rapid.T) {
typ := plugin.CapabilityID{
Capability: stringgen.Draw(t, "Capability"),
Prefix: stringgen.Draw(t, "Prefix"),
Version: stringgen.Draw(t, "Version"),
}
b, err := typ.MarshalText()
if err != nil {
t.Skipf("unmarshalable value: %v", err)
}
t.Logf("InterfaceType(%q)", b)
var roundtrip plugin.CapabilityID
if err := roundtrip.UnmarshalText(b); err != nil {
t.Fatal(err)
}
if typ != roundtrip {
t.Errorf("roundtrip = %+v, want %+v", roundtrip, typ)
}
jb, err := json.Marshal(string(b))
if err != nil {
t.Fatal(err)
}
var oldparser pluginCapabilityID
if err := oldparser.unmarshalJSON(jb); err != nil {
t.Fatal(err)
}
if typ != plugin.CapabilityID(oldparser) {
t.Errorf("new parser = %+v, old parser = %+v", typ, oldparser)
}
})
}
func TestCapabilityID_JSONMarshalUnmarshal(t *testing.T) {
type rt struct {
Type plugin.CapabilityID
}
a := rt{
Type: plugin.CapabilityID{
Capability: "foo",
Prefix: "bar",
Version: "baz",
tests := []struct {
name string
id plugin.CapabilityID
want string
}{
{
name: "standard",
id: plugin.CapabilityID{Prefix: "docker", Capability: "networkdriver", Version: "1.0"},
want: "docker.networkdriver/1.0",
},
{
name: "dotted prefix",
id: plugin.CapabilityID{Prefix: "com.example", Capability: "driver", Version: "1.0"},
want: "com.example.driver/1.0",
},
{
name: "empty prefix",
id: plugin.CapabilityID{Capability: "driver", Version: "1.0"},
want: ".driver/1.0",
},
{
name: "empty version",
id: plugin.CapabilityID{Prefix: "docker", Capability: "driver"},
want: "docker.driver/",
},
{
name: "zero value",
id: plugin.CapabilityID{},
want: "./",
},
}
b, err := json.Marshal(a)
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
b, err := tc.id.MarshalText()
if err != nil {
t.Fatal(err)
}
if string(b) != tc.want {
t.Errorf("MarshalText() = %q, want %q", b, tc.want)
}
var got plugin.CapabilityID
if err := got.UnmarshalText(b); err != nil {
t.Fatal(err)
}
if got != tc.id {
t.Errorf("roundtrip = %+v, want %+v", got, tc.id)
}
})
}
}
// TestCapabilityID_JSONMarshalUnmarshal verifies that encoding/json uses
// the text marshaling methods and preserves the JSON representation.
func TestCapabilityID_JSONMarshalUnmarshal(t *testing.T) {
id := plugin.CapabilityID{Prefix: "docker", Capability: "networkdriver", Version: "1.0"}
data, err := json.Marshal(id)
if err != nil {
t.Fatal(err)
}
t.Logf("JSON: %s", b)
if want := `"docker.networkdriver/1.0"`; string(data) != want {
t.Errorf("json.Marshal() = %s, want %s", data, want)
}
var roundtrip rt
if err := json.Unmarshal(b, &roundtrip); err != nil {
var got plugin.CapabilityID
if err := json.Unmarshal(data, &got); err != nil {
t.Fatal(err)
}
if a != roundtrip {
t.Errorf("roundtrip = %+v, want %+v", roundtrip, a)
if got != id {
t.Errorf("JSON roundtrip = %+v, want %+v", got, id)
}
}
func TestCapabilityID_JSONUnmarshal(t *testing.T) {
tests := []struct {
name string
data string
want plugin.CapabilityID
}{
{
name: "standard",
data: `"docker.networkdriver/1.0"`,
want: plugin.CapabilityID{
Prefix: "docker",
Capability: "networkdriver",
Version: "1.0",
},
},
{
name: "dotted prefix",
data: `"com.example.driver/1.0"`,
want: plugin.CapabilityID{
Prefix: "com.example",
Capability: "driver",
Version: "1.0",
},
},
{
name: "no version",
data: `"docker.driver"`,
want: plugin.CapabilityID{
Prefix: "docker",
Capability: "driver",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var got plugin.CapabilityID
if err := json.Unmarshal([]byte(tc.data), &got); err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("UnmarshalJSON(%s) = %+v, want %+v", tc.data, got, tc.want)
}
})
}
}
-2
View File
@@ -61,5 +61,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
-1
View File
@@ -1 +0,0 @@
testdata/rapid/**