80687e7336
Support package-names with dashes in them We were smart about aliasing if you have name-collisions, but not if your package name is something that's not a valid identifier, like `"path/to/my-package"`, which Go for better or worse allows. Now we remove all the invalid characters (in practice mainly dashes, dots, and leading digits). Fixes #231. Test plan: make check
327 lines
9.7 KiB
Go
327 lines
9.7 KiB
Go
package generate
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Khan/genqlient/internal/testutil"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
const (
|
|
dataDir = "testdata/queries"
|
|
errorsDir = "testdata/errors"
|
|
)
|
|
|
|
// buildGoFile returns an error if the given Go code is not valid.
|
|
//
|
|
// namePrefix is used for the temp-file, and is just for debugging.
|
|
func buildGoFile(namePrefix string, content []byte) error {
|
|
// We need to put this within the current module, rather than in
|
|
// /tmp, so that it can access internal/testutil.
|
|
f, err := os.CreateTemp("./testdata/tmp", namePrefix+"_*.go")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
f.Close()
|
|
os.Remove(f.Name())
|
|
}()
|
|
|
|
_, err = f.Write(content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command("go", "build", f.Name())
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return fmt.Errorf("generated code does not compile: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TestGenerate is a snapshot-based test of code-generation.
|
|
//
|
|
// This file just has the test runner; the actual data is all in
|
|
// testdata/queries. Specifically, the schema used for all the queries is in
|
|
// schema.graphql; the queries themselves are in TestName.graphql. The test
|
|
// asserts that running genqlient on that query produces the generated code in
|
|
// the snapshot-file TestName.graphql.go.
|
|
//
|
|
// To update the snapshots (if the code-generator has changed), run the test
|
|
// with `UPDATE_SNAPSHOTS=1`; it will fail the tests and print any diffs, but
|
|
// update the snapshots. Make sure to check that the output is sensible; the
|
|
// snapshots don't even get compiled!
|
|
func TestGenerate(t *testing.T) {
|
|
files, err := os.ReadDir(dataDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
sourceFilename := file.Name()
|
|
if sourceFilename == "schema.graphql" || !strings.HasSuffix(sourceFilename, ".graphql") {
|
|
continue
|
|
}
|
|
goFilename := sourceFilename + ".go"
|
|
queriesFilename := sourceFilename + ".json"
|
|
|
|
t.Run(sourceFilename, func(t *testing.T) {
|
|
generated, err := Generate(&Config{
|
|
Schema: []string{filepath.Join(dataDir, "schema.graphql")},
|
|
Operations: []string{filepath.Join(dataDir, sourceFilename)},
|
|
Package: "test",
|
|
Generated: goFilename,
|
|
ExportOperations: queriesFilename,
|
|
ContextType: "-",
|
|
Bindings: map[string]*TypeBinding{
|
|
"ID": {Type: "github.com/Khan/genqlient/internal/testutil.ID"},
|
|
"DateTime": {Type: "time.Time"},
|
|
"Date": {
|
|
Type: "time.Time",
|
|
Marshaler: "github.com/Khan/genqlient/internal/testutil.MarshalDate",
|
|
Unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate",
|
|
},
|
|
"Junk": {Type: "interface{}"},
|
|
"ComplexJunk": {Type: "[]map[string]*[]*map[string]interface{}"},
|
|
"Pokemon": {
|
|
Type: "github.com/Khan/genqlient/internal/testutil.Pokemon",
|
|
ExpectExactFields: "{ species level }",
|
|
},
|
|
"PokemonInput": {Type: "github.com/Khan/genqlient/internal/testutil.Pokemon"},
|
|
},
|
|
AllowBrokenFeatures: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for filename, content := range generated {
|
|
t.Run(filename, func(t *testing.T) {
|
|
testutil.Cupaloy.SnapshotT(t, string(content))
|
|
})
|
|
}
|
|
|
|
t.Run("Build", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping build due to -short")
|
|
}
|
|
|
|
err := buildGoFile(sourceFilename, generated[goFilename])
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func getDefaultConfig(t *testing.T) *Config {
|
|
// Parse the config that `genqlient --init` generates, to make sure that
|
|
// works.
|
|
var config Config
|
|
b, err := os.ReadFile("default_genqlient.yaml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = yaml.UnmarshalStrict(b, &config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &config
|
|
}
|
|
|
|
// TestGenerateWithConfig tests several configuration options that affect
|
|
// generated code but don't require particular query structures to test.
|
|
//
|
|
// It runs a simple query from TestGenerate with several different genqlient
|
|
// configurations. It uses snapshots, just like TestGenerate.
|
|
func TestGenerateWithConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
baseDir string // relative to dataDir
|
|
operations []string // overrides the default set below
|
|
config *Config // omits Schema and Operations, set below.
|
|
}{
|
|
{"DefaultConfig", "", nil, getDefaultConfig(t)},
|
|
{"Subpackage", "", nil, &Config{
|
|
Generated: "mypkg/myfile.go",
|
|
}},
|
|
{"SubpackageConfig", "mypkg", nil, &Config{
|
|
Generated: "myfile.go", // (relative to genqlient.yaml)
|
|
}},
|
|
{"PackageName", "", nil, &Config{
|
|
Generated: "myfile.go",
|
|
Package: "mypkg",
|
|
}},
|
|
{"ExportOperations", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
ExportOperations: "operations.json",
|
|
}},
|
|
{"CustomContext", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext",
|
|
}},
|
|
{"CustomContextWithAlias", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
ContextType: "github.com/Khan/genqlient/internal/testutil/junk---fun.name.MyContext",
|
|
}},
|
|
{"StructReferences", "", nil, &Config{
|
|
StructReferences: true,
|
|
Generated: "generated-structrefs.go",
|
|
}},
|
|
{"PackageBindings", "", nil, &Config{
|
|
PackageBindings: []*PackageBinding{
|
|
{Package: "github.com/Khan/genqlient/internal/testutil"},
|
|
},
|
|
}},
|
|
{"NoContext", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
ContextType: "-",
|
|
}},
|
|
{"ClientGetter", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromContext",
|
|
}},
|
|
{"ClientGetterCustomContext", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromMyContext",
|
|
ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext",
|
|
}},
|
|
{"ClientGetterNoContext", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromNowhere",
|
|
ContextType: "-",
|
|
}},
|
|
{"Extensions", "", nil, &Config{
|
|
Generated: "generated.go",
|
|
Extensions: true,
|
|
}},
|
|
{"OptionalValue", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{
|
|
Generated: "generated.go",
|
|
Optional: "value",
|
|
}},
|
|
{"OptionalPointer", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{
|
|
Generated: "generated.go",
|
|
Optional: "pointer",
|
|
}},
|
|
}
|
|
|
|
sourceFilename := "SimpleQuery.graphql"
|
|
|
|
for _, test := range tests {
|
|
config := test.config
|
|
baseDir := filepath.Join(dataDir, test.baseDir)
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := config.ValidateAndFillDefaults(baseDir)
|
|
config.Schema = []string{filepath.Join(dataDir, "schema.graphql")}
|
|
if test.operations == nil {
|
|
config.Operations = []string{filepath.Join(dataDir, sourceFilename)}
|
|
} else {
|
|
config.Operations = make([]string, len(test.operations))
|
|
for i := range test.operations {
|
|
config.Operations[i] = filepath.Join(dataDir, test.operations[i])
|
|
}
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
generated, err := Generate(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for filename, content := range generated {
|
|
t.Run(filename, func(t *testing.T) {
|
|
testutil.Cupaloy.SnapshotT(t, string(content))
|
|
})
|
|
}
|
|
|
|
t.Run("Build", func(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping build due to -short")
|
|
}
|
|
|
|
err := buildGoFile(sourceFilename,
|
|
generated[config.Generated])
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGenerateErrors is a snapshot-based test of error text.
|
|
//
|
|
// For each .go or .graphql file in testdata/errors, it asserts that the given
|
|
// query returns an error, and that that error's string-text matches the
|
|
// snapshot. The snapshotting is useful to ensure we don't accidentally make
|
|
// the text less readable, drop the line numbers, etc. We include both .go and
|
|
// .graphql tests for some of the test cases, to make sure the line numbers
|
|
// work in both cases. Tests may include a .schema.graphql file of their own,
|
|
// or use the shared schema.graphql in the same directory for convenience.
|
|
func TestGenerateErrors(t *testing.T) {
|
|
files, err := os.ReadDir(errorsDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
sourceFilename := file.Name()
|
|
if !strings.HasSuffix(sourceFilename, ".graphql") &&
|
|
!strings.HasSuffix(sourceFilename, ".go") ||
|
|
strings.HasSuffix(sourceFilename, ".schema.graphql") ||
|
|
sourceFilename == "schema.graphql" {
|
|
continue
|
|
}
|
|
|
|
baseFilename := strings.TrimSuffix(sourceFilename, filepath.Ext(sourceFilename))
|
|
testFilename := strings.ReplaceAll(sourceFilename, ".", "/")
|
|
|
|
// Schema is either <base>.schema.graphql, or <dir>/schema.graphql if
|
|
// that doesn't exist.
|
|
schemaFilename := baseFilename + ".schema.graphql"
|
|
if _, err := os.Stat(filepath.Join(errorsDir, schemaFilename)); err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
schemaFilename = "schema.graphql"
|
|
} else {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
t.Run(testFilename, func(t *testing.T) {
|
|
_, err := Generate(&Config{
|
|
Schema: []string{filepath.Join(errorsDir, schemaFilename)},
|
|
Operations: []string{filepath.Join(errorsDir, sourceFilename)},
|
|
Package: "test",
|
|
Generated: os.DevNull,
|
|
ContextType: "context.Context",
|
|
Bindings: map[string]*TypeBinding{
|
|
"ValidScalar": {Type: "string"},
|
|
"InvalidScalar": {Type: "bogus"},
|
|
"Pokemon": {
|
|
Type: "github.com/Khan/genqlient/internal/testutil.Pokemon",
|
|
ExpectExactFields: "{ species level }",
|
|
},
|
|
},
|
|
AllowBrokenFeatures: true,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected an error")
|
|
}
|
|
|
|
testutil.Cupaloy.SnapshotT(t, err.Error())
|
|
})
|
|
}
|
|
}
|