e88305ecbd
## Summary: In previous commits I added support to genqlient for interfaces, inline fragments, and, most recently, named fragments of concrete (object) type. This leaves only named fragments of interface type! Like other named fragments, these are useful for code-sharing, especially if you want some code that can handle the same fields of several different types. As seems to be inevitable with genqlient, this was mostly pretty straightforward, although there turned out to be surprisingly many places we needed to add some handling; almost anywhere that touches interfaces *or* named fragments needed some updates. But it's all hopefully fairly clear code. As a part of this change I made three semi-related improvements: 1. I refactored the handling of descriptions (i.e. GoDoc), because it was getting more and more confusing and duplicative. I'm still not sure how much of it it makes sense to inline vs. separate, but I think this is better than it was. This resulted in some minor changes to descriptions, generally in the direction of making things more consistent. 2. I bumped the minimum Go version to 1.14 so we can guarantee support for duplicate interface methods. These are useful for abstract-in-absstract spreads; we generate an interface for the fragment, and (if the fragment-type implements the scope-type) we embed it into the interface we generate for its spread-context, and if the two have a duplicated field we thus duplicate the method. It wouldn't be impossible to support this on 1.13 (maybe just by omitting said embed) but it didn't seem worth it. This also removes a few special-cases in tests. 3. I added a bunch of code to better format syntax errors in the generated code (which we see from `gofmt`). This is mostly just an internal improvement; I wrote it because I got annoyed while hunting down a few such errors.. Fixes, at last, #8. Issue: https://github.com/Khan/genqlient/issues/8 ## Test plan: make check Author: benjaminjkraft Reviewers: dnerdy, benjaminjkraft, aberkan, MiguelCastillo Required Reviewers: Approved By: dnerdy Checks: ✅ Lint, ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint Pull Request URL: https://github.com/Khan/genqlient/pull/79
265 lines
7.9 KiB
Go
265 lines
7.9 KiB
Go
package generate
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Khan/genqlient/internal/testutil"
|
|
)
|
|
|
|
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 := ioutil.TempFile("./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 := ioutil.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: filepath.Join(dataDir, "schema.graphql"),
|
|
Operations: []string{filepath.Join(dataDir, sourceFilename)},
|
|
Package: "test",
|
|
Generated: goFilename,
|
|
ExportOperations: queriesFilename,
|
|
Bindings: map[string]*TypeBinding{
|
|
"ID": {Type: "github.com/Khan/genqlient/internal/testutil.ID"},
|
|
"DateTime": {Type: "time.Time"},
|
|
"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")
|
|
} else if sourceFilename == "Omitempty.graphql" {
|
|
t.Skip("TODO: enable after fixing " +
|
|
"https://github.com/Khan/genqlient/issues/43")
|
|
}
|
|
|
|
err := buildGoFile(sourceFilename, generated[goFilename])
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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
|
|
fakeConfigFilename string
|
|
config *Config // omits Schema and Operations, set below.
|
|
}{
|
|
{"DefaultConfig", "genqlient.yaml", defaultConfig},
|
|
{"Subpackage", "genqlient.yaml", &Config{
|
|
Generated: "mypkg/myfile.go",
|
|
ContextType: "context.Context", // (from defaultConfig)
|
|
}},
|
|
{"SubpackageConfig", "mypkg/genqlient.yaml", &Config{
|
|
Generated: "myfile.go", // (relative to genqlient.yaml)
|
|
ContextType: "context.Context",
|
|
}},
|
|
{"PackageName", "genqlient.yaml", &Config{
|
|
Generated: "myfile.go",
|
|
Package: "mypkg",
|
|
ContextType: "context.Context",
|
|
}},
|
|
{"ExportOperations", "genqlient.yaml", &Config{
|
|
Generated: "generated.go",
|
|
ExportOperations: "operations.json",
|
|
ContextType: "context.Context",
|
|
}},
|
|
{"CustomContext", "genqlient.yaml", &Config{
|
|
Generated: "generated.go",
|
|
ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext",
|
|
}},
|
|
{"NoContext", "genqlient.yaml", &Config{
|
|
Generated: "generated.go",
|
|
ContextType: "",
|
|
}},
|
|
{"ClientGetter", "genqlient.yaml", &Config{
|
|
Generated: "generated.go",
|
|
ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromContext",
|
|
ContextType: "context.Context",
|
|
}},
|
|
{"ClientGetterCustomContext", "genqlient.yaml", &Config{
|
|
Generated: "generated.go",
|
|
ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromMyContext",
|
|
ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext",
|
|
}},
|
|
{"ClientGetterNoContext", "genqlient.yaml", &Config{
|
|
Generated: "generated.go",
|
|
ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromNowhere",
|
|
ContextType: "",
|
|
}},
|
|
}
|
|
|
|
sourceFilename := "SimpleQuery.graphql"
|
|
|
|
for _, test := range tests {
|
|
config := test.config
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := config.ValidateAndFillDefaults(
|
|
filepath.Join(dataDir, test.fakeConfigFilename))
|
|
config.Schema = filepath.Join(dataDir, "schema.graphql")
|
|
config.Operations = []string{filepath.Join(dataDir, sourceFilename)}
|
|
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)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGenerate is a snapshot-based test of error text.
|
|
//
|
|
// For each .go or .graphql file in testdata/errors, and corresponding
|
|
// .schema.graphql file, 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, to make sure the
|
|
// line numbers work in both cases.
|
|
func TestGenerateErrors(t *testing.T) {
|
|
files, err := ioutil.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") {
|
|
continue
|
|
}
|
|
|
|
baseFilename := strings.TrimSuffix(sourceFilename, filepath.Ext(sourceFilename))
|
|
schemaFilename := baseFilename + ".schema.graphql"
|
|
|
|
t.Run(sourceFilename, func(t *testing.T) {
|
|
_, err := Generate(&Config{
|
|
Schema: filepath.Join(errorsDir, schemaFilename),
|
|
Operations: []string{filepath.Join(errorsDir, sourceFilename)},
|
|
Package: "test",
|
|
Generated: os.DevNull,
|
|
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())
|
|
})
|
|
}
|
|
}
|